feat: release notes (#298)

This commit is contained in:
Carlos Florêncio
2026-02-26 23:08:15 +00:00
committed by GitHub
parent ea1b731fd6
commit 8ee5b56904
48 changed files with 2235 additions and 365 deletions
+54
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
../.agents/skills
+37
View File
@@ -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
+15
View File
@@ -23,6 +23,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm -C apps install --frozen-lockfile run: pnpm -C apps install --frozen-lockfile
- name: Build web (standalone) - name: Build web (standalone)
env:
KANDEV_VERSION: ${{ github.ref_name }}
run: pnpm -C apps --filter @kandev/web build run: pnpm -C apps --filter @kandev/web build
- name: Package web bundle - name: Package web bundle
run: bash scripts/release/package-web.sh run: bash scripts/release/package-web.sh
@@ -116,6 +118,18 @@ jobs:
needs: build-bundles needs: build-bundles
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Download bundle artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -144,6 +158,7 @@ jobs:
- name: Publish release assets - name: Publish release assets
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
body_path: RELEASE_NOTES.md
files: | files: |
dist/release-assets/kandev-*.tar.gz dist/release-assets/kandev-*.tar.gz
dist/release-assets/kandev-*.tar.gz.sha256 dist/release-assets/kandev-*.tar.gz.sha256
+10
View File
@@ -1,4 +1,14 @@
repos: 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 # Backend (Go) hooks
- repo: local - repo: local
hooks: hooks:
+18 -5
View File
@@ -78,13 +78,21 @@ apps/backend/
│ ├── editors/ # Editor integration │ ├── editors/ # Editor integration
│ ├── events/ # Event bus for internal pub/sub │ ├── events/ # Event bus for internal pub/sub
│ ├── gateway/ # WebSocket gateway │ ├── gateway/ # WebSocket gateway
│ ├── github/ # GitHub API integration (PRs, reviews, webhooks)
│ ├── integration/ # External integrations │ ├── integration/ # External integrations
│ ├── lsp/ # LSP server │ ├── lsp/ # LSP server
│ ├── mcp/ # MCP protocol support │ ├── mcp/ # MCP protocol support
│ ├── notifications/ # Notification system │ ├── notifications/ # Notification system
│ ├── persistence/ # Persistence layer │ ├── persistence/ # Persistence layer
│ ├── prompts/ # Prompt management │ ├── 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 │ ├── sysprompt/ # System prompt injection
│ ├── task/
│ │ └── ...
│ ├── tools/ # Tool integrations
│ ├── user/ # User management │ ├── user/ # User management
│ ├── workflow/ # Workflow engine │ ├── workflow/ # Workflow engine
│ │ ├── engine/ # Typed state-machine engine (trigger evaluation, action callbacks, transition store) │ │ ├── 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. **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 │ ├── session-runtime/ # shell, processes, git, context
│ ├── workspace/ # workspaces, repos, branches │ ├── workspace/ # workspaces, repos, branches
│ ├── settings/ # executors, agents, editors, prompts │ ├── 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 │ └── ui/ # preview, connection, active state
├── hydration/ # SSR merge strategies ├── hydration/ # SSR merge strategies
hooks/domains/{kanban,session,workspace,settings}/ # Domain-organized hooks hooks/domains/{kanban,session,workspace,settings}/ # Domain-organized hooks
lib/api/domains/ # API clients lib/api/domains/ # API clients
├── kanban-api, session-api, workspace-api, settings-api, process-api ├── kanban-api, session-api, workspace-api, settings-api, process-api
├── plan-api, queue-api, workflow-api, stats-api ├── plan-api, queue-api, workflow-api, stats-api, github-api
├── user-shell-api, debug-api ├── user-shell-api, debug-api, secrets-api, sprites-api, vscode-api
``` ```
**Key State Paths:** **Key State Paths:**
@@ -284,6 +293,10 @@ lib/api/domains/ # API clients
## Best Practices ## 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) ### Code Quality (enforced by linters)
Static analysis runs in CI and pre-commit. New code **must** stay within these limits: 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
+547
View File
@@ -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
@@ -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) { func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserSettingsRequest) (dto.UserSettingsResponse, error) {
settings, err := c.svc.UpdateUserSettings(ctx, &service.UpdateUserSettingsRequest{ settings, err := c.svc.UpdateUserSettings(ctx, &service.UpdateUserSettingsRequest{
WorkspaceID: req.WorkspaceID, WorkspaceID: req.WorkspaceID,
KanbanViewMode: req.KanbanViewMode, KanbanViewMode: req.KanbanViewMode,
WorkflowFilterID: req.WorkflowFilterID, WorkflowFilterID: req.WorkflowFilterID,
RepositoryIDs: req.RepositoryIDs, RepositoryIDs: req.RepositoryIDs,
InitialSetupComplete: req.InitialSetupComplete, InitialSetupComplete: req.InitialSetupComplete,
PreferredShell: req.PreferredShell, PreferredShell: req.PreferredShell,
DefaultEditorID: req.DefaultEditorID, DefaultEditorID: req.DefaultEditorID,
EnablePreviewOnClick: req.EnablePreviewOnClick, EnablePreviewOnClick: req.EnablePreviewOnClick,
ChatSubmitKey: req.ChatSubmitKey, ChatSubmitKey: req.ChatSubmitKey,
ReviewAutoMarkOnScroll: req.ReviewAutoMarkOnScroll, ReviewAutoMarkOnScroll: req.ReviewAutoMarkOnScroll,
LspAutoStartLanguages: req.LspAutoStartLanguages, ShowReleaseNotification: req.ShowReleaseNotification,
LspAutoInstallLanguages: req.LspAutoInstallLanguages, ReleaseNotesLastSeenVersion: req.ReleaseNotesLastSeenVersion,
LspServerConfigs: req.LspServerConfigs, LspAutoStartLanguages: req.LspAutoStartLanguages,
SavedLayouts: req.SavedLayouts, LspAutoInstallLanguages: req.LspAutoInstallLanguages,
LspServerConfigs: req.LspServerConfigs,
SavedLayouts: req.SavedLayouts,
}) })
if err != nil { if err != nil {
return dto.UserSettingsResponse{}, err return dto.UserSettingsResponse{}, err
+52 -46
View File
@@ -14,22 +14,24 @@ type UserDTO struct {
} }
type UserSettingsDTO struct { type UserSettingsDTO struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
WorkspaceID string `json:"workspace_id"` WorkspaceID string `json:"workspace_id"`
KanbanViewMode string `json:"kanban_view_mode"` KanbanViewMode string `json:"kanban_view_mode"`
WorkflowFilterID string `json:"workflow_filter_id"` WorkflowFilterID string `json:"workflow_filter_id"`
RepositoryIDs []string `json:"repository_ids"` RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"` InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"` PreferredShell string `json:"preferred_shell"`
DefaultEditorID string `json:"default_editor_id"` DefaultEditorID string `json:"default_editor_id"`
EnablePreviewOnClick bool `json:"enable_preview_on_click"` EnablePreviewOnClick bool `json:"enable_preview_on_click"`
ChatSubmitKey string `json:"chat_submit_key"` ChatSubmitKey string `json:"chat_submit_key"`
ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"`
LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` ShowReleaseNotification bool `json:"show_release_notification"`
LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"`
LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` LspAutoStartLanguages []string `json:"lsp_auto_start_languages"`
SavedLayouts []models.SavedLayout `json:"saved_layouts"` LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"`
UpdatedAt string `json:"updated_at"` 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 { type UserResponse struct {
@@ -48,20 +50,22 @@ type ShellOption struct {
} }
type UpdateUserSettingsRequest struct { type UpdateUserSettingsRequest struct {
WorkspaceID *string `json:"workspace_id,omitempty"` WorkspaceID *string `json:"workspace_id,omitempty"`
KanbanViewMode *string `json:"kanban_view_mode,omitempty"` KanbanViewMode *string `json:"kanban_view_mode,omitempty"`
WorkflowFilterID *string `json:"workflow_filter_id,omitempty"` WorkflowFilterID *string `json:"workflow_filter_id,omitempty"`
RepositoryIDs *[]string `json:"repository_ids,omitempty"` RepositoryIDs *[]string `json:"repository_ids,omitempty"`
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"` InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
PreferredShell *string `json:"preferred_shell,omitempty"` PreferredShell *string `json:"preferred_shell,omitempty"`
DefaultEditorID *string `json:"default_editor_id,omitempty"` DefaultEditorID *string `json:"default_editor_id,omitempty"`
EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"`
ChatSubmitKey *string `json:"chat_submit_key,omitempty"` ChatSubmitKey *string `json:"chat_submit_key,omitempty"`
ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll,omitempty"` ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll,omitempty"`
LspAutoStartLanguages *[]string `json:"lsp_auto_start_languages,omitempty"` ShowReleaseNotification *bool `json:"show_release_notification,omitempty"`
LspAutoInstallLanguages *[]string `json:"lsp_auto_install_languages,omitempty"` ReleaseNotesLastSeenVersion *string `json:"release_notes_last_seen_version,omitempty"`
LspServerConfigs *map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` LspAutoStartLanguages *[]string `json:"lsp_auto_start_languages,omitempty"`
SavedLayouts *[]models.SavedLayout `json:"saved_layouts,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 { func FromUser(user *models.User) UserDTO {
@@ -75,21 +79,23 @@ func FromUser(user *models.User) UserDTO {
func FromUserSettings(settings *models.UserSettings) UserSettingsDTO { func FromUserSettings(settings *models.UserSettings) UserSettingsDTO {
return UserSettingsDTO{ return UserSettingsDTO{
UserID: settings.UserID, UserID: settings.UserID,
WorkspaceID: settings.WorkspaceID, WorkspaceID: settings.WorkspaceID,
KanbanViewMode: settings.KanbanViewMode, KanbanViewMode: settings.KanbanViewMode,
WorkflowFilterID: settings.WorkflowFilterID, WorkflowFilterID: settings.WorkflowFilterID,
RepositoryIDs: settings.RepositoryIDs, RepositoryIDs: settings.RepositoryIDs,
InitialSetupComplete: settings.InitialSetupComplete, InitialSetupComplete: settings.InitialSetupComplete,
PreferredShell: settings.PreferredShell, PreferredShell: settings.PreferredShell,
DefaultEditorID: settings.DefaultEditorID, DefaultEditorID: settings.DefaultEditorID,
EnablePreviewOnClick: settings.EnablePreviewOnClick, EnablePreviewOnClick: settings.EnablePreviewOnClick,
ChatSubmitKey: settings.ChatSubmitKey, ChatSubmitKey: settings.ChatSubmitKey,
ReviewAutoMarkOnScroll: settings.ReviewAutoMarkOnScroll, ReviewAutoMarkOnScroll: settings.ReviewAutoMarkOnScroll,
LspAutoStartLanguages: settings.LspAutoStartLanguages, ShowReleaseNotification: settings.ShowReleaseNotification,
LspAutoInstallLanguages: settings.LspAutoInstallLanguages, ReleaseNotesLastSeenVersion: settings.ReleaseNotesLastSeenVersion,
LspServerConfigs: settings.LspServerConfigs, LspAutoStartLanguages: settings.LspAutoStartLanguages,
SavedLayouts: settings.SavedLayouts, LspAutoInstallLanguages: settings.LspAutoInstallLanguages,
UpdatedAt: settings.UpdatedAt.Format(time.RFC3339), LspServerConfigs: settings.LspServerConfigs,
SavedLayouts: settings.SavedLayouts,
UpdatedAt: settings.UpdatedAt.Format(time.RFC3339),
} }
} }
+19 -17
View File
@@ -13,23 +13,25 @@ type User struct {
} }
type UserSettings struct { type UserSettings struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
WorkspaceID string `json:"workspace_id"` WorkspaceID string `json:"workspace_id"`
KanbanViewMode string `json:"kanban_view_mode"` KanbanViewMode string `json:"kanban_view_mode"`
WorkflowFilterID string `json:"workflow_filter_id"` WorkflowFilterID string `json:"workflow_filter_id"`
RepositoryIDs []string `json:"repository_ids"` RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"` InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"` PreferredShell string `json:"preferred_shell"`
DefaultEditorID string `json:"default_editor_id"` DefaultEditorID string `json:"default_editor_id"`
EnablePreviewOnClick bool `json:"enable_preview_on_click"` EnablePreviewOnClick bool `json:"enable_preview_on_click"`
ChatSubmitKey string `json:"chat_submit_key"` // "enter" | "cmd_enter" ChatSubmitKey string `json:"chat_submit_key"` // "enter" | "cmd_enter"
ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"`
LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` ShowReleaseNotification bool `json:"show_release_notification"`
LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"`
LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` LspAutoStartLanguages []string `json:"lsp_auto_start_languages"`
SavedLayouts []SavedLayout `json:"saved_layouts"` LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"`
CreatedAt time.Time `json:"created_at"` LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"`
UpdatedAt time.Time `json:"updated_at"` 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. // SavedLayout represents a user-saved dockview layout configuration.
+40 -30
View File
@@ -26,20 +26,22 @@ type Service struct {
} }
type UpdateUserSettingsRequest struct { type UpdateUserSettingsRequest struct {
WorkspaceID *string WorkspaceID *string
KanbanViewMode *string KanbanViewMode *string
WorkflowFilterID *string WorkflowFilterID *string
RepositoryIDs *[]string RepositoryIDs *[]string
InitialSetupComplete *bool InitialSetupComplete *bool
PreferredShell *string PreferredShell *string
DefaultEditorID *string DefaultEditorID *string
EnablePreviewOnClick *bool EnablePreviewOnClick *bool
ChatSubmitKey *string ChatSubmitKey *string
ReviewAutoMarkOnScroll *bool ReviewAutoMarkOnScroll *bool
LspAutoStartLanguages *[]string ShowReleaseNotification *bool
LspAutoInstallLanguages *[]string ReleaseNotesLastSeenVersion *string
LspServerConfigs *map[string]map[string]interface{} LspAutoStartLanguages *[]string
SavedLayouts *[]models.SavedLayout LspAutoInstallLanguages *[]string
LspServerConfigs *map[string]map[string]interface{}
SavedLayouts *[]models.SavedLayout
} }
func NewService(repo store.Repository, eventBus bus.EventBus, log *logger.Logger) *Service { 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 { if req.ReviewAutoMarkOnScroll != nil {
settings.ReviewAutoMarkOnScroll = *req.ReviewAutoMarkOnScroll settings.ReviewAutoMarkOnScroll = *req.ReviewAutoMarkOnScroll
} }
if req.ShowReleaseNotification != nil {
settings.ShowReleaseNotification = *req.ShowReleaseNotification
}
if req.ReleaseNotesLastSeenVersion != nil {
settings.ReleaseNotesLastSeenVersion = *req.ReleaseNotesLastSeenVersion
}
return nil return nil
} }
@@ -191,22 +199,24 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models
return return
} }
data := map[string]interface{}{ data := map[string]interface{}{
"user_id": settings.UserID, "user_id": settings.UserID,
"workspace_id": settings.WorkspaceID, "workspace_id": settings.WorkspaceID,
"kanban_view_mode": settings.KanbanViewMode, "kanban_view_mode": settings.KanbanViewMode,
"workflow_filter_id": settings.WorkflowFilterID, "workflow_filter_id": settings.WorkflowFilterID,
"repository_ids": settings.RepositoryIDs, "repository_ids": settings.RepositoryIDs,
"initial_setup_complete": settings.InitialSetupComplete, "initial_setup_complete": settings.InitialSetupComplete,
"preferred_shell": settings.PreferredShell, "preferred_shell": settings.PreferredShell,
"default_editor_id": settings.DefaultEditorID, "default_editor_id": settings.DefaultEditorID,
"enable_preview_on_click": settings.EnablePreviewOnClick, "enable_preview_on_click": settings.EnablePreviewOnClick,
"chat_submit_key": settings.ChatSubmitKey, "chat_submit_key": settings.ChatSubmitKey,
"review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll,
"lsp_auto_start_languages": settings.LspAutoStartLanguages, "show_release_notification": settings.ShowReleaseNotification,
"lsp_auto_install_languages": settings.LspAutoInstallLanguages, "release_notes_last_seen_version": settings.ReleaseNotesLastSeenVersion,
"lsp_server_configs": settings.LspServerConfigs, "lsp_auto_start_languages": settings.LspAutoStartLanguages,
"saved_layouts": settings.SavedLayouts, "lsp_auto_install_languages": settings.LspAutoInstallLanguages,
"updated_at": settings.UpdatedAt.Format(time.RFC3339), "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 { 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)) s.logger.Error("failed to publish user settings event", zap.Error(err))
@@ -25,6 +25,69 @@ func makeLayouts(n int) []models.SavedLayout {
return layouts 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) { func TestApplySavedLayouts(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+38 -28
View File
@@ -130,20 +130,22 @@ func (r *sqliteRepository) UpsertUserSettings(ctx context.Context, settings *mod
savedLayouts = []models.SavedLayout{} savedLayouts = []models.SavedLayout{}
} }
settingsPayload, err := json.Marshal(map[string]interface{}{ settingsPayload, err := json.Marshal(map[string]interface{}{
"workspace_id": settings.WorkspaceID, "workspace_id": settings.WorkspaceID,
"kanban_view_mode": settings.KanbanViewMode, "kanban_view_mode": settings.KanbanViewMode,
"workflow_filter_id": settings.WorkflowFilterID, "workflow_filter_id": settings.WorkflowFilterID,
"repository_ids": settings.RepositoryIDs, "repository_ids": settings.RepositoryIDs,
"initial_setup_complete": settings.InitialSetupComplete, "initial_setup_complete": settings.InitialSetupComplete,
"preferred_shell": settings.PreferredShell, "preferred_shell": settings.PreferredShell,
"default_editor_id": settings.DefaultEditorID, "default_editor_id": settings.DefaultEditorID,
"enable_preview_on_click": settings.EnablePreviewOnClick, "enable_preview_on_click": settings.EnablePreviewOnClick,
"chat_submit_key": settings.ChatSubmitKey, "chat_submit_key": settings.ChatSubmitKey,
"review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll,
"lsp_auto_start_languages": lspAutoStart, "show_release_notification": settings.ShowReleaseNotification,
"lsp_auto_install_languages": lspAutoInstall, "release_notes_last_seen_version": settings.ReleaseNotesLastSeenVersion,
"lsp_server_configs": lspServerConfigs, "lsp_auto_start_languages": lspAutoStart,
"saved_layouts": savedLayouts, "lsp_auto_install_languages": lspAutoInstall,
"lsp_server_configs": lspServerConfigs,
"saved_layouts": savedLayouts,
}) })
if err != nil { if err != nil {
return err return err
@@ -186,20 +188,22 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
return settings, nil return settings, nil
} }
var payload struct { var payload struct {
WorkspaceID string `json:"workspace_id"` WorkspaceID string `json:"workspace_id"`
KanbanViewMode string `json:"kanban_view_mode"` KanbanViewMode string `json:"kanban_view_mode"`
WorkflowFilterID string `json:"workflow_filter_id"` WorkflowFilterID string `json:"workflow_filter_id"`
RepositoryIDs []string `json:"repository_ids"` RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"` InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"` PreferredShell string `json:"preferred_shell"`
DefaultEditorID string `json:"default_editor_id"` DefaultEditorID string `json:"default_editor_id"`
EnablePreviewOnClick bool `json:"enable_preview_on_click"` EnablePreviewOnClick bool `json:"enable_preview_on_click"`
ChatSubmitKey string `json:"chat_submit_key"` ChatSubmitKey string `json:"chat_submit_key"`
ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll"` ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll"`
LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` ShowReleaseNotification *bool `json:"show_release_notification"`
LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"`
LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` LspAutoStartLanguages []string `json:"lsp_auto_start_languages"`
SavedLayouts []models.SavedLayout `json:"saved_layouts"` 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 { if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil {
return nil, err return nil, err
@@ -223,6 +227,12 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
} else { } else {
settings.ReviewAutoMarkOnScroll = true settings.ReviewAutoMarkOnScroll = true
} }
if payload.ShowReleaseNotification != nil {
settings.ShowReleaseNotification = *payload.ShowReleaseNotification
} else {
settings.ShowReleaseNotification = true
}
settings.ReleaseNotesLastSeenVersion = payload.ReleaseNotesLastSeenVersion
settings.LspAutoStartLanguages = payload.LspAutoStartLanguages settings.LspAutoStartLanguages = payload.LspAutoStartLanguages
if settings.LspAutoStartLanguages == nil { if settings.LspAutoStartLanguages == nil {
settings.LspAutoStartLanguages = []string{} settings.LspAutoStartLanguages = []string{}
+21
View File
@@ -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"]],
},
};
+2
View File
@@ -13,6 +13,8 @@
"clean": "pnpm -r run clean && rm -rf node_modules" "clean": "pnpm -r run clean && rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@types/node": "^20", "@types/node": "^20",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "^5" "typescript": "^5"
+354 -15
View File
@@ -12,6 +12,12 @@ importers:
.: .:
devDependencies: 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': '@types/node':
specifier: ^20 specifier: ^20
version: 20.19.28 version: 20.19.28
@@ -388,7 +394,7 @@ importers:
version: 0.55.1 version: 0.55.1
next: next:
specifier: 16.1.1 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: next-themes:
specifier: ^0.4.6 specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) 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) version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
styled-jsx: styled-jsx:
specifier: 5.1.6 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: tailwind-merge:
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.4.0 version: 3.4.0
@@ -738,6 +744,75 @@ packages:
'@codemirror/view@6.39.10': '@codemirror/view@6.39.10':
resolution: {integrity: sha512-QfT/PXhiiP76PxMnX0RQVPDQrqfRt9wr9QhInNHnEUu4PWoNS8QwwcIDEneXFChJv22y+Yu/Cz5lFMTPz+h16w==} 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': '@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
@@ -3335,6 +3410,9 @@ packages:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
array-ify@1.0.0:
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
array-includes@3.1.9: array-includes@3.1.9:
resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3559,6 +3637,9 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
compare-func@2.0.0:
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -3573,6 +3654,19 @@ packages:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'} 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: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -3598,6 +3692,14 @@ packages:
cose-base@2.2.0: cose-base@2.2.0:
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} 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: cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -3786,6 +3888,10 @@ packages:
damerau-levenshtein@1.0.8: damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} 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: data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@@ -3926,6 +4032,10 @@ packages:
dompurify@3.3.1: dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
dot-prop@5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
dotenv@17.2.3: dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -4391,6 +4501,11 @@ packages:
get-tsconfig@4.13.0: get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 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: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -4399,6 +4514,10 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'}
globals@14.0.0: globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4531,6 +4650,9 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
imurmurhash@0.1.4: imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
@@ -4538,6 +4660,10 @@ packages:
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 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: inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
@@ -4669,6 +4795,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
is-obj@3.0.0: is-obj@3.0.0:
resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -4962,9 +5092,27 @@ packages:
lodash-es@4.17.23: lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} 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: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 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: lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -5073,6 +5221,14 @@ packages:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'} 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: merge-descriptors@2.0.0:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -5787,6 +5943,10 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve-pkg-maps@1.0.0: resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -5962,6 +6122,10 @@ packages:
space-separated-tokens@2.0.2: space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stable-hash@0.0.5: stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
@@ -6983,6 +7147,115 @@ snapshots:
style-mod: 4.1.3 style-mod: 4.1.3
w3c-keyname: 2.2.8 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': {} '@date-fns/tz@1.4.1': {}
'@dnd-kit/accessibility@3.1.1(react@19.2.3)': '@dnd-kit/accessibility@3.1.1(react@19.2.3)':
@@ -9413,6 +9686,8 @@ snapshots:
call-bound: 1.0.4 call-bound: 1.0.4
is-array-buffer: 3.0.5 is-array-buffer: 3.0.5
array-ify@1.0.0: {}
array-includes@3.1.9: array-includes@3.1.9:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@@ -9674,6 +9949,11 @@ snapshots:
commander@8.3.0: {} 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: {} concat-map@0.0.1: {}
confbox@0.1.8: {} confbox@0.1.8: {}
@@ -9682,6 +9962,18 @@ snapshots:
content-type@1.0.5: {} 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: {} convert-source-map@2.0.0: {}
cookie-signature@1.2.2: {} cookie-signature@1.2.2: {}
@@ -9703,6 +9995,13 @@ snapshots:
dependencies: dependencies:
layout-base: 2.0.1 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): cosmiconfig@9.0.0(typescript@5.9.3):
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
@@ -9914,6 +10213,8 @@ snapshots:
damerau-levenshtein@1.0.8: {} damerau-levenshtein@1.0.8: {}
dargs@8.1.0: {}
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1: {}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
@@ -10035,6 +10336,10 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/trusted-types': 2.0.7 '@types/trusted-types': 2.0.7
dot-prop@5.3.0:
dependencies:
is-obj: 2.0.0
dotenv@17.2.3: {} dotenv@17.2.3: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
@@ -10256,8 +10561,8 @@ snapshots:
'@next/eslint-plugin-next': 16.1.1 '@next/eslint-plugin-next': 16.1.1
eslint: 9.39.2(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9 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))
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))
eslint-plugin-jsx-a11y: 6.10.2(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: 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)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -10279,7 +10584,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@@ -10290,22 +10595,22 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: 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: transitivePeerDependencies:
- supports-color - 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: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@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-import-resolver-node: 0.3.9 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: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -10316,7 +10621,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9 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 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -10737,6 +11042,12 @@ snapshots:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 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: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -10745,6 +11056,10 @@ snapshots:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
global-directory@4.0.1:
dependencies:
ini: 4.1.1
globals@14.0.0: {} globals@14.0.0: {}
globals@16.4.0: {} globals@16.4.0: {}
@@ -10889,10 +11204,14 @@ snapshots:
parent-module: 1.0.1 parent-module: 1.0.1
resolve-from: 4.0.0 resolve-from: 4.0.0
import-meta-resolve@4.2.0: {}
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
inherits@2.0.4: {} inherits@2.0.4: {}
ini@4.1.1: {}
inline-style-parser@0.2.7: {} inline-style-parser@0.2.7: {}
input-otp@1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): 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-number@7.0.0: {}
is-obj@2.0.0: {}
is-obj@3.0.0: {} is-obj@3.0.0: {}
is-plain-obj@4.1.0: {} is-plain-obj@4.1.0: {}
@@ -11248,8 +11569,20 @@ snapshots:
lodash-es@4.17.23: {} lodash-es@4.17.23: {}
lodash.camelcase@4.3.0: {}
lodash.kebabcase@4.1.1: {}
lodash.merge@4.6.2: {} 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: {} lodash@4.17.21: {}
log-symbols@6.0.0: log-symbols@6.0.0:
@@ -11464,6 +11797,10 @@ snapshots:
media-typer@1.1.0: {} media-typer@1.1.0: {}
meow@12.1.1: {}
meow@13.2.0: {}
merge-descriptors@2.0.0: {} merge-descriptors@2.0.0: {}
merge-stream@2.0.0: {} merge-stream@2.0.0: {}
@@ -11779,7 +12116,7 @@ snapshots:
react: 19.2.3 react: 19.2.3
react-dom: 19.2.3(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: dependencies:
'@next/env': 16.1.1 '@next/env': 16.1.1
'@swc/helpers': 0.5.15 '@swc/helpers': 0.5.15
@@ -11788,7 +12125,7 @@ snapshots:
postcss: 8.4.31 postcss: 8.4.31
react: 19.2.3 react: 19.2.3
react-dom: 19.2.3(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: optionalDependencies:
'@next/swc-darwin-arm64': 16.1.1 '@next/swc-darwin-arm64': 16.1.1
'@next/swc-darwin-x64': 16.1.1 '@next/swc-darwin-x64': 16.1.1
@@ -12466,6 +12803,8 @@ snapshots:
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}
resolve@1.22.11: resolve@1.22.11:
@@ -12773,6 +13112,8 @@ snapshots:
space-separated-tokens@2.0.2: {} space-separated-tokens@2.0.2: {}
split2@4.2.0: {}
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
stackback@0.0.2: {} stackback@0.0.2: {}
@@ -12897,12 +13238,10 @@ snapshots:
dependencies: dependencies:
inline-style-parser: 0.2.7 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: dependencies:
client-only: 0.0.1 client-only: 0.0.1
react: 19.2.3 react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.6
stylis@4.3.6: {} stylis@4.3.6: {}
+3 -45
View File
@@ -9,8 +9,9 @@ import {
listTaskSessionMessages, listTaskSessionMessages,
} from "@/lib/api"; } from "@/lib/api";
import { snapshotToState } from "@/lib/ssr/mapper"; import { snapshotToState } from "@/lib/ssr/mapper";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
import type { AppState } from "@/lib/state/store"; 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. // Server Component: runs on the server for SSR and data hydration.
type PageProps = { 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( function buildUserSettingsState(
resp: UserSettingsResponse | null, resp: UserSettingsResponse | null,
workspaceId: string | null, workspaceId: string | null,
): AppState["userSettings"] { ): AppState["userSettings"] {
const s = resp?.settings; return { ...mapUserSettingsResponse(resp), workspaceId };
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,
};
} }
function resolveActiveId<T extends { id: string }>( function resolveActiveId<T extends { id: string }>(
+2 -48
View File
@@ -23,6 +23,7 @@ import type {
} from "@/lib/types/http"; } from "@/lib/types/http";
import type { Terminal } from "@/hooks/domains/session/use-terminals"; import type { Terminal } from "@/hooks/domains/session/use-terminals";
import { snapshotToState, taskToState } from "@/lib/ssr/mapper"; import { snapshotToState, taskToState } from "@/lib/ssr/mapper";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
import { TaskPageContent } from "@/components/task/task-page-content"; import { TaskPageContent } from "@/components/task/task-page-content";
function buildWorktreeState(allSessions: TaskSession[]) { function buildWorktreeState(allSessions: TaskSession[]) {
@@ -50,53 +51,6 @@ function buildWorktreeState(allSessions: TaskSession[]) {
}; };
} }
type UserSettingsSettings = NonNullable<UserSettingsResponse["settings"]>;
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 = { type BuildSessionPageStateParams = {
task: Task; task: Task;
sessionId: string; sessionId: string;
@@ -206,7 +160,7 @@ function buildSessionPageState(p: BuildSessionPageStateParams) {
agentsLoaded: true, agentsLoaded: true,
executorsLoaded: false, executorsLoaded: false,
}, },
userSettings: buildUserSettingsFromResponse(userSettingsResponse), userSettings: mapUserSettingsResponse(userSettingsResponse),
}; };
} }
+21
View File
@@ -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 (
<StateProvider initialState={initialState}>
<ChangelogSettings />
</StateProvider>
);
}
+3 -13
View File
@@ -1,6 +1,7 @@
import { EditorsSettings } from "@/components/settings/editors-settings"; import { EditorsSettings } from "@/components/settings/editors-settings";
import { StateProvider } from "@/components/state-provider"; import { StateProvider } from "@/components/state-provider";
import { fetchUserSettings, listEditors } from "@/lib/api"; import { fetchUserSettings, listEditors } from "@/lib/api";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
export default async function GeneralEditorsPage() { export default async function GeneralEditorsPage() {
let initialState = {}; let initialState = {};
@@ -9,25 +10,14 @@ export default async function GeneralEditorsPage() {
listEditors({ cache: "no-store" }), listEditors({ cache: "no-store" }),
fetchUserSettings({ cache: "no-store" }), fetchUserSettings({ cache: "no-store" }),
]); ]);
const settings = settingsResponse.settings; const mapped = mapUserSettingsResponse(settingsResponse);
initialState = { initialState = {
editors: { editors: {
items: editorsResponse.editors ?? [], items: editorsResponse.editors ?? [],
loaded: true, loaded: true,
loading: false, loading: false,
}, },
userSettings: settings userSettings: mapped.loaded ? mapped : undefined,
? {
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,
}; };
} catch { } catch {
initialState = {}; initialState = {};
+3 -17
View File
@@ -1,28 +1,14 @@
import { GeneralSettings } from "@/components/settings/general-settings"; import { GeneralSettings } from "@/components/settings/general-settings";
import { StateProvider } from "@/components/state-provider"; import { StateProvider } from "@/components/state-provider";
import { fetchUserSettings } from "@/lib/api"; import { fetchUserSettings } from "@/lib/api";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
export default async function GeneralSettingsPage() { export default async function GeneralSettingsPage() {
let initialState = {}; let initialState = {};
try { try {
const response = await fetchUserSettings({ cache: "no-store" }); const response = await fetchUserSettings({ cache: "no-store" });
const settings = response.settings; const mapped = mapUserSettingsResponse(response);
initialState = { initialState = { userSettings: mapped.loaded ? mapped : undefined };
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,
};
} catch { } catch {
initialState = {}; initialState = {};
} }
@@ -2,7 +2,9 @@
import Link from "next/link"; import Link from "next/link";
import { Button } from "@kandev/ui/button"; import { Button } from "@kandev/ui/button";
import { TooltipProvider } from "@kandev/ui/tooltip";
import { IconMenu2 } from "@tabler/icons-react"; import { IconMenu2 } from "@tabler/icons-react";
import { ReleaseNotesButton } from "../release-notes/release-notes-button";
import { MobileMenuSheet } from "./mobile-menu-sheet"; import { MobileMenuSheet } from "./mobile-menu-sheet";
import { useAppStore } from "@/components/state-provider"; import { useAppStore } from "@/components/state-provider";
@@ -12,6 +14,8 @@ type KanbanHeaderMobileProps = {
searchQuery?: string; searchQuery?: string;
onSearchChange?: (query: string) => void; onSearchChange?: (query: string) => void;
isSearchLoading?: boolean; isSearchLoading?: boolean;
showReleaseNotesButton: boolean;
onOpenReleaseNotes: () => void;
}; };
export function KanbanHeaderMobile({ export function KanbanHeaderMobile({
@@ -20,6 +24,8 @@ export function KanbanHeaderMobile({
searchQuery = "", searchQuery = "",
onSearchChange, onSearchChange,
isSearchLoading = false, isSearchLoading = false,
showReleaseNotesButton,
onOpenReleaseNotes,
}: KanbanHeaderMobileProps) { }: KanbanHeaderMobileProps) {
const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen); const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen);
const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen);
@@ -30,15 +36,22 @@ export function KanbanHeaderMobile({
<Link href="/" className="text-xl font-bold hover:opacity-80"> <Link href="/" className="text-xl font-bold hover:opacity-80">
KanDev KanDev
</Link> </Link>
<Button <div className="flex items-center gap-2">
variant="outline" {showReleaseNotesButton && (
size="icon" <TooltipProvider>
onClick={() => setMenuOpen(true)} <ReleaseNotesButton hasUnseen onClick={onOpenReleaseNotes} />
className="cursor-pointer h-10 w-10" </TooltipProvider>
> )}
<IconMenu2 className="h-5 w-5" /> <Button
<span className="sr-only">Open menu</span> variant="outline"
</Button> size="icon"
onClick={() => setMenuOpen(true)}
className="cursor-pointer h-10 w-10"
>
<IconMenu2 className="h-5 w-5" />
<span className="sr-only">Open menu</span>
</Button>
</div>
</header> </header>
<MobileMenuSheet <MobileMenuSheet
open={isMenuOpen} open={isMenuOpen}
+48 -13
View File
@@ -16,6 +16,8 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { KanbanDisplayDropdown } from "../kanban-display-dropdown"; import { KanbanDisplayDropdown } from "../kanban-display-dropdown";
import { RefreshReviewsButton } from "../github/refresh-reviews-button"; import { RefreshReviewsButton } from "../github/refresh-reviews-button";
import { ReleaseNotesButton } from "../release-notes/release-notes-button";
import { ReleaseNotesDialog } from "../release-notes/release-notes-dialog";
import { TaskSearchInput } from "./task-search-input"; import { TaskSearchInput } from "./task-search-input";
import { KanbanHeaderMobile } from "./kanban-header-mobile"; import { KanbanHeaderMobile } from "./kanban-header-mobile";
import { MobileMenuSheet } from "./mobile-menu-sheet"; import { MobileMenuSheet } from "./mobile-menu-sheet";
@@ -23,6 +25,7 @@ import { linkToTasks } from "@/lib/links";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { useAppStore } from "@/components/state-provider"; import { useAppStore } from "@/components/state-provider";
import { useKanbanDisplaySettings } from "@/hooks/use-kanban-display-settings"; import { useKanbanDisplaySettings } from "@/hooks/use-kanban-display-settings";
import { useReleaseNotes } from "@/hooks/use-release-notes";
type KanbanHeaderProps = { type KanbanHeaderProps = {
onCreateTask: () => void; onCreateTask: () => void;
@@ -98,6 +101,8 @@ function TabletHeader({
toggleValue, toggleValue,
handleViewChange, handleViewChange,
setMenuOpen, setMenuOpen,
showReleaseNotesButton,
onOpenReleaseNotes,
}: { }: {
onCreateTask: () => void; onCreateTask: () => void;
searchQuery: string; searchQuery: string;
@@ -106,6 +111,8 @@ function TabletHeader({
toggleValue: string; toggleValue: string;
handleViewChange: (value: string) => void; handleViewChange: (value: string) => void;
setMenuOpen: (open: boolean) => void; setMenuOpen: (open: boolean) => void;
showReleaseNotesButton: boolean;
onOpenReleaseNotes: () => void;
}) { }) {
return ( return (
<header className="flex items-center justify-between p-4 pb-3 gap-3"> <header className="flex items-center justify-between p-4 pb-3 gap-3">
@@ -133,6 +140,7 @@ function TabletHeader({
className="h-8" className="h-8"
itemClassName="h-8 w-8" itemClassName="h-8 w-8"
/> />
{showReleaseNotesButton && <ReleaseNotesButton hasUnseen onClick={onOpenReleaseNotes} />}
</TooltipProvider> </TooltipProvider>
<Button <Button
variant="outline" variant="outline"
@@ -155,6 +163,8 @@ function DesktopHeader({
isSearchLoading, isSearchLoading,
toggleValue, toggleValue,
handleViewChange, handleViewChange,
showReleaseNotesButton,
onOpenReleaseNotes,
}: { }: {
onCreateTask: () => void; onCreateTask: () => void;
searchQuery: string; searchQuery: string;
@@ -162,6 +172,8 @@ function DesktopHeader({
isSearchLoading: boolean; isSearchLoading: boolean;
toggleValue: string; toggleValue: string;
handleViewChange: (value: string) => void; handleViewChange: (value: string) => void;
showReleaseNotesButton: boolean;
onOpenReleaseNotes: () => void;
}) { }) {
return ( return (
<header className="relative flex items-center justify-between p-4 pb-3"> <header className="relative flex items-center justify-between p-4 pb-3">
@@ -199,6 +211,7 @@ function DesktopHeader({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<RefreshReviewsButton /> <RefreshReviewsButton />
{showReleaseNotesButton && <ReleaseNotesButton hasUnseen onClick={onOpenReleaseNotes} />}
<KanbanDisplayDropdown /> <KanbanDisplayDropdown />
<Link href="/settings" className="cursor-pointer"> <Link href="/settings" className="cursor-pointer">
<Button variant="outline" className="cursor-pointer"> <Button variant="outline" className="cursor-pointer">
@@ -225,6 +238,7 @@ export function KanbanHeader({
const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen);
const { kanbanViewMode, onViewModeChange } = useKanbanDisplaySettings(); const { kanbanViewMode, onViewModeChange } = useKanbanDisplaySettings();
const releaseNotes = useReleaseNotes();
const toggleValue = getToggleValue(currentPage, kanbanViewMode); const toggleValue = getToggleValue(currentPage, kanbanViewMode);
@@ -240,20 +254,25 @@ export function KanbanHeader({
} }
}; };
const releaseNotesProps = {
showReleaseNotesButton: releaseNotes.showTopbarButton,
onOpenReleaseNotes: releaseNotes.openDialog,
};
let header: React.ReactNode;
if (isMobile) { if (isMobile) {
return ( header = (
<KanbanHeaderMobile <KanbanHeaderMobile
workspaceId={workspaceId} workspaceId={workspaceId}
currentPage={currentPage} currentPage={currentPage}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
isSearchLoading={isSearchLoading} isSearchLoading={isSearchLoading}
{...releaseNotesProps}
/> />
); );
} } else if (isTablet) {
header = (
if (isTablet) {
return (
<> <>
<TabletHeader <TabletHeader
onCreateTask={onCreateTask} onCreateTask={onCreateTask}
@@ -263,6 +282,7 @@ export function KanbanHeader({
toggleValue={toggleValue} toggleValue={toggleValue}
handleViewChange={handleViewChange} handleViewChange={handleViewChange}
setMenuOpen={setMenuOpen} setMenuOpen={setMenuOpen}
{...releaseNotesProps}
/> />
<MobileMenuSheet <MobileMenuSheet
open={isMenuOpen} open={isMenuOpen}
@@ -275,16 +295,31 @@ export function KanbanHeader({
/> />
</> </>
); );
} else {
header = (
<DesktopHeader
onCreateTask={onCreateTask}
searchQuery={searchQuery}
onSearchChange={onSearchChange}
isSearchLoading={isSearchLoading}
toggleValue={toggleValue}
handleViewChange={handleViewChange}
{...releaseNotesProps}
/>
);
} }
return ( return (
<DesktopHeader <>
onCreateTask={onCreateTask} {header}
searchQuery={searchQuery} {releaseNotes.hasNotes && (
onSearchChange={onSearchChange} <ReleaseNotesDialog
isSearchLoading={isSearchLoading} open={releaseNotes.dialogOpen}
toggleValue={toggleValue} onOpenChange={releaseNotes.closeDialog}
handleViewChange={handleViewChange} entries={releaseNotes.unseenEntries}
/> latestVersion={releaseNotes.latestVersion}
/>
)}
</>
); );
} }
@@ -0,0 +1,27 @@
"use client";
import { Button } from "@kandev/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { IconSparkles } from "@tabler/icons-react";
type ReleaseNotesButtonProps = {
hasUnseen: boolean;
onClick: () => void;
};
export function ReleaseNotesButton({ hasUnseen, onClick }: ReleaseNotesButtonProps) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={onClick} className="cursor-pointer relative">
<IconSparkles className="h-4 w-4" />
{hasUnseen && (
<span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-primary border-2 border-background" />
)}
<span className="sr-only">What&apos;s New</span>
</Button>
</TooltipTrigger>
<TooltipContent>What&apos;s New</TooltipContent>
</Tooltip>
);
}
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
What&apos;s New
<Badge variant="secondary">v{latestVersion}</Badge>
</DialogTitle>
<DialogDescription>{buildDescription(entries)}</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh] pr-4">
<div className="space-y-4">
{entries.map((entry, index) => (
<div key={entry.version}>
{entries.length > 1 && (
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline">v{entry.version}</Badge>
{entry.date && (
<span className="text-xs text-muted-foreground">{entry.date}</span>
)}
</div>
)}
<div className="text-sm">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{entry.notes}
</ReactMarkdown>
</div>
{index < entries.length - 1 && <Separator className="mt-4" />}
</div>
))}
</div>
</ScrollArea>
<div className="pt-2 border-t border-border">
<a
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
View full release on GitHub
<IconExternalLink className="h-3 w-3" />
</a>
</div>
</DialogContent>
</Dialog>
);
}
@@ -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 (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Badge variant="secondary">v{entry.version}</Badge>
{entry.date && (
<span className="text-sm font-normal text-muted-foreground">{entry.date}</span>
)}
</CardTitle>
<a
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
View on GitHub
<IconExternalLink className="h-3 w-3" />
</a>
</div>
</CardHeader>
<CardContent>
<div className="text-sm">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{entry.notes}
</ReactMarkdown>
</div>
</CardContent>
</Card>
);
}
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 <p className="text-sm text-muted-foreground">No changelog entries available.</p>;
}
const pageNumbers = buildPageNumbers(currentPage, totalPages);
return (
<div className="space-y-4">
{pageEntries.map((entry) => (
<ChangelogEntryCard key={entry.version} entry={entry} />
))}
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{pageNumbers.map((page, i) =>
page === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<PaginationLink
isActive={currentPage === page}
onClick={() => setCurrentPage(page)}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
className={
currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}
@@ -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 (
<Card>
<CardHeader>
<CardTitle className="text-base">Topbar Release Notification</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="release-notification-toggle">Show notification for new releases</Label>
<p className="text-xs text-muted-foreground">
When enabled, a sparkle icon appears in the topbar when a new version is released
</p>
</div>
<Switch
id="release-notification-toggle"
checked={userSettings.showReleaseNotification}
onCheckedChange={handleToggle}
disabled={isSaving}
className="cursor-pointer"
/>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Reset seen releases</Label>
<p className="text-xs text-muted-foreground">
Clear your last seen version so the topbar notification appears again
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleResetLastSeen}
disabled={isSaving || !userSettings.releaseNotesLastSeenVersion}
className="cursor-pointer"
>
Reset
</Button>
</div>
</CardContent>
</Card>
);
}
@@ -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 (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold">Changelog</h2>
<p className="text-sm text-muted-foreground mt-1">
View all releases and manage notification preferences
</p>
</div>
<Separator />
<SettingsSection
icon={<IconBell className="h-5 w-5" />}
title="Notifications"
description="Control the release notes notification in the topbar"
>
<ChangelogNotificationCard />
</SettingsSection>
<Separator />
<SettingsSection title="Release History" description="All versions and their release notes">
<ChangelogList />
</SettingsSection>
</div>
);
}
@@ -212,6 +212,8 @@ function mapEditorSettingsFields(
return { return {
chatSubmitKey: s.chat_submit_key ?? "cmd_enter", chatSubmitKey: s.chat_submit_key ?? "cmd_enter",
reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, 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 ?? [], lspAutoStartLanguages: s.lsp_auto_start_languages ?? [],
lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [],
lspServerConfigs: s.lsp_server_configs ?? {}, lspServerConfigs: s.lsp_server_configs ?? {},
@@ -13,6 +13,7 @@ import {
IconKey, IconKey,
IconMessageCircle, IconMessageCircle,
IconBrandGithub, IconBrandGithub,
IconSparkles,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { import {
Sidebar, Sidebar,
@@ -299,6 +300,16 @@ export function SettingsAppSidebar() {
<ExecutorsSidebarSection pathname={pathname} executors={executors} /> <ExecutorsSidebarSection pathname={pathname} executors={executors} />
<SecretsSidebarSection pathname={pathname} /> <SecretsSidebarSection pathname={pathname} />
{/* Changelog */}
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/settings/changelog"}>
<Link href="/settings/changelog">
<IconSparkles className="h-4 w-4" />
<span>Changelog</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+1 -23
View File
@@ -4,29 +4,7 @@ import { useEffect } from "react";
import { fetchUserSettings, listEditors } from "@/lib/api"; import { fetchUserSettings, listEditors } from "@/lib/api";
import { getWebSocketClient } from "@/lib/ws/connection"; import { getWebSocketClient } from "@/lib/ws/connection";
import { useAppStore } from "@/components/state-provider"; import { useAppStore } from "@/components/state-provider";
import type { UserSettingsState } from "@/lib/state/slices/settings/types"; import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
// 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,
};
}
export function useEditors() { export function useEditors() {
const editors = useAppStore((state) => state.editors.items); const editors = useAppStore((state) => state.editors.items);
+102
View File
@@ -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<ChangelogEntry[]>([]);
// 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,
};
}
+21 -34
View File
@@ -4,6 +4,7 @@ import { fetchUserSettings, updateUserSettings } from "@/lib/api";
import { mapSelectedRepositoryIds } from "@/lib/kanban/filters"; import { mapSelectedRepositoryIds } from "@/lib/kanban/filters";
import { useAppStore } from "@/components/state-provider"; import { useAppStore } from "@/components/state-provider";
import { useRepositories } from "@/hooks/domains/workspace/use-repositories"; import { useRepositories } from "@/hooks/domains/workspace/use-repositories";
import { mapUserSettingsResponse } from "@/lib/ssr/user-settings";
import type { Repository } from "@/lib/types/http"; import type { Repository } from "@/lib/types/http";
import type { UserSettingsState } from "@/lib/state/slices/settings/types"; import type { UserSettingsState } from "@/lib/state/slices/settings/types";
@@ -25,24 +26,31 @@ type CommitPayload = {
kanbanViewMode?: string | null; 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 { function buildNormalizedSettings(next: CommitPayload, current: DisplaySettings): DisplaySettings {
const repositoryIds = Array.from(new Set(next.repositoryIds)).sort();
return { return {
workspaceId: next.workspaceId, workspaceId: next.workspaceId,
workflowId: next.workflowId, workflowId: next.workflowId,
kanbanViewMode: kanbanViewMode:
next.kanbanViewMode !== undefined ? next.kanbanViewMode : (current.kanbanViewMode ?? null), next.kanbanViewMode !== undefined ? next.kanbanViewMode : (current.kanbanViewMode ?? null),
repositoryIds, repositoryIds: Array.from(new Set(next.repositoryIds)).sort(),
preferredShell: next.preferredShell ?? current.preferredShell ?? null, preferredShell: next.preferredShell ?? current.preferredShell ?? null,
shellOptions: current.shellOptions ?? [],
defaultEditorId: current.defaultEditorId ?? null,
enablePreviewOnClick: next.enablePreviewOnClick ?? current.enablePreviewOnClick, enablePreviewOnClick: next.enablePreviewOnClick ?? current.enablePreviewOnClick,
chatSubmitKey: current.chatSubmitKey ?? "cmd_enter", ...carryForwardSettings(current),
reviewAutoMarkOnScroll: current.reviewAutoMarkOnScroll ?? true,
lspAutoStartLanguages: current.lspAutoStartLanguages ?? [],
lspAutoInstallLanguages: current.lspAutoInstallLanguages ?? [],
lspServerConfigs: current.lspServerConfigs ?? {},
savedLayouts: current.savedLayouts ?? [],
loaded: true, loaded: true,
}; };
} }
@@ -82,29 +90,6 @@ function useUserSettingsRef(userSettings: DisplaySettings) {
return userSettingsRef; 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<string>(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( function useLoadUserSettings(
loaded: boolean, loaded: boolean,
setUserSettings: (settings: DisplaySettings) => void, setUserSettings: (settings: DisplaySettings) => void,
@@ -114,7 +99,9 @@ function useLoadUserSettings(
fetchUserSettings({ cache: "no-store" }) fetchUserSettings({ cache: "no-store" })
.then((data) => { .then((data) => {
if (!data?.settings) return; if (!data?.settings) return;
setUserSettings(mapFetchedUserSettings(data)); const mapped = mapUserSettingsResponse(data);
if (!mapped.loaded) return;
setUserSettings(mapped);
}) })
.catch(() => { .catch(() => {
/* Ignore settings fetch errors for now. */ /* Ignore settings fetch errors for now. */
+2
View File
@@ -36,6 +36,8 @@ export async function updateUserSettings(
enable_preview_on_click?: boolean; enable_preview_on_click?: boolean;
chat_submit_key?: "enter" | "cmd_enter"; chat_submit_key?: "enter" | "cmd_enter";
review_auto_mark_on_scroll?: boolean; review_auto_mark_on_scroll?: boolean;
show_release_notification?: boolean;
release_notes_last_seen_version?: string;
lsp_auto_start_languages?: string[]; lsp_auto_start_languages?: string[];
lsp_auto_install_languages?: string[]; lsp_auto_install_languages?: string[];
lsp_server_configs?: Record<string, Record<string, unknown>>; lsp_server_configs?: Record<string, Record<string, unknown>>;
+11
View File
@@ -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;
}
+19
View File
@@ -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}`;
}
+62
View File
@@ -0,0 +1,62 @@
import type { SavedLayout, UserSettingsResponse } from "@/lib/types/http";
export type UserSettingsData = NonNullable<UserSettingsResponse["settings"]>;
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,
};
}
@@ -30,6 +30,8 @@ export const defaultSettingsState: SettingsSliceState = {
enablePreviewOnClick: false, enablePreviewOnClick: false,
chatSubmitKey: "cmd_enter", chatSubmitKey: "cmd_enter",
reviewAutoMarkOnScroll: true, reviewAutoMarkOnScroll: true,
showReleaseNotification: true,
releaseNotesLastSeenVersion: null,
lspAutoStartLanguages: [], lspAutoStartLanguages: [],
lspAutoInstallLanguages: [], lspAutoInstallLanguages: [],
lspServerConfigs: {}, lspServerConfigs: {},
@@ -108,6 +108,8 @@ export type UserSettingsState = {
enablePreviewOnClick: boolean; enablePreviewOnClick: boolean;
chatSubmitKey: "enter" | "cmd_enter"; chatSubmitKey: "enter" | "cmd_enter";
reviewAutoMarkOnScroll: boolean; reviewAutoMarkOnScroll: boolean;
showReleaseNotification: boolean;
releaseNotesLastSeenVersion: string | null;
lspAutoStartLanguages: string[]; lspAutoStartLanguages: string[];
lspAutoInstallLanguages: string[]; lspAutoInstallLanguages: string[];
lspServerConfigs: Record<string, Record<string, unknown>>; lspServerConfigs: Record<string, Record<string, unknown>>;
+2
View File
@@ -347,6 +347,8 @@ export type UserSettingsUpdatedPayload = {
enable_preview_on_click?: boolean; enable_preview_on_click?: boolean;
chat_submit_key?: string; chat_submit_key?: string;
review_auto_mark_on_scroll?: boolean; review_auto_mark_on_scroll?: boolean;
show_release_notification?: boolean;
release_notes_last_seen_version?: string;
lsp_auto_start_languages?: string[]; lsp_auto_start_languages?: string[];
lsp_auto_install_languages?: string[]; lsp_auto_install_languages?: string[];
saved_layouts?: SavedLayout[]; saved_layouts?: SavedLayout[];
+2
View File
@@ -358,6 +358,8 @@ export type UserSettings = {
enable_preview_on_click?: boolean; enable_preview_on_click?: boolean;
chat_submit_key?: "enter" | "cmd_enter"; chat_submit_key?: "enter" | "cmd_enter";
review_auto_mark_on_scroll?: boolean; review_auto_mark_on_scroll?: boolean;
show_release_notification?: boolean;
release_notes_last_seen_version?: string;
lsp_auto_start_languages?: string[]; lsp_auto_start_languages?: string[];
lsp_auto_install_languages?: string[]; lsp_auto_install_languages?: string[];
lsp_server_configs?: Record<string, Record<string, unknown>>; lsp_server_configs?: Record<string, Record<string, unknown>>;
+3
View File
@@ -20,6 +20,9 @@ export function registerUsersHandlers(store: StoreApi<AppState>): WsHandlers {
enablePreviewOnClick: message.payload.enable_preview_on_click ?? false, enablePreviewOnClick: message.payload.enable_preview_on_click ?? false,
chatSubmitKey: (message.payload.chat_submit_key as "enter" | "cmd_enter") ?? "cmd_enter", chatSubmitKey: (message.payload.chat_submit_key as "enter" | "cmd_enter") ?? "cmd_enter",
reviewAutoMarkOnScroll: message.payload.review_auto_mark_on_scroll ?? true, 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 ?? [], lspAutoStartLanguages: message.payload.lsp_auto_start_languages ?? [],
lspAutoInstallLanguages: message.payload.lsp_auto_install_languages ?? [], lspAutoInstallLanguages: message.payload.lsp_auto_install_languages ?? [],
savedLayouts: message.payload.saved_layouts ?? [], savedLayouts: message.payload.saved_layouts ?? [],
+2
View File
@@ -3,7 +3,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"predev": "node scripts/generate-release-notes.mjs && node scripts/generate-changelog.mjs",
"dev": "next dev --hostname 0.0.0.0", "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", "build": "cross-env NODE_ENV=production next build",
"start": "cross-env NODE_ENV=production next start", "start": "cross-env NODE_ENV=production next start",
"lint": "eslint --max-warnings 0", "lint": "eslint --max-warnings 0",
+70
View File
@@ -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();
@@ -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();
+45
View File
@@ -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"
+26 -8
View File
@@ -85,7 +85,7 @@ Versioning:
Prerequisites: Prerequisites:
- Run from a clean working tree on branch main. - Run from a clean working tree on branch main.
- Required tools: git, node, npm, pnpm. - Required tools: git, node, npm, pnpm, git-cliff.
Examples: Examples:
# Fully interactive release wizard # Fully interactive release wizard
@@ -320,7 +320,7 @@ compute_next_versions() {
ensure_prerequisites() { ensure_prerequisites() {
log_step "1/5" "Checking 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 if command_exists "$tool"; then
log_ok "$tool" log_ok "$tool"
else else
@@ -419,8 +419,10 @@ print_plan() {
fi fi
if [[ "$APP_SELECTED" -eq 1 ]]; then if [[ "$APP_SELECTED" -eq 1 ]]; then
echo " $(dim "Steps for App:")" echo " $(dim "Steps for App:")"
echo " $(dim "1.") Create annotated git tag $NEXT_APP_TAG" echo " $(dim "1.") Generate CHANGELOG.md (git-cliff)"
echo " $(dim "2.") Push tag to origin (triggers GitHub Actions)" 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 fi
echo echo
} }
@@ -432,6 +434,8 @@ commit_message() {
echo "release: app $NEXT_APP_TAG, cli $NEXT_CLI_VERSION" echo "release: app $NEXT_APP_TAG, cli $NEXT_CLI_VERSION"
elif [[ "$CLI_SELECTED" -eq 1 ]]; then elif [[ "$CLI_SELECTED" -eq 1 ]]; then
echo "release: cli $NEXT_CLI_VERSION" echo "release: cli $NEXT_CLI_VERSION"
elif [[ "$APP_SELECTED" -eq 1 ]]; then
echo "release: app $NEXT_APP_TAG"
else else
echo "" echo ""
fi fi
@@ -467,6 +471,18 @@ apply_cli_release() {
log_ok "CLI built" 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() { commit_and_push_if_needed() {
local msg local msg
msg="$(commit_message)" msg="$(commit_message)"
@@ -474,16 +490,17 @@ commit_and_push_if_needed() {
log_step "4/5" "Committing and pushing" 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 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 commit -m \"$msg\""
echo " $(yellow "[dry-run]") git push origin $CURRENT_BRANCH" echo " $(yellow "[dry-run]") git push origin $CURRENT_BRANCH"
return 0 return 0
fi fi
log "Staging apps/cli/package.json..." log "Staging release files..."
git -C "$ROOT_DIR" add apps/cli/package.json [[ "$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 if git -C "$ROOT_DIR" diff --cached --quiet; then
log_skip "No staged changes to commit" log_skip "No staged changes to commit"
return 0 return 0
@@ -612,6 +629,7 @@ main() {
fi fi
apply_cli_release apply_cli_release
generate_changelog
commit_and_push_if_needed commit_and_push_if_needed
create_and_push_app_tag create_and_push_app_tag
publish_cli publish_cli