kandown 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +468 -0
- package/bin/kandown.js +42 -0
- package/dist/index.html +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.21.1 — 2026-07-20 — "CLI Live Changelog"
|
|
4
|
+
|
|
5
|
+
- **Added**: **CLI Live Changelog Display** — the CLI automatically parses and displays formatted release changelogs from `CHANGELOG.md` directly in the terminal during auto-updates, `kandown update`, and version notice popups.
|
|
6
|
+
- **Added**: **Release Guidelines in `AGENTS.md`** — mandatory rule requiring every release to include a comprehensive, detailed English changelog in `CHANGELOG.md` and attached to the release commit body.
|
|
7
|
+
- **Changed**: Included `CHANGELOG.md` in published npm package `files` array so terminal displays have access to release notes in production installs.
|
|
8
|
+
|
|
9
|
+
## 0.21.0 — 2026-07-20 — "Fable Features & Integrated MCP"
|
|
10
|
+
|
|
11
|
+
- **Added**: **Integrated MCP Server (`kandown mcp`)** — stdio JSON-RPC 2.0 server for MCP hosts (Claude Desktop, VSCode, Glama, etc.) exposing `list_tasks`, `get_task`, `create_task`, `move_task`, `update_task`, `add_report`, `list_columns`.
|
|
12
|
+
- **Added**: **Full TUI CRUD & Workflow (`kandown board`)** — interactive creation (`n` with inline syntax), editing in `$EDITOR` (`e`), archiving (`x`), deletion with prompt (`D`), fuzzy search (`/`), filter cycling (`f`), cheatsheet modal overlay (`?`), and undo (`u`).
|
|
13
|
+
- **Added**: **Diagnostic command `kandown doctor [--fix]`** — checks CLI/HTML version alignment, daemon liveness, port status, `kandown.json` validity, frontmatter syntax, and auto-resolves duplicate task files.
|
|
14
|
+
- **Added**: **Multi-project Manager (`kandown projects`)** — scans active localhost daemons (ports 2048-2150) and lists running instances with PID, port, and project paths (`--json` supported).
|
|
15
|
+
- **Added**: **Real-Time SSE (`GET /api/events`)** — Server-Sent Events endpoint backed by `chokidar` file watcher for instant live updates in the web UI when agents or CLI edit task files.
|
|
16
|
+
- **Added**: **Quick-Add Inline Parser** — parse `#tag`, `@assignee`, `p1-p4`, `due:date`, `+t12` directly from title strings across web UI, TUI, and `kandown create`.
|
|
17
|
+
- **Added**: **Web Multi-Selection & Floating Action Bar (`BulkActionBar`)** — select multiple tasks with `Cmd`/`Ctrl`/`Shift` click or checkboxes to bulk move or delete tasks in one click.
|
|
18
|
+
- **Added**: **WIP Limits & Visual Indicators** — column limits (`board.wipLimits`) with warning badges when limits are exceeded.
|
|
19
|
+
- **Added**: **Swimlanes & Group By Selector** — filter bar dropdown to group board tasks by Priority, Assignee, or Epic.
|
|
20
|
+
- **Added**: **Épics (`epic: <id>`) & Task Templates** — frontmatter `epic` tracking with card badges, and `.kandown/templates/*.md` card template loading.
|
|
21
|
+
- **Added**: **Due Dates & Calendar View** — overdue and upcoming due-date summary banner in ListView.
|
|
22
|
+
- **Added**: **Import & Export (`kandown export` / `kandown import`)** — export board to JSON or CSV, and import from Trello JSON exports or Markdown headers.
|
|
23
|
+
- **Added**: **Git Task Timeline API (`GET /api/git/history`)** — endpoint serving task modification history (`git log --follow`).
|
|
24
|
+
- **Added**: **Outgoing Webhooks (`notifications.webhookUrl`)** — POST JSON notifications on status changes to Slack, Discord, or n8n.
|
|
25
|
+
- **Added**: **Enriched Agent Context & Config** — `readAgentDoc` automatically injects recent task git commits into agent instructions, and `agent.extraArgs` is configurable in Web & CLI Settings.
|
|
26
|
+
- **Changed**: TUI move context menu proposes the next column to the right by default for natural left-to-right Kanban flow.
|
|
27
|
+
|
|
28
|
+
## 0.20.0 — 2026-07-19 — "No More Stale Copies"
|
|
29
|
+
|
|
30
|
+
- **Changed**: **`kandown init` no longer copies `AGENT.md`/`AGENT_KANDOWN.md` into new projects** — both `kandown work` and the TUI's agent launcher (`a` key) now read the rules straight from the installed package's `templates/AGENT_KANDOWN.md` at call time, so there's no per-project snapshot left to go stale. Existing projects with an old copy are left untouched — nothing is auto-deleted.
|
|
31
|
+
- **Changed**: **The TUI's agent launcher now uses the same layered rules as `kandown work`** — base rules from the package, plus optional `~/.kandown/instructions.md` (global) and `.kandown/instructions.md` (project). Previously it read a project-local `AGENT_KANDOWN.md`/`AGENT.md` copy that could silently drift from what `kandown work` printed; the two entry points can no longer disagree.
|
|
32
|
+
- **Changed**: **`kandown init`'s injected `AGENTS.md`/`CLAUDE.md` line no longer promises a local fallback file** — dropped "if you can't run the CLI, read `.kandown/AGENT_KANDOWN.md` instead" since that file is no longer shipped and the scenario (an agent that can follow `AGENTS.md` but can't run a shell command) is effectively nonexistent.
|
|
33
|
+
|
|
34
|
+
## 0.19.1 — 2026-07-19 — "Migration Fixes"
|
|
35
|
+
|
|
36
|
+
- **Fixed**: **False "conflict" on legacy task migration** — `migrateTasksToTopLevel` reported `.kandown/tasks/` and `./tasks/` as conflicting (and silently left an old install un-migrated) whenever `.kandown/tasks/` existed but was actually empty, even though there was nothing to protect. It now checks the legacy folder's real content first and only defers to the "don't clobber" guard when there's genuinely something to migrate.
|
|
37
|
+
- **Fixed**: **Legacy folder cleanup never actually ran** — `cleanupLegacyTasksDir` called `fs.rmSync(dir, { recursive: false })` on an already-confirmed-empty directory, which throws `EISDIR` (Node's `rmSync` requires `recursive: true` to remove a directory at all, even an empty one — introduced by the earlier `rmdirSync` → `rmSync` cleanup in v0.19.0). Found by testing this release's migration fix against kandown's own dev install before shipping.
|
|
38
|
+
|
|
39
|
+
## 0.19.0 — 2026-07-19 — "Upgrade Notices"
|
|
40
|
+
|
|
41
|
+
- **Added**: **Breaking-change migration notices** — when an interactive `kandown` command detects it just crossed a release with user-facing changes (starting with v0.18.0's `shell` removal), it prints a one-time notice explaining what changed and how to adapt. Fires right after a successful auto-update, and also via a lightweight version-seen tracker (`.version-seen.json` next to the install) that catches upgrades made outside the auto-updater — manual `npm install -g kandown`, pnpm/yarn/bun, etc. TTY-only and never fires for scripted/one-shot commands.
|
|
42
|
+
- **Changed**: **New tagline** — "Too Many Ideas, Not Enough Agents." Kandown helps you queue tasks in an elegant and clever way.
|
|
43
|
+
- **Fixed**: A `const` array referencing the CLI's color palette was declared before that palette existed in module load order, which would have crashed every single `kandown` invocation the moment the migration-notice feature above was exercised. Caught by testing before release — moved the declaration after the palette.
|
|
44
|
+
|
|
45
|
+
## 0.18.0 — 2026-07-19 — "Agent Workflow"
|
|
46
|
+
|
|
47
|
+
- **Added**: **`kandown work` — the agent entrypoint** — a single command that prints the full agent rules (served fresh from the installed CLI version, never a stale per-project copy) plus a live board digest: column counts, tasks per column with blocked-by annotations, and a computed "next actionable task" (closest to done, unblocked, highest priority). One call gives an AI agent both its rules and its context.
|
|
48
|
+
- **Added**: **Layered instructions** — optional `~/.kandown/instructions.md` (applies to every kandown project on this machine) and `.kandown/instructions.md` (this project only) are appended after the base rules in `kandown work` output, letting users customize agent behavior globally or per project without touching the agent file.
|
|
49
|
+
- **Changed**: **`kandown init` no longer injects a rules block into `AGENTS.md`/`CLAUDE.md`** — it appends a single line pointing the agent at `kandown work` instead. Removes the drift problem of a rules copy going stale the moment the package updates, and cuts the injected footprint from a paragraph to one line.
|
|
50
|
+
- **Changed**: **Task commands promoted to top-level, `shell` prefix removed entirely** — `kandown list/show/create/move/assign/commit` (previously `kandown shell <cmd>`, no alias kept). These are the most basic operations of the product; nesting them under a wrapper word only added friction for the scripts and AI agents they're built for. New `kandown tasks` prints the cheatsheet for this group.
|
|
51
|
+
- **Fixed**: internal function names and comments across `bin/kandown.js` no longer reference the removed `shell` wrapper.
|
|
52
|
+
|
|
53
|
+
## 0.17.2 — 2026-07-19 — "CLI Hardening"
|
|
54
|
+
|
|
55
|
+
- **Added**: **CLI/daemon hardening pass** — daemon routes now use per-daemon `X-Kandown-Token` auth, remove wildcard CORS, cap request bodies at 10MB, and keep the daemon alive after recoverable fatal handlers.
|
|
56
|
+
- **Added**: **Safer persistence** — task, config, board, and daemon metadata writes now use atomic temp-file renames, with a daemon spawn lock to avoid duplicate startup races.
|
|
57
|
+
- **Fixed**: **CLI update policy** — update checks are skipped for daemon/shell/non-TTY flows, throttled for 24h, and handle prerelease semver comparisons safely.
|
|
58
|
+
- **Fixed**: **stdout/stderr contract** — machine-readable shell output stays clean on stdout while UI decorations and status logs go to stderr.
|
|
59
|
+
- **Fixed**: **Board/TUI resilience** — daemon startup, board scrolling, and move-target UX received stability fixes from the FABLE CLI pass.
|
|
60
|
+
- **Changed**: **Credits and package metadata** — license, package author, Settings links, and README credits now point to Vanessa Depraute, GitHub `vava-nessa`, and `vanessadepraute.dev`.
|
|
61
|
+
|
|
62
|
+
## 0.17.1 — 2026-07-08 — "Column Reorder"
|
|
63
|
+
|
|
64
|
+
- **Fixed**: **Column reorder drag feedback** — dragging a column now shows a clear vertical insertion line between columns, including the final slot after the last column, so the destination is obvious before dropping.
|
|
65
|
+
- **Changed**: **Column reorder gesture handling** — normal columns only start reordering from the gripper handle, preventing card drag and column drag from fighting for the same gesture.
|
|
66
|
+
- **Changed**: **Column order persistence** — reordered columns are saved from the visible board order, so status-derived columns cannot corrupt config indices.
|
|
67
|
+
|
|
68
|
+
## 0.17.0 — 2026-07-08 — "Column Reorder"
|
|
69
|
+
|
|
70
|
+
- **Added**: **Native HTML5 drag-and-drop column reordering** — grip columns by the 6-dot gripper icon (left of column name) and drag to a new position. Columns snap, config updates and persists automatically.
|
|
71
|
+
- **Fixed**: **Column drag-and-drop handler signature** — `handleColumnDragOver` now works correctly with HTML5 native drag events (removed unnecessary target index param).
|
|
72
|
+
|
|
73
|
+
## 0.16.0 — 2026-07-08 — "Compact Mode"
|
|
74
|
+
|
|
75
|
+
- **Added**: **Compact board density** — column density can now use a space-saving layout where empty columns collapse into a thin strip (100px wide) or stack vertically when multiple consecutive columns are empty. Normal columns keep their full width. In compact mode the empty-column cards show only an icon + name, minimizing wasted space for boards with many columns.
|
|
76
|
+
- **Added**: **`isEmptyCompact` column prop** — `Column.tsx` now renders a minimal icon+label placeholder when there are no tasks and the compact density is active.
|
|
77
|
+
- **Changed**: **Board layout refactored through `columnGroups`** — the board now groups consecutive empty columns into `compact-single` (one column strip) or `compact-stack` (vertical stack), improving density for task-heavy boards.
|
|
78
|
+
- **Removed**: **Task placeholder t119** — removed unfinished placeholder task file.
|
|
79
|
+
|
|
80
|
+
## 0.15.4 — 2026-07-08 — "Update Animation"
|
|
81
|
+
|
|
82
|
+
- **Added**: **Animated spinner + progress bar with percentage for the auto-updater** — the `npm install` phase now shows a real-time filling bar (`████░░░ 45%`) with a Braille spinner, giving visual feedback during the 10–30s update. Falls back to plain text when stdout is piped.
|
|
83
|
+
- **Fixed**: **Column CSS `group` conflict** — changed `group` to `group/column` to prevent style collisions when columns are nested inside other `group` containers.
|
|
84
|
+
|
|
85
|
+
## 0.15.3 — 2026-07-08 — "Cosmetic Change"
|
|
86
|
+
|
|
87
|
+
- **Changed**: **Category tag moved next to task ID in cards** — the `[bracket]` category tag previously displayed below the title now sits inline to the right of the task number (`#102 [optimization]`), keeping the card header compact and scannable.
|
|
88
|
+
|
|
89
|
+
## 0.15.2 — 2026-07-07 — "Safe Ports"
|
|
90
|
+
|
|
91
|
+
- **Fixed**: **2nd concurrent project looked dead in the browser** — when running `kandown` in a second project folder, the daemon correctly moved to port **2049**, but Chrome/Firefox/Safari refuse to load it (`net::ERR_UNSAFE_PORT`) because 2049 is the well-known **NFS** port. The server answered fine (curl worked) yet the browser showed an error page. Port allocation now skips every port in the browsers' restricted-ports list (`BROWSER_UNSAFE_PORTS`, sourced from Chromium's `net/base/port_util.cc`), so the 2nd project lands on a browser-loadable port (2050) instead. The default 2048 stays safe and stable for the primary project. `--port <n>` pointing at an unsafe port is now rejected with a clear message.
|
|
92
|
+
|
|
93
|
+
## 0.15.1 — 2026-07-07 — "Parallel Daemons"
|
|
94
|
+
|
|
95
|
+
- **Fixed**: **Multi-project daemon port clash** — launching `kandown` from a second project folder no longer kills the first project's web daemon and steals its port. Each project now gets its own daemon on an auto-allocated port (A=2048, B=2049, C=2050, …), so multiple kandown boards can finally run in parallel.
|
|
96
|
+
- Root cause #1: `listenOnAvailablePort()` unconditionally killed whatever kandown sat on the default port — even when it belonged to a *different* project. The preemptive stale check now only reclaims same-project zombies; other-project daemons fall through to the port-scan loop (which already skips them).
|
|
97
|
+
- Root cause #2: the parent↔child startup handshake deleted the freshly-written daemon metadata on any transient HTTP fetch failure. Node's `fetch` (undici) doesn't recover from the initial `ECONNREFUSED` window and reported healthy local daemons as down for seconds, orphaning them with a spurious "Daemon failed to start". `getDaemonStatus()` is now non-destructive on transient fetch failures (metadata is removed only on a *real* conflict), and startup detection uses a dependency-free TCP probe (`net.createConnection`) instead of HTTP.
|
|
98
|
+
- Port range widened 2048–2060 → **2048–2150** (103 slots); startup timeout 5s → 8s.
|
|
99
|
+
|
|
100
|
+
## 0.15.0 — 2026-07-07 — "Boot Splash"
|
|
101
|
+
|
|
102
|
+
- **Added**: **Boot splash** — au lancement du projet, le titre `kandown` + badge `v<version>` reste affiché pendant 5 secondes, puis fond en fade-out pour ne laisser que le logo. Le nom du projet ouvert prend ensuite sa place comme titre de page du header, avec une transition douce.
|
|
103
|
+
- **Added**: **Document title dynamique** — l'onglet du navigateur reflète désormais le projet courant (`<Projet> · Kandown`) au lieu d'un titre statique.
|
|
104
|
+
|
|
105
|
+
## 0.14.1 — 2026-07-07 — "Resilience Pass"
|
|
106
|
+
|
|
107
|
+
- **Added**: **Typed error hierarchy** (`src/lib/errors.ts`) — `KandownError` base with `BrowserNotSupported`, `PermissionDenied`, `DiskFull`, `Corrupted`, `FileRead` subclasses, plus a `Result<T>` discriminated union and `isRetryableError`. One shared vocabulary for the whole web UI.
|
|
108
|
+
- **Added**: **Retry with exponential backoff** (`src/lib/retry.ts`) — `withRetry()` wraps fallible async operations and retries only transient failures (disk full, network). Used by every store write path so a user freeing space between attempts recovers automatically.
|
|
109
|
+
- **Added**: **React error boundary** (`src/components/ErrorBoundary.tsx`). A top-level boundary catches whole-app crashes with Retry + Copy-report; a granular boundary around the board means a malformed task crashing the render no longer takes down the drawer (unsaved edits stay safe).
|
|
110
|
+
- **Added**: **Global error handlers** (`src/lib/globalErrors.ts`) — `window.onerror` + `unhandledrejection` listeners installed before React mount, with throttled toasts (max 3 per 5s, dedup) so a tight rejection loop can't flood the UI.
|
|
111
|
+
- **Added**: **Browser support gating** (t100) — `openFolder` checks `supportsFileSystemAccess()` first and shows an actionable toast on Firefox/Safari instead of crashing with `TypeError: showDirectoryPicker is not a function`.
|
|
112
|
+
- **Fixed**: **Ghost-task silent corruption** (t102) — `readTaskFileStrict()` returns a typed `TaskReadResult` distinguishing not-found (benign) from permission/corrupted (actionable). `readAllTasks` migrated so unreadable files are dropped + reported via `failedTaskIds` and an "N tasks could not be loaded" warning instead of returning empty ghost tasks.
|
|
113
|
+
- **Fixed**: **Store rollback completeness** (t104) — every mutating action now captures `columns` + `taskContents` + `searchMatches` and restores all three on failure. `persistColumnOrder` returns `{ failedIds }` via `Promise.allSettled` and partial-failure callers warn + reload from disk.
|
|
114
|
+
- **Fixed**: **Disk full / quota handling** (t105) — filesystem writes map `QuotaExceededError` to a typed `DiskFullError` and close streams in a `finally`. Toast reads "Disk is full — <action> was not saved. Free up space and try again." (8s).
|
|
115
|
+
- **Fixed**: **reloadBoard preserves previous state** (t106) — adds `isReloading`, `lastReloadError`, `failedTaskIds` state. Hard failure keeps the previous board visible + warning; partial failure keeps readable tasks and warns.
|
|
116
|
+
- **Fixed**: **File watcher silent failures** (t107) — watcher callbacks wrapped in try/catch; per-task reads guarded so one bad file doesn't abort the tick. Auto-disables after 5 consecutive tick failures and emits a `watcherError` event; Header shows an amber banner with "Restart watcher" + "Reload" buttons.
|
|
117
|
+
- **Fixed**: **IndexedDB unhandled rejection at startup** (t108) — module-level `listRecentProjects()` has `.catch()`; `openFolder`/`openRecentProject` wrap IDB calls so private browsing never blocks project opening.
|
|
118
|
+
- **Fixed**: **Revoked-handle recovery** (t109) — `verifyPermission()` swallows internal throws; `openRecentProject` is transactional (captures + rolls back state) and auto-removes dead entries from recent projects with a clear warning.
|
|
119
|
+
- **Fixed**: **Drawer data loss on save failure** (t110) — unsaved edits are stashed into a per-task recovery buffer when the drawer is force-closed, and restored on next open. Close guard prompts before discarding; footer shows "Retry save" + "● unsaved" indicator when there's a pending error.
|
|
120
|
+
- **Fixed**: **Silent config corruption** (t111) — `readConfigFileStrict()` distinguishes not-found (silent) from corrupted (warn + back up to `kandown.json.backup`). Null-safe spreading means `"board": null` no longer crashes with `TypeError`. CLI `loadConfig` mirrors the fix and warns to stderr.
|
|
121
|
+
- **Fixed**: **`Promise.all` → `Promise.allSettled`** (t116) — `readAllTasks`, `persistColumnOrder`, `renameColumn`, `deleteColumn` all tolerate per-task failures instead of failing the whole batch.
|
|
122
|
+
- **Fixed**: **CLI agent launch error handling** (t112) — `board-reader.ts` per-task guards; `launcher.ts` step-by-step guarded launch with task-status rollback if the agent fails to spawn (tmux missing, binary not found). `child.on('error')` no longer crashes the TUI.
|
|
123
|
+
- **Fixed**: **CLI `copyRecursive` crash + HTTP error leaks** (t113) — `copyRecursive` returns per-file errors; `appendAgentReference` TOCTOU-safe; `serveApp` 500 no longer leaks paths to HTTP clients; `listenOnAvailablePort` treats `EACCES` like `EADDRINUSE`.
|
|
124
|
+
- **Fixed**: **TUI error handling** (t114) — board.tsx adds `boardError` state with a recoverable "Press r to retry, q to quit" view; `openDetail`/`persistConfig` wrapped; empty agent-picker shows a hint instead of an empty box.
|
|
125
|
+
- **Added**: **Toast `warning` severity** with longer duration (6s) for actionable messages, rendered in amber.
|
|
126
|
+
|
|
127
|
+
## 0.14.0 — 2026-06-26 — "Task Groups"
|
|
128
|
+
|
|
129
|
+
- **Added**: **User preference for default task group state** (`board.stackDefaultState`). Until now, when several tasks shared the same `[bracket]` or `#hashtag` title tag, they always rendered as a single collapsible stack (click to expand). The new setting in **Settings → Board → Task groups** lets users pick:
|
|
130
|
+
- **Collapsed (stacked)** — default, current behavior. One summary card per group with a count and a preview line; click to expand.
|
|
131
|
+
- **Expanded (all visible)** — every task in the group is rendered inline as its own card, identical to how untagged tasks display. Useful when the board is mainly used as a flat list rather than a high-density overview.
|
|
132
|
+
- The active search filter still forces expansion regardless of the setting, so search match highlights always remain visible. Per-stack collapse / expand toggles on click still work in both modes.
|
|
133
|
+
- **Added**: **CLI parity** — the same `board.stackDefaultState` setting is now configurable from the TUI settings screen (under the **Board** section), so terminal-only users can toggle it without opening the web UI.
|
|
134
|
+
- **Added**: **New `board.stackDefaultState` key in `templates/kandown.json`** so fresh installs (`kandown init`) ship with the field explicitly set to `'collapsed'`. Existing projects without the key inherit `'collapsed'` through the existing deep-merge in `readConfigFile` / `loadConfig` — no migration needed.
|
|
135
|
+
- **Added**: **English + French translations** for the new setting: `stackDefaultState` ("Task groups" / "Groupes de tâches"), `stackDefaultStateDesc`, `stackCollapsed` ("Collapsed (stacked)" / "Pliées (empilées)"), `stackExpanded` ("Expanded (all visible)" / "Dépliées (toutes visibles)"). Other locales fall back to English until they're updated.
|
|
136
|
+
- **Changed**: **`Column.tsx` now combines the user preference with the search filter** when computing `CardStack`'s `defaultExpanded` prop: `config.board.stackDefaultState === 'expanded' || !!filters.search`. Previously the prop only reflected the search state.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## 0.13.1 — 2026-06-20 — "Transparent Favicon"
|
|
140
|
+
|
|
141
|
+
- **Fixed**: **Favicon had a solid dark surface that disappeared on dark browser tabs.** The 0.13.0 favicon used a `#0a0a0a` rounded background that blended with Chrome's `#202124` tab strip and Safari's dark chrome, leaving a near-invisible blob. The new \`public/favicon.svg\` has a transparent background and recolors the K via \`@media (prefers-color-scheme: dark)\` so the contrast holds in both light and dark browser themes. The lime slash (\`#cef867\`) is the constant brand element on top. \`favicon-{16,32,48}.png\` and \`favicon.ico\` are regenerated with transparent backgrounds. PWA / OS-launcher icons (apple-touch-icon, android-chrome-192/-512, mstile-150, og-image) keep their dark surface — they live on home screens, app drawers, Windows tiles, and social cards where a solid background is required by the platform.
|
|
142
|
+
|
|
143
|
+
## 0.13.0 — 2026-06-20 — "TUI Agents"
|
|
144
|
+
|
|
145
|
+
- **Added**: **Agent hook HTTP for IDE / Electron integration** (`c5ce628`). The CLI daemon now forwards a task to a host process (IDE, Electron main, custom server) over plain HTTP. Wire format is JSON: `{ action, task, context }` POSTed to a URL configured via `KANDOWN_AGENT_HOOK_URL`. The web UI surfaces a "Send to Agent" button in the task drawer; the TUI binds `g` to the same action. Both are strictly opt-in — hidden / no-op unless the env var is set on the daemon.
|
|
146
|
+
- **Fixed**: **Agent launch pre-fills the task prompt** (t117, `4ce2281`). Pressing `A` on a task and picking **opencode** previously launched a blank TUI because `opencode [project]` takes a project *path* as its positional, not a message. Now uses `opencode --prompt "<message>"` so the default TUI command opens with the task context already in the composer. Bonus fix: **gemini** now uses `--prompt-interactive` (`-i`) instead of `-p/--prompt` (which Gemini's help describes as non-interactive headless) so the TUI stays interactive after the prompt runs. The tmux split-window path also forwards `KANDOWN_CONTEXT_FILE`/`KANDOWN_TASK_ID`/`KANDOWN_DIR` to the new pane (a tmux pane inherits the *server's* env, not this process's overrides).
|
|
147
|
+
- **Added**: **Task `depends_on` with terminal-status gate** (`40c26c6`). New `depends_on: [T-001, T-007]` frontmatter field on tasks. `moveTask(id, "done")` is rejected at the store level if any dependency isn't yet in a terminal status, with a descriptive error that surfaces in both the web and TUI UIs. The web cards show a `↪N` chip on blocked tasks; the TUI mirrors the same chip and blocks drag/drop into the terminal column with an inline `Blocked: tX ← tA, tB` message. The task detail view (web + TUI) lists the raw dependency IDs.
|
|
148
|
+
- **Added**: **Shellable CLI task commands** (`506b65f`). `kandown task list`, `kandown task show <id>`, `kandown task create <id>`, `kandown task move <id> <col>`, `kandown task assign <id> <agent>`, and `kandown task commit` — all JSON-stable and pipe-friendly, so agents and shell scripts can drive the board without touching the TUI.
|
|
149
|
+
- **Added**: **Keyboard shortcuts cheatsheet modal** (`afaac2c`). Press `?` in the TUI to open a modal listing every keybinding (board navigation, agent launch, daemon control, drag/drop, etc.). Closes on any key.
|
|
150
|
+
- **Added**: **Brand refresh — full favicon / app-icon set** (`2469f1b`). The new Kandown mark (white K crossed by a lime `#cef867` diagonal slash on a dark surface) replaces the old single-color QuiverAI favicon. Ships as `favicon.svg` (1.2 KB vector), PNG fallbacks at 16/32/48 px, a multi-size `favicon.ico`, `apple-touch-icon` (180), `android-chrome-192`/`512` (PWA manifest, with `purpose: "any maskable"`), `mstile-150` (Windows tile), and an `og-image` (1200×630) for social previews. The `kandownlogo.png` source is committed at the project root; the cropped/padded version is in `public/`. Vite's single-file build inlines the whole set into the bundle — no extra wiring.
|
|
151
|
+
- **Fixed**: **`bin/tui.js` rebuild gap.** Commit `40c26c6` shipped depends_on TUI features in source but didn't rebuild `bin/tui.js`, so v0.12.0 users running the bundled CLI wouldn't have gotten the new `↪N` chips, the `depends on:` detail line, or the move-gate. The `4ce2281` commit's `pnpm build:cli` step regenerates the bundle and closes that gap. No source change is required.
|
|
152
|
+
|
|
153
|
+
## 0.12.0 — 2026-06-18 — "Top-Level Tasks"
|
|
154
|
+
|
|
155
|
+
- **Changed**: **Tasks moved to the project root.** Task files (and `./tasks/archive/`) now live at the project root in `./tasks/`, while `.kandown/` only holds config (`kandown.json`), the web UI (`kandown.html`), agent docs (`AGENT.md`, `AGENT_KANDOWN.md`), and daemon runtime metadata. The previous layout (`.kandown/tasks/*.md`) is no longer supported.
|
|
156
|
+
- **Added**: Silent one-time migration. The first time a project is opened after this change, any `.kandown/tasks/*.md` is moved to `./tasks/` (plus the `archive/` subfolder). The legacy `.kandown/tasks/` directory is removed if it ends up empty. No user action required — the migration runs in the background on startup and logs a single line to the CLI.
|
|
157
|
+
- **Changed**: `kandown init` now creates `tasks/` at the project root instead of inside `.kandown/`. The init banner shows the new layout explicitly.
|
|
158
|
+
- **Changed**: The web app (File System Access mode) now asks the user to pick the **project root** (the parent of `.kandown/`) rather than `.kandown/` directly. The app derives both `.kandown/` (config) and `./tasks/` (tasks) from it. Server mode already routes through the CLI REST API, so only the server-side path needed updating.
|
|
159
|
+
- **Added**: New `POST /api/migrate-tasks` endpoint and `serverMigrateTasks()` browser helper. The web app calls it on startup in server mode before reading tasks, so the legacy → new layout move happens transparently.
|
|
160
|
+
- **Added**: `bin/kandown.js → getProjectRoot(kandownDir)`, `getTasksDir(kandownDir)`, `migrateTasksToTopLevel(kandownDir)`, plus the matching helpers in `src/cli/lib/board-reader.ts` (`getTasksDir`). All previous `kandownDir + 'tasks'` paths are now `getTasksDir(kandownDir)`.
|
|
161
|
+
- **Fixed**: `archiveTask` / `unarchiveTask` REST endpoints were referenced in the route handler but never defined (would 404 on every archive action). Implemented them on the new tasks path.
|
|
162
|
+
- **Changed**: All 48 i18n locales — `emptyState.selectFolderDesc` now tells the user to pick the project root and references both folders.
|
|
163
|
+
- **Changed**: `templates/AGENT.md`, `templates/AGENT_KANDOWN.md`, and `templates/README.md` document the new layout. The legacy `templates/.kandown/tasks/` (empty) is removed; sample tasks already lived at `templates/tasks/` at the top level.
|
|
164
|
+
- **Changed**: Default Empty State text and CLI init banner updated to reflect the new layout.
|
|
165
|
+
|
|
166
|
+
## 0.11.2 — 2026-06-17 — "TUI Category Cards"
|
|
167
|
+
|
|
168
|
+
- **Fixed**: Category task rows now use a proper dark background (`#222`) instead of the too-light ANSI `gray`. All task text (ID, title) is white for full readability on the dark block. Category tag remains magenta.
|
|
169
|
+
|
|
170
|
+
## 0.11.1 — 2026-06-17 — "TUI Category Cards"
|
|
171
|
+
|
|
172
|
+
- **Changed**: TUI board now renders tasks with a `[category]` bracket tag or `#hashtag` in their title as a **3-line dark-gray block**: task ID on line 1, category on line 2, clean title on line 3. Tasks without a category render as a single line, unchanged. A separator line (`───`) is inserted between all tasks for improved readability.
|
|
173
|
+
- **Added**: New `CategoryTaskRow` component for the 3-line category block; `SingleTaskRow` preserves the original single-line layout for untagged tasks.
|
|
174
|
+
- **Added**: `getTitleCategory()` helper — extracts bracket tags and hashtags from task titles to determine which row type to use.
|
|
175
|
+
- **Changed**: `KanbanColumn` now dynamically selects the row component per task and injects separator lines automatically.
|
|
176
|
+
- **Changed**: Dragging state colors (`yellow` background) now apply to the entire `CategoryTaskRow` block consistently.
|
|
177
|
+
|
|
178
|
+
## 0.11.0 — 2026-06-16 — "Lean Drawer"
|
|
179
|
+
|
|
180
|
+
- **Removed**: The subtask editor in the task drawer (add/remove/check subtasks and the per-subtask description/report fields). The subtask data model is untouched — tasks still keep their `- [ ]` / `- [x]` checklist in the .md file, and the per-card progress bar on the board still shows `done/total`. Editing subtasks just happens in the task file directly now, not in the drawer.
|
|
181
|
+
- **Changed**: The drawer body is now strictly stacked: title → DESCRIPTION (full width) → REPORT (full width, below description). The previous side-by-side 7/3 grid is gone, so each editor gets full room to breathe and feels closer to a writing surface than a form.
|
|
182
|
+
- **Removed**: The header subtask count chip (`done/total doneSubtasks`) — no more subtasks in the drawer header.
|
|
183
|
+
- **Removed**: The `focusedSubtaskIdx` state and the `toggleSubtask` / `changeSubtask` / `removeSubtask` / `addSubtask` / `insertSubtaskAfter` / `handleDescriptionChange` / `handleReportChange` handlers — all dead code now that the subtask editor is gone. The `SubtaskItem` import is gone too (the component file is left in the tree since the data model still uses `Subtask`).
|
|
184
|
+
|
|
185
|
+
## 0.10.1 — 2026-06-16 — "Clean Drawer"
|
|
186
|
+
|
|
187
|
+
- **Removed**: The metadata edit block (Priority, Assignee, Tags, Due, Owner, Tools) at the top of the task editor drawer. The drawer is now strictly title + description + report + subtasks. Frontmatter metadata is still managed in the task `.md` file directly, or surfaced on the board via the per-card "Show metadata" toggle. The unused `FieldRow` helper, the `tagsValue` derived string, the `Priority`/`OwnerType` type imports, and the `fields` selector are gone too.
|
|
188
|
+
|
|
189
|
+
## 0.10.0 — 2026-06-16 — "Linear Look"
|
|
190
|
+
|
|
191
|
+
- **Changed**: The board got a full Linear-style relook — pure black background in dark mode, off-white in light, with a refined two-layer dot grid (the "taches") that adapts to the theme. Default columns are now neutral; any per-column tints you set via the 3-dot menu use a restrained 6% opacity.
|
|
192
|
+
- **Changed**: The default "kandown" skin is rebuilt around Linear tokens — pure black background (`0 0% 0%`), cards at `0 0% 6%` with very subtle borders in dark; off-white background with white cards in light. The 4 colored skins (Graphite, Sage, Cobalt, Rose) were also retuned to match — darker bases, more restrained hues.
|
|
193
|
+
- **Changed**: Cards and card stacks are now `rounded-lg` with a lighter, hand-tuned shadow that lifts on hover. No more heavy `shadow-sm/md` defaults.
|
|
194
|
+
- **Changed**: Column headers are tighter and more refined — smaller icons, lighter typography, more breathing room. The Add task button at the bottom of each column blends in better.
|
|
195
|
+
- **Removed**: The SVG noise-overlay grain (Board + EmptyState + CSS) — it was making the board feel noisy and the CSS class is gone.
|
|
196
|
+
- **Fixed**: The `class="dark"` attribute was hardcoded in `index.html`, blocking the `theme: "auto"` mode from working. The hardcoded class is removed so the OS preference drives the theme.
|
|
197
|
+
- **Fixed**: `kandown.json` is now set to `theme: "auto"` and `columnColors: {}` by default, so a fresh install starts in Linear neutral mode without the previously-saved saturated column tints.
|
|
198
|
+
|
|
199
|
+
## 0.9.0 — 2026-06-16 — "Archive & Markdown"
|
|
200
|
+
|
|
201
|
+
- **Added**: Archive folder and view — tasks can now be archived via a button in the drawer, which moves the file to `.kandown/tasks/archive/`, sets `archived: true` in the frontmatter, and hides it from the active board. A new header button toggles a dedicated Archive view listing all archived tasks with a one-click Restore action.
|
|
202
|
+
- **Added**: `archive` and `restore` actions on the store, plus matching CLI server endpoints (`POST /api/tasks/:id/archive` and `…/unarchive`) and dev-mode vite middleware routes so the feature works in browser and server modes.
|
|
203
|
+
- **Changed**: Unified markdown editor across the drawer — the task Report field and all subtask descriptions/reports now use the same BlockNote editor as the task description body, with the same slash menu, block types, and markdown round-trip guarantees. Wysimark and its `marked`-based preview have been removed entirely.
|
|
204
|
+
- **Removed**: `src/components/ui/MarkdownEditor.tsx` (Wysimark), all wysimark CSS rules, and the `@wysimark/react` + `marked` dependencies.
|
|
205
|
+
- **Fixed**: Legacy subtask `report:` / `description:` indented lines are now recognized by `extractSubtasks` and migrated to the canonical `[REPORT]` / `[DESC]` markers on the first open+save. Previously these lines were silently dropped on save, causing silent loss of subtask reports in older task files.
|
|
206
|
+
- **Fixed**: YAML block scalar serializer no longer pads truly empty lines with two spaces, so the frontmatter `report: |` block stays byte-stable across open/save round-trips (no cosmetic git diff noise).
|
|
207
|
+
|
|
208
|
+
## 0.8.0 — 2026-06-04 — "Project Daemon"
|
|
209
|
+
|
|
210
|
+
- **Added**: Per-project web daemon — `kandown` now starts/reconnects a background server by default so the browser keeps working after quitting the TUI.
|
|
211
|
+
- **Added**: `kandown daemon start|stop|status` commands for explicit daemon lifecycle control.
|
|
212
|
+
- **Added**: TUI daemon status and `d` shortcut to start/stop the current project's web daemon without leaving the board.
|
|
213
|
+
- **Changed**: The board TUI has a more colorful, user-friendly header, column accents, daemon status pill, and clearer status/hint area.
|
|
214
|
+
- **Changed**: New `.kandown/` installs ignore daemon runtime metadata via `.kandown/.gitignore`.
|
|
215
|
+
- **Fixed**: Restarting the daemon from the TUI preserves the last custom port used by the project.
|
|
216
|
+
|
|
217
|
+
## 0.7.5 — 2026-06-04 — "TUI Drag"
|
|
218
|
+
|
|
219
|
+
- **Added**: Real terminal drag-and-drop in the TUI board — press a task, drag over another column, and release to move it.
|
|
220
|
+
- **Fixed**: TUI rendering now uses Ink's managed alternate screen with a fixed-height root frame, preventing duplicated/glitchy scrollback redraws.
|
|
221
|
+
- **Fixed**: TUI mouse control sequences are now written through Ink's stdout helper and button-motion tracking is enabled for drag events.
|
|
222
|
+
- **Changed**: TUI board mouse handling now follows the Herdr-style press → drag → hover target → release commit flow while keeping click menus and keyboard moves as fallbacks.
|
|
223
|
+
|
|
224
|
+
## 0.7.4 — 2026-06-04 — "Move Sync"
|
|
225
|
+
|
|
226
|
+
- **Fixed**: Web drag-and-drop in CLI server mode now persists task moves to `tasks/*.md` instead of only updating optimistic UI state.
|
|
227
|
+
- **Fixed**: Web ↔ TUI sync after task moves — moved tasks now keep their new status after server polling reloads the board.
|
|
228
|
+
- **Changed**: Column order persistence now uses the shared filesystem adapter in both browser File System Access mode and CLI REST server mode.
|
|
229
|
+
|
|
230
|
+
## 0.7.3 — 2026-06-04 — "Port Reclaim"
|
|
231
|
+
|
|
232
|
+
- **Added**: Stale process detection — when `kandown` detects its preferred port (2048) is already occupied by another kandown process for the same project, it automatically kills the zombie and reclaims the port instead of silently moving to 2049.
|
|
233
|
+
- **Added**: Cross-project awareness — if the port is occupied by a kandown from a *different* project, the port is skipped and the next available port is used (no interference between projects).
|
|
234
|
+
- **Added**: Non-kandown processes on the target port are left untouched — only kandown zombies are auto-cleaned.
|
|
235
|
+
- **Changed**: `listenOnAvailablePort()` now includes a `detectStaleKandown()` pre-check that inspects the process command line and working directory before attempting to listen.
|
|
236
|
+
|
|
237
|
+
## 0.7.2 — 2026-06-04 — "Live Sync"
|
|
238
|
+
|
|
239
|
+
- **Fixed**: CLI TUI watcher — `persistent: true` removed (obsolete in chokidar v4, could prevent events), `stabilityThreshold` 50→25ms, `alwaysStat: true` added for reliable change detection.
|
|
240
|
+
- **Fixed**: CLI TUI polling fallback tightened from 500ms to 300ms for faster external change detection.
|
|
241
|
+
- **Fixed**: Web app `reloadBoard()` was a dead no-op in local File System Access API mode — `if (!isServerMode()) return;` blocked all watcher-driven refreshes. Added full local-mode path that reads from `FileSystemDirectoryHandle` and rebuilds the board.
|
|
242
|
+
- **Fixed**: Web app server mode (when served via `npx kandown`) had no file watcher at all — `setupWatcher()` was never called. Added REST API polling every 2 seconds so the board stays in sync with external edits.
|
|
243
|
+
- **Changed**: Browser-side `FileWatcher` polling interval 500→300ms, debounce delay 200→150ms for snappier sync.
|
|
244
|
+
- **Changed**: `openServerProject()` now calls `setupWatcher()` to activate server-mode polling on open.
|
|
245
|
+
|
|
246
|
+
## 0.7.1 — 2026-06-04 — "Mouse v2"
|
|
247
|
+
|
|
248
|
+
- **Fixed**: Complete rewrite of mouse support — no more stdin interception (fragile with Ink). Mouse sequences are now detected directly inside Ink's `useInput` handler.
|
|
249
|
+
- **Fixed**: Context menu now renders INLINE within the column, directly under the task that was clicked — not at a calculated global offset.
|
|
250
|
+
- **Fixed**: Mouse click detection now correctly accounts for Ink stripping the ESC prefix from sequences.
|
|
251
|
+
- **Added**: `m` key opens context menu on the focused task (full keyboard support for all mouse features).
|
|
252
|
+
- **Added**: `useMouseMode()` hook — simply enables terminal mouse tracking via ANSI codes, no stdin interception.
|
|
253
|
+
- **Added**: `parseMouseInput()` — parses SGR mouse coordinates from Ink's useInput `input` string.
|
|
254
|
+
- **Added**: `InlineContextMenu` component — compact 2-line menu rendered inside the column flow.
|
|
255
|
+
- **Changed**: Menu options navigable with j/k + Enter (same as all other TUI interactions).
|
|
256
|
+
- **Changed**: Header hint updates dynamically: shows mode-specific instructions (browse, context-menu, move-target).
|
|
257
|
+
- **Changed**: Version displayed in TUI header (auto-read from package.json).
|
|
258
|
+
|
|
259
|
+
## 0.7.0 — 2026-06-04 — "Mouse & Move"
|
|
260
|
+
|
|
261
|
+
- **Added**: Full mouse support in the TUI board — click on tasks, menus, and move placeholders using SGR extended mouse mode (\x1b[?1006h) with X10 fallback.
|
|
262
|
+
- **Added**: Context menu on task click — small, sober popup with "Open task" and "Move task" options (keyboard + mouse).
|
|
263
|
+
- **Added**: Move-task flow — select "Move task" from context menu, then click a yellow ↓ placeholder in any other column to move the task there (drag-and-drop alternative for TUI).
|
|
264
|
+
- **Added**: `useMouse` React hook (`src/cli/hooks/use-mouse.ts`) — enables terminal mouse tracking, parses SGR/X10 click events, passes keyboard data through to Ink.
|
|
265
|
+
- **Added**: `TaskContextMenu` component (`src/cli/components/task-context-menu.tsx`) — reusable inline popup with j/k/Enter/Escape + click support.
|
|
266
|
+
- **Added**: Version number displayed in TUI header next to KANDOWN logo (auto-read from package.json on every launch).
|
|
267
|
+
- **Added**: Move-target placeholder component with yellow highlight, keyboard navigation (←/→), and click-to-move.
|
|
268
|
+
- **Added**: Click-outside-to-cancel for context menu and move mode.
|
|
269
|
+
- **Changed**: Board header hint dynamically updates based on current mode (browse, context-menu, move-target).
|
|
270
|
+
- **Changed**: Board screen refactored to 5 modes: browse, detail, agent-picker, context-menu, move-target.
|
|
271
|
+
|
|
272
|
+
## 0.6.1 — 2026-06-04 — "Server Mode Fixes"
|
|
273
|
+
|
|
274
|
+
- **Fixed**: Dark/light mode toggle was broken in CLI server mode — `updateConfig` and `loadConfig` silently returned when `dirHandle` was null.
|
|
275
|
+
- **Fixed**: Settings button appeared to quit the project in server mode — `SettingsPage` refused to render without a `dirHandle`.
|
|
276
|
+
- **Fixed**: Server-mode project name displayed `.kandown` instead of the actual project name (`kandown`).
|
|
277
|
+
- **Changed**: Dev server default port moved from 5173 to 5176.
|
|
278
|
+
|
|
279
|
+
## 0.6.0 — 2026-06-03 — "Auto-Updater v2"
|
|
280
|
+
|
|
281
|
+
- **Added**: Non-blocking auto-updater using async `spawn` instead of `execSync` — CLI no longer freezes during update checks.
|
|
282
|
+
- **Added**: Lock file (`.update.lock`) with 60s auto-expiry to prevent concurrent update races.
|
|
283
|
+
- **Added**: pnpm fallback — tries `pnpm install -g` if `npm install -g` fails.
|
|
284
|
+
- **Added**: Post-install version verification — confirms the update actually landed before respawning.
|
|
285
|
+
- **Added**: `--no-update-check` flag — respawned children skip the update loop.
|
|
286
|
+
- **Added**: `resolveKandownBin()` — resolves the global kandown binary across npm/pnpm installs.
|
|
287
|
+
- **Added**: `semverGt()` — proper semver comparison replacing string equality checks.
|
|
288
|
+
- **Changed**: Graceful fallback on every failure point — update failure never crashes the CLI, current version continues normally.
|
|
289
|
+
- **Changed**: Removed dead code (unused `isMacos` variable, unnecessary `--experimental-vm-modules` injection).
|
|
290
|
+
- **Fixed**: Respawn logic now works for both global installs and npx.
|
|
291
|
+
|
|
292
|
+
## 0.5.0 — 2026-06-03 — "Minor Update"
|
|
293
|
+
|
|
294
|
+
- **Added**: Graph visualization output directory with cache, metadata, and an HTML viewer.
|
|
295
|
+
- **Added**: DESIGN_IMPROVEMENTS.md documentation file.
|
|
296
|
+
- **Audit**: Source code analysis audit added.
|
|
297
|
+
- **Chore**: Refreshed generated graph output.
|
|
298
|
+
|
|
299
|
+
## 0.4.0 — 2026-05-04 — "CLI Launch Fix"
|
|
300
|
+
|
|
301
|
+
- **Added**: BlockNote now powers task description editing with a markdown-native schema and anti-pollution guards.
|
|
302
|
+
- **Added**: Syntax-highlighted code blocks in the BlockNote editor.
|
|
303
|
+
- **Added**: Premium semantic design system updates, refreshed header components, and Cobalt as the default skin.
|
|
304
|
+
- **Added**: Tags now render with strikethrough when every task using that tag is Done.
|
|
305
|
+
- **Fixed**: `kandown` server mode no longer injects `window.__KANDOWN_ROOT__` into bundled JavaScript when parser strings contain literal `</head>`.
|
|
306
|
+
- **Fixed**: Single-file HTML builds now repair escaped regex lookbehind openers from inlined Shiki grammars, preventing browser syntax crashes on launch.
|
|
307
|
+
- **Fixed**: Dark-mode readability across UI components and BlockNote code blocks.
|
|
308
|
+
- **Changed**: Component styling now consistently uses semantic color variables.
|
|
309
|
+
- **Changed**: Embed output was simplified for cleaner markdown.
|
|
310
|
+
- **Removed**: Obsolete placeholder project-board tasks.
|
|
311
|
+
|
|
312
|
+
## 0.3.5 — 2026-04-25 — "Server Mode Task CRUD Fix"
|
|
313
|
+
|
|
314
|
+
- **Fixed**: Task creation, deletion, drawer save, and board reload now work in server mode (`kandown` CLI) — all mutations go through the REST API instead of requiring `tasksDirHandle`.
|
|
315
|
+
- **Changed**: Server-mode store actions no longer require `tasksDirHandle` — they pass `null` to `filesystem.ts` helpers which bypass it when `isServerMode()` is true.
|
|
316
|
+
- **Changed**: `moveTask` and `reorderInColumn` skip file persistence in server mode (full reload handles sync).
|
|
317
|
+
- **Added**: `readAllTasksServer()` — reads all tasks via the REST API for board reload in server mode.
|
|
318
|
+
- **Changed**: README now strongly recommends `npm install -g kandown` over `npx`.
|
|
319
|
+
|
|
320
|
+
## 0.3.4 — 2026-04-25 — "Browser Ready Check"
|
|
321
|
+
|
|
322
|
+
- **Fixed**: `openInBrowser()` now waits up to 2s for the server to be ready (via HTTP HEAD probe) before opening the URL, preventing `ERR_UNSAFE_PORT` and race conditions when multiple instances start simultaneously.
|
|
323
|
+
- **Fixed**: Port range scan improved — always starts from 2048 when no explicit port is set.
|
|
324
|
+
|
|
325
|
+
## 0.3.3 — 2026-04-25 — "Auto-update Loop Fix"
|
|
326
|
+
|
|
327
|
+
- **Fixed**: Auto-update now spawns the newly installed global binary directly (via `npm prefix`), preventing `npx` from re-resolving the old cached version and causing an infinite update loop.
|
|
328
|
+
- **Fixed**: `npx kandown` now auto-refreshes `kandown.html` on every serve, so CLI upgrades propagate to the web UI without needing a separate `kandown update`.
|
|
329
|
+
|
|
330
|
+
## 0.3.0 — 2026-04-25 — "Server Mode"
|
|
331
|
+
|
|
332
|
+
- **Added**: Full REST API server in `bin/kandown.js` for all file operations (`GET/PUT /api/config`, `/api/board`, `/api/tasks`, `/api/tasks/:id`)
|
|
333
|
+
- **Added**: `src/lib/filesystem.ts` server-mode helpers that proxy all file operations to the CLI REST API via `fetch()`
|
|
334
|
+
- **Added**: `openServerProject()` store action — auto-loads the project on mount with zero user interaction when served via `npx kandown`
|
|
335
|
+
- **Added**: `isServerMode()` detection and `getServerRoot()` path accessor
|
|
336
|
+
- **Changed**: Board now renders when `isOpen` is true (server mode) OR `dirHandle` is set (file mode)
|
|
337
|
+
- **Changed**: CLI HTTP server routes `/api/*` to `handleApi()` with full CRUD for config, board, and tasks
|
|
338
|
+
- **Changed**: EmptyState shows loading spinner during server-mode auto-load, then a passive message (no button needed)
|
|
339
|
+
- **Fixed**: Command palette is now exactly centered in the middle of the screen
|
|
340
|
+
- **Fixed**: When a new task is created, the title and description are now empty by default, and the editor drawer opens natively focusing the title
|
|
341
|
+
|
|
342
|
+
## 0.2.3 — 2026-04-20 — "EmptyState Server Mode Fix"
|
|
343
|
+
|
|
344
|
+
- **Fixed**: When served via `npx kandown`, the web app now detects server mode and shows a contextual "Open this project" button instead of the generic select-folder UI. User grants folder access once, browser remembers it for next time.
|
|
345
|
+
|
|
346
|
+
## 0.2.2 — 2026-04-20 — "Header Version Badge"
|
|
347
|
+
|
|
348
|
+
- **Added**: Version badge (`v0.2.2`) displayed in red in the web app header, top-left, next to the logo.
|
|
349
|
+
|
|
350
|
+
## 0.2.1 — 2026-04-20 — "Auto-Open Fix"
|
|
351
|
+
|
|
352
|
+
- **Fixed**: `npx kandown` now auto-opens the correct project in the web UI instead of showing the empty "Select a project" screen. The CLI injects `window.__KANDOWN_ROOT__` and the app tries to match it against previously granted folder permissions on mount.
|
|
353
|
+
|
|
354
|
+
## 0.2.0 — 2026-04-20 — "Version Display + Auto-Update"
|
|
355
|
+
|
|
356
|
+
- **Added**: `kandown -v` / `--version` flag — prints the current CLI version.
|
|
357
|
+
- **Added**: Version displayed in CLI help banner, TUI settings header, and web app Settings "About" section.
|
|
358
|
+
- **Added**: Web app Settings now has an "About" section showing current version and a manual update check against the npm registry.
|
|
359
|
+
- **Changed**: CLI auto-update now runs before **every** command (not just `kandown` with no args). If a new version is found, runs `npm install -g kandown` and respawns — no prompt, no ask.
|
|
360
|
+
- **Changed**: `kandown help` shows the current version in the banner.
|
|
361
|
+
- **Added**: Bracket tags (e.g. `[optimization]`) in task titles are now rendered bold next to the task ID on board cards.
|
|
362
|
+
- **Added**: `scripts/inject-version.js` — generates `src/lib/version.ts` at build time from `package.json` version. `package.json` is the single source of truth for version.
|
|
363
|
+
|
|
364
|
+
## 0.1.5 — 2026-04-20 — "Live Reload + Auto-Update"
|
|
365
|
+
|
|
366
|
+
- **Added**: CLI checks for a newer npm version on startup (non-blocking, background check). Warns if the user is outdated.
|
|
367
|
+
- **Added**: Live file watching in the TUI — board auto-reloads when task files or `kandown.json` change. No need to press `r`.
|
|
368
|
+
- **Changed**: `npx kandown` now auto-inits `.kandown/` if not found — zero manual setup required.
|
|
369
|
+
- **Fixed**: TUI crashed on fresh install with `Cannot find package 'react-devtools-core'`.
|
|
370
|
+
- **Fixed**: TUI crashed with `Dynamic require of "assert" is not supported` in Node.js ESM context.
|
|
371
|
+
- **Fixed**: `self is not defined` error — added self/window polyfills and `DEV=false` to prevent Ink from loading react-devtools-core.
|
|
372
|
+
- Added `chokidar` and `signal-exit` as explicit runtime dependencies.
|
|
373
|
+
|
|
374
|
+
## 0.1.3 — 2026-04-20 — "CLI Launch Fix"
|
|
375
|
+
|
|
376
|
+
- **Fixed**: TUI crashed on fresh install with `Cannot find package 'react-devtools-core'` — promoted `react-devtools-core` from optional peer dep to regular dependency so npm installs it for users.
|
|
377
|
+
|
|
378
|
+
## 0.1.2 — 2026-04-19 — "Zero Deps"
|
|
379
|
+
|
|
380
|
+
- **Fixed**: `npx kandown init` was hanging because npm had to install 11 runtime dependencies (React, Three.js, Ink, etc.) before running the CLI. All dependencies are now bundled into the CLI binary — the published package has zero runtime deps.
|
|
381
|
+
- Moved all dependencies to devDependencies — the web app and TUI are fully pre-built.
|
|
382
|
+
- Added auto-bump rule to AGENTS.md for critical bug fixes.
|
|
383
|
+
|
|
384
|
+
## 0.1.1 — 2026-04-19 — "Release Pipeline"
|
|
385
|
+
|
|
386
|
+
- Added pre-release warning banner in README.
|
|
387
|
+
- Fixed `package.json` bin path and repository URL normalization (`npm pkg fix`).
|
|
388
|
+
- Added GitHub Actions workflow for automated npm publishing on version tags.
|
|
389
|
+
- Added version name system and changelog-in-commit-body requirement to bump instructions.
|
|
390
|
+
|
|
391
|
+
## 0.1.0 — 2026-04-19 — "Pre-Alpha"
|
|
392
|
+
|
|
393
|
+
First release. File-based Kanban engine backed by plain markdown — zero backend, zero database, no account, AI-agent friendly.
|
|
394
|
+
|
|
395
|
+
### Core
|
|
396
|
+
|
|
397
|
+
- Task files (`tasks/*.md`) are the single source of truth — title, status, order, priority, tags, assignee, subtasks, notes, and completion reports all live in one markdown file per task.
|
|
398
|
+
- Board columns derived from task frontmatter `status` field, with custom columns stored in `kandown.json`.
|
|
399
|
+
- Unknown task statuses appear as temporary columns in the UI until explicitly added to settings.
|
|
400
|
+
- Drag-and-drop between columns with optimistic file writes and automatic rollback on failure.
|
|
401
|
+
- Reorder tasks within a column by drag.
|
|
402
|
+
- Task drawer for editing title, metadata fields, subtasks, and body content with 150ms debounced autosave.
|
|
403
|
+
- Subtask progress tracking on board cards (checkbox count).
|
|
404
|
+
- Guarded card deletion — hover trash icon, first click arms, second click confirms.
|
|
405
|
+
- Keyboard shortcut `⌘⌫` / `Ctrl+Backspace` to delete current task from the drawer after confirmation.
|
|
406
|
+
|
|
407
|
+
### Web Application
|
|
408
|
+
|
|
409
|
+
- Single-file web app (`kandown.html`) built with React 19, Vite, Tailwind CSS, and Zustand.
|
|
410
|
+
- Uses the browser File System Access API — no server needed, works offline.
|
|
411
|
+
- Board view and list view, toggled with `⌘1` / `⌘2`.
|
|
412
|
+
- Command palette (`⌘K`) for quick actions and task search.
|
|
413
|
+
- Content search across titles, IDs, task body, subtasks, tags, assignee, and priority with highlighted preview snippets on cards.
|
|
414
|
+
- Owner type filtering (human vs AI-agent tasks).
|
|
415
|
+
- Filter bar with search input, active chips, and clear action.
|
|
416
|
+
- Recent projects stored in IndexedDB for quick reopening.
|
|
417
|
+
- Animated task counts with spring transitions.
|
|
418
|
+
|
|
419
|
+
### Appearance
|
|
420
|
+
|
|
421
|
+
- Project-level theme modes: `auto` (follows system), `light`, `dark`.
|
|
422
|
+
- 5 built-in skins: Kandown (default), Graphite, Sage, Cobalt, Rose — all using shadcn-compatible CSS tokens.
|
|
423
|
+
- 5 font presets: Inter, System, Serif, Mono, Rounded.
|
|
424
|
+
- Column color accents with expanded translucent backgrounds including black variants.
|
|
425
|
+
- Cards blend into colored columns (50% white in light mode, 50% black in dark mode).
|
|
426
|
+
- Tabler icons on board column headers for status visual scanning.
|
|
427
|
+
|
|
428
|
+
### Settings
|
|
429
|
+
|
|
430
|
+
- Dense settings sidebar with search, compact controls, and contextual hover help.
|
|
431
|
+
- Configurable task metadata fields (priority, assignee, tags, due date, owner type, tools) — disabled fields hide across drawer, cards, list view, and filters.
|
|
432
|
+
- Configurable notifications: browser alerts, in-page sound cues, status-change alerts, debounced edit alerts, subtask-completion alerts.
|
|
433
|
+
|
|
434
|
+
### Internationalization
|
|
435
|
+
|
|
436
|
+
- 33+ languages supported, including: English, French, Chinese, Japanese, Korean, Spanish, Portuguese, German, Italian, Russian, Arabic, Hindi, Thai, Malay, Tamil, Telugu, and more.
|
|
437
|
+
- Localized UI labels, settings descriptions, and filter controls.
|
|
438
|
+
|
|
439
|
+
### CLI
|
|
440
|
+
|
|
441
|
+
- `npx kandown init` — scaffolds `.kandown/` with web app, config, templates, and AI-agent documentation.
|
|
442
|
+
- `npx kandown` — starts a zero-dependency local HTTP server + opens the browser + launches the board TUI.
|
|
443
|
+
- `npx kandown board` — interactive kanban board TUI only (Ink / React for terminals).
|
|
444
|
+
- `npx kandown update` — replaces installed `kandown.html` with latest package build.
|
|
445
|
+
- `npx kandown settings` — terminal settings editor for `kandown.json`.
|
|
446
|
+
- Automatic port fallback from 2048 to 2060, or `--port <n>` for a specific port.
|
|
447
|
+
- `--path`, `--force`, `--no-agents` init flags.
|
|
448
|
+
|
|
449
|
+
### Board TUI
|
|
450
|
+
|
|
451
|
+
- Full-screen terminal kanban built with Ink — renders the same columns and tasks as the web UI.
|
|
452
|
+
- Vim-style navigation (`h/j/k/l`) and arrow keys.
|
|
453
|
+
- Task detail view with scrollable content (`Enter`).
|
|
454
|
+
- Agent picker (`a` key) — auto-detects installed AI agents (Claude Code, Codex, Gemini CLI, Goose, Aider, OpenCode).
|
|
455
|
+
- Sets task to "In Progress" and injects system prompt from `AGENT_KANDOWN_COMPACT.md`.
|
|
456
|
+
- tmux integration: opens agent in a split pane if inside tmux, otherwise hands over the terminal.
|
|
457
|
+
|
|
458
|
+
### AI-Agent Integration
|
|
459
|
+
|
|
460
|
+
- `AGENT_KANDOWN.md` — full agent instructions shipped with `kandown init`, teaches AI agents how to create, move, and complete tasks.
|
|
461
|
+
- `AGENT_KANDOWN_COMPACT.md` — condensed version injected into CLI agent prompts.
|
|
462
|
+
- Task files designed for AI readability — one file per task, frontmatter-based state, no index synchronization needed.
|
|
463
|
+
|
|
464
|
+
### Infrastructure
|
|
465
|
+
|
|
466
|
+
- GitHub Actions workflow for automated npm publishing on version tags (`v*`).
|
|
467
|
+
- Annotated release tags with version names.
|
|
468
|
+
- Changelog-based GitHub Releases.
|
package/bin/kandown.js
CHANGED
|
@@ -411,6 +411,40 @@ function printBreakingChangeNotices(fromVersion, toVersion) {
|
|
|
411
411
|
* silently rather than guessing; the checkForUpdate path covers that specific
|
|
412
412
|
* transition for auto-updaters, and every version from here on is covered.
|
|
413
413
|
*/
|
|
414
|
+
function getChangelogForVersion(version) {
|
|
415
|
+
const changelogPath = join(PKG_ROOT, 'CHANGELOG.md');
|
|
416
|
+
if (!existsSync(changelogPath)) return null;
|
|
417
|
+
try {
|
|
418
|
+
const text = readFileSync(changelogPath, 'utf8');
|
|
419
|
+
const escapedVer = version.replace(/\./g, '\\.');
|
|
420
|
+
const regex = new RegExp(`##\\s+${escapedVer}[\\s\\S]*?(?=\\n##\\s+|$)`, 'i');
|
|
421
|
+
const match = text.match(regex);
|
|
422
|
+
if (!match) return null;
|
|
423
|
+
return match[0].trim();
|
|
424
|
+
} catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function printVersionChangelog(version) {
|
|
430
|
+
const section = getChangelogForVersion(version);
|
|
431
|
+
if (!section) return;
|
|
432
|
+
const lines = section.split('\n');
|
|
433
|
+
log('');
|
|
434
|
+
log(` ${c.bold}${c.cyan}${lines[0].replace(/^##\s+/, '📋 Release ')}${c.reset}`);
|
|
435
|
+
log(` ${c.dim}${'─'.repeat(55)}${c.reset}`);
|
|
436
|
+
for (let i = 1; i < lines.length; i++) {
|
|
437
|
+
const line = lines[i];
|
|
438
|
+
if (!line.trim()) continue;
|
|
439
|
+
if (line.startsWith('- **')) {
|
|
440
|
+
log(` ${c.green}•${c.reset} ${line.slice(2)}`);
|
|
441
|
+
} else {
|
|
442
|
+
log(` ${c.dim}${line}${c.reset}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
log('');
|
|
446
|
+
}
|
|
447
|
+
|
|
414
448
|
const VERSION_SEEN_CACHE = join(PKG_ROOT, '.version-seen.json');
|
|
415
449
|
|
|
416
450
|
function checkVersionSeenNotices() {
|
|
@@ -425,6 +459,7 @@ function checkVersionSeenNotices() {
|
|
|
425
459
|
} catch { /* first run, or corrupted cache — treat as unknown */ }
|
|
426
460
|
|
|
427
461
|
if (lastSeen && lastSeen !== current) {
|
|
462
|
+
printVersionChangelog(current);
|
|
428
463
|
printBreakingChangeNotices(lastSeen, current);
|
|
429
464
|
}
|
|
430
465
|
if (lastSeen !== current) {
|
|
@@ -3028,6 +3063,13 @@ function cmdImport(rawArgs) {
|
|
|
3028
3063
|
success(`Imported ${count} tasks into board`);
|
|
3029
3064
|
}
|
|
3030
3065
|
|
|
3066
|
+
async function cmdUpdate(rawArgs) {
|
|
3067
|
+
const current = getCurrentVersion();
|
|
3068
|
+
log(`${c.bold}kandown update${c.reset} ${c.dim}— checking version notice & updates…${c.reset}`);
|
|
3069
|
+
printVersionChangelog(current);
|
|
3070
|
+
await checkForUpdate(['node', 'kandown', ...rawArgs]);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3031
3073
|
const SCRIPTED_COMMANDS = new Set([
|
|
3032
3074
|
'list', 'ls', 'show', 'create', 'new', 'move', 'assign', 'commit', 'tasks', 'work', 'daemon',
|
|
3033
3075
|
'doctor', 'undo', 'projects', 'export', 'import',
|
package/dist/index.html
CHANGED
|
@@ -113,7 +113,7 @@ Error generating stack: `+y.message+`
|
|
|
113
113
|
|
|
114
114
|
## Subtasks
|
|
115
115
|
|
|
116
|
-
`}}async function Zz(t,e){try{return await(await(await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}async function rce(t,e){try{return await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`),!0}catch{return!1}}async function Gz(t,e){try{return await t.getDirectoryHandle("archive",{create:e})}catch{return null}}async function sce(t){if(Ht())return tS();const e=new Set;for await(const n of t.values())n.kind==="file"&&n.name.endsWith(".md")&&e.add(n.name.slice(0,-3));try{const n=await t.getDirectoryHandle("archive",{create:!1});for await(const a of n.values())a.kind==="file"&&a.name.endsWith(".md")&&e.add(a.name.slice(0,-3))}catch{}return[...e].sort((n,a)=>n.localeCompare(a,void 0,{numeric:!0}))}async function Es(t,e){if(Ht())try{const i=await nS(e);return Rg(i)}catch{return yj(e)}const a=await(async i=>{try{return await(await(await i.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}})(t)??await Zz(t,e);return a!==null?Rg(a):yj(e)}async function oce(t,e){if(Ht())try{const a=await nS(e);return{ok:!0,task:Rg(a)}}catch(a){return a.name==="NotFoundError"?{ok:!1,reason:"not-found"}:{ok:!1,reason:"unknown",error:a}}let n=null;try{n=await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{}if(n===null)try{n=await Zz(t,e)}catch(a){return{ok:!1,reason:"unknown",error:a}}if(n===null)return{ok:!1,reason:"not-found"};if(n.trim()==="")return{ok:!1,reason:"corrupted",error:new Error(`Task file ${e}.md is empty`)};try{return{ok:!0,task:Rg(n)}}catch(a){return{ok:!1,reason:"corrupted",error:a}}}async function iu(t,e,n,a){const i=J4(n,a);if(Ht())return Xoe(e,i);try{const o=await(await(await rce(t,e)?await Gz(t,!0):t).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}}catch(r){throw nk(r,`${e}.md`)}}async function _j(t,e){if(Ht())return Qoe(e);try{await t.removeEntry(`${e}.md`)}catch{}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}async function cce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/archive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function pce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/unarchive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function lce(t,e,n,a){const i=J4(n,a);if(Ht())return cce(e,i);try{const o=await(await(await Gz(t,!0)).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}try{await t.removeEntry(`${e}.md`)}catch{}}catch(r){throw nk(r,`archive/${e}.md`)}}async function dce(t,e,n,a){const i=J4(n,a);if(Ht())return pce(e,i);try{const s=await(await t.getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await s.write(i)}finally{await s.close()}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}catch(r){throw nk(r,`${e}.md`)}}const uce="kanban-md",mce=1,ap="recentProjects";function aS(){return new Promise((t,e)=>{const n=indexedDB.open(uce,mce);n.onerror=()=>e(n.error),n.onsuccess=()=>t(n.result),n.onupgradeneeded=()=>{const a=n.result;a.objectStoreNames.contains(ap)||a.createObjectStore(ap,{keyPath:"id"})}})}async function P2(t){const e=await aS();return new Promise((n,a)=>{const i=e.transaction(ap,"readwrite");i.objectStore(ap).put(t),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function Dy(){const t=await aS();return new Promise((e,n)=>{const i=t.transaction(ap,"readonly").objectStore(ap).getAll();i.onsuccess=()=>{const r=i.result||[];r.sort((s,o)=>o.lastOpened-s.lastOpened),e(r.slice(0,10))},i.onerror=()=>n(i.error)})}async function fce(t){const e=await aS();return new Promise((n,a)=>{const i=e.transaction(ap,"readwrite");i.objectStore(ap).delete(t),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function kj(t,e=!0){const n={mode:e?"readwrite":"read"};try{return await t.queryPermission(n)==="granted"||await t.requestPermission(n)==="granted"}catch(a){return console.warn("[FS] verifyPermission: handle is no longer valid:",a),!1}}function Wz(t=Ci){const e=t.board.columns;return e[e.length-1]??"Done"}function gce(t,e=Ci){return t===Wz(e)||t.toLowerCase()==="archived"}class hce{dirHandle=null;tasksDirHandle=null;intervalId=null;configHash=null;taskHashes=new Map;knownTaskIds=new Set;listeners=new Map;debounceTimers=new Map;debounceDelay=150;consecutiveErrors=0;maxConsecutiveErrors=5;disabled=!1;eventSource=null;start(e,n){this.dirHandle=e,this.tasksDirHandle=n,this.consecutiveErrors=0,this.disabled=!1,Ht()&&this.startServerSse(),e&&n&&(this.initHashes(),this.intervalId=setInterval(()=>void this.tick(),300))}startServerSse(){if(typeof window>"u"||!Ht()||this.eventSource)return;const e=window.__KANDOWN_TOKEN__,n=e?`/api/events?token=${encodeURIComponent(e)}`:"/api/events";try{this.eventSource=new EventSource(n),this.eventSource.onmessage=a=>{try{const i=JSON.parse(a.data);i.type==="change"&&(i.taskId?this.emit("taskChanged",i.taskId):(this.emit("configChanged"),this.emit("taskChanged","")))}catch{}}}catch(a){console.warn("[Watcher] EventSource init failed:",a)}}stop(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.configHash=null,this.taskHashes.clear(),this.knownTaskIds.clear(),this.listeners.clear(),this.debounceTimers.forEach(e=>clearTimeout(e)),this.debounceTimers.clear(),this.consecutiveErrors=0,this.disabled=!1}isDisabled(){return this.disabled}on(e,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{this.listeners.get(e)?.delete(n)}}getKnownTaskIds(){return Array.from(this.knownTaskIds)}async initHashes(){if(!(!this.dirHandle||!this.tasksDirHandle))try{const e=await wj(this.dirHandle);e!==null&&(this.configHash=await this.hash(e)),await this.syncTaskDir(!1)}catch(e){console.warn("[Watcher] initHashes error:",e)}}async tick(){if(!(!this.dirHandle||!this.tasksDirHandle)&&!this.disabled)try{const e=await wj(this.dirHandle);if(e!==null){const n=await this.hash(e);this.configHash!==null&&n!==this.configHash&&(this.configHash=n,this.debouncedEmit("configChanged"))}for(const n of[...this.knownTaskIds])try{const a=await xj(this.tasksDirHandle,n);if(a!==null){const i=await this.hash(a),r=this.taskHashes.get(n);r!==void 0&&i!==r&&(this.taskHashes.set(n,i),this.debouncedEmit("taskChanged",n))}}catch(a){console.warn(`[Watcher] Error reading task ${n}:`,a)}await this.syncTaskDir(!0),this.consecutiveErrors=0}catch(e){this.consecutiveErrors++,console.error(`[Watcher] Tick failed (${this.consecutiveErrors}/${this.maxConsecutiveErrors}):`,e),this.consecutiveErrors>=this.maxConsecutiveErrors&&this.disable(`File watcher stopped after ${this.maxConsecutiveErrors} consecutive errors: ${e.message}`)}}disable(e){this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.disabled=!0,this.emit("watcherError",e)}debouncedEmit(e,...n){const a=e+JSON.stringify(n),i=this.debounceTimers.get(a);i&&clearTimeout(i);const r=setTimeout(()=>{this.debounceTimers.delete(a),this.emit(e,...n)},this.debounceDelay);this.debounceTimers.set(a,r)}async syncTaskDir(e){if(!this.tasksDirHandle)return;let n;try{n=this.tasksDirHandle.values()}catch(a){throw a}for await(const a of n){if(a.kind!=="file"||!a.name.endsWith(".md"))continue;const i=a.name.replace(".md","");if(!this.knownTaskIds.has(i))try{this.knownTaskIds.add(i);const r=await xj(this.tasksDirHandle,i);r!==null&&(this.taskHashes.set(i,await this.hash(r)),e&&this.debouncedEmit("newTaskDetected",i))}catch(r){console.warn(`[Watcher] syncTaskDir: failed to read ${i}:`,r)}}}async hash(e){const a=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(i)).map(s=>s.toString(16).padStart(2,"0")).join("")}emit(e,...n){this.listeners.get(e)?.forEach(i=>{if(e==="configChanged"){i();return}if(e==="taskChanged"){const[s]=n;i(s);return}if(e==="newTaskDetected"){const[s]=n;i(s);return}const[r]=n;i(r)})}}async function wj(t){try{return await(await(await t.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function xj(t,e){try{return await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}const al=new hce,bce={soft:[{frequency:520,durationMs:90,delayMs:0,type:"sine"},{frequency:720,durationMs:120,delayMs:95,type:"sine"}],chime:[{frequency:660,durationMs:120,delayMs:0,type:"triangle"},{frequency:880,durationMs:160,delayMs:125,type:"triangle"}],ping:[{frequency:920,durationMs:110,delayMs:0,type:"sine"}],pop:[{frequency:260,durationMs:55,delayMs:0,type:"square"},{frequency:520,durationMs:80,delayMs:60,type:"square"}]};let Aj=null;function Kz(){return"Notification"in window?Notification.permission:"unsupported"}async function vce(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function M2({title:t,body:e,config:n}){if(n.notifications.webhookUrl&&fetch(n.notifications.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event:"kandown_notification",title:t,body:e,timestamp:new Date().toISOString()})}).catch(()=>{}),n.notifications.sound&&yce(n.notifications.soundId),!(!n.notifications.browser||Kz()!=="granted"))try{new Notification(t,{body:e})}catch{}}function yce(t){const e=bce[t],n=window.AudioContext??window.webkitAudioContext;if(!(!n||e.length===0))try{Aj??=new n;const a=Aj;a.state==="suspended"&&a.resume();const i=a.currentTime+.01;e.forEach(r=>{const s=a.createOscillator(),o=a.createGain(),c=i+r.delayMs/1e3,p=c+r.durationMs/1e3;s.type=r.type??"sine",s.frequency.setValueAtTime(r.frequency,c),o.gain.setValueAtTime(1e-4,c),o.gain.exponentialRampToValueAtTime(.08,c+.01),o.gain.exponentialRampToValueAtTime(1e-4,p),s.connect(o),o.connect(a.destination),s.start(c),s.stop(p+.02)})}catch{}}async function Mf(t,e={}){const{maxAttempts:n=3,baseDelayMs:a=500,retryableCheck:i=Woe}=e;let r;for(let s=1;s<=n;s++)try{return await t()}catch(o){if(r=o,!i(o))throw o;if(s<n){const c=a*Math.pow(2,s-1);await new Promise(p=>setTimeout(p,c))}}throw r}function _ce(t){const e=t.toLowerCase(),n=new Date;if(e==="today")return n.toISOString().split("T")[0];if(e==="tomorrow"){const i=new Date(n);return i.setDate(i.getDate()+1),i.toISOString().split("T")[0]}const a=["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];if(a.includes(e)){const i=a.indexOf(e),r=new Date(n);let s=i-r.getDay();return s<=0&&(s+=7),r.setDate(r.getDate()+s),r.toISOString().split("T")[0]}return t}function kce(t){let e=t.trim(),n;const a=[];let i,r;const s=[];return e=e.replace(/(?:^|\s)p([1-4])(?:\s|$)/i,(c,p)=>(n=`P${p}`," ")),e=e.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g,(c,p)=>(a.push(p.toLowerCase())," ")),e=e.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g,(c,p)=>(i=p," ")),e=e.replace(/(?:^|\s)due:([^\s]+)/i,(c,p)=>(r=_ce(p)," ")),e=e.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g,(c,p)=>(s.push(p)," ")),{title:e.replace(/\s+/g," ").trim()||t,...n?{priority:n}:{},...a.length>0?{tags:a}:{},...i?{assignee:i}:{},...r?{due:r}:{},...s.length>0?{depends_on:s}:{}}}function wce(t){let e=-1;for(const n of t)for(const a of n.tasks){const i=a.id.match(/^t(\d+)$/);if(i){const r=parseInt(i[1],10);r>e&&(e=r)}}return"t"+(e+1)}async function xce(){const t=await tS();return await Promise.all(t.map(async n=>{const{frontmatter:a,body:i}=await Vz(n),r={...a,id:a.id||n,status:a.status||"Backlog"},{subtasks:s,bodyWithoutSubtasks:o}=Ds(i);return{id:n,frontmatter:r,body:o,subtasks:s}}))}async function Ace(t){const e=await sce(t),n=await Promise.all(e.map(async r=>{const s=await oce(t,r);return{id:r,result:s}})),a=[],i=[];for(const{id:r,result:s}of n)if(s.ok){const o={...s.task.frontmatter,id:s.task.frontmatter.id||r,status:s.task.frontmatter.status||"Backlog"},{subtasks:c,bodyWithoutSubtasks:p}=Ds(s.task.body);a.push({id:r,frontmatter:o,body:p,subtasks:c})}else{if(s.reason==="not-found")continue;i.push(r)}return{tasks:a,failedIds:i}}async function Cj(t,e,n){const a=[];for(const s of e){const o=s.name;s.tasks.forEach((c,p)=>{const d=(async()=>{const{frontmatter:m,body:f}=await Es(t,c.id);await iu(t,c.id,{...m,id:c.id,status:o,order:p},f)})();a.push({id:c.id,promise:d})})}const i=await Promise.allSettled(a.map(s=>s.promise)),r=[];return i.forEach((s,o)=>{s.status==="rejected"&&r.push(a[o].id)}),{failedIds:r}}function rl(t){Zoe(t.ui.theme,t.ui.skin,t.ui.font,t.ui.background)}function y3(t){return{title:t.frontmatter.title||t.id,status:t.frontmatter.status||"Backlog",body:t.body,subtasks:t.subtasks}}function L2(t){fu.clear(),t.forEach(e=>{fu.set(e.id,y3(e))})}function Cce(t){const e=t.split(/[\\/]+/).filter(Boolean),n=e[e.length-1];return n===".kandown"?e[e.length-2]??"Project":n??"Project"}function Nce(t,e){return e.reduce((n,a,i)=>{const r=t[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function Sce(t,e){const n=t.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""})),a=e.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""}));return t.title!==e.title||t.body!==e.body||JSON.stringify(n)!==JSON.stringify(a)}let $ce=0;const fu=new Map,Lf=new Map;let j2=null;const me=Roe((t,e)=>({isOpen:!1,loading:!1,dirHandle:null,projectName:null,tasksDirHandle:null,boardTitle:"Project Kanban",columns:[],archivedTasks:[],showArchives:!1,showMetadata:!0,taskContents:new Map,searchMatches:new Map,viewMode:localStorage.getItem("kandown:view")||"board",density:localStorage.getItem("kandown:density")||"comfortable",filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},commandOpen:!1,cheatsheetOpen:!1,drawerTaskId:null,drawerData:null,currentPage:"board",config:Ci,recentProjects:[],toasts:[],agentHook:null,isReloading:!1,lastReloadError:null,failedTaskIds:[],watcherError:null,hasUnsavedDrawerEdits:!1,lastSaveError:null,drawerRecoveryData:new Map,selectedTaskIds:[],toggleTaskSelection:n=>{t(a=>({selectedTaskIds:a.selectedTaskIds.includes(n)?a.selectedTaskIds.filter(s=>s!==n):[...a.selectedTaskIds,n]}))},clearTaskSelection:()=>t({selectedTaskIds:[]}),bulkMoveTasks:async n=>{const{selectedTaskIds:a,columns:i,moveTask:r}=e();for(const s of a){let o="Backlog";for(const c of i)if(c.tasks.some(p=>p.id===s)){o=c.name;break}await r(s,o,n)}t({selectedTaskIds:[]})},bulkDeleteTasks:async()=>{const{selectedTaskIds:n,deleteTask:a}=e();for(const i of n)await a(i);t({selectedTaskIds:[]})},drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{if(!qz()){const c=navigator.userAgent||"this browser";e().toast(new Goe(c).message,"error",8e3);return}let n;try{n=await nce()}catch(c){c instanceof eS?e().toast("Permission denied — please grant access to the project folder","error"):e().toast("Failed to open folder: "+c.message,"error");return}if(!n)return;const{projectHandle:a,kandownHandle:i,tasksHandle:r}=n,s=a.name;t({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`);const o=Ht()?T2():null;try{await P2({id:a.name,name:a.name,handle:a,lastOpened:Date.now(),...o?{kandownDir:o}:{}})}catch(c){console.warn("[Store] Failed to save recent project:",c)}await e().loadConfig(),await e().reloadBoard();try{const c=await Dy();t({recentProjects:c})}catch(c){console.warn("[Store] Failed to load recent projects:",c)}e().setupWatcher()},openRecentProject:async n=>{const a={dirHandle:e().dirHandle,tasksDirHandle:e().tasksDirHandle,projectName:e().projectName};if(!await kj(n.handle,!0)){let r=!1;try{await jy(n.handle),r=!0}catch{r=!1}if(!r){e().toast(`"${n.name}" is no longer accessible. Removed from recent projects.`,"warning",8e3);try{await fce(n.id);const s=await Dy();t({recentProjects:s})}catch{}return}e().toast("Permission denied — please grant access to the folder","error");return}try{const r=await jy(n.handle),s=await v3(n.handle),o=n.handle.name;t({dirHandle:r,tasksDirHandle:s,projectName:o}),window.history.pushState({},"",`?p=${encodeURIComponent(o)}`);try{await P2({...n,lastOpened:Date.now()})}catch(c){console.warn("[Store] Failed to update recent project:",c)}await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()}catch(r){t(a),e().toast(`Failed to open project: ${r.message}`,"error")}},openServerProject:async()=>{t({loading:!0});try{const n=T2();if(!n)throw new Error("No server root");await vj();const a=Cce(n),i=await Uz();rl(i);const r=await tS(),s=await Promise.all(r.map(async f=>{const{frontmatter:h,body:b}=await Vz(f),k={...h,id:h.id||f,status:h.status||"Backlog"},{subtasks:w,bodyWithoutSubtasks:A}=Ds(b);return{id:f,frontmatter:k,body:A,subtasks:w}}));L2(s);const o=s.map(f=>({frontmatter:f.frontmatter,body:qd(f.body,f.subtasks)})),c=$2(o,i.board.columns),p=E2(o),d=c.reduce((f,h)=>f+h.tasks.length,0),m=new Map;if(d<=10)for(const f of s)m.set(f.frontmatter.id,{frontmatter:f.frontmatter,subtasks:f.subtasks,body:f.body});t({loading:!1,isOpen:!0,config:i,columns:c,archivedTasks:p,boardTitle:"Project Kanban",projectName:a,taskContents:m,searchMatches:new Map}),window.history.pushState({},"",`?p=${encodeURIComponent(a)}`),e().setupWatcher(),e().refreshAgentHook()}catch{t({loading:!1,isOpen:!1}),e().toast("Impossible de charger le projet. Relancez `kandown`.","error")}},tryAutoOpenServerProject:async()=>{if(!Ht())return;const n=T2();if(!n)return;await vj();const a=await Dy(),i=a.find(p=>p.kandownDir===n);if(!i){await e().openServerProject();return}if(!await kj(i.handle,!0)){await e().openServerProject();return}const s=await jy(i.handle),o=await v3(i.handle),c=i.handle.name;t({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await P2({...i,lastOpened:Date.now()}),await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=e();if(!(!n&&!Ht()))try{const a=await ace(n);if(a.ok){t({config:a.config}),rl(a.config);return}if(a.reason==="corrupted"){if(a.rawContent&&n)try{const r=await(await n.getFileHandle("kandown.json.backup",{create:!0})).createWritable();try{await r.write(a.rawContent)}finally{await r.close()}}catch{}e().toast("kandown.json is corrupted — using default settings. A backup was saved as kandown.json.backup.","warning",1e4)}t({config:Ci}),rl(Ci)}catch{t({config:Ci}),rl(Ci)}},updateConfig:async n=>{const{dirHandle:a,config:i}=e();if(!a&&!Ht())return;const r=n(i);t({config:r}),rl(r);try{await ice(a,r)}catch(s){const o=s;o instanceof $o?e().toast("Disk is full — settings were not saved.","error",8e3):e().toast("Failed to save config: "+o.message,"error")}},reloadBoard:async()=>{const{tasksDirHandle:n,config:a}=e();t({isReloading:!0,lastReloadError:null});try{if(Ht()){const i=await xce();L2(i);const r=i.map(d=>({frontmatter:d.frontmatter,body:qd(d.body,d.subtasks)})),s=$2(r,a.board.columns),o=E2(r);t({boardTitle:"Project Kanban",columns:s,archivedTasks:o});const c=s.reduce((d,m)=>d+m.tasks.length,0),p=new Map;if(c<=10)for(const d of i)p.set(d.frontmatter.id,{frontmatter:d.frontmatter,subtasks:d.subtasks,body:d.body});t({taskContents:p,searchMatches:new Map,failedTaskIds:[],isReloading:!1})}else if(n){const{tasks:i,failedIds:r}=await Ace(n);L2(i);const s=i.map(m=>({frontmatter:m.frontmatter,body:qd(m.body,m.subtasks)})),o=$2(s,a.board.columns),c=E2(s);t({boardTitle:"Project Kanban",columns:o,archivedTasks:c});const p=o.reduce((m,f)=>m+f.tasks.length,0),d=new Map;if(p<=10)for(const m of i)d.set(m.frontmatter.id,{frontmatter:m.frontmatter,subtasks:m.subtasks,body:m.body});if(r.length>0){const m=r.length===1?`Task ${r[0]} could not be loaded`:`${r.length} tasks could not be loaded`;e().toast(m,"warning",8e3)}t({taskContents:d,searchMatches:new Map,failedTaskIds:r,isReloading:!1})}else t({isReloading:!1})}catch(i){const r=i.message||String(i);t({isReloading:!1,lastReloadError:`Failed to reload board: ${r}`}),e().toast(`Board reload failed — showing last loaded state (${r})`,"warning",8e3)}},moveTask:async(n,a,i,r)=>{const{columns:s,config:o,taskContents:c,searchMatches:p}=e(),d=Ht();if(!d&&!e().tasksDirHandle)return;const m=s.find(E=>E.name===a),f=s.find(E=>E.name===i);if(!m||!f)return;const h=m.tasks.findIndex(E=>E.id===n);if(h===-1)return;const b=m.tasks[h];if(!b)return;if(gce(i,o)){const E=new Map,T=Wz(o).toLowerCase();for(const D of s)for(const O of D.tasks){const H=O.frontmatter&&(O.frontmatter.archived===!0||O.frontmatter.archived==="true");E.set(O.id,{exists:!0,resolved:H||O.id===n||D.name.toLowerCase()===T})}const L=[];for(const D of b.dependsOn){if(typeof D!="string"||!D.trim()||D===n)continue;const O=E.get(D);(!O||!O.resolved)&&L.push(D)}if(L.length>0){const D=L.length===1?L[0]:`${L.slice(0,-1).join(", ")} and ${L[L.length-1]}`;e().toast(`Cannot move ${n} to ${i}: blocked by ${D}`,"error");return}}const k=s.map(E=>({...E,tasks:[...E.tasks]})),w=k.find(E=>E.name===a),A=k.find(E=>E.name===i),[S]=w.tasks.splice(h,1);/done|termin|closed|complet/i.test(i)?S.checked=!0:S.checked=!1,r!==void 0?A.tasks.splice(r,0,S):A.tasks.push(S),t({columns:k});try{const{tasksDirHandle:E}=e();if(!E&&!d)return;const T=a===i?k.filter(D=>D.name===i):k.filter(D=>D.name===a||D.name===i),{failedIds:L}=await Mf(()=>Cj(E??null,T,o.board.columns),{maxAttempts:3});if(L.length>0){const D=L.length===1?`Could not save move for ${L[0]}`:`${L.length} tasks could not be moved`;e().toast(D,"warning",8e3),await e().reloadBoard()}}catch(E){const T=E;T instanceof $o?e().toast("Disk is full — move was not saved. Free up space and try again.","error",8e3):e().toast("Failed to save: "+T.message,"error"),t({columns:s,taskContents:c,searchMatches:p})}},reorderInColumn:async(n,a,i)=>{const{columns:r,tasksDirHandle:s,config:o,taskContents:c,searchMatches:p}=e();if(!s&&!Ht())return;const d=r.map(h=>({...h,tasks:[...h.tasks]})),m=d.find(h=>h.name===n);if(!m)return;const[f]=m.tasks.splice(a,1);m.tasks.splice(i,0,f),t({columns:d});try{const{tasksDirHandle:h}=e(),b=Ht();if(!h&&!b)return;const{failedIds:k}=await Mf(()=>Cj(h??null,[m],o.board.columns),{maxAttempts:3});k.length>0&&(e().toast(`Could not save reorder for ${k.length} task(s)`,"warning",8e3),await e().reloadBoard())}catch(h){const b=h;b instanceof $o?e().toast("Disk is full — reorder was not saved.","error",8e3):e().toast("Failed to save: "+b.message,"error"),t({columns:r,taskContents:c,searchMatches:p})}},addColumn:async n=>{const a=n.trim();if(!a)return;const{config:i}=e();i.board.columns.some(r=>r.toLowerCase()===a.toLowerCase())||(await e().updateConfig(r=>({...r,board:{...r.board,columns:[...r.board.columns,a]}})),await e().reloadBoard())},renameColumn:async(n,a)=>{const i=a.trim(),{columns:r,tasksDirHandle:s,config:o}=e();if(!s||!i||i.toLowerCase()===n.toLowerCase())return;if(r.some(d=>d.name.toLowerCase()===i.toLowerCase())){e().toast("Column already exists","error");return}const c=r,p=r.map(d=>d.name===n?{...d,name:i}:d);t({columns:p});try{const d=c.find(m=>m.name===n);if(d){const f=(await Promise.allSettled(d.tasks.map(async(h,b)=>{const{frontmatter:k,body:w}=await Es(s,h.id);await iu(s,h.id,{...k,id:h.id,status:i,order:b},w)}))).filter(h=>h.status==="rejected").length;f>0&&e().toast(`${f} task(s) could not be renamed`,"warning",8e3)}await e().updateConfig(m=>{const f={...m.board.columnColors??{}},h=f[n.toLowerCase()];h&&(f[i.toLowerCase()]=h,delete f[n.toLowerCase()]);const b=m.board.columns.some(k=>k.toLowerCase()===n.toLowerCase())?m.board.columns:[...m.board.columns,n];return{...m,board:{...m.board,columns:b.map(k=>k.toLowerCase()===n.toLowerCase()?i:k),columnColors:f}}}),await e().reloadBoard()}catch(d){e().toast("Failed to rename column: "+d.message,"error"),t({columns:c})}},reorderColumns:async(n,a)=>{const{config:i,columns:r,tasksDirHandle:s}=e();if(n<0||a<0||n>=r.length||a>=r.length||n===a||!s&&!Ht())return;const o=Array.from(r),[c]=o.splice(n,1);o.splice(a,0,c),t({columns:o});const p=o.map(d=>d.name);try{await e().updateConfig(d=>({...d,board:{...d.board,columns:p}})),await e().reloadBoard()}catch(d){e().toast("Failed to reorder columns: "+d.message,"error"),t({columns:r})}},deleteColumn:async n=>{const{columns:a,tasksDirHandle:i}=e();if(!i&&!Ht())return;const r=a.find(o=>o.name===n);if(!r)return;const s=a;t({columns:a.filter(o=>o.name!==n)});try{const c=(await Promise.allSettled(r.tasks.map(p=>_j(i,p.id)))).filter(p=>p.status==="rejected").length;c>0&&e().toast(`${c} task(s) could not be deleted`,"warning",8e3),await e().updateConfig(p=>{const d={...p.board.columnColors??{}};return delete d[n.toLowerCase()],{...p,board:{...p.board,columns:p.board.columns.filter(m=>m.toLowerCase()!==n.toLowerCase()),columnColors:d}}}),await e().reloadBoard(),e().toast("Column deleted")}catch(o){e().toast("Failed to delete column: "+o.message,"error"),t({columns:s})}},createTask:async(n,a)=>{const{columns:i,tasksDirHandle:r,config:s,taskContents:o,searchMatches:c}=e();if(!r&&!Ht()||!i.length)return null;const p=n||s.board.columns[0]||i[0].name,d=wce(i),m=i.find(S=>S.name===p)?.tasks.length??0,f=a?kce(a):null,h={id:d,title:f?.title||"",checked:!1,dependsOn:f?.depends_on||[],tags:f?.tags||[],assignee:f?.assignee||null,priority:f?.priority||(s.fields.priority?s.board.defaultPriority:null),ownerType:s.fields.ownerType?s.board.defaultOwnerType:"",progress:null,frontmatter:{}},b=i.map(S=>S.name===p?{...S,tasks:[...S.tasks,h]}:S),k=new Map(o),w={id:d,title:f?.title||"",status:p,order:m,priority:f?.priority||(s.fields.priority?s.board.defaultPriority:""),tags:f?.tags||[],assignee:f?.assignee||"",due:f?.due||"",depends_on:f?.depends_on||[],created:new Date().toISOString().slice(0,10),ownerType:s.fields.ownerType?s.board.defaultOwnerType:"",tools:""},A="";k.set(d,{frontmatter:w,subtasks:[],body:A}),t({columns:b,taskContents:k});try{const S=r||null;return await Mf(()=>iu(S,d,w,A),{maxAttempts:3}),e().toast(`Created ${d.replace(/^t/,"")}`),await e().openDrawer(d),d}catch(S){const E=S;return E instanceof $o?e().toast("Disk is full — task was not created.","error",8e3):e().toast("Failed to create: "+E.message,"error"),t({columns:i,taskContents:o,searchMatches:c}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r,searchMatches:s}=e();if(!i&&!Ht())return;const o=a.map(d=>({...d,tasks:d.tasks.filter(m=>m.id!==n)}));t({columns:o});const c=new Map(r);c.delete(n);const p=new Map(s);p.delete(n),t({taskContents:c,searchMatches:p});try{await _j(i||null,n),e().toast("Deleted")}catch(d){const m=d;e().toast("Failed to delete: "+m.message,"error"),t({columns:a,taskContents:r,searchMatches:s})}},archiveTask:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a||null,n);await lce(a||null,n,{...i,archived:!0},r),e().drawerTaskId===n&&e().closeDrawer(),await e().reloadBoard(),e().toast("Archived")}catch(i){const r=i;r instanceof $o?e().toast("Disk is full — task was not archived.","error",8e3):e().toast("Failed to archive: "+r.message,"error")}},unarchiveTask:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a||null,n),s={...i};delete s.archived,await dce(a||null,n,s,r),await e().reloadBoard(),e().toast("Restored")}catch(i){const r=i;r instanceof $o?e().toast("Disk is full — task was not restored.","error",8e3):e().toast("Failed to restore: "+r.message,"error")}},setShowArchives:n=>t({showArchives:n}),setShowMetadata:n=>t({showMetadata:n}),openDrawer:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a,n),{subtasks:s,bodyWithoutSubtasks:o}=Ds(r),c={frontmatter:i,subtasks:s,body:o,savedAt:Date.now()},p=e().drawerRecoveryData.get(n),d=p?{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body}:{frontmatter:i,subtasks:s,body:o},m=new Map(e().drawerRecoveryData);m.delete(n),t({drawerTaskId:n,drawerData:d,drawerBaseVersion:c,conflictState:null,showConflictModal:!1,hasUnsavedDrawerEdits:!!p,lastSaveError:null,drawerRecoveryData:m}),p&&e().toast("Restored your unsaved edits for this task","info")}catch(i){e().toast("Failed to open: "+i.message,"error")}},closeDrawer:()=>t({drawerTaskId:null,drawerData:null,drawerBaseVersion:null,conflictState:null,showConflictModal:!1,hasUnsavedDrawerEdits:!1,lastSaveError:null}),markDrawerDirty:()=>t({hasUnsavedDrawerEdits:!0}),forceCloseDrawer:()=>{const{drawerTaskId:n,drawerData:a}=e();if(n&&a){const i=new Map(e().drawerRecoveryData);i.set(n,{frontmatter:a.frontmatter,subtasks:a.subtasks,body:a.body}),t({drawerRecoveryData:i})}e().closeDrawer()},updateDrawerData:n=>{const{drawerData:a}=e();a&&t({drawerData:n(a)})},saveDrawer:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=e();if(!n||!a)return;const s=qd(a.body,a.subtasks),o={...a.frontmatter,id:n};try{await Mf(()=>iu(i||null,n,o,s),{maxAttempts:3}),e().toast("Saved");const c=new Map(e().drawerRecoveryData);c.delete(n),t({drawerTaskId:null,drawerData:null,hasUnsavedDrawerEdits:!1,lastSaveError:null,drawerRecoveryData:c});const p=new Map(r);p.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),t({taskContents:p}),await e().reloadBoard()}catch(c){const p=c,d=p instanceof $o?"Disk is full — your edits are kept. Free up space and retry.":"Failed to save: "+p.message;e().toast(d,"error",8e3),t({lastSaveError:d})}},saveDrawerMetadata:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=e();if(!(!n||!a))try{const s=qd(a.body,a.subtasks),o={...a.frontmatter,id:n};await Mf(()=>iu(i||null,n,o,s),{maxAttempts:3});const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),t({taskContents:c,hasUnsavedDrawerEdits:!1,lastSaveError:null}),await e().reloadBoard()}catch(s){const o=s,c=o instanceof $o?"Disk is full — your edits are kept.":"Failed to save: "+o.message;t({hasUnsavedDrawerEdits:!0,lastSaveError:c})}},setViewMode:n=>{localStorage.setItem("kandown:view",n),t({viewMode:n})},setDensity:n=>{localStorage.setItem("kandown:density",n),t({density:n})},setFilter:(n,a)=>{if(t(i=>({filters:{...i.filters,[n]:a}})),n==="search"){const{columns:i,tasksDirHandle:r,taskContents:s}=e(),o=a,c=i.flatMap(p=>p.tasks.map(d=>d.id));if(r){const p=c.filter(d=>!s.has(d));p.length>0?e().loadTaskContents(p).then(()=>{e().computeSearchMatches(o)}):e().computeSearchMatches(o)}}},clearFilters:()=>t({filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},searchMatches:new Map}),setCommandOpen:n=>t({commandOpen:n}),setCheatsheetOpen:n=>t({cheatsheetOpen:n}),refreshAgentHook:async()=>{if(!Ht()){t({agentHook:null});return}const n=await Joe();t({agentHook:n?.agentHook??null})},sendTaskToAgent:async n=>{const a=e().agentHook;if(!a){e().toast("Agent hook not configured","error");return}e().toast(`Sending to ${a.label}…`);const i=await ece(n);if(i===null){e().toast("Could not reach the daemon","error");return}i.ok?e().toast(`Sent to ${a.label}`):e().toast(i.error||"Agent hook failed","error")},setCurrentPage:n=>t({currentPage:n}),loadTaskContents:async n=>{const{tasksDirHandle:a}=e();if(!a)return;const i=new Map(e().taskContents);await Promise.all(n.map(async r=>{if(!i.has(r))try{const{frontmatter:s,body:o}=await Es(a,r),{subtasks:c,bodyWithoutSubtasks:p}=Ds(o);i.set(r,{frontmatter:s,subtasks:c,body:p})}catch{}})),t({taskContents:i})},computeSearchMatches:n=>{if(!n.trim()){t({searchMatches:new Map});return}const{taskContents:a}=e(),i=new Map,r=n.toLowerCase();for(const[s,o]of a){const c=Bz(o,r);c.length>0&&i.set(s,c)}t({searchMatches:i})},toast:(n,a="success",i)=>{const r=++$ce,s=i??(a==="error"||a==="warning"?6e3:2500);t(o=>({toasts:[...o.toasts,{id:r,message:n,type:a}]})),setTimeout(()=>{t(o=>({toasts:o.toasts.filter(c=>c.id!==r)}))},s)},dismissToast:n=>t(a=>({toasts:a.toasts.filter(i=>i.id!==n)})),resolveConflict:async n=>{const{conflictState:a,drawerData:i,tasksDirHandle:r,drawerTaskId:s,drawerBaseVersion:o}=e();if(!(!a||!r||!s))if(n==="reload"){const{frontmatter:c,body:p}=await Es(r,s),{subtasks:d,bodyWithoutSubtasks:m}=Ds(p);t({drawerData:{frontmatter:c,subtasks:d,body:m},drawerBaseVersion:{frontmatter:c,subtasks:d,body:m,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),e().toast("Reloaded from disk")}else if(n==="overwrite"){if(i&&s&&o){const c=qd(i.body,i.subtasks),p={...i.frontmatter,id:s};try{await iu(r,s,p,c),t({drawerBaseVersion:{...i,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),e().toast("Overwritten remote changes")}catch(d){e().toast("Failed to overwrite: "+d.message,"error")}}}else t({conflictState:null,showConflictModal:!1})},setupWatcher:()=>{if(Ht()){j2&&clearInterval(j2),j2=setInterval(()=>{e().reloadBoard()},2e3);return}const{dirHandle:n,tasksDirHandle:a}=e();if(!n||!a)return;al.stop(),Lf.forEach(s=>clearTimeout(s)),Lf.clear(),al.start(n,a);const i=(s,o)=>{const c=Lf.get(s);c&&clearTimeout(c);const p=Math.max(2e3,e().config.notifications.editDebounceMs),d=setTimeout(()=>{Lf.delete(s);const m=e().config;m.notifications.taskEdits&&M2({title:"Task edited",body:`${o} changed on disk.`,config:m})},p);Lf.set(s,d)},r=async s=>{const{tasksDirHandle:o,config:c}=e();if(!o)return;let p,d;try{({frontmatter:p,body:d}=await Es(o,s))}catch(A){console.warn(`[Watcher] notifyTaskChange: failed to read ${s}:`,A);return}const{subtasks:m,bodyWithoutSubtasks:f}=Ds(d),h={id:s,frontmatter:{...p,id:p.id||s,status:p.status||"Backlog"},body:f,subtasks:m},b=y3(h),k=fu.get(s);if(!k){fu.set(s,b);return}c.notifications.statusChanges&&k.status!==b.status&&M2({title:"Task status changed",body:`${b.title}: ${k.status} → ${b.status}`,config:c});const w=Nce(k.subtasks,b.subtasks);c.notifications.subtaskCompletions&&w>0&&M2({title:"Subtask completed",body:w===1?`${b.title}: 1 subtask completed.`:`${b.title}: ${w} subtasks completed.`,config:c}),Sce(k,b)&&i(s,b.title),fu.set(s,b)};al.on("configChanged",()=>{try{e().loadConfig(),e().toast("Settings updated externally","info")}catch(s){console.error("[Watcher] configChanged handler error:",s)}}),al.on("taskChanged",async s=>{try{const{drawerTaskId:o,drawerBaseVersion:c,tasksDirHandle:p}=e();if(await r(s),o===s&&c&&p){let d,m;try{({frontmatter:d,body:m}=await Es(p,s))}catch(E){console.warn(`[Watcher] taskChanged: failed to re-read ${s}:`,E);return}const{subtasks:f,bodyWithoutSubtasks:h}=Ds(m),b=c,k=JSON.stringify(b.frontmatter)!==JSON.stringify(d),w=b.body!==h,A=JSON.stringify(b.subtasks)!==JSON.stringify(f);if(!k&&!w&&!A)return;let S="none";k&&(w||A)?S="full":k?S="metadata-only":(w||A)&&(S="body-only"),t({conflictState:{taskId:s,type:S,local:b,remote:{frontmatter:d,body:h,subtasks:f}},showConflictModal:S==="full"})}else await e().reloadBoard()}catch(o){console.error(`[Watcher] taskChanged handler error for ${s}:`,o)}}),al.on("newTaskDetected",async s=>{try{const{tasksDirHandle:o}=e();if(o){let c,p;try{({frontmatter:c,body:p}=await Es(o,s))}catch(f){console.warn(`[Watcher] newTaskDetected: failed to read ${s}:`,f),e().toast(`New task ${s} detected but could not be loaded`,"warning");return}const{subtasks:d,bodyWithoutSubtasks:m}=Ds(p);fu.set(s,y3({id:s,frontmatter:{...c,id:c.id||s,status:c.status||"Backlog"},body:m,subtasks:d}))}await e().reloadBoard()}catch(o){console.error(`[Watcher] newTaskDetected handler error for ${s}:`,o)}}),al.on("watcherError",s=>{t({watcherError:s}),e().toast(s,"warning",1e4)})},restartWatcher:()=>{const{dirHandle:n,tasksDirHandle:a}=e();!n||!a||(t({watcherError:null}),e().setupWatcher(),e().toast("File watcher restarted"))}}));Dy().then(t=>{me.setState({recentProjects:t})}).catch(t=>{console.warn("[Store] Failed to hydrate recent projects:",t)});rl(Ci);window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{rl(me.getState().config)});const Ece=Object.freeze(Object.defineProperty({__proto__:null,useStore:me},Symbol.toStringTag,{value:"Module"}));function Tce(){const t=me(o=>o.config),e=me(o=>o.updateConfig),[n,a]=$.useState(!1);if($.useEffect(()=>{a(!0)},[]),!n)return null;const i=t.ui.theme==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t.ui.theme,r=i==="dark",s=()=>{const o=r?"light":"dark";e(c=>({...c,ui:{...c.ui,theme:o}}))};return _.jsx(tt.button,{onClick:s,className:h3("relative inline-flex items-center rounded-full transition-all duration-300","border border-black/[0.08] dark:border-white/[0.12]","bg-black/[0.05] dark:bg-white/[0.08]","h-8 w-[52px] px-1"),title:r?"Switch to light mode":"Switch to dark mode",children:_.jsx(tt.div,{className:h3("inline-flex items-center justify-center rounded-full shadow-md",r?"bg-[#1e293b] border border-white/[0.15]":"bg-black border border-black/20","h-[26px] w-[26px]"),animate:{x:r?22:0},transition:{type:"spring",stiffness:500,damping:30},children:_.jsx(tt.div,{initial:{rotate:-90,scale:.5,opacity:0},animate:{rotate:0,scale:1,opacity:1},transition:{duration:.25,ease:"easeOut"},children:r?_.jsx(Zn.Moon,{size:13,className:"text-sky-300"}):_.jsx(Zn.Sun,{size:13,className:"text-amber-400"})},i)})})}function Pce(t){const e=Mse(t,{stiffness:180,damping:22,mass:.8});return $.useEffect(()=>{e.set(t)},[t,e]),wz(e,a=>Math.round(a).toString())}const _3="0.21.0",Mce=({className:t})=>_.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 150 150",className:t,fill:"currentColor",children:[_.jsx("path",{d:"m57.6 64.6 0.1-0.1v-31.3c-0.1-3.5-2.7-5.6-5.7-5.6h-16.3v59.6c0.2-1.3 0.9-2.6 1.7-3.2l20.2-19.4z"}),_.jsx("path",{d:"m87.6 43.8c-3.4 0.1-7.1 1.3-9.9 3.7l-38.5 38.7c-2.1 2.1-3.4 4.8-3.5 7.5v26.7l77.5-76.4v-0.2h-25.6z"}),_.jsx("path",{d:"m108.1 96.4-6 5-22.4-20.8-14.6 14.2 21.1 21.4-5 4.1c-0.6 0.5-0.3 0.9 0.2 0.9h27.6c0.4 0 0.7-0.4 0.7-0.6v-24.8c0-0.7-1.1-0.3-1.6 0.3v0.3z"})]});function Lce(){const{t}=Nn(),e=me(R=>R.dirHandle),n=me(R=>R.isOpen),a=me(R=>R.projectName),i=me(R=>R.archivedTasks.length),r=me(R=>R.showArchives),s=me(R=>R.setShowArchives),o=me(R=>R.columns),c=me(R=>R.openFolder),p=me(R=>R.reloadBoard),d=me(R=>R.createTask),m=me(R=>R.setCommandOpen),f=me(R=>R.viewMode),h=me(R=>R.setViewMode),b=me(R=>R.density),k=me(R=>R.setDensity),w=me(R=>R.setCurrentPage),A=me(R=>R.recentProjects),S=me(R=>R.openRecentProject),E=me(R=>R.filters),T=me(R=>R.setFilter),L=me(R=>R.clearFilters),D=me(R=>R.config.fields),O=me(R=>R.lastReloadError),H=me(R=>R.watcherError),z=me(R=>R.restartWatcher),[j,K]=$.useState(!1),[Y,ee]=$.useState(!0),U=$.useRef(null),J=$.useRef(null),W=o.reduce((R,Z)=>R+Z.tasks.length,0),V=Pce(W),B=[];D.priority&&E.priority&&B.push({type:"priority",label:E.priority,value:E.priority}),D.tags&&E.tag&&B.push({type:"tag",label:"#"+E.tag,value:E.tag}),D.assignee&&E.assignee&&B.push({type:"assignee",label:"@"+E.assignee,value:E.assignee});const ne=[{label:t("filterBar.ownerAll"),value:""},{label:t("filterBar.ownerHuman"),value:"human"},{label:t("filterBar.ownerAI"),value:"ai"}],I=B.length>0||E.search||D.ownerType&&E.ownerType;return $.useEffect(()=>{if(!j)return;const R=Z=>{U.current&&!U.current.contains(Z.target)&&K(!1)};return document.addEventListener("mousedown",R),()=>document.removeEventListener("mousedown",R)},[j]),$.useEffect(()=>{const R=setTimeout(()=>ee(!1),5e3);return()=>clearTimeout(R)},[]),$.useEffect(()=>{document.title=a?`${a} · Kandown`:"Kandown"},[a]),_.jsxs(_.Fragment,{children:[(O||H)&&_.jsxs("div",{className:"flex items-center gap-2 px-5 py-1.5 bg-amber-500/10 border-b border-amber-500/30 text-[12px] text-amber-700 dark:text-amber-300",children:[_.jsx("span",{className:"flex-1 truncate",children:H?`⚠ ${H}`:`⚠ ${O}`}),H&&_.jsx("button",{type:"button",onClick:z,className:"px-2 py-0.5 rounded bg-amber-500/20 hover:bg-amber-500/30 text-[11.5px] font-medium",children:"Restart watcher"}),_.jsx("button",{type:"button",onClick:p,className:"px-2 py-0.5 rounded bg-amber-500/20 hover:bg-amber-500/30 text-[11.5px] font-medium",children:"Reload"})]}),_.jsxs("header",{className:"flex items-center justify-between px-5 h-[64px] border-b border-border bg-card/80 backdrop-blur-xl relative z-10",children:[_.jsxs("div",{className:"flex items-center gap-4 min-w-0",children:[_.jsx("div",{className:"flex items-center gap-2.5 flex-shrink-0",children:_.jsxs("button",{onClick:()=>window.history.pushState({},"",window.location.pathname),className:"flex items-center gap-2 cursor-pointer",children:[_.jsx(Mce,{className:"w-[34px] h-[34px] dark:text-white text-black"}),_.jsx(Xi,{mode:"wait",initial:!1,children:Y?_.jsxs(tt.span,{className:"flex items-center gap-2",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.45},children:[_.jsx("span",{className:"text-[15px] font-semibold tracking-tight text-fg",children:"kandown"}),_.jsxs("span",{className:"inline-flex items-center h-5 px-1.5 text-[10.5px] font-semibold text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-500/15 rounded-md",children:["v",_3]})]},"boot"):a?_.jsx(tt.span,{className:"text-[15px] font-semibold tracking-tight text-fg truncate max-w-[240px]",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.45},children:a},"project"):null})]})}),e&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),_.jsxs("div",{className:"flex items-center gap-2 px-3 h-9 bg-secondary/60 border border-border rounded-xl min-w-[200px] max-w-[280px] focus-within:border-border-focus focus-within:bg-secondary transition-all",children:[_.jsx(Zn.Search,{size:14,className:"text-fg-muted/60 flex-shrink-0"}),_.jsx("input",{ref:J,type:"text",placeholder:t("filterBar.searchPlaceholder"),value:E.search,onChange:R=>T("search",R.target.value),className:"bg-transparent border-none outline-none text-fg text-[13px] w-full placeholder:text-fg-muted/60"}),E.search?_.jsx("button",{onClick:()=>T("search",""),className:"text-fg-muted/60 hover:text-fg flex-shrink-0",children:_.jsx(Zn.X,{size:14})}):_.jsx("kbd",{className:"inline-flex items-center h-5 px-1.5 text-[10px] font-medium text-fg-muted/50 bg-black/[0.04] dark:bg-white/[0.08] rounded border border-black/[0.06] dark:border-white/[0.1]",children:"⌘K"})]}),_.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap overflow-hidden",children:[_.jsx(Xi,{children:B.map(R=>_.jsxs(tt.button,{initial:{opacity:0,scale:.9},animate:{opacity:1,scale:1},exit:{opacity:0,scale:.9},transition:{duration:.15},onClick:()=>T(R.type,null),className:"inline-flex items-center gap-1 h-6 px-2.5 text-[12px] text-fg bg-black/[0.05] dark:bg-white/[0.1] border border-black/[0.08] dark:border-white/[0.12] rounded-lg hover:bg-black/[0.08] dark:hover:bg-white/[0.15] transition-colors",children:[R.label,_.jsx(Zn.X,{size:10,className:"text-fg-muted/60"})]},R.type+R.value))}),D.ownerType&&_.jsx("div",{className:"flex items-center h-6 border border-black/[0.06] dark:border-white/[0.1] rounded-lg overflow-hidden",children:ne.map(R=>_.jsx("button",{onClick:()=>T("ownerType",R.value),className:`h-full px-2.5 text-[12px] transition-colors ${E.ownerType===R.value?"bg-black/[0.06] dark:bg-white/[0.12] text-fg":"text-fg-muted/70 hover:text-fg"}`,children:R.label},R.value))}),I&&_.jsx("button",{onClick:L,className:"text-[12px] text-fg-muted/60 hover:text-fg transition-colors",children:t("filterBar.clearAll")})]})]}),e&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),_.jsxs("div",{className:"relative flex-shrink-0",ref:U,children:[_.jsxs("button",{onClick:()=>K(R=>!R),className:"flex items-center gap-1.5 px-2.5 py-1.5 text-[13px] text-fg-muted hover:text-fg hover:bg-black/[0.04] dark:hover:bg-white/[0.06] rounded-lg transition-colors border border-transparent hover:border-black/[0.06] dark:hover:border-white/[0.1]",children:[_.jsx(Zn.Folder,{size:13,className:"text-fg-muted/70"}),_.jsxs("span",{className:"font-medium",children:[".",e.name]}),_.jsx(Zn.ChevronDown,{size:11,className:"opacity-50"})]}),_.jsx(Xi,{children:j&&_.jsx(tt.div,{initial:{opacity:0,y:-4,scale:.98},animate:{opacity:1,y:0,scale:1},exit:{opacity:0,y:-4,scale:.98},transition:{duration:.12},className:"absolute top-full left-0 mt-2 min-w-[240px] glass rounded-xl shadow-[0_16px_48px_rgba(0,0,0,0.5)] overflow-hidden z-50",children:_.jsxs("div",{className:"py-1.5",children:[A.length>0&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-muted/60",children:t("header.recentProjects")}),A.map(R=>_.jsxs("button",{onClick:()=>{K(!1),S(R)},className:"w-full flex items-center gap-2 px-3 py-2 text-[13.5px] text-left hover:bg-black/[0.04] dark:hover:bg-white/[0.06] transition-colors",children:[_.jsx(Zn.Folder,{size:12,className:"text-fg-muted/60"}),_.jsx("span",{className:"truncate",children:R.name}),R.id===e.name&&_.jsx(Zn.Check,{size:12,className:"ml-auto text-emerald-500"})]},R.id)),_.jsx("div",{className:"h-px bg-black/[0.06] dark:bg-white/[0.08] my-1.5 mx-2"})]}),_.jsxs("button",{onClick:()=>{K(!1),c()},className:"w-full flex items-center gap-2 px-3 py-2 text-[13.5px] text-left hover:bg-black/[0.04] dark:hover:bg-white/[0.06] transition-colors",children:[_.jsx(Zn.Plus,{size:12,className:"text-fg-muted/60"}),_.jsx("span",{children:t("header.openFolder...")})]})]})})})]})]})]}),_.jsx("div",{className:"flex items-center gap-2 flex-shrink-0",children:n||e?_.jsxs(_.Fragment,{children:[_.jsxs("div",{className:"flex items-center gap-2 mr-2 text-[12.5px] text-fg-muted/70",children:[_.jsx("span",{className:"inline-block w-1.5 h-1.5 rounded-full bg-emerald-500"}),_.jsx(tt.span,{className:"tabular-nums font-medium",children:V}),_.jsx("span",{children:t("header.tasks")})]}),_.jsxs("div",{className:"flex items-center bg-black/[0.04] dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.1] rounded-xl p-0.5 h-10",children:[_.jsx("button",{onClick:()=>h("board"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${f==="board"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:t("common.board"),children:_.jsx(Zn.LayoutBoard,{size:18})}),_.jsx("button",{onClick:()=>h("list"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${f==="list"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:t("common.list"),children:_.jsx(Zn.LayoutList,{size:18})})]}),_.jsx(na,{variant:"icon",icon:"Archive",onClick:()=>s(!r),title:r?t("header.backToBoard"):`${t("header.archives")} (${i})`,className:r?"text-accent":""}),_.jsx(na,{variant:"icon",icon:"Density",onClick:()=>k(b==="compact"?"comfortable":"compact"),title:`Density: ${b}`}),_.jsx(na,{variant:"icon",icon:"Settings",onClick:()=>w("settings"),title:t("common.settings")}),_.jsx(Tce,{}),_.jsx("div",{className:"w-px h-5 bg-black/[0.08] dark:bg-white/[0.08] mx-1"}),_.jsx(na,{variant:"secondary",icon:"Search",label:t("common.search"),shortcut:"⌘K",onClick:()=>m(!0),title:"Command palette (⌘K)"}),_.jsx(na,{variant:"icon",icon:"Refresh",onClick:p,title:`${t("common.reload")} (R)`}),_.jsx(na,{variant:"primary",icon:"Plus",label:t("common.newTask"),shortcut:"N",onClick:()=>d()})]}):_.jsx(na,{variant:"primary",label:t("common.openFolder"),onClick:c})})]})]})}/**
|
|
116
|
+
`}}async function Zz(t,e){try{return await(await(await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}async function rce(t,e){try{return await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`),!0}catch{return!1}}async function Gz(t,e){try{return await t.getDirectoryHandle("archive",{create:e})}catch{return null}}async function sce(t){if(Ht())return tS();const e=new Set;for await(const n of t.values())n.kind==="file"&&n.name.endsWith(".md")&&e.add(n.name.slice(0,-3));try{const n=await t.getDirectoryHandle("archive",{create:!1});for await(const a of n.values())a.kind==="file"&&a.name.endsWith(".md")&&e.add(a.name.slice(0,-3))}catch{}return[...e].sort((n,a)=>n.localeCompare(a,void 0,{numeric:!0}))}async function Es(t,e){if(Ht())try{const i=await nS(e);return Rg(i)}catch{return yj(e)}const a=await(async i=>{try{return await(await(await i.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}})(t)??await Zz(t,e);return a!==null?Rg(a):yj(e)}async function oce(t,e){if(Ht())try{const a=await nS(e);return{ok:!0,task:Rg(a)}}catch(a){return a.name==="NotFoundError"?{ok:!1,reason:"not-found"}:{ok:!1,reason:"unknown",error:a}}let n=null;try{n=await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{}if(n===null)try{n=await Zz(t,e)}catch(a){return{ok:!1,reason:"unknown",error:a}}if(n===null)return{ok:!1,reason:"not-found"};if(n.trim()==="")return{ok:!1,reason:"corrupted",error:new Error(`Task file ${e}.md is empty`)};try{return{ok:!0,task:Rg(n)}}catch(a){return{ok:!1,reason:"corrupted",error:a}}}async function iu(t,e,n,a){const i=J4(n,a);if(Ht())return Xoe(e,i);try{const o=await(await(await rce(t,e)?await Gz(t,!0):t).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}}catch(r){throw nk(r,`${e}.md`)}}async function _j(t,e){if(Ht())return Qoe(e);try{await t.removeEntry(`${e}.md`)}catch{}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}async function cce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/archive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function pce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/unarchive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function lce(t,e,n,a){const i=J4(n,a);if(Ht())return cce(e,i);try{const o=await(await(await Gz(t,!0)).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}try{await t.removeEntry(`${e}.md`)}catch{}}catch(r){throw nk(r,`archive/${e}.md`)}}async function dce(t,e,n,a){const i=J4(n,a);if(Ht())return pce(e,i);try{const s=await(await t.getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await s.write(i)}finally{await s.close()}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}catch(r){throw nk(r,`${e}.md`)}}const uce="kanban-md",mce=1,ap="recentProjects";function aS(){return new Promise((t,e)=>{const n=indexedDB.open(uce,mce);n.onerror=()=>e(n.error),n.onsuccess=()=>t(n.result),n.onupgradeneeded=()=>{const a=n.result;a.objectStoreNames.contains(ap)||a.createObjectStore(ap,{keyPath:"id"})}})}async function P2(t){const e=await aS();return new Promise((n,a)=>{const i=e.transaction(ap,"readwrite");i.objectStore(ap).put(t),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function Dy(){const t=await aS();return new Promise((e,n)=>{const i=t.transaction(ap,"readonly").objectStore(ap).getAll();i.onsuccess=()=>{const r=i.result||[];r.sort((s,o)=>o.lastOpened-s.lastOpened),e(r.slice(0,10))},i.onerror=()=>n(i.error)})}async function fce(t){const e=await aS();return new Promise((n,a)=>{const i=e.transaction(ap,"readwrite");i.objectStore(ap).delete(t),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function kj(t,e=!0){const n={mode:e?"readwrite":"read"};try{return await t.queryPermission(n)==="granted"||await t.requestPermission(n)==="granted"}catch(a){return console.warn("[FS] verifyPermission: handle is no longer valid:",a),!1}}function Wz(t=Ci){const e=t.board.columns;return e[e.length-1]??"Done"}function gce(t,e=Ci){return t===Wz(e)||t.toLowerCase()==="archived"}class hce{dirHandle=null;tasksDirHandle=null;intervalId=null;configHash=null;taskHashes=new Map;knownTaskIds=new Set;listeners=new Map;debounceTimers=new Map;debounceDelay=150;consecutiveErrors=0;maxConsecutiveErrors=5;disabled=!1;eventSource=null;start(e,n){this.dirHandle=e,this.tasksDirHandle=n,this.consecutiveErrors=0,this.disabled=!1,Ht()&&this.startServerSse(),e&&n&&(this.initHashes(),this.intervalId=setInterval(()=>void this.tick(),300))}startServerSse(){if(typeof window>"u"||!Ht()||this.eventSource)return;const e=window.__KANDOWN_TOKEN__,n=e?`/api/events?token=${encodeURIComponent(e)}`:"/api/events";try{this.eventSource=new EventSource(n),this.eventSource.onmessage=a=>{try{const i=JSON.parse(a.data);i.type==="change"&&(i.taskId?this.emit("taskChanged",i.taskId):(this.emit("configChanged"),this.emit("taskChanged","")))}catch{}}}catch(a){console.warn("[Watcher] EventSource init failed:",a)}}stop(){this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.configHash=null,this.taskHashes.clear(),this.knownTaskIds.clear(),this.listeners.clear(),this.debounceTimers.forEach(e=>clearTimeout(e)),this.debounceTimers.clear(),this.consecutiveErrors=0,this.disabled=!1}isDisabled(){return this.disabled}on(e,n){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(n),()=>{this.listeners.get(e)?.delete(n)}}getKnownTaskIds(){return Array.from(this.knownTaskIds)}async initHashes(){if(!(!this.dirHandle||!this.tasksDirHandle))try{const e=await wj(this.dirHandle);e!==null&&(this.configHash=await this.hash(e)),await this.syncTaskDir(!1)}catch(e){console.warn("[Watcher] initHashes error:",e)}}async tick(){if(!(!this.dirHandle||!this.tasksDirHandle)&&!this.disabled)try{const e=await wj(this.dirHandle);if(e!==null){const n=await this.hash(e);this.configHash!==null&&n!==this.configHash&&(this.configHash=n,this.debouncedEmit("configChanged"))}for(const n of[...this.knownTaskIds])try{const a=await xj(this.tasksDirHandle,n);if(a!==null){const i=await this.hash(a),r=this.taskHashes.get(n);r!==void 0&&i!==r&&(this.taskHashes.set(n,i),this.debouncedEmit("taskChanged",n))}}catch(a){console.warn(`[Watcher] Error reading task ${n}:`,a)}await this.syncTaskDir(!0),this.consecutiveErrors=0}catch(e){this.consecutiveErrors++,console.error(`[Watcher] Tick failed (${this.consecutiveErrors}/${this.maxConsecutiveErrors}):`,e),this.consecutiveErrors>=this.maxConsecutiveErrors&&this.disable(`File watcher stopped after ${this.maxConsecutiveErrors} consecutive errors: ${e.message}`)}}disable(e){this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.disabled=!0,this.emit("watcherError",e)}debouncedEmit(e,...n){const a=e+JSON.stringify(n),i=this.debounceTimers.get(a);i&&clearTimeout(i);const r=setTimeout(()=>{this.debounceTimers.delete(a),this.emit(e,...n)},this.debounceDelay);this.debounceTimers.set(a,r)}async syncTaskDir(e){if(!this.tasksDirHandle)return;let n;try{n=this.tasksDirHandle.values()}catch(a){throw a}for await(const a of n){if(a.kind!=="file"||!a.name.endsWith(".md"))continue;const i=a.name.replace(".md","");if(!this.knownTaskIds.has(i))try{this.knownTaskIds.add(i);const r=await xj(this.tasksDirHandle,i);r!==null&&(this.taskHashes.set(i,await this.hash(r)),e&&this.debouncedEmit("newTaskDetected",i))}catch(r){console.warn(`[Watcher] syncTaskDir: failed to read ${i}:`,r)}}}async hash(e){const a=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(i)).map(s=>s.toString(16).padStart(2,"0")).join("")}emit(e,...n){this.listeners.get(e)?.forEach(i=>{if(e==="configChanged"){i();return}if(e==="taskChanged"){const[s]=n;i(s);return}if(e==="newTaskDetected"){const[s]=n;i(s);return}const[r]=n;i(r)})}}async function wj(t){try{return await(await(await t.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function xj(t,e){try{return await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}const al=new hce,bce={soft:[{frequency:520,durationMs:90,delayMs:0,type:"sine"},{frequency:720,durationMs:120,delayMs:95,type:"sine"}],chime:[{frequency:660,durationMs:120,delayMs:0,type:"triangle"},{frequency:880,durationMs:160,delayMs:125,type:"triangle"}],ping:[{frequency:920,durationMs:110,delayMs:0,type:"sine"}],pop:[{frequency:260,durationMs:55,delayMs:0,type:"square"},{frequency:520,durationMs:80,delayMs:60,type:"square"}]};let Aj=null;function Kz(){return"Notification"in window?Notification.permission:"unsupported"}async function vce(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function M2({title:t,body:e,config:n}){if(n.notifications.webhookUrl&&fetch(n.notifications.webhookUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event:"kandown_notification",title:t,body:e,timestamp:new Date().toISOString()})}).catch(()=>{}),n.notifications.sound&&yce(n.notifications.soundId),!(!n.notifications.browser||Kz()!=="granted"))try{new Notification(t,{body:e})}catch{}}function yce(t){const e=bce[t],n=window.AudioContext??window.webkitAudioContext;if(!(!n||e.length===0))try{Aj??=new n;const a=Aj;a.state==="suspended"&&a.resume();const i=a.currentTime+.01;e.forEach(r=>{const s=a.createOscillator(),o=a.createGain(),c=i+r.delayMs/1e3,p=c+r.durationMs/1e3;s.type=r.type??"sine",s.frequency.setValueAtTime(r.frequency,c),o.gain.setValueAtTime(1e-4,c),o.gain.exponentialRampToValueAtTime(.08,c+.01),o.gain.exponentialRampToValueAtTime(1e-4,p),s.connect(o),o.connect(a.destination),s.start(c),s.stop(p+.02)})}catch{}}async function Mf(t,e={}){const{maxAttempts:n=3,baseDelayMs:a=500,retryableCheck:i=Woe}=e;let r;for(let s=1;s<=n;s++)try{return await t()}catch(o){if(r=o,!i(o))throw o;if(s<n){const c=a*Math.pow(2,s-1);await new Promise(p=>setTimeout(p,c))}}throw r}function _ce(t){const e=t.toLowerCase(),n=new Date;if(e==="today")return n.toISOString().split("T")[0];if(e==="tomorrow"){const i=new Date(n);return i.setDate(i.getDate()+1),i.toISOString().split("T")[0]}const a=["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];if(a.includes(e)){const i=a.indexOf(e),r=new Date(n);let s=i-r.getDay();return s<=0&&(s+=7),r.setDate(r.getDate()+s),r.toISOString().split("T")[0]}return t}function kce(t){let e=t.trim(),n;const a=[];let i,r;const s=[];return e=e.replace(/(?:^|\s)p([1-4])(?:\s|$)/i,(c,p)=>(n=`P${p}`," ")),e=e.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g,(c,p)=>(a.push(p.toLowerCase())," ")),e=e.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g,(c,p)=>(i=p," ")),e=e.replace(/(?:^|\s)due:([^\s]+)/i,(c,p)=>(r=_ce(p)," ")),e=e.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g,(c,p)=>(s.push(p)," ")),{title:e.replace(/\s+/g," ").trim()||t,...n?{priority:n}:{},...a.length>0?{tags:a}:{},...i?{assignee:i}:{},...r?{due:r}:{},...s.length>0?{depends_on:s}:{}}}function wce(t){let e=-1;for(const n of t)for(const a of n.tasks){const i=a.id.match(/^t(\d+)$/);if(i){const r=parseInt(i[1],10);r>e&&(e=r)}}return"t"+(e+1)}async function xce(){const t=await tS();return await Promise.all(t.map(async n=>{const{frontmatter:a,body:i}=await Vz(n),r={...a,id:a.id||n,status:a.status||"Backlog"},{subtasks:s,bodyWithoutSubtasks:o}=Ds(i);return{id:n,frontmatter:r,body:o,subtasks:s}}))}async function Ace(t){const e=await sce(t),n=await Promise.all(e.map(async r=>{const s=await oce(t,r);return{id:r,result:s}})),a=[],i=[];for(const{id:r,result:s}of n)if(s.ok){const o={...s.task.frontmatter,id:s.task.frontmatter.id||r,status:s.task.frontmatter.status||"Backlog"},{subtasks:c,bodyWithoutSubtasks:p}=Ds(s.task.body);a.push({id:r,frontmatter:o,body:p,subtasks:c})}else{if(s.reason==="not-found")continue;i.push(r)}return{tasks:a,failedIds:i}}async function Cj(t,e,n){const a=[];for(const s of e){const o=s.name;s.tasks.forEach((c,p)=>{const d=(async()=>{const{frontmatter:m,body:f}=await Es(t,c.id);await iu(t,c.id,{...m,id:c.id,status:o,order:p},f)})();a.push({id:c.id,promise:d})})}const i=await Promise.allSettled(a.map(s=>s.promise)),r=[];return i.forEach((s,o)=>{s.status==="rejected"&&r.push(a[o].id)}),{failedIds:r}}function rl(t){Zoe(t.ui.theme,t.ui.skin,t.ui.font,t.ui.background)}function y3(t){return{title:t.frontmatter.title||t.id,status:t.frontmatter.status||"Backlog",body:t.body,subtasks:t.subtasks}}function L2(t){fu.clear(),t.forEach(e=>{fu.set(e.id,y3(e))})}function Cce(t){const e=t.split(/[\\/]+/).filter(Boolean),n=e[e.length-1];return n===".kandown"?e[e.length-2]??"Project":n??"Project"}function Nce(t,e){return e.reduce((n,a,i)=>{const r=t[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function Sce(t,e){const n=t.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""})),a=e.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""}));return t.title!==e.title||t.body!==e.body||JSON.stringify(n)!==JSON.stringify(a)}let $ce=0;const fu=new Map,Lf=new Map;let j2=null;const me=Roe((t,e)=>({isOpen:!1,loading:!1,dirHandle:null,projectName:null,tasksDirHandle:null,boardTitle:"Project Kanban",columns:[],archivedTasks:[],showArchives:!1,showMetadata:!0,taskContents:new Map,searchMatches:new Map,viewMode:localStorage.getItem("kandown:view")||"board",density:localStorage.getItem("kandown:density")||"comfortable",filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},commandOpen:!1,cheatsheetOpen:!1,drawerTaskId:null,drawerData:null,currentPage:"board",config:Ci,recentProjects:[],toasts:[],agentHook:null,isReloading:!1,lastReloadError:null,failedTaskIds:[],watcherError:null,hasUnsavedDrawerEdits:!1,lastSaveError:null,drawerRecoveryData:new Map,selectedTaskIds:[],toggleTaskSelection:n=>{t(a=>({selectedTaskIds:a.selectedTaskIds.includes(n)?a.selectedTaskIds.filter(s=>s!==n):[...a.selectedTaskIds,n]}))},clearTaskSelection:()=>t({selectedTaskIds:[]}),bulkMoveTasks:async n=>{const{selectedTaskIds:a,columns:i,moveTask:r}=e();for(const s of a){let o="Backlog";for(const c of i)if(c.tasks.some(p=>p.id===s)){o=c.name;break}await r(s,o,n)}t({selectedTaskIds:[]})},bulkDeleteTasks:async()=>{const{selectedTaskIds:n,deleteTask:a}=e();for(const i of n)await a(i);t({selectedTaskIds:[]})},drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{if(!qz()){const c=navigator.userAgent||"this browser";e().toast(new Goe(c).message,"error",8e3);return}let n;try{n=await nce()}catch(c){c instanceof eS?e().toast("Permission denied — please grant access to the project folder","error"):e().toast("Failed to open folder: "+c.message,"error");return}if(!n)return;const{projectHandle:a,kandownHandle:i,tasksHandle:r}=n,s=a.name;t({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`);const o=Ht()?T2():null;try{await P2({id:a.name,name:a.name,handle:a,lastOpened:Date.now(),...o?{kandownDir:o}:{}})}catch(c){console.warn("[Store] Failed to save recent project:",c)}await e().loadConfig(),await e().reloadBoard();try{const c=await Dy();t({recentProjects:c})}catch(c){console.warn("[Store] Failed to load recent projects:",c)}e().setupWatcher()},openRecentProject:async n=>{const a={dirHandle:e().dirHandle,tasksDirHandle:e().tasksDirHandle,projectName:e().projectName};if(!await kj(n.handle,!0)){let r=!1;try{await jy(n.handle),r=!0}catch{r=!1}if(!r){e().toast(`"${n.name}" is no longer accessible. Removed from recent projects.`,"warning",8e3);try{await fce(n.id);const s=await Dy();t({recentProjects:s})}catch{}return}e().toast("Permission denied — please grant access to the folder","error");return}try{const r=await jy(n.handle),s=await v3(n.handle),o=n.handle.name;t({dirHandle:r,tasksDirHandle:s,projectName:o}),window.history.pushState({},"",`?p=${encodeURIComponent(o)}`);try{await P2({...n,lastOpened:Date.now()})}catch(c){console.warn("[Store] Failed to update recent project:",c)}await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()}catch(r){t(a),e().toast(`Failed to open project: ${r.message}`,"error")}},openServerProject:async()=>{t({loading:!0});try{const n=T2();if(!n)throw new Error("No server root");await vj();const a=Cce(n),i=await Uz();rl(i);const r=await tS(),s=await Promise.all(r.map(async f=>{const{frontmatter:h,body:b}=await Vz(f),k={...h,id:h.id||f,status:h.status||"Backlog"},{subtasks:w,bodyWithoutSubtasks:A}=Ds(b);return{id:f,frontmatter:k,body:A,subtasks:w}}));L2(s);const o=s.map(f=>({frontmatter:f.frontmatter,body:qd(f.body,f.subtasks)})),c=$2(o,i.board.columns),p=E2(o),d=c.reduce((f,h)=>f+h.tasks.length,0),m=new Map;if(d<=10)for(const f of s)m.set(f.frontmatter.id,{frontmatter:f.frontmatter,subtasks:f.subtasks,body:f.body});t({loading:!1,isOpen:!0,config:i,columns:c,archivedTasks:p,boardTitle:"Project Kanban",projectName:a,taskContents:m,searchMatches:new Map}),window.history.pushState({},"",`?p=${encodeURIComponent(a)}`),e().setupWatcher(),e().refreshAgentHook()}catch{t({loading:!1,isOpen:!1}),e().toast("Impossible de charger le projet. Relancez `kandown`.","error")}},tryAutoOpenServerProject:async()=>{if(!Ht())return;const n=T2();if(!n)return;await vj();const a=await Dy(),i=a.find(p=>p.kandownDir===n);if(!i){await e().openServerProject();return}if(!await kj(i.handle,!0)){await e().openServerProject();return}const s=await jy(i.handle),o=await v3(i.handle),c=i.handle.name;t({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await P2({...i,lastOpened:Date.now()}),await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=e();if(!(!n&&!Ht()))try{const a=await ace(n);if(a.ok){t({config:a.config}),rl(a.config);return}if(a.reason==="corrupted"){if(a.rawContent&&n)try{const r=await(await n.getFileHandle("kandown.json.backup",{create:!0})).createWritable();try{await r.write(a.rawContent)}finally{await r.close()}}catch{}e().toast("kandown.json is corrupted — using default settings. A backup was saved as kandown.json.backup.","warning",1e4)}t({config:Ci}),rl(Ci)}catch{t({config:Ci}),rl(Ci)}},updateConfig:async n=>{const{dirHandle:a,config:i}=e();if(!a&&!Ht())return;const r=n(i);t({config:r}),rl(r);try{await ice(a,r)}catch(s){const o=s;o instanceof $o?e().toast("Disk is full — settings were not saved.","error",8e3):e().toast("Failed to save config: "+o.message,"error")}},reloadBoard:async()=>{const{tasksDirHandle:n,config:a}=e();t({isReloading:!0,lastReloadError:null});try{if(Ht()){const i=await xce();L2(i);const r=i.map(d=>({frontmatter:d.frontmatter,body:qd(d.body,d.subtasks)})),s=$2(r,a.board.columns),o=E2(r);t({boardTitle:"Project Kanban",columns:s,archivedTasks:o});const c=s.reduce((d,m)=>d+m.tasks.length,0),p=new Map;if(c<=10)for(const d of i)p.set(d.frontmatter.id,{frontmatter:d.frontmatter,subtasks:d.subtasks,body:d.body});t({taskContents:p,searchMatches:new Map,failedTaskIds:[],isReloading:!1})}else if(n){const{tasks:i,failedIds:r}=await Ace(n);L2(i);const s=i.map(m=>({frontmatter:m.frontmatter,body:qd(m.body,m.subtasks)})),o=$2(s,a.board.columns),c=E2(s);t({boardTitle:"Project Kanban",columns:o,archivedTasks:c});const p=o.reduce((m,f)=>m+f.tasks.length,0),d=new Map;if(p<=10)for(const m of i)d.set(m.frontmatter.id,{frontmatter:m.frontmatter,subtasks:m.subtasks,body:m.body});if(r.length>0){const m=r.length===1?`Task ${r[0]} could not be loaded`:`${r.length} tasks could not be loaded`;e().toast(m,"warning",8e3)}t({taskContents:d,searchMatches:new Map,failedTaskIds:r,isReloading:!1})}else t({isReloading:!1})}catch(i){const r=i.message||String(i);t({isReloading:!1,lastReloadError:`Failed to reload board: ${r}`}),e().toast(`Board reload failed — showing last loaded state (${r})`,"warning",8e3)}},moveTask:async(n,a,i,r)=>{const{columns:s,config:o,taskContents:c,searchMatches:p}=e(),d=Ht();if(!d&&!e().tasksDirHandle)return;const m=s.find(E=>E.name===a),f=s.find(E=>E.name===i);if(!m||!f)return;const h=m.tasks.findIndex(E=>E.id===n);if(h===-1)return;const b=m.tasks[h];if(!b)return;if(gce(i,o)){const E=new Map,T=Wz(o).toLowerCase();for(const D of s)for(const O of D.tasks){const H=O.frontmatter&&(O.frontmatter.archived===!0||O.frontmatter.archived==="true");E.set(O.id,{exists:!0,resolved:H||O.id===n||D.name.toLowerCase()===T})}const L=[];for(const D of b.dependsOn){if(typeof D!="string"||!D.trim()||D===n)continue;const O=E.get(D);(!O||!O.resolved)&&L.push(D)}if(L.length>0){const D=L.length===1?L[0]:`${L.slice(0,-1).join(", ")} and ${L[L.length-1]}`;e().toast(`Cannot move ${n} to ${i}: blocked by ${D}`,"error");return}}const k=s.map(E=>({...E,tasks:[...E.tasks]})),w=k.find(E=>E.name===a),A=k.find(E=>E.name===i),[S]=w.tasks.splice(h,1);/done|termin|closed|complet/i.test(i)?S.checked=!0:S.checked=!1,r!==void 0?A.tasks.splice(r,0,S):A.tasks.push(S),t({columns:k});try{const{tasksDirHandle:E}=e();if(!E&&!d)return;const T=a===i?k.filter(D=>D.name===i):k.filter(D=>D.name===a||D.name===i),{failedIds:L}=await Mf(()=>Cj(E??null,T,o.board.columns),{maxAttempts:3});if(L.length>0){const D=L.length===1?`Could not save move for ${L[0]}`:`${L.length} tasks could not be moved`;e().toast(D,"warning",8e3),await e().reloadBoard()}}catch(E){const T=E;T instanceof $o?e().toast("Disk is full — move was not saved. Free up space and try again.","error",8e3):e().toast("Failed to save: "+T.message,"error"),t({columns:s,taskContents:c,searchMatches:p})}},reorderInColumn:async(n,a,i)=>{const{columns:r,tasksDirHandle:s,config:o,taskContents:c,searchMatches:p}=e();if(!s&&!Ht())return;const d=r.map(h=>({...h,tasks:[...h.tasks]})),m=d.find(h=>h.name===n);if(!m)return;const[f]=m.tasks.splice(a,1);m.tasks.splice(i,0,f),t({columns:d});try{const{tasksDirHandle:h}=e(),b=Ht();if(!h&&!b)return;const{failedIds:k}=await Mf(()=>Cj(h??null,[m],o.board.columns),{maxAttempts:3});k.length>0&&(e().toast(`Could not save reorder for ${k.length} task(s)`,"warning",8e3),await e().reloadBoard())}catch(h){const b=h;b instanceof $o?e().toast("Disk is full — reorder was not saved.","error",8e3):e().toast("Failed to save: "+b.message,"error"),t({columns:r,taskContents:c,searchMatches:p})}},addColumn:async n=>{const a=n.trim();if(!a)return;const{config:i}=e();i.board.columns.some(r=>r.toLowerCase()===a.toLowerCase())||(await e().updateConfig(r=>({...r,board:{...r.board,columns:[...r.board.columns,a]}})),await e().reloadBoard())},renameColumn:async(n,a)=>{const i=a.trim(),{columns:r,tasksDirHandle:s,config:o}=e();if(!s||!i||i.toLowerCase()===n.toLowerCase())return;if(r.some(d=>d.name.toLowerCase()===i.toLowerCase())){e().toast("Column already exists","error");return}const c=r,p=r.map(d=>d.name===n?{...d,name:i}:d);t({columns:p});try{const d=c.find(m=>m.name===n);if(d){const f=(await Promise.allSettled(d.tasks.map(async(h,b)=>{const{frontmatter:k,body:w}=await Es(s,h.id);await iu(s,h.id,{...k,id:h.id,status:i,order:b},w)}))).filter(h=>h.status==="rejected").length;f>0&&e().toast(`${f} task(s) could not be renamed`,"warning",8e3)}await e().updateConfig(m=>{const f={...m.board.columnColors??{}},h=f[n.toLowerCase()];h&&(f[i.toLowerCase()]=h,delete f[n.toLowerCase()]);const b=m.board.columns.some(k=>k.toLowerCase()===n.toLowerCase())?m.board.columns:[...m.board.columns,n];return{...m,board:{...m.board,columns:b.map(k=>k.toLowerCase()===n.toLowerCase()?i:k),columnColors:f}}}),await e().reloadBoard()}catch(d){e().toast("Failed to rename column: "+d.message,"error"),t({columns:c})}},reorderColumns:async(n,a)=>{const{config:i,columns:r,tasksDirHandle:s}=e();if(n<0||a<0||n>=r.length||a>=r.length||n===a||!s&&!Ht())return;const o=Array.from(r),[c]=o.splice(n,1);o.splice(a,0,c),t({columns:o});const p=o.map(d=>d.name);try{await e().updateConfig(d=>({...d,board:{...d.board,columns:p}})),await e().reloadBoard()}catch(d){e().toast("Failed to reorder columns: "+d.message,"error"),t({columns:r})}},deleteColumn:async n=>{const{columns:a,tasksDirHandle:i}=e();if(!i&&!Ht())return;const r=a.find(o=>o.name===n);if(!r)return;const s=a;t({columns:a.filter(o=>o.name!==n)});try{const c=(await Promise.allSettled(r.tasks.map(p=>_j(i,p.id)))).filter(p=>p.status==="rejected").length;c>0&&e().toast(`${c} task(s) could not be deleted`,"warning",8e3),await e().updateConfig(p=>{const d={...p.board.columnColors??{}};return delete d[n.toLowerCase()],{...p,board:{...p.board,columns:p.board.columns.filter(m=>m.toLowerCase()!==n.toLowerCase()),columnColors:d}}}),await e().reloadBoard(),e().toast("Column deleted")}catch(o){e().toast("Failed to delete column: "+o.message,"error"),t({columns:s})}},createTask:async(n,a)=>{const{columns:i,tasksDirHandle:r,config:s,taskContents:o,searchMatches:c}=e();if(!r&&!Ht()||!i.length)return null;const p=n||s.board.columns[0]||i[0].name,d=wce(i),m=i.find(S=>S.name===p)?.tasks.length??0,f=a?kce(a):null,h={id:d,title:f?.title||"",checked:!1,dependsOn:f?.depends_on||[],tags:f?.tags||[],assignee:f?.assignee||null,priority:f?.priority||(s.fields.priority?s.board.defaultPriority:null),ownerType:s.fields.ownerType?s.board.defaultOwnerType:"",progress:null,frontmatter:{}},b=i.map(S=>S.name===p?{...S,tasks:[...S.tasks,h]}:S),k=new Map(o),w={id:d,title:f?.title||"",status:p,order:m,priority:f?.priority||(s.fields.priority?s.board.defaultPriority:""),tags:f?.tags||[],assignee:f?.assignee||"",due:f?.due||"",depends_on:f?.depends_on||[],created:new Date().toISOString().slice(0,10),ownerType:s.fields.ownerType?s.board.defaultOwnerType:"",tools:""},A="";k.set(d,{frontmatter:w,subtasks:[],body:A}),t({columns:b,taskContents:k});try{const S=r||null;return await Mf(()=>iu(S,d,w,A),{maxAttempts:3}),e().toast(`Created ${d.replace(/^t/,"")}`),await e().openDrawer(d),d}catch(S){const E=S;return E instanceof $o?e().toast("Disk is full — task was not created.","error",8e3):e().toast("Failed to create: "+E.message,"error"),t({columns:i,taskContents:o,searchMatches:c}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r,searchMatches:s}=e();if(!i&&!Ht())return;const o=a.map(d=>({...d,tasks:d.tasks.filter(m=>m.id!==n)}));t({columns:o});const c=new Map(r);c.delete(n);const p=new Map(s);p.delete(n),t({taskContents:c,searchMatches:p});try{await _j(i||null,n),e().toast("Deleted")}catch(d){const m=d;e().toast("Failed to delete: "+m.message,"error"),t({columns:a,taskContents:r,searchMatches:s})}},archiveTask:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a||null,n);await lce(a||null,n,{...i,archived:!0},r),e().drawerTaskId===n&&e().closeDrawer(),await e().reloadBoard(),e().toast("Archived")}catch(i){const r=i;r instanceof $o?e().toast("Disk is full — task was not archived.","error",8e3):e().toast("Failed to archive: "+r.message,"error")}},unarchiveTask:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a||null,n),s={...i};delete s.archived,await dce(a||null,n,s,r),await e().reloadBoard(),e().toast("Restored")}catch(i){const r=i;r instanceof $o?e().toast("Disk is full — task was not restored.","error",8e3):e().toast("Failed to restore: "+r.message,"error")}},setShowArchives:n=>t({showArchives:n}),setShowMetadata:n=>t({showMetadata:n}),openDrawer:async n=>{const{tasksDirHandle:a}=e();if(!(!a&&!Ht()))try{const{frontmatter:i,body:r}=await Es(a,n),{subtasks:s,bodyWithoutSubtasks:o}=Ds(r),c={frontmatter:i,subtasks:s,body:o,savedAt:Date.now()},p=e().drawerRecoveryData.get(n),d=p?{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body}:{frontmatter:i,subtasks:s,body:o},m=new Map(e().drawerRecoveryData);m.delete(n),t({drawerTaskId:n,drawerData:d,drawerBaseVersion:c,conflictState:null,showConflictModal:!1,hasUnsavedDrawerEdits:!!p,lastSaveError:null,drawerRecoveryData:m}),p&&e().toast("Restored your unsaved edits for this task","info")}catch(i){e().toast("Failed to open: "+i.message,"error")}},closeDrawer:()=>t({drawerTaskId:null,drawerData:null,drawerBaseVersion:null,conflictState:null,showConflictModal:!1,hasUnsavedDrawerEdits:!1,lastSaveError:null}),markDrawerDirty:()=>t({hasUnsavedDrawerEdits:!0}),forceCloseDrawer:()=>{const{drawerTaskId:n,drawerData:a}=e();if(n&&a){const i=new Map(e().drawerRecoveryData);i.set(n,{frontmatter:a.frontmatter,subtasks:a.subtasks,body:a.body}),t({drawerRecoveryData:i})}e().closeDrawer()},updateDrawerData:n=>{const{drawerData:a}=e();a&&t({drawerData:n(a)})},saveDrawer:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=e();if(!n||!a)return;const s=qd(a.body,a.subtasks),o={...a.frontmatter,id:n};try{await Mf(()=>iu(i||null,n,o,s),{maxAttempts:3}),e().toast("Saved");const c=new Map(e().drawerRecoveryData);c.delete(n),t({drawerTaskId:null,drawerData:null,hasUnsavedDrawerEdits:!1,lastSaveError:null,drawerRecoveryData:c});const p=new Map(r);p.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),t({taskContents:p}),await e().reloadBoard()}catch(c){const p=c,d=p instanceof $o?"Disk is full — your edits are kept. Free up space and retry.":"Failed to save: "+p.message;e().toast(d,"error",8e3),t({lastSaveError:d})}},saveDrawerMetadata:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=e();if(!(!n||!a))try{const s=qd(a.body,a.subtasks),o={...a.frontmatter,id:n};await Mf(()=>iu(i||null,n,o,s),{maxAttempts:3});const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),t({taskContents:c,hasUnsavedDrawerEdits:!1,lastSaveError:null}),await e().reloadBoard()}catch(s){const o=s,c=o instanceof $o?"Disk is full — your edits are kept.":"Failed to save: "+o.message;t({hasUnsavedDrawerEdits:!0,lastSaveError:c})}},setViewMode:n=>{localStorage.setItem("kandown:view",n),t({viewMode:n})},setDensity:n=>{localStorage.setItem("kandown:density",n),t({density:n})},setFilter:(n,a)=>{if(t(i=>({filters:{...i.filters,[n]:a}})),n==="search"){const{columns:i,tasksDirHandle:r,taskContents:s}=e(),o=a,c=i.flatMap(p=>p.tasks.map(d=>d.id));if(r){const p=c.filter(d=>!s.has(d));p.length>0?e().loadTaskContents(p).then(()=>{e().computeSearchMatches(o)}):e().computeSearchMatches(o)}}},clearFilters:()=>t({filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},searchMatches:new Map}),setCommandOpen:n=>t({commandOpen:n}),setCheatsheetOpen:n=>t({cheatsheetOpen:n}),refreshAgentHook:async()=>{if(!Ht()){t({agentHook:null});return}const n=await Joe();t({agentHook:n?.agentHook??null})},sendTaskToAgent:async n=>{const a=e().agentHook;if(!a){e().toast("Agent hook not configured","error");return}e().toast(`Sending to ${a.label}…`);const i=await ece(n);if(i===null){e().toast("Could not reach the daemon","error");return}i.ok?e().toast(`Sent to ${a.label}`):e().toast(i.error||"Agent hook failed","error")},setCurrentPage:n=>t({currentPage:n}),loadTaskContents:async n=>{const{tasksDirHandle:a}=e();if(!a)return;const i=new Map(e().taskContents);await Promise.all(n.map(async r=>{if(!i.has(r))try{const{frontmatter:s,body:o}=await Es(a,r),{subtasks:c,bodyWithoutSubtasks:p}=Ds(o);i.set(r,{frontmatter:s,subtasks:c,body:p})}catch{}})),t({taskContents:i})},computeSearchMatches:n=>{if(!n.trim()){t({searchMatches:new Map});return}const{taskContents:a}=e(),i=new Map,r=n.toLowerCase();for(const[s,o]of a){const c=Bz(o,r);c.length>0&&i.set(s,c)}t({searchMatches:i})},toast:(n,a="success",i)=>{const r=++$ce,s=i??(a==="error"||a==="warning"?6e3:2500);t(o=>({toasts:[...o.toasts,{id:r,message:n,type:a}]})),setTimeout(()=>{t(o=>({toasts:o.toasts.filter(c=>c.id!==r)}))},s)},dismissToast:n=>t(a=>({toasts:a.toasts.filter(i=>i.id!==n)})),resolveConflict:async n=>{const{conflictState:a,drawerData:i,tasksDirHandle:r,drawerTaskId:s,drawerBaseVersion:o}=e();if(!(!a||!r||!s))if(n==="reload"){const{frontmatter:c,body:p}=await Es(r,s),{subtasks:d,bodyWithoutSubtasks:m}=Ds(p);t({drawerData:{frontmatter:c,subtasks:d,body:m},drawerBaseVersion:{frontmatter:c,subtasks:d,body:m,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),e().toast("Reloaded from disk")}else if(n==="overwrite"){if(i&&s&&o){const c=qd(i.body,i.subtasks),p={...i.frontmatter,id:s};try{await iu(r,s,p,c),t({drawerBaseVersion:{...i,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),e().toast("Overwritten remote changes")}catch(d){e().toast("Failed to overwrite: "+d.message,"error")}}}else t({conflictState:null,showConflictModal:!1})},setupWatcher:()=>{if(Ht()){j2&&clearInterval(j2),j2=setInterval(()=>{e().reloadBoard()},2e3);return}const{dirHandle:n,tasksDirHandle:a}=e();if(!n||!a)return;al.stop(),Lf.forEach(s=>clearTimeout(s)),Lf.clear(),al.start(n,a);const i=(s,o)=>{const c=Lf.get(s);c&&clearTimeout(c);const p=Math.max(2e3,e().config.notifications.editDebounceMs),d=setTimeout(()=>{Lf.delete(s);const m=e().config;m.notifications.taskEdits&&M2({title:"Task edited",body:`${o} changed on disk.`,config:m})},p);Lf.set(s,d)},r=async s=>{const{tasksDirHandle:o,config:c}=e();if(!o)return;let p,d;try{({frontmatter:p,body:d}=await Es(o,s))}catch(A){console.warn(`[Watcher] notifyTaskChange: failed to read ${s}:`,A);return}const{subtasks:m,bodyWithoutSubtasks:f}=Ds(d),h={id:s,frontmatter:{...p,id:p.id||s,status:p.status||"Backlog"},body:f,subtasks:m},b=y3(h),k=fu.get(s);if(!k){fu.set(s,b);return}c.notifications.statusChanges&&k.status!==b.status&&M2({title:"Task status changed",body:`${b.title}: ${k.status} → ${b.status}`,config:c});const w=Nce(k.subtasks,b.subtasks);c.notifications.subtaskCompletions&&w>0&&M2({title:"Subtask completed",body:w===1?`${b.title}: 1 subtask completed.`:`${b.title}: ${w} subtasks completed.`,config:c}),Sce(k,b)&&i(s,b.title),fu.set(s,b)};al.on("configChanged",()=>{try{e().loadConfig(),e().toast("Settings updated externally","info")}catch(s){console.error("[Watcher] configChanged handler error:",s)}}),al.on("taskChanged",async s=>{try{const{drawerTaskId:o,drawerBaseVersion:c,tasksDirHandle:p}=e();if(await r(s),o===s&&c&&p){let d,m;try{({frontmatter:d,body:m}=await Es(p,s))}catch(E){console.warn(`[Watcher] taskChanged: failed to re-read ${s}:`,E);return}const{subtasks:f,bodyWithoutSubtasks:h}=Ds(m),b=c,k=JSON.stringify(b.frontmatter)!==JSON.stringify(d),w=b.body!==h,A=JSON.stringify(b.subtasks)!==JSON.stringify(f);if(!k&&!w&&!A)return;let S="none";k&&(w||A)?S="full":k?S="metadata-only":(w||A)&&(S="body-only"),t({conflictState:{taskId:s,type:S,local:b,remote:{frontmatter:d,body:h,subtasks:f}},showConflictModal:S==="full"})}else await e().reloadBoard()}catch(o){console.error(`[Watcher] taskChanged handler error for ${s}:`,o)}}),al.on("newTaskDetected",async s=>{try{const{tasksDirHandle:o}=e();if(o){let c,p;try{({frontmatter:c,body:p}=await Es(o,s))}catch(f){console.warn(`[Watcher] newTaskDetected: failed to read ${s}:`,f),e().toast(`New task ${s} detected but could not be loaded`,"warning");return}const{subtasks:d,bodyWithoutSubtasks:m}=Ds(p);fu.set(s,y3({id:s,frontmatter:{...c,id:c.id||s,status:c.status||"Backlog"},body:m,subtasks:d}))}await e().reloadBoard()}catch(o){console.error(`[Watcher] newTaskDetected handler error for ${s}:`,o)}}),al.on("watcherError",s=>{t({watcherError:s}),e().toast(s,"warning",1e4)})},restartWatcher:()=>{const{dirHandle:n,tasksDirHandle:a}=e();!n||!a||(t({watcherError:null}),e().setupWatcher(),e().toast("File watcher restarted"))}}));Dy().then(t=>{me.setState({recentProjects:t})}).catch(t=>{console.warn("[Store] Failed to hydrate recent projects:",t)});rl(Ci);window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{rl(me.getState().config)});const Ece=Object.freeze(Object.defineProperty({__proto__:null,useStore:me},Symbol.toStringTag,{value:"Module"}));function Tce(){const t=me(o=>o.config),e=me(o=>o.updateConfig),[n,a]=$.useState(!1);if($.useEffect(()=>{a(!0)},[]),!n)return null;const i=t.ui.theme==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t.ui.theme,r=i==="dark",s=()=>{const o=r?"light":"dark";e(c=>({...c,ui:{...c.ui,theme:o}}))};return _.jsx(tt.button,{onClick:s,className:h3("relative inline-flex items-center rounded-full transition-all duration-300","border border-black/[0.08] dark:border-white/[0.12]","bg-black/[0.05] dark:bg-white/[0.08]","h-8 w-[52px] px-1"),title:r?"Switch to light mode":"Switch to dark mode",children:_.jsx(tt.div,{className:h3("inline-flex items-center justify-center rounded-full shadow-md",r?"bg-[#1e293b] border border-white/[0.15]":"bg-black border border-black/20","h-[26px] w-[26px]"),animate:{x:r?22:0},transition:{type:"spring",stiffness:500,damping:30},children:_.jsx(tt.div,{initial:{rotate:-90,scale:.5,opacity:0},animate:{rotate:0,scale:1,opacity:1},transition:{duration:.25,ease:"easeOut"},children:r?_.jsx(Zn.Moon,{size:13,className:"text-sky-300"}):_.jsx(Zn.Sun,{size:13,className:"text-amber-400"})},i)})})}function Pce(t){const e=Mse(t,{stiffness:180,damping:22,mass:.8});return $.useEffect(()=>{e.set(t)},[t,e]),wz(e,a=>Math.round(a).toString())}const _3="0.21.1",Mce=({className:t})=>_.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 150 150",className:t,fill:"currentColor",children:[_.jsx("path",{d:"m57.6 64.6 0.1-0.1v-31.3c-0.1-3.5-2.7-5.6-5.7-5.6h-16.3v59.6c0.2-1.3 0.9-2.6 1.7-3.2l20.2-19.4z"}),_.jsx("path",{d:"m87.6 43.8c-3.4 0.1-7.1 1.3-9.9 3.7l-38.5 38.7c-2.1 2.1-3.4 4.8-3.5 7.5v26.7l77.5-76.4v-0.2h-25.6z"}),_.jsx("path",{d:"m108.1 96.4-6 5-22.4-20.8-14.6 14.2 21.1 21.4-5 4.1c-0.6 0.5-0.3 0.9 0.2 0.9h27.6c0.4 0 0.7-0.4 0.7-0.6v-24.8c0-0.7-1.1-0.3-1.6 0.3v0.3z"})]});function Lce(){const{t}=Nn(),e=me(R=>R.dirHandle),n=me(R=>R.isOpen),a=me(R=>R.projectName),i=me(R=>R.archivedTasks.length),r=me(R=>R.showArchives),s=me(R=>R.setShowArchives),o=me(R=>R.columns),c=me(R=>R.openFolder),p=me(R=>R.reloadBoard),d=me(R=>R.createTask),m=me(R=>R.setCommandOpen),f=me(R=>R.viewMode),h=me(R=>R.setViewMode),b=me(R=>R.density),k=me(R=>R.setDensity),w=me(R=>R.setCurrentPage),A=me(R=>R.recentProjects),S=me(R=>R.openRecentProject),E=me(R=>R.filters),T=me(R=>R.setFilter),L=me(R=>R.clearFilters),D=me(R=>R.config.fields),O=me(R=>R.lastReloadError),H=me(R=>R.watcherError),z=me(R=>R.restartWatcher),[j,K]=$.useState(!1),[Y,ee]=$.useState(!0),U=$.useRef(null),J=$.useRef(null),W=o.reduce((R,Z)=>R+Z.tasks.length,0),V=Pce(W),B=[];D.priority&&E.priority&&B.push({type:"priority",label:E.priority,value:E.priority}),D.tags&&E.tag&&B.push({type:"tag",label:"#"+E.tag,value:E.tag}),D.assignee&&E.assignee&&B.push({type:"assignee",label:"@"+E.assignee,value:E.assignee});const ne=[{label:t("filterBar.ownerAll"),value:""},{label:t("filterBar.ownerHuman"),value:"human"},{label:t("filterBar.ownerAI"),value:"ai"}],I=B.length>0||E.search||D.ownerType&&E.ownerType;return $.useEffect(()=>{if(!j)return;const R=Z=>{U.current&&!U.current.contains(Z.target)&&K(!1)};return document.addEventListener("mousedown",R),()=>document.removeEventListener("mousedown",R)},[j]),$.useEffect(()=>{const R=setTimeout(()=>ee(!1),5e3);return()=>clearTimeout(R)},[]),$.useEffect(()=>{document.title=a?`${a} · Kandown`:"Kandown"},[a]),_.jsxs(_.Fragment,{children:[(O||H)&&_.jsxs("div",{className:"flex items-center gap-2 px-5 py-1.5 bg-amber-500/10 border-b border-amber-500/30 text-[12px] text-amber-700 dark:text-amber-300",children:[_.jsx("span",{className:"flex-1 truncate",children:H?`⚠ ${H}`:`⚠ ${O}`}),H&&_.jsx("button",{type:"button",onClick:z,className:"px-2 py-0.5 rounded bg-amber-500/20 hover:bg-amber-500/30 text-[11.5px] font-medium",children:"Restart watcher"}),_.jsx("button",{type:"button",onClick:p,className:"px-2 py-0.5 rounded bg-amber-500/20 hover:bg-amber-500/30 text-[11.5px] font-medium",children:"Reload"})]}),_.jsxs("header",{className:"flex items-center justify-between px-5 h-[64px] border-b border-border bg-card/80 backdrop-blur-xl relative z-10",children:[_.jsxs("div",{className:"flex items-center gap-4 min-w-0",children:[_.jsx("div",{className:"flex items-center gap-2.5 flex-shrink-0",children:_.jsxs("button",{onClick:()=>window.history.pushState({},"",window.location.pathname),className:"flex items-center gap-2 cursor-pointer",children:[_.jsx(Mce,{className:"w-[34px] h-[34px] dark:text-white text-black"}),_.jsx(Xi,{mode:"wait",initial:!1,children:Y?_.jsxs(tt.span,{className:"flex items-center gap-2",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.45},children:[_.jsx("span",{className:"text-[15px] font-semibold tracking-tight text-fg",children:"kandown"}),_.jsxs("span",{className:"inline-flex items-center h-5 px-1.5 text-[10.5px] font-semibold text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-500/15 rounded-md",children:["v",_3]})]},"boot"):a?_.jsx(tt.span,{className:"text-[15px] font-semibold tracking-tight text-fg truncate max-w-[240px]",initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},transition:{duration:.45},children:a},"project"):null})]})}),e&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),_.jsxs("div",{className:"flex items-center gap-2 px-3 h-9 bg-secondary/60 border border-border rounded-xl min-w-[200px] max-w-[280px] focus-within:border-border-focus focus-within:bg-secondary transition-all",children:[_.jsx(Zn.Search,{size:14,className:"text-fg-muted/60 flex-shrink-0"}),_.jsx("input",{ref:J,type:"text",placeholder:t("filterBar.searchPlaceholder"),value:E.search,onChange:R=>T("search",R.target.value),className:"bg-transparent border-none outline-none text-fg text-[13px] w-full placeholder:text-fg-muted/60"}),E.search?_.jsx("button",{onClick:()=>T("search",""),className:"text-fg-muted/60 hover:text-fg flex-shrink-0",children:_.jsx(Zn.X,{size:14})}):_.jsx("kbd",{className:"inline-flex items-center h-5 px-1.5 text-[10px] font-medium text-fg-muted/50 bg-black/[0.04] dark:bg-white/[0.08] rounded border border-black/[0.06] dark:border-white/[0.1]",children:"⌘K"})]}),_.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap overflow-hidden",children:[_.jsx(Xi,{children:B.map(R=>_.jsxs(tt.button,{initial:{opacity:0,scale:.9},animate:{opacity:1,scale:1},exit:{opacity:0,scale:.9},transition:{duration:.15},onClick:()=>T(R.type,null),className:"inline-flex items-center gap-1 h-6 px-2.5 text-[12px] text-fg bg-black/[0.05] dark:bg-white/[0.1] border border-black/[0.08] dark:border-white/[0.12] rounded-lg hover:bg-black/[0.08] dark:hover:bg-white/[0.15] transition-colors",children:[R.label,_.jsx(Zn.X,{size:10,className:"text-fg-muted/60"})]},R.type+R.value))}),D.ownerType&&_.jsx("div",{className:"flex items-center h-6 border border-black/[0.06] dark:border-white/[0.1] rounded-lg overflow-hidden",children:ne.map(R=>_.jsx("button",{onClick:()=>T("ownerType",R.value),className:`h-full px-2.5 text-[12px] transition-colors ${E.ownerType===R.value?"bg-black/[0.06] dark:bg-white/[0.12] text-fg":"text-fg-muted/70 hover:text-fg"}`,children:R.label},R.value))}),I&&_.jsx("button",{onClick:L,className:"text-[12px] text-fg-muted/60 hover:text-fg transition-colors",children:t("filterBar.clearAll")})]})]}),e&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),_.jsxs("div",{className:"relative flex-shrink-0",ref:U,children:[_.jsxs("button",{onClick:()=>K(R=>!R),className:"flex items-center gap-1.5 px-2.5 py-1.5 text-[13px] text-fg-muted hover:text-fg hover:bg-black/[0.04] dark:hover:bg-white/[0.06] rounded-lg transition-colors border border-transparent hover:border-black/[0.06] dark:hover:border-white/[0.1]",children:[_.jsx(Zn.Folder,{size:13,className:"text-fg-muted/70"}),_.jsxs("span",{className:"font-medium",children:[".",e.name]}),_.jsx(Zn.ChevronDown,{size:11,className:"opacity-50"})]}),_.jsx(Xi,{children:j&&_.jsx(tt.div,{initial:{opacity:0,y:-4,scale:.98},animate:{opacity:1,y:0,scale:1},exit:{opacity:0,y:-4,scale:.98},transition:{duration:.12},className:"absolute top-full left-0 mt-2 min-w-[240px] glass rounded-xl shadow-[0_16px_48px_rgba(0,0,0,0.5)] overflow-hidden z-50",children:_.jsxs("div",{className:"py-1.5",children:[A.length>0&&_.jsxs(_.Fragment,{children:[_.jsx("div",{className:"px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-muted/60",children:t("header.recentProjects")}),A.map(R=>_.jsxs("button",{onClick:()=>{K(!1),S(R)},className:"w-full flex items-center gap-2 px-3 py-2 text-[13.5px] text-left hover:bg-black/[0.04] dark:hover:bg-white/[0.06] transition-colors",children:[_.jsx(Zn.Folder,{size:12,className:"text-fg-muted/60"}),_.jsx("span",{className:"truncate",children:R.name}),R.id===e.name&&_.jsx(Zn.Check,{size:12,className:"ml-auto text-emerald-500"})]},R.id)),_.jsx("div",{className:"h-px bg-black/[0.06] dark:bg-white/[0.08] my-1.5 mx-2"})]}),_.jsxs("button",{onClick:()=>{K(!1),c()},className:"w-full flex items-center gap-2 px-3 py-2 text-[13.5px] text-left hover:bg-black/[0.04] dark:hover:bg-white/[0.06] transition-colors",children:[_.jsx(Zn.Plus,{size:12,className:"text-fg-muted/60"}),_.jsx("span",{children:t("header.openFolder...")})]})]})})})]})]})]}),_.jsx("div",{className:"flex items-center gap-2 flex-shrink-0",children:n||e?_.jsxs(_.Fragment,{children:[_.jsxs("div",{className:"flex items-center gap-2 mr-2 text-[12.5px] text-fg-muted/70",children:[_.jsx("span",{className:"inline-block w-1.5 h-1.5 rounded-full bg-emerald-500"}),_.jsx(tt.span,{className:"tabular-nums font-medium",children:V}),_.jsx("span",{children:t("header.tasks")})]}),_.jsxs("div",{className:"flex items-center bg-black/[0.04] dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.1] rounded-xl p-0.5 h-10",children:[_.jsx("button",{onClick:()=>h("board"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${f==="board"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:t("common.board"),children:_.jsx(Zn.LayoutBoard,{size:18})}),_.jsx("button",{onClick:()=>h("list"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${f==="list"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:t("common.list"),children:_.jsx(Zn.LayoutList,{size:18})})]}),_.jsx(na,{variant:"icon",icon:"Archive",onClick:()=>s(!r),title:r?t("header.backToBoard"):`${t("header.archives")} (${i})`,className:r?"text-accent":""}),_.jsx(na,{variant:"icon",icon:"Density",onClick:()=>k(b==="compact"?"comfortable":"compact"),title:`Density: ${b}`}),_.jsx(na,{variant:"icon",icon:"Settings",onClick:()=>w("settings"),title:t("common.settings")}),_.jsx(Tce,{}),_.jsx("div",{className:"w-px h-5 bg-black/[0.08] dark:bg-white/[0.08] mx-1"}),_.jsx(na,{variant:"secondary",icon:"Search",label:t("common.search"),shortcut:"⌘K",onClick:()=>m(!0),title:"Command palette (⌘K)"}),_.jsx(na,{variant:"icon",icon:"Refresh",onClick:p,title:`${t("common.reload")} (R)`}),_.jsx(na,{variant:"primary",icon:"Plus",label:t("common.newTask"),shortcut:"N",onClick:()=>d()})]}):_.jsx(na,{variant:"primary",label:t("common.openFolder"),onClick:c})})]})]})}/**
|
|
117
117
|
* @license @tabler/icons-react v3.41.1 - MIT
|
|
118
118
|
*
|
|
119
119
|
* This source code is licensed under the MIT license.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kandown",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "File-based Kanban engine backed by Markdown. Zero backend, AI-agent friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"templates",
|
|
13
13
|
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"keywords": [
|