kandown 0.17.2 → 0.19.0

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/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  <h1 align="center">Kandown</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>File-based Kanban board backed by plain Markdown.</strong><br>
8
+ <strong>Too Many Ideas, Not Enough Agents.</strong><br>
9
+ Kandown helps you queue tasks in an elegant and clever way — file-based Kanban backed by plain Markdown.<br>
9
10
  Zero backend · Zero database · No account · AI-agent friendly
10
11
  </p>
11
12
 
@@ -91,8 +92,74 @@ The web daemon stays alive after you quit the TUI so the browser keeps working.
91
92
  | `kandown daemon start` | Start/reconnect this project's web daemon |
92
93
  | `kandown daemon stop` | Stop this project's web daemon |
93
94
  | `kandown update` | Update `kandown.html` to latest |
95
+ | `kandown list` \| `show` \| `create` \| `move` \| `assign` \| `commit` | One-shot task commands (see below) |
96
+ | `kandown tasks` | Full help for the one-shot task commands |
97
+ | `kandown work` | **For AI agents:** print the agent rules + a live board digest (see below) |
94
98
  | `kandown help` | CLI help |
95
99
 
100
+ ### One-shot task commands (scriptable, agent-friendly)
101
+
102
+ Top-level, non-interactive commands for scripting, CI, and AI agents — no wrapper prefix, just `kandown <command>`. Every command auto-inits `.kandown/` on first use, same as the interactive CLI.
103
+
104
+ | Command | Description |
105
+ |---|---|
106
+ | `kandown list` | List tasks — `[-s status] [-a assignee] [-t tag] [-p priority] [--archived] [--json]` |
107
+ | `kandown show <id>` | Print a task file's raw content |
108
+ | `kandown create "title"` | Create a task — `[-p priority] [-a assignee] [-t tag ...] [--to status] [--id custom-id] [--json]` |
109
+ | `kandown move <id> <status>` | Move a task — `<status>` is a column name or `"archived"` |
110
+ | `kandown assign <id> [name]` | Assign a task (omit name to unassign) |
111
+ | `kandown commit [-m "message"]` | `git add tasks/ .kandown/kandown.json` + `git commit` |
112
+
113
+ ```bash
114
+ kandown list --json | jq '.[] | select(.priority=="P1")'
115
+ kandown create "Refactor auth middleware" -p P1 -t backend
116
+ kandown move t42 Done
117
+ kandown assign t42 alice
118
+ kandown commit -m "tasks: add auth refactor"
119
+ ```
120
+
121
+ **Output contract:** stdout carries data only (task ids, JSON, tables) — everything decorative (`✓ Created…`, warnings, errors) goes to stderr. This keeps `ID=$(kandown create "...")` and `kandown list --json | jq ...` clean and composable. Exit code `0` on success, non-zero on error — safe to check in scripts.
122
+
123
+ **No update checks:** the task commands and `kandown daemon` never touch the npm registry, so they stay fast and fully offline-capable — ideal for CI and for AI agents driving the board directly.
124
+
125
+ ### `kandown work` — the agent entrypoint
126
+
127
+ `kandown init` no longer copies a block of rules into your `AGENTS.md`/`CLAUDE.md` — it appends a single line instead:
128
+
129
+ > This project uses **kandown** for task management. **Always run `kandown work` when starting a new task** — it prints the current rules and board state, kept in sync with the installed CLI version.
130
+
131
+ Running `kandown work` prints, as plain markdown on stdout:
132
+
133
+ 1. **The agent rules** — always fresh, served straight from the installed CLI version instead of a copy that goes stale the moment the package updates.
134
+ 2. **Global instructions** (optional) — `~/.kandown/instructions.md`, applied to every kandown project on this machine (personal conventions, team-wide rules).
135
+ 3. **Project instructions** (optional) — `.kandown/instructions.md`, this project only (stack quirks, "always use pnpm", commit message language, etc).
136
+ 4. **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) — so the agent gets its context in the same call.
137
+
138
+ This removes the drift problem of a rules block frozen into every project's `AGENTS.md` at init time, keeps the injected footprint to one line, and lets you layer instructions globally or per project without touching the agent file at all.
139
+
140
+ > **Upgrading from before v0.18.0?** `kandown shell <cmd>` was removed (no alias) — the commands are now top-level: `kandown list/show/create/move/assign/commit`. Existing projects keep their old `AGENTS.md`/`CLAUDE.md` block until you re-run `kandown init`. The CLI prints a one-time notice about this the next time you run an interactive command after updating — see [Environment variables](#environment-variables) if you want to silence version-check output entirely.
141
+
142
+ ---
143
+
144
+ ## Environment variables
145
+
146
+ | Variable | Effect |
147
+ |---|---|
148
+ | `KANDOWN_NO_UPDATE=1` | Disable the background auto-update check entirely (recommended for CI) |
149
+ | `KANDOWN_DEBUG=1` | Print full stack traces on unexpected errors instead of a one-line summary |
150
+ | `KANDOWN_AGENT_HOOK_URL` | POST endpoint that receives tasks sent via the "Send to Agent" button / TUI `g` key |
151
+ | `KANDOWN_AGENT_HOOK_LABEL` | Custom label for the agent hook button (default: `Agent`) |
152
+ | `KANDOWN_AGENT_HOOK_HEADERS` | JSON object of extra HTTP headers to send with the agent hook request |
153
+
154
+ Interactive runs of `kandown` check npm for updates at most once every 24 hours (never for the task commands or `daemon`, never when stdout isn't a terminal) and auto-install silently when one is found.
155
+
156
+ ---
157
+
158
+ ## Security notes
159
+
160
+ - The local web daemon binds to `127.0.0.1` only and issues a random per-project API token on startup (stored in the gitignored `.kandown/daemon.json`, injected into the served page). Every API route except the read-only `GET /api/daemon` identity check requires it — a stray browser tab on another site can't read or write your tasks through it.
161
+ - `.kandown/daemon.json` and `.kandown/daemon.lock` are runtime-only files; `kandown init` adds them to `.kandown/.gitignore` automatically.
162
+
96
163
  ---
97
164
 
98
165
  ## Features
package/bin/kandown.js CHANGED
@@ -50,6 +50,7 @@ globalThis.require = createRequire(import.meta.url);
50
50
  import { fileURLToPath } from 'node:url';
51
51
  import { createServer } from 'node:http';
52
52
  import { dirname, join, resolve } from 'node:path';
53
+ import { homedir } from 'node:os';
53
54
  import {
54
55
  existsSync,
55
56
  mkdirSync,
@@ -328,6 +329,7 @@ async function checkForUpdate(argv = process.argv) {
328
329
  }
329
330
 
330
331
  tuiDone('✓', `${c.green}Updated to v${postVersion}${c.reset} — restarting…`);
332
+ printBreakingChangeNotices(current, postVersion);
331
333
  log('');
332
334
 
333
335
  // 📖 Step 5: Respawn with the new version.
@@ -366,6 +368,69 @@ const c = {
366
368
  cyan: '\x1b[36m',
367
369
  };
368
370
 
371
+ /**
372
+ * 📖 One-time migration notices shown right after a successful auto-update,
373
+ * when the jump crosses a release that changed user-facing behavior (i.e.
374
+ * `fromVersion < notice.version <= toVersion`). Keeps upgraders from being
375
+ * silently surprised by a removed command or a changed convention — full
376
+ * details always live in CHANGELOG.md. Add an entry here for any future
377
+ * breaking release; harmless no-op for anyone already past that version.
378
+ */
379
+ const BREAKING_CHANGE_NOTICES = [
380
+ {
381
+ version: '0.18.0',
382
+ lines: [
383
+ `${c.yellow}⚠ Breaking change in v0.18.0:${c.reset}`,
384
+ ` ${c.dim}•${c.reset} ${c.cyan}kandown shell <cmd>${c.reset} is gone — commands are now top-level: ${c.cyan}kandown list/show/create/move/assign/commit${c.reset}`,
385
+ ` ${c.dim}•${c.reset} ${c.cyan}kandown init${c.reset} now injects one line into AGENTS.md/CLAUDE.md pointing agents at ${c.cyan}kandown work${c.reset} — existing projects keep their old block until you re-run ${c.cyan}kandown init${c.reset}`,
386
+ ` ${c.dim}•${c.reset} New: ${c.cyan}kandown work${c.reset} prints the agent rules + a live board digest — the recommended way to brief an AI agent`,
387
+ ` ${c.dim}Full changelog:${c.reset} https://github.com/vava-nessa/kandown/blob/main/CHANGELOG.md`,
388
+ ],
389
+ },
390
+ ];
391
+
392
+ function printBreakingChangeNotices(fromVersion, toVersion) {
393
+ for (const notice of BREAKING_CHANGE_NOTICES) {
394
+ if (semverGt(fromVersion, notice.version) < 0 && semverGt(toVersion, notice.version) >= 0) {
395
+ log('');
396
+ for (const line of notice.lines) log(line);
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * 📖 Version-seen tracker: catches breaking-change notices for upgrades that
403
+ * DON'T go through checkForUpdate's own auto-update flow — a manual
404
+ * `npm install -g kandown`, pnpm/yarn/bun global installs, or any future
405
+ * package manager. Records the last version this install printed notices for
406
+ * in a small cache file next to the package; on the next interactive launch,
407
+ * any BREAKING_CHANGE_NOTICES entry crossed since then is shown once.
408
+ * TTY-only and skipped for scripted commands — never fires in a script/CI.
409
+ * First run after this mechanism ships has no prior cache, so it seeds
410
+ * silently rather than guessing; the checkForUpdate path covers that specific
411
+ * transition for auto-updaters, and every version from here on is covered.
412
+ */
413
+ const VERSION_SEEN_CACHE = join(PKG_ROOT, '.version-seen.json');
414
+
415
+ function checkVersionSeenNotices() {
416
+ if (!process.stdout.isTTY) return;
417
+ const current = getCurrentVersion();
418
+ if (!current) return;
419
+
420
+ let lastSeen = null;
421
+ try {
422
+ const raw = JSON.parse(readFileSync(VERSION_SEEN_CACHE, 'utf8'));
423
+ if (typeof raw?.lastSeen === 'string') lastSeen = raw.lastSeen;
424
+ } catch { /* first run, or corrupted cache — treat as unknown */ }
425
+
426
+ if (lastSeen && lastSeen !== current) {
427
+ printBreakingChangeNotices(lastSeen, current);
428
+ }
429
+ if (lastSeen !== current) {
430
+ try { writeFileSync(VERSION_SEEN_CACHE, JSON.stringify({ lastSeen: current })); } catch { /* best-effort */ }
431
+ }
432
+ }
433
+
369
434
  /**
370
435
  * 📖 Atomic write (M6): write to a sibling temp file then rename over the
371
436
  * target. A crash/kill mid-write can no longer leave a truncated task file or
@@ -383,7 +448,7 @@ function atomicWriteFileSync(path, content) {
383
448
  }
384
449
 
385
450
  // 📖 Output contract (M2): stdout carries DATA ONLY (ids, JSON, tables, help)
386
- // so `$(kandown shell create ...)` and `| jq` pipelines stay clean. Every
451
+ // so `$(kandown create ...)` and `| jq` pipelines stay clean. Every
387
452
  // decoration — status lines, warnings, errors, progress — goes to stderr.
388
453
  /** Data output → stdout. Use ONLY for content the command was asked to produce. */
389
454
  const out = (msg) => console.log(msg);
@@ -457,7 +522,14 @@ ${c.bold}Commands:${c.reset}
457
522
  ${c.cyan}settings${c.reset} Open the settings TUI
458
523
  ${c.cyan}daemon${c.reset} Manage the per-project web daemon (start|stop|status)
459
524
  ${c.cyan}update${c.reset} Update kandown.html to the latest version
460
- ${c.cyan}shell${c.reset} Run shellable task commands (list/show/create/move/assign/commit)
525
+ ${c.cyan}list${c.reset} List tasks ${c.dim}(-s status, -a assignee, -t tag, -p priority, --json)${c.reset}
526
+ ${c.cyan}show${c.reset} Print a task's raw file content
527
+ ${c.cyan}create${c.reset} Create a task ${c.dim}(-p priority, -a assignee, -t tag, --to status)${c.reset}
528
+ ${c.cyan}move${c.reset} Move a task to a column (or "archived")
529
+ ${c.cyan}assign${c.reset} Assign / unassign a task
530
+ ${c.cyan}commit${c.reset} git add + commit the task files
531
+ ${c.cyan}tasks${c.reset} Full help for the commands above
532
+ ${c.cyan}work${c.reset} ${c.bold}For AI agents:${c.reset} print the agent rules + live board digest
461
533
  ${c.cyan}help${c.reset} Show this help
462
534
 
463
535
  ${c.bold}Options:${c.reset}
@@ -471,9 +543,10 @@ ${c.bold}Examples:${c.reset}
471
543
  ${c.dim}$${c.reset} npx kandown init
472
544
  ${c.dim}$${c.reset} npx kandown init --path docs/kanban
473
545
  ${c.dim}$${c.reset} npx kandown init --no-agents
474
- ${c.dim}$${c.reset} npx kandown shell list --json
475
- ${c.dim}$${c.reset} npx kandown shell create "Refactor auth" -p P1
476
- ${c.dim}$${c.reset} npx kandown shell commit -m "tasks: refactor auth"
546
+ ${c.dim}$${c.reset} npx kandown work ${c.dim}# what an AI agent should run first${c.reset}
547
+ ${c.dim}$${c.reset} npx kandown list --json
548
+ ${c.dim}$${c.reset} npx kandown create "Refactor auth" -p P1
549
+ ${c.dim}$${c.reset} npx kandown commit -m "tasks: refactor auth"
477
550
  `);
478
551
  }
479
552
 
@@ -548,12 +621,7 @@ function appendAgentReference(cwd, agentsFile, kandownPath) {
548
621
  ${marker}
549
622
  ## Task management
550
623
 
551
- **IMPORTANT:** Before touching any task files, you MUST read \`${kandownPath}/AGENT_KANDOWN.md\`.
552
-
553
- This project uses a file-based kanban:
554
- - **Tasks live in \`./tasks/t-xxx.md\`** at the project root — each task file owns its status
555
- - **Config lives in \`${kandownPath}/kandown.json\`** (columns, appearance, agent settings)
556
- - **Completion workflow:** set task frontmatter \`status: Done\` + write the completion report
624
+ This project uses **kandown** for task management. **Always run \`kandown work\` when starting a new task** — it prints the current rules and board state, kept in sync with the installed CLI version. (Tasks live in \`./tasks/*.md\`; if you can't run the CLI, read \`${kandownPath}/AGENT_KANDOWN.md\` instead.)
557
625
  `;
558
626
 
559
627
  try {
@@ -574,12 +642,7 @@ function createAgentsFileIfMissing(cwd, kandownPath) {
574
642
  <!-- kandown:agent-ref -->
575
643
  ## Task management
576
644
 
577
- **IMPORTANT:** Before touching any task files, you MUST read \`AGENT_KANDOWN.md\`.
578
-
579
- This project uses a file-based kandown:
580
- - **Tasks live in \`./tasks/t-xxx.md\`** at the project root — each task file owns its status
581
- - **Config lives in \`${kandownPath}/kandown.json\`** (columns, appearance, agent settings)
582
- - **Completion workflow:** set task frontmatter \`status: Done\` + write the completion report
645
+ This project uses **kandown** for task management. **Always run \`kandown work\` when starting a new task** — it prints the current rules and board state, kept in sync with the installed CLI version. (Tasks live in \`./tasks/*.md\`; if you can't run the CLI, read \`${kandownPath}/AGENT_KANDOWN.md\` instead.)
583
646
  `;
584
647
  writeFileSync(agentsPath, content, 'utf8');
585
648
  return true;
@@ -883,9 +946,9 @@ function cmdUpdate(rawArgs) {
883
946
  success(`Updated ${args.path}/kandown.html`);
884
947
  }
885
948
 
886
- /* ═════════════ Shellable task commands ═════════════ */
949
+ /* ═════════════ One-shot task commands ═════════════ */
887
950
  /**
888
- * 📖 Self-contained minimal YAML frontmatter parser/writer for the CLI shell
951
+ * 📖 Self-contained minimal YAML frontmatter parser/writer for the one-shot task
889
952
  * commands. The web app uses a richer schema (parseSimpleYaml) in the
890
953
  * browser; the CLI keeps its own because it can't import the browser bundle
891
954
  * and these commands only need to round-trip a known small set of scalar
@@ -1028,7 +1091,7 @@ function nextTaskId(kandownDir) {
1028
1091
  return 't' + (maxN + 1);
1029
1092
  }
1030
1093
 
1031
- function shellPad(str, len) {
1094
+ function padCell(str, len) {
1032
1095
  const s = String(str);
1033
1096
  // 📖 Only truncate when the string is strictly LONGER than the column —
1034
1097
  // an exact fit must render as-is (`>=` chopped the last char of e.g. the
@@ -1037,8 +1100,8 @@ function shellPad(str, len) {
1037
1100
  return s + ' '.repeat(len - s.length);
1038
1101
  }
1039
1102
 
1040
- function shellParseArgs(argv) {
1041
- // 📖 Minimal flag parser for the shell subcommands. Stops at the first
1103
+ function taskParseArgs(argv) {
1104
+ // 📖 Minimal flag parser for the task commands. Stops at the first
1042
1105
  // positional arg so `kandown create "Some title with -dash"` works.
1043
1106
  const out = { positional: [], flags: {} };
1044
1107
  for (let i = 0; i < argv.length; i++) {
@@ -1058,9 +1121,9 @@ function shellParseArgs(argv) {
1058
1121
  return out;
1059
1122
  }
1060
1123
 
1061
- function shellResolveStatus(config, status) {
1124
+ function resolveStatusArg(config, status) {
1062
1125
  // 📖 Status can be a configured column name OR the reserved `archived`
1063
- // sentinel. Match is case-insensitive to keep shell usage forgiving.
1126
+ // sentinel. Match is case-insensitive to stay forgiving in scripts.
1064
1127
  if (!status) return null;
1065
1128
  const lower = status.toLowerCase();
1066
1129
  if (lower === 'archived') return 'archived';
@@ -1071,11 +1134,11 @@ function shellResolveStatus(config, status) {
1071
1134
  return null;
1072
1135
  }
1073
1136
 
1074
- function shellList(rawArgs) {
1137
+ function cmdList(rawArgs) {
1075
1138
  const { kandownDir } = ensureKandownDir(rawArgs);
1076
- const args = shellParseArgs(rawArgs);
1139
+ const args = taskParseArgs(rawArgs);
1077
1140
  const config = readKandownConfig(kandownDir);
1078
- const statusFilter = args.flags.status ? shellResolveStatus(config, args.flags.status) : null;
1141
+ const statusFilter = args.flags.status ? resolveStatusArg(config, args.flags.status) : null;
1079
1142
  if (args.flags.status && !statusFilter) {
1080
1143
  err(`Unknown status: ${args.flags.status}`);
1081
1144
  process.exit(1);
@@ -1111,28 +1174,28 @@ function shellList(rawArgs) {
1111
1174
 
1112
1175
  if (rows.length === 0) {
1113
1176
  // 📖 Placeholder goes to stderr — an empty result on stdout stays empty
1114
- // so `[ -z "$(kandown shell list ...)" ]` style checks behave.
1177
+ // so `[ -z "$(kandown list ...)" ]` style checks behave.
1115
1178
  log(c.dim + '(no tasks)' + c.reset);
1116
1179
  return;
1117
1180
  }
1118
1181
 
1119
1182
  const idW = Math.max(2, ...rows.map(r => r.id.length));
1120
- out(`${c.dim}${shellPad('ID', idW)} ${shellPad('STATUS', 14)} ${shellPad('PRI', 4)} ${shellPad('ASSIGNEE', 12)} TITLE${c.reset}`);
1183
+ out(`${c.dim}${padCell('ID', idW)} ${padCell('STATUS', 14)} ${padCell('PRI', 4)} ${padCell('ASSIGNEE', 12)} TITLE${c.reset}`);
1121
1184
  for (const r of rows) {
1122
1185
  const status = (r.fm.status || 'Backlog') + (r.fm.archived === true || r.fm.archived === 'true' ? ' (archived)' : '');
1123
1186
  const pri = r.fm.priority || '';
1124
1187
  const assignee = r.fm.assignee || '';
1125
1188
  const title = (r.fm.title || '(untitled)').replace(/\n/g, ' ');
1126
- out(`${shellPad(r.id, idW)} ${shellPad(status, 14)} ${shellPad(pri, 4)} ${shellPad(assignee, 12)} ${title}`);
1189
+ out(`${padCell(r.id, idW)} ${padCell(status, 14)} ${padCell(pri, 4)} ${padCell(assignee, 12)} ${title}`);
1127
1190
  }
1128
1191
  }
1129
1192
 
1130
- function shellShow(rawArgs) {
1193
+ function cmdShow(rawArgs) {
1131
1194
  const { kandownDir } = ensureKandownDir(rawArgs);
1132
- const args = shellParseArgs(rawArgs);
1195
+ const args = taskParseArgs(rawArgs);
1133
1196
  const id = args.positional[0];
1134
1197
  if (!id) {
1135
- err('Usage: kandown shell show <id>');
1198
+ err('Usage: kandown show <id>');
1136
1199
  process.exit(1);
1137
1200
  }
1138
1201
  const path = findTaskFile(kandownDir, id);
@@ -1149,7 +1212,7 @@ function shellShow(rawArgs) {
1149
1212
  * resolved when it lives in the terminal column OR is archived. Unknown
1150
1213
  * ids and self-references never block.
1151
1214
  */
1152
- function shellTaskIsResolved(kandownDir, id, terminalLower) {
1215
+ function depIsResolved(kandownDir, id, terminalLower) {
1153
1216
  if (!/^[a-zA-Z0-9_-]+$/.test(id)) return true; // unknown / invalid → don't block
1154
1217
  const path = findTaskFile(kandownDir, id);
1155
1218
  if (!path) return true; // unknown id → don't block
@@ -1158,17 +1221,17 @@ function shellTaskIsResolved(kandownDir, id, terminalLower) {
1158
1221
  return isArch || (parsed.frontmatter.status || '').toLowerCase() === terminalLower;
1159
1222
  }
1160
1223
 
1161
- function shellCreate(rawArgs) {
1224
+ function cmdCreate(rawArgs) {
1162
1225
  const { kandownDir } = ensureKandownDir(rawArgs);
1163
- const args = shellParseArgs(rawArgs);
1226
+ const args = taskParseArgs(rawArgs);
1164
1227
  const title = args.positional.join(' ').trim();
1165
1228
  if (!title) {
1166
- err('Usage: kandown shell create "title" [-p priority] [-a assignee] [-t tag] [--to status]');
1229
+ err('Usage: kandown create "title" [-p priority] [-a assignee] [-t tag] [--to status]');
1167
1230
  process.exit(1);
1168
1231
  }
1169
1232
  const config = readKandownConfig(kandownDir);
1170
1233
  const defaultStatus = (config && config.board && config.board.columns && config.board.columns[0]) || 'Backlog';
1171
- const targetStatus = args.flags.to ? shellResolveStatus(config, args.flags.to) : defaultStatus;
1234
+ const targetStatus = args.flags.to ? resolveStatusArg(config, args.flags.to) : defaultStatus;
1172
1235
  if (args.flags.to && !targetStatus) {
1173
1236
  err(`Unknown status: ${args.flags.to}`);
1174
1237
  process.exit(1);
@@ -1207,17 +1270,17 @@ function shellCreate(rawArgs) {
1207
1270
  }
1208
1271
  }
1209
1272
 
1210
- function shellMove(rawArgs) {
1273
+ function cmdMove(rawArgs) {
1211
1274
  const { kandownDir } = ensureKandownDir(rawArgs);
1212
- const args = shellParseArgs(rawArgs);
1275
+ const args = taskParseArgs(rawArgs);
1213
1276
  const [id, rawStatus] = args.positional;
1214
1277
  const targetStatus = rawStatus || args.flags.to;
1215
1278
  if (!id || !targetStatus) {
1216
- err('Usage: kandown shell move <id> <status>');
1279
+ err('Usage: kandown move <id> <status>');
1217
1280
  process.exit(1);
1218
1281
  }
1219
1282
  const config = readKandownConfig(kandownDir);
1220
- const resolved = shellResolveStatus(config, targetStatus);
1283
+ const resolved = resolveStatusArg(config, targetStatus);
1221
1284
  if (!resolved) {
1222
1285
  err(`Unknown status: ${targetStatus}`);
1223
1286
  process.exit(1);
@@ -1238,7 +1301,7 @@ function shellMove(rawArgs) {
1238
1301
  const blocked = [];
1239
1302
  for (const dep of deps) {
1240
1303
  if (typeof dep !== 'string' || !dep.trim() || dep === id) continue;
1241
- if (!shellTaskIsResolved(kandownDir, dep, terminalLower)) blocked.push(dep);
1304
+ if (!depIsResolved(kandownDir, dep, terminalLower)) blocked.push(dep);
1242
1305
  }
1243
1306
  if (blocked.length > 0) {
1244
1307
  const list = blocked.length === 1 ? blocked[0] : `${blocked.slice(0, -1).join(', ')} and ${blocked[blocked.length - 1]}`;
@@ -1267,12 +1330,12 @@ function shellMove(rawArgs) {
1267
1330
  log(`${c.green}✓${c.reset} ${c.bold}${id}${c.reset} → ${resolved}`);
1268
1331
  }
1269
1332
 
1270
- function shellAssign(rawArgs) {
1333
+ function cmdAssign(rawArgs) {
1271
1334
  const { kandownDir } = ensureKandownDir(rawArgs);
1272
- const args = shellParseArgs(rawArgs);
1335
+ const args = taskParseArgs(rawArgs);
1273
1336
  const [id, name] = args.positional;
1274
1337
  if (!id) {
1275
- err('Usage: kandown shell assign <id> [name] (omit name to unassign)');
1338
+ err('Usage: kandown assign <id> [name] (omit name to unassign)');
1276
1339
  process.exit(1);
1277
1340
  }
1278
1341
  const path = findTaskFile(kandownDir, id);
@@ -1287,9 +1350,9 @@ function shellAssign(rawArgs) {
1287
1350
  log(`${c.green}✓${c.reset} ${c.bold}${id}${c.reset} → ${name ? c.cyan + name : c.dim + '(unassigned)'}${c.reset}`);
1288
1351
  }
1289
1352
 
1290
- function shellCommit(rawArgs) {
1353
+ function cmdCommit(rawArgs) {
1291
1354
  const { kandownDir } = ensureKandownDir(rawArgs);
1292
- const args = shellParseArgs(rawArgs);
1355
+ const args = taskParseArgs(rawArgs);
1293
1356
  const projectRoot = getProjectRoot(kandownDir);
1294
1357
  // 📖 Refuse to commit if not inside a git repo — silently failing here would
1295
1358
  // let the user believe they versioned their tasks when they didn't.
@@ -1335,34 +1398,16 @@ function shellCommit(rawArgs) {
1335
1398
  }
1336
1399
  }
1337
1400
 
1338
- function cmdShell(subcmd, rawArgs) {
1339
- switch (subcmd) {
1340
- case 'list':
1341
- case 'ls':
1342
- shellList(rawArgs);
1343
- return;
1344
- case 'show':
1345
- shellShow(rawArgs);
1346
- return;
1347
- case 'create':
1348
- case 'new':
1349
- shellCreate(rawArgs);
1350
- return;
1351
- case 'move':
1352
- shellMove(rawArgs);
1353
- return;
1354
- case 'assign':
1355
- shellAssign(rawArgs);
1356
- return;
1357
- case 'commit':
1358
- shellCommit(rawArgs);
1359
- return;
1360
- case undefined:
1361
- case 'help':
1362
- case '--help':
1363
- case '-h':
1364
- out(`
1365
- ${c.bold}kandown shell${c.reset} ${c.dim}· task commands (one-shot, scriptable)${c.reset}
1401
+ /**
1402
+ * 📖 Prints the one-shot task-command cheatsheet. Reachable as `kandown tasks`
1403
+ * (or `kandown tasks help`) — the commands themselves (list/show/create/move/
1404
+ * assign/commit) are top-level, not nested under a "shell" prefix, since
1405
+ * they're the most basic operations of the product and scripts/agents should
1406
+ * never need to remember a wrapper word to reach them.
1407
+ */
1408
+ function printTaskCommandsHelp() {
1409
+ out(`
1410
+ ${c.bold}kandown${c.reset} ${c.dim}· one-shot task commands (scriptable, agent-friendly)${c.reset}
1366
1411
 
1367
1412
  ${c.bold}Commands:${c.reset}
1368
1413
  ${c.cyan}list${c.reset} [-s status] [-a assignee] [-t tag] [-p priority] [--archived] [--json]
@@ -1373,18 +1418,151 @@ ${c.bold}Commands:${c.reset}
1373
1418
  ${c.cyan}commit${c.reset} [-m "message"] (git add tasks/ + .kandown/kandown.json + git commit)
1374
1419
 
1375
1420
  ${c.bold}Examples:${c.reset}
1376
- ${c.dim}$${c.reset} kandown shell list --json | jq '.[] | select(.priority=="P1")'
1377
- ${c.dim}$${c.reset} kandown shell create "Refactor auth middleware" -p P1 -t backend
1378
- ${c.dim}$${c.reset} kandown shell move t42 Done
1379
- ${c.dim}$${c.reset} kandown shell assign t42 alice
1380
- ${c.dim}$${c.reset} kandown shell commit -m "tasks: add auth refactor"
1421
+ ${c.dim}$${c.reset} kandown list --json | jq '.[] | select(.priority=="P1")'
1422
+ ${c.dim}$${c.reset} kandown create "Refactor auth middleware" -p P1 -t backend
1423
+ ${c.dim}$${c.reset} kandown move t42 Done
1424
+ ${c.dim}$${c.reset} kandown assign t42 alice
1425
+ ${c.dim}$${c.reset} kandown commit -m "tasks: add auth refactor"
1381
1426
  `);
1382
- return;
1383
- default:
1384
- err(`Unknown shell command: ${subcmd}`);
1385
- log(` Run ${c.cyan}kandown shell help${c.reset} for the list.`);
1386
- process.exit(1);
1427
+ }
1428
+
1429
+ /* ═════════════ kandown work — agent entrypoint ═════════════ */
1430
+ /**
1431
+ * 📖 `kandown work` replaces the "paste the rules into AGENTS.md/CLAUDE.md"
1432
+ * pattern: instead of a stale copy embedded in the project's agent file at
1433
+ * init time, the CLI serves the rules fresh on every invocation — always in
1434
+ * sync with the installed version — plus a live digest of the board so the
1435
+ * agent gets its marching orders and its context in one call.
1436
+ *
1437
+ * Layering (base → global → project), each optional except the base:
1438
+ * 1. Base rules — templates/AGENT_KANDOWN.md, shipped with the package.
1439
+ * 2. Global — ~/.kandown/instructions.md, applies to every project
1440
+ * on this machine (team conventions, personal style).
1441
+ * 3. Project — .kandown/instructions.md, this project only.
1442
+ * Later layers are appended, not merged — the agent reads all of them.
1443
+ */
1444
+
1445
+ const PRIORITY_RANK = { P1: 0, P2: 1, P3: 2, P4: 3 };
1446
+ function priorityRank(p) {
1447
+ return Object.prototype.hasOwnProperty.call(PRIORITY_RANK, p) ? PRIORITY_RANK[p] : 4;
1448
+ }
1449
+
1450
+ /**
1451
+ * 📖 Builds the "Current board" section: column counts, tasks per column
1452
+ * (with blocked-by annotations), and a "Next actionable task" pick — the
1453
+ * task in the most-advanced non-terminal column (closest to done, so
1454
+ * in-flight work finishes before new work starts) with no unresolved
1455
+ * `depends_on`, tie-broken by priority. Mirrors the same gate logic used by
1456
+ * `move`/the TUI/the web store.
1457
+ */
1458
+ function buildBoardDigest(kandownDir) {
1459
+ const config = readKandownConfig(kandownDir);
1460
+ const columns = (config && config.board && Array.isArray(config.board.columns) && config.board.columns.length > 0)
1461
+ ? config.board.columns
1462
+ : ['Backlog', 'Todo', 'In Progress', 'Review', 'Done'];
1463
+ const terminalCol = columns[columns.length - 1];
1464
+ const terminalLower = terminalCol.toLowerCase();
1465
+
1466
+ const rows = [];
1467
+ for (const id of listAllTaskIds(kandownDir)) {
1468
+ const path = findTaskFile(kandownDir, id);
1469
+ if (!path) continue;
1470
+ let content;
1471
+ try { content = readFileSync(path, 'utf8'); } catch { continue; }
1472
+ const parsed = parseFrontmatter(content);
1473
+ const fm = parsed.frontmatter;
1474
+ const isArchived = fm.archived === true || fm.archived === 'true' || path.includes('/archive/');
1475
+ if (isArchived) continue;
1476
+ rows.push({ id, fm });
1477
+ }
1478
+
1479
+ if (rows.length === 0) {
1480
+ return `## Current board\n\n(no tasks yet)`;
1481
+ }
1482
+
1483
+ const resolved = new Map();
1484
+ for (const r of rows) {
1485
+ const isArch = r.fm.archived === true || r.fm.archived === 'true';
1486
+ resolved.set(r.id, isArch || (r.fm.status || '').toLowerCase() === terminalLower);
1487
+ }
1488
+
1489
+ const byColumn = new Map(columns.map(col => [col, []]));
1490
+ for (const r of rows) {
1491
+ const status = r.fm.status || columns[0];
1492
+ const col = columns.find(c => c.toLowerCase() === String(status).toLowerCase()) || columns[0];
1493
+ const deps = Array.isArray(r.fm.depends_on) ? r.fm.depends_on : [];
1494
+ const blocked = deps.filter(d => typeof d === 'string' && d.trim() && d !== r.id && !resolved.get(d));
1495
+ // 📖 `col` always resolves to one of `columns` (falls back to columns[0]),
1496
+ // which the Map was seeded with above — the entry always exists.
1497
+ byColumn.get(col).push({ ...r, blocked });
1498
+ }
1499
+
1500
+ const lines = ['## Current board', ''];
1501
+ lines.push(`**Columns:** ${columns.map(col => `${col} (${(byColumn.get(col) || []).length})`).join(' · ')}`);
1502
+
1503
+ for (const col of columns) {
1504
+ const tasks = (byColumn.get(col) || []).sort((a, b) => priorityRank(a.fm.priority) - priorityRank(b.fm.priority));
1505
+ if (tasks.length === 0) continue;
1506
+ lines.push('', `### ${col}`);
1507
+ for (const t of tasks) {
1508
+ const pri = t.fm.priority ? `[${t.fm.priority}] ` : '';
1509
+ const assignee = t.fm.assignee ? ` (@${t.fm.assignee})` : '';
1510
+ const blockedStr = t.blocked.length > 0 ? ` ⛔ blocked by ${t.blocked.join(', ')}` : '';
1511
+ lines.push(`- ${t.id} ${pri}${t.fm.title || '(untitled)'}${assignee}${blockedStr}`);
1512
+ }
1513
+ }
1514
+
1515
+ const actionable = rows.filter(r => {
1516
+ const status = r.fm.status || columns[0];
1517
+ if (status.toLowerCase() === terminalLower) return false;
1518
+ const deps = Array.isArray(r.fm.depends_on) ? r.fm.depends_on : [];
1519
+ return !deps.some(d => typeof d === 'string' && d.trim() && d !== r.id && !resolved.get(d));
1520
+ });
1521
+ const next = actionable.sort((a, b) => {
1522
+ const idxA = columns.findIndex(col => col.toLowerCase() === (a.fm.status || columns[0]).toLowerCase());
1523
+ const idxB = columns.findIndex(col => col.toLowerCase() === (b.fm.status || columns[0]).toLowerCase());
1524
+ if (idxA !== idxB) return idxB - idxA; // most-advanced non-terminal column first
1525
+ return priorityRank(a.fm.priority) - priorityRank(b.fm.priority);
1526
+ })[0];
1527
+
1528
+ lines.push('', '### Next actionable task');
1529
+ lines.push(next
1530
+ ? `→ **${next.id}** — ${next.fm.title || '(untitled)'} (${next.fm.priority || 'no priority'}, ${next.fm.status || columns[0]})`
1531
+ : 'None — every task is done, archived, or blocked.');
1532
+
1533
+ return lines.join('\n');
1534
+ }
1535
+
1536
+ async function cmdWork(rawArgs) {
1537
+ const { kandownDir } = ensureKandownDir(rawArgs);
1538
+
1539
+ let baseRules = '';
1540
+ try {
1541
+ baseRules = readFileSync(join(PKG_ROOT, 'templates', 'AGENT_KANDOWN.md'), 'utf8').trim();
1542
+ } catch (e) {
1543
+ warn(`Could not read base rules (${e.message})`);
1387
1544
  }
1545
+
1546
+ let globalInstructions = '';
1547
+ const globalPath = join(homedir(), '.kandown', 'instructions.md');
1548
+ if (existsSync(globalPath)) {
1549
+ try { globalInstructions = readFileSync(globalPath, 'utf8').trim(); }
1550
+ catch (e) { warn(`Could not read global instructions (${e.message})`); }
1551
+ }
1552
+
1553
+ let projectInstructions = '';
1554
+ const projectPath = join(kandownDir, 'instructions.md');
1555
+ if (existsSync(projectPath)) {
1556
+ try { projectInstructions = readFileSync(projectPath, 'utf8').trim(); }
1557
+ catch (e) { warn(`Could not read project instructions (${e.message})`); }
1558
+ }
1559
+
1560
+ const sections = [baseRules];
1561
+ if (globalInstructions) sections.push(`## Global instructions\n\n${globalInstructions}`);
1562
+ if (projectInstructions) sections.push(`## Project-specific instructions\n\n${projectInstructions}`);
1563
+ sections.push(buildBoardDigest(kandownDir));
1564
+
1565
+ out(sections.filter(Boolean).join('\n\n---\n\n'));
1388
1566
  }
1389
1567
 
1390
1568
  function parsePort(value) {
@@ -2503,13 +2681,20 @@ if (cmd === '--version' || cmd === '-v') {
2503
2681
  const skipUpdate = process.argv.slice(2).includes('--no-update-check');
2504
2682
 
2505
2683
  // 📖 Update policy (M1): the check only runs for INTERACTIVE commands.
2506
- // `shell` (scripts/agents) and `daemon` (spawned children, status probes)
2507
- // must never pay a network round-trip nor risk a mid-pipeline respawn.
2508
- // Inside checkForUpdate there are further guards: 24h throttle, TTY-only,
2509
- // and the KANDOWN_NO_UPDATE=1 opt-out.
2510
- const SCRIPTED_COMMANDS = new Set(['shell', 'daemon']);
2684
+ // The one-shot task commands (scripts/agents) and `daemon` (spawned
2685
+ // children, status probes) must never pay a network round-trip nor risk a
2686
+ // mid-pipeline respawn. Inside checkForUpdate there are further guards: 24h
2687
+ // throttle, TTY-only, and the KANDOWN_NO_UPDATE=1 opt-out.
2688
+ const SCRIPTED_COMMANDS = new Set([
2689
+ 'list', 'ls', 'show', 'create', 'new', 'move', 'assign', 'commit', 'tasks', 'work', 'daemon',
2690
+ ]);
2511
2691
  if (!skipUpdate && !SCRIPTED_COMMANDS.has(cmd)) await checkForUpdate(process.argv);
2512
2692
 
2693
+ // 📖 Catches breaking-change notices for upgrades that bypassed the
2694
+ // auto-updater above (manual `npm install -g kandown`, pnpm/yarn/bun, a
2695
+ // respawned child, etc). See checkVersionSeenNotices' doc comment.
2696
+ if (!SCRIPTED_COMMANDS.has(cmd)) checkVersionSeenNotices();
2697
+
2513
2698
  switch (cmd) {
2514
2699
  case 'init':
2515
2700
  cmdInit(rest);
@@ -2532,13 +2717,47 @@ switch (cmd) {
2532
2717
  cmdUpdate(rest);
2533
2718
  break;
2534
2719
 
2535
- case 'shell': {
2536
- // 📖 Two-level command: `kandown shell <subcmd> [...args]`. Pull the
2537
- // first non-flag arg as the subcommand and forward the rest.
2538
- const [shellSubcmd, ...shellRest] = rest;
2539
- cmdShell(shellSubcmd, shellRest);
2720
+ // 📖 One-shot task commands — top-level, no "shell" wrapper. These are the
2721
+ // most basic operations of the product (list/read/create/move/assign a
2722
+ // task, commit the board to git); nesting them under a prefix only added
2723
+ // friction for scripts and AI agents, which is exactly who these are for.
2724
+ case 'list':
2725
+ case 'ls':
2726
+ cmdList(rest);
2727
+ break;
2728
+
2729
+ case 'show':
2730
+ cmdShow(rest);
2731
+ break;
2732
+
2733
+ case 'create':
2734
+ case 'new':
2735
+ cmdCreate(rest);
2736
+ break;
2737
+
2738
+ case 'move':
2739
+ cmdMove(rest);
2740
+ break;
2741
+
2742
+ case 'assign':
2743
+ cmdAssign(rest);
2744
+ break;
2745
+
2746
+ case 'commit':
2747
+ cmdCommit(rest);
2748
+ break;
2749
+
2750
+ case 'tasks':
2751
+ // 📖 `kandown tasks` — cheatsheet for the commands above (`list help`
2752
+ // etc. would collide with real usage, so the index lives on its own verb).
2753
+ printTaskCommandsHelp();
2754
+ break;
2755
+
2756
+ case 'work':
2757
+ // 📖 `kandown work` — the agent entrypoint (rules + live board digest).
2758
+ // See cmdWork's doc comment for the full rationale.
2759
+ await cmdWork(rest);
2540
2760
  break;
2541
- }
2542
2761
 
2543
2762
  case 'help':
2544
2763
  case '--help':
package/dist/index.html CHANGED
@@ -113,7 +113,7 @@ Error generating stack: `+y.message+`
113
113
 
114
114
  ## Subtasks
115
115
 
116
- `}}async function Uz(t,e){try{return await(await(await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}async function Joe(t,e){try{return await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`),!0}catch{return!1}}async function Vz(t,e){try{return await t.getDirectoryHandle("archive",{create:e})}catch{return null}}async function ece(t){if(qt())return eS();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(qt())try{const i=await tS(e);return Rg(i)}catch{return vj(e)}const a=await(async i=>{try{return await(await(await i.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}})(t)??await Uz(t,e);return a!==null?Rg(a):vj(e)}async function tce(t,e){if(qt())try{const a=await tS(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 Uz(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=Q4(n,a);if(qt())return Voe(e,i);try{const o=await(await(await Joe(t,e)?await Vz(t,!0):t).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}}catch(r){throw tk(r,`${e}.md`)}}async function yj(t,e){if(qt())return Zoe(e);try{await t.removeEntry(`${e}.md`)}catch{}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}async function nce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/archive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function ace(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/unarchive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function ice(t,e,n,a){const i=Q4(n,a);if(qt())return nce(e,i);try{const o=await(await(await Vz(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 tk(r,`archive/${e}.md`)}}async function rce(t,e,n,a){const i=Q4(n,a);if(qt())return ace(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 tk(r,`${e}.md`)}}const sce="kanban-md",oce=1,ap="recentProjects";function nS(){return new Promise((t,e)=>{const n=indexedDB.open(sce,oce);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 T2(t){const e=await nS();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 jy(){const t=await nS();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 cce(t){const e=await nS();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 _j(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 Zz(t=Ci){const e=t.board.columns;return e[e.length-1]??"Done"}function pce(t,e=Ci){return t===Zz(e)||t.toLowerCase()==="archived"}class lce{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;start(e,n){this.dirHandle=e,this.tasksDirHandle=n,this.consecutiveErrors=0,this.disabled=!1,this.initHashes(),this.intervalId=setInterval(()=>void this.tick(),300)}stop(){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 kj(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 kj(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 wj(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 wj(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 kj(t){try{return await(await(await t.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function wj(t,e){try{return await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}const al=new lce,dce={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 xj=null;function Gz(){return"Notification"in window?Notification.permission:"unsupported"}async function uce(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function P2({title:t,body:e,config:n}){if(n.notifications.sound&&mce(n.notifications.soundId),!(!n.notifications.browser||Gz()!=="granted"))try{new Notification(t,{body:e})}catch{}}function mce(t){const e=dce[t],n=window.AudioContext??window.webkitAudioContext;if(!(!n||e.length===0))try{xj??=new n;const a=xj;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=Boe}=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 fce(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 gce(){const t=await eS();return await Promise.all(t.map(async n=>{const{frontmatter:a,body:i}=await qz(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 hce(t){const e=await ece(t),n=await Promise.all(e.map(async r=>{const s=await tce(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 Aj(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 v3(t){return{title:t.frontmatter.title||t.id,status:t.frontmatter.status||"Backlog",body:t.body,subtasks:t.subtasks}}function M2(t){fu.clear(),t.forEach(e=>{fu.set(e.id,v3(e))})}function bce(t){const e=t.split(/[\\/]+/).filter(Boolean),n=e[e.length-1];return n===".kandown"?e[e.length-2]??"Project":n??"Project"}function vce(t,e){return e.reduce((n,a,i)=>{const r=t[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function yce(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 L2=null;const ge=Poe((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,drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{if(!Hz()){const c=navigator.userAgent||"this browser";e().toast(new Hoe(c).message,"error",8e3);return}let n;try{n=await Yoe()}catch(c){c instanceof J4?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=qt()?E2():null;try{await T2({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 jy();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 _j(n.handle,!0)){let r=!1;try{await Ly(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 cce(n.id);const s=await jy();t({recentProjects:s})}catch{}return}e().toast("Permission denied — please grant access to the folder","error");return}try{const r=await Ly(n.handle),s=await b3(n.handle),o=n.handle.name;t({dirHandle:r,tasksDirHandle:s,projectName:o}),window.history.pushState({},"",`?p=${encodeURIComponent(o)}`);try{await T2({...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=E2();if(!n)throw new Error("No server root");await bj();const a=bce(n),i=await Bz();rl(i);const r=await eS(),s=await Promise.all(r.map(async f=>{const{frontmatter:h,body:b}=await qz(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}}));M2(s);const o=s.map(f=>({frontmatter:f.frontmatter,body:qd(f.body,f.subtasks)})),c=S2(o,i.board.columns),p=$2(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(!qt())return;const n=E2();if(!n)return;await bj();const a=await jy(),i=a.find(p=>p.kandownDir===n);if(!i){await e().openServerProject();return}if(!await _j(i.handle,!0)){await e().openServerProject();return}const s=await Ly(i.handle),o=await b3(i.handle),c=i.handle.name;t({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await T2({...i,lastOpened:Date.now()}),await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=e();if(!(!n&&!qt()))try{const a=await Xoe(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&&!qt())return;const r=n(i);t({config:r}),rl(r);try{await Qoe(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(qt()){const i=await gce();M2(i);const r=i.map(d=>({frontmatter:d.frontmatter,body:qd(d.body,d.subtasks)})),s=S2(r,a.board.columns),o=$2(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 hce(n);M2(i);const s=i.map(m=>({frontmatter:m.frontmatter,body:qd(m.body,m.subtasks)})),o=S2(s,a.board.columns),c=$2(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=qt();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(pce(i,o)){const E=new Map,T=Zz(o).toLowerCase();for(const D of s)for(const F of D.tasks){const H=F.frontmatter&&(F.frontmatter.archived===!0||F.frontmatter.archived==="true");E.set(F.id,{exists:!0,resolved:H||F.id===n||D.name.toLowerCase()===T})}const L=[];for(const D of b.dependsOn){if(typeof D!="string"||!D.trim()||D===n)continue;const F=E.get(D);(!F||!F.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(()=>Aj(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&&!qt())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=qt();if(!h&&!b)return;const{failedIds:k}=await Mf(()=>Aj(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&&!qt())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&&!qt())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=>yj(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=>{const{columns:a,tasksDirHandle:i,config:r,taskContents:s,searchMatches:o}=e();if(!i&&!qt()||!a.length)return null;const c=n||r.board.columns[0]||a[0].name,p=fce(a),d=a.find(w=>w.name===c)?.tasks.length??0,m={id:p,title:"",checked:!1,dependsOn:[],tags:[],assignee:null,priority:r.fields.priority?r.board.defaultPriority:null,ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",progress:null,frontmatter:{}},f=a.map(w=>w.name===c?{...w,tasks:[...w.tasks,m]}:w),h=new Map(s),b={id:p,title:"",status:c,order:d,priority:r.fields.priority?r.board.defaultPriority:"",tags:[],assignee:"",created:new Date().toISOString().slice(0,10),ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",tools:""},k="";h.set(p,{frontmatter:b,subtasks:[],body:k}),t({columns:f,taskContents:h});try{const w=i||null;return await Mf(()=>iu(w,p,b,k),{maxAttempts:3}),e().toast(`Created ${p.replace(/^t/,"")}`),await e().openDrawer(p),p}catch(w){const A=w;return A instanceof $o?e().toast("Disk is full — task was not created.","error",8e3):e().toast("Failed to create: "+A.message,"error"),t({columns:a,taskContents:s,searchMatches:o}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r,searchMatches:s}=e();if(!i&&!qt())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 yj(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&&!qt()))try{const{frontmatter:i,body:r}=await Es(a||null,n);await ice(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&&!qt()))try{const{frontmatter:i,body:r}=await Es(a||null,n),s={...i};delete s.archived,await rce(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&&!qt()))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(!qt()){t({agentHook:null});return}const n=await Goe();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 Woe(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=zz(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(qt()){L2&&clearInterval(L2),L2=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&&P2({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=v3(h),k=fu.get(s);if(!k){fu.set(s,b);return}c.notifications.statusChanges&&k.status!==b.status&&P2({title:"Task status changed",body:`${b.title}: ${k.status} → ${b.status}`,config:c});const w=vce(k.subtasks,b.subtasks);c.notifications.subtaskCompletions&&w>0&&P2({title:"Subtask completed",body:w===1?`${b.title}: 1 subtask completed.`:`${b.title}: ${w} subtasks completed.`,config:c}),yce(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,v3({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"))}}));jy().then(t=>{ge.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(ge.getState().config)});const kce=Object.freeze(Object.defineProperty({__proto__:null,useStore:ge},Symbol.toStringTag,{value:"Module"}));function wce(){const t=ge(o=>o.config),e=ge(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(at.button,{onClick:s,className:g3("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(at.div,{className:g3("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(at.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 xce(t){const e=Nse(t,{stiffness:180,damping:22,mass:.8});return $.useEffect(()=>{e.set(t)},[t,e]),_z(e,a=>Math.round(a).toString())}const y3="0.17.2",Ace=({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 Cce(){const{t}=Cn(),e=ge(R=>R.dirHandle),n=ge(R=>R.isOpen),a=ge(R=>R.projectName),i=ge(R=>R.archivedTasks.length),r=ge(R=>R.showArchives),s=ge(R=>R.setShowArchives),o=ge(R=>R.columns),c=ge(R=>R.openFolder),p=ge(R=>R.reloadBoard),d=ge(R=>R.createTask),m=ge(R=>R.setCommandOpen),f=ge(R=>R.viewMode),h=ge(R=>R.setViewMode),b=ge(R=>R.density),k=ge(R=>R.setDensity),w=ge(R=>R.setCurrentPage),A=ge(R=>R.recentProjects),S=ge(R=>R.openRecentProject),E=ge(R=>R.filters),T=ge(R=>R.setFilter),L=ge(R=>R.clearFilters),D=ge(R=>R.config.fields),F=ge(R=>R.lastReloadError),H=ge(R=>R.watcherError),O=ge(R=>R.restartWatcher),[j,K]=$.useState(!1),[Q,ee]=$.useState(!0),U=$.useRef(null),J=$.useRef(null),W=o.reduce((R,Z)=>R+Z.tasks.length,0),V=xce(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:[(F||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}`:`⚠ ${F}`}),H&&_.jsx("button",{type:"button",onClick:O,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(Ace,{className:"w-[34px] h-[34px] dark:text-white text-black"}),_.jsx(Sr,{mode:"wait",initial:!1,children:Q?_.jsxs(at.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",y3]})]},"boot"):a?_.jsx(at.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(Sr,{children:B.map(R=>_.jsxs(at.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(Sr,{children:j&&_.jsx(at.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(at.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(wce,{}),_.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 Uz(t,e){try{return await(await(await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}async function Joe(t,e){try{return await(await t.getDirectoryHandle("archive",{create:!1})).getFileHandle(`${e}.md`),!0}catch{return!1}}async function Vz(t,e){try{return await t.getDirectoryHandle("archive",{create:e})}catch{return null}}async function ece(t){if(qt())return eS();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(qt())try{const i=await tS(e);return Rg(i)}catch{return vj(e)}const a=await(async i=>{try{return await(await(await i.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}})(t)??await Uz(t,e);return a!==null?Rg(a):vj(e)}async function tce(t,e){if(qt())try{const a=await tS(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 Uz(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=Q4(n,a);if(qt())return Voe(e,i);try{const o=await(await(await Joe(t,e)?await Vz(t,!0):t).getFileHandle(`${e}.md`,{create:!0})).createWritable();try{await o.write(i)}finally{await o.close()}}catch(r){throw tk(r,`${e}.md`)}}async function yj(t,e){if(qt())return Zoe(e);try{await t.removeEntry(`${e}.md`)}catch{}try{await(await t.getDirectoryHandle("archive",{create:!1})).removeEntry(`${e}.md`)}catch{}}async function nce(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/archive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function ace(t,e){await hs(`/api/tasks/${encodeURIComponent(t)}/unarchive`,{method:"POST",body:e,headers:{"Content-Type":"text/plain"}})}async function ice(t,e,n,a){const i=Q4(n,a);if(qt())return nce(e,i);try{const o=await(await(await Vz(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 tk(r,`archive/${e}.md`)}}async function rce(t,e,n,a){const i=Q4(n,a);if(qt())return ace(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 tk(r,`${e}.md`)}}const sce="kanban-md",oce=1,ap="recentProjects";function nS(){return new Promise((t,e)=>{const n=indexedDB.open(sce,oce);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 T2(t){const e=await nS();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 jy(){const t=await nS();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 cce(t){const e=await nS();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 _j(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 Zz(t=Ci){const e=t.board.columns;return e[e.length-1]??"Done"}function pce(t,e=Ci){return t===Zz(e)||t.toLowerCase()==="archived"}class lce{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;start(e,n){this.dirHandle=e,this.tasksDirHandle=n,this.consecutiveErrors=0,this.disabled=!1,this.initHashes(),this.intervalId=setInterval(()=>void this.tick(),300)}stop(){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 kj(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 kj(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 wj(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 wj(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 kj(t){try{return await(await(await t.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function wj(t,e){try{return await(await(await t.getFileHandle(`${e}.md`)).getFile()).text()}catch{return null}}const al=new lce,dce={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 xj=null;function Gz(){return"Notification"in window?Notification.permission:"unsupported"}async function uce(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function P2({title:t,body:e,config:n}){if(n.notifications.sound&&mce(n.notifications.soundId),!(!n.notifications.browser||Gz()!=="granted"))try{new Notification(t,{body:e})}catch{}}function mce(t){const e=dce[t],n=window.AudioContext??window.webkitAudioContext;if(!(!n||e.length===0))try{xj??=new n;const a=xj;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=Boe}=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 fce(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 gce(){const t=await eS();return await Promise.all(t.map(async n=>{const{frontmatter:a,body:i}=await qz(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 hce(t){const e=await ece(t),n=await Promise.all(e.map(async r=>{const s=await tce(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 Aj(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 v3(t){return{title:t.frontmatter.title||t.id,status:t.frontmatter.status||"Backlog",body:t.body,subtasks:t.subtasks}}function M2(t){fu.clear(),t.forEach(e=>{fu.set(e.id,v3(e))})}function bce(t){const e=t.split(/[\\/]+/).filter(Boolean),n=e[e.length-1];return n===".kandown"?e[e.length-2]??"Project":n??"Project"}function vce(t,e){return e.reduce((n,a,i)=>{const r=t[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function yce(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 L2=null;const ge=Poe((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,drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{if(!Hz()){const c=navigator.userAgent||"this browser";e().toast(new Hoe(c).message,"error",8e3);return}let n;try{n=await Yoe()}catch(c){c instanceof J4?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=qt()?E2():null;try{await T2({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 jy();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 _j(n.handle,!0)){let r=!1;try{await Ly(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 cce(n.id);const s=await jy();t({recentProjects:s})}catch{}return}e().toast("Permission denied — please grant access to the folder","error");return}try{const r=await Ly(n.handle),s=await b3(n.handle),o=n.handle.name;t({dirHandle:r,tasksDirHandle:s,projectName:o}),window.history.pushState({},"",`?p=${encodeURIComponent(o)}`);try{await T2({...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=E2();if(!n)throw new Error("No server root");await bj();const a=bce(n),i=await Bz();rl(i);const r=await eS(),s=await Promise.all(r.map(async f=>{const{frontmatter:h,body:b}=await qz(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}}));M2(s);const o=s.map(f=>({frontmatter:f.frontmatter,body:qd(f.body,f.subtasks)})),c=S2(o,i.board.columns),p=$2(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(!qt())return;const n=E2();if(!n)return;await bj();const a=await jy(),i=a.find(p=>p.kandownDir===n);if(!i){await e().openServerProject();return}if(!await _j(i.handle,!0)){await e().openServerProject();return}const s=await Ly(i.handle),o=await b3(i.handle),c=i.handle.name;t({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await T2({...i,lastOpened:Date.now()}),await e().loadConfig(),await e().reloadBoard(),e().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=e();if(!(!n&&!qt()))try{const a=await Xoe(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&&!qt())return;const r=n(i);t({config:r}),rl(r);try{await Qoe(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(qt()){const i=await gce();M2(i);const r=i.map(d=>({frontmatter:d.frontmatter,body:qd(d.body,d.subtasks)})),s=S2(r,a.board.columns),o=$2(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 hce(n);M2(i);const s=i.map(m=>({frontmatter:m.frontmatter,body:qd(m.body,m.subtasks)})),o=S2(s,a.board.columns),c=$2(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=qt();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(pce(i,o)){const E=new Map,T=Zz(o).toLowerCase();for(const D of s)for(const F of D.tasks){const H=F.frontmatter&&(F.frontmatter.archived===!0||F.frontmatter.archived==="true");E.set(F.id,{exists:!0,resolved:H||F.id===n||D.name.toLowerCase()===T})}const L=[];for(const D of b.dependsOn){if(typeof D!="string"||!D.trim()||D===n)continue;const F=E.get(D);(!F||!F.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(()=>Aj(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&&!qt())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=qt();if(!h&&!b)return;const{failedIds:k}=await Mf(()=>Aj(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&&!qt())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&&!qt())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=>yj(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=>{const{columns:a,tasksDirHandle:i,config:r,taskContents:s,searchMatches:o}=e();if(!i&&!qt()||!a.length)return null;const c=n||r.board.columns[0]||a[0].name,p=fce(a),d=a.find(w=>w.name===c)?.tasks.length??0,m={id:p,title:"",checked:!1,dependsOn:[],tags:[],assignee:null,priority:r.fields.priority?r.board.defaultPriority:null,ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",progress:null,frontmatter:{}},f=a.map(w=>w.name===c?{...w,tasks:[...w.tasks,m]}:w),h=new Map(s),b={id:p,title:"",status:c,order:d,priority:r.fields.priority?r.board.defaultPriority:"",tags:[],assignee:"",created:new Date().toISOString().slice(0,10),ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",tools:""},k="";h.set(p,{frontmatter:b,subtasks:[],body:k}),t({columns:f,taskContents:h});try{const w=i||null;return await Mf(()=>iu(w,p,b,k),{maxAttempts:3}),e().toast(`Created ${p.replace(/^t/,"")}`),await e().openDrawer(p),p}catch(w){const A=w;return A instanceof $o?e().toast("Disk is full — task was not created.","error",8e3):e().toast("Failed to create: "+A.message,"error"),t({columns:a,taskContents:s,searchMatches:o}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r,searchMatches:s}=e();if(!i&&!qt())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 yj(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&&!qt()))try{const{frontmatter:i,body:r}=await Es(a||null,n);await ice(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&&!qt()))try{const{frontmatter:i,body:r}=await Es(a||null,n),s={...i};delete s.archived,await rce(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&&!qt()))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(!qt()){t({agentHook:null});return}const n=await Goe();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 Woe(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=zz(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(qt()){L2&&clearInterval(L2),L2=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&&P2({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=v3(h),k=fu.get(s);if(!k){fu.set(s,b);return}c.notifications.statusChanges&&k.status!==b.status&&P2({title:"Task status changed",body:`${b.title}: ${k.status} → ${b.status}`,config:c});const w=vce(k.subtasks,b.subtasks);c.notifications.subtaskCompletions&&w>0&&P2({title:"Subtask completed",body:w===1?`${b.title}: 1 subtask completed.`:`${b.title}: ${w} subtasks completed.`,config:c}),yce(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,v3({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"))}}));jy().then(t=>{ge.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(ge.getState().config)});const kce=Object.freeze(Object.defineProperty({__proto__:null,useStore:ge},Symbol.toStringTag,{value:"Module"}));function wce(){const t=ge(o=>o.config),e=ge(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(at.button,{onClick:s,className:g3("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(at.div,{className:g3("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(at.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 xce(t){const e=Nse(t,{stiffness:180,damping:22,mass:.8});return $.useEffect(()=>{e.set(t)},[t,e]),_z(e,a=>Math.round(a).toString())}const y3="0.19.0",Ace=({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 Cce(){const{t}=Cn(),e=ge(R=>R.dirHandle),n=ge(R=>R.isOpen),a=ge(R=>R.projectName),i=ge(R=>R.archivedTasks.length),r=ge(R=>R.showArchives),s=ge(R=>R.setShowArchives),o=ge(R=>R.columns),c=ge(R=>R.openFolder),p=ge(R=>R.reloadBoard),d=ge(R=>R.createTask),m=ge(R=>R.setCommandOpen),f=ge(R=>R.viewMode),h=ge(R=>R.setViewMode),b=ge(R=>R.density),k=ge(R=>R.setDensity),w=ge(R=>R.setCurrentPage),A=ge(R=>R.recentProjects),S=ge(R=>R.openRecentProject),E=ge(R=>R.filters),T=ge(R=>R.setFilter),L=ge(R=>R.clearFilters),D=ge(R=>R.config.fields),F=ge(R=>R.lastReloadError),H=ge(R=>R.watcherError),O=ge(R=>R.restartWatcher),[j,K]=$.useState(!1),[Q,ee]=$.useState(!0),U=$.useRef(null),J=$.useRef(null),W=o.reduce((R,Z)=>R+Z.tasks.length,0),V=xce(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:[(F||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}`:`⚠ ${F}`}),H&&_.jsx("button",{type:"button",onClick:O,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(Ace,{className:"w-[34px] h-[34px] dark:text-white text-black"}),_.jsx(Sr,{mode:"wait",initial:!1,children:Q?_.jsxs(at.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",y3]})]},"boot"):a?_.jsx(at.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(Sr,{children:B.map(R=>_.jsxs(at.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(Sr,{children:j&&_.jsx(at.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(at.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(wce,{}),_.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.17.2",
3
+ "version": "0.19.0",
4
4
  "description": "File-based Kanban engine backed by Markdown. Zero backend, AI-agent friendly.",
5
5
  "type": "module",
6
6
  "bin": {