kandown 0.17.2 → 0.18.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 +64 -0
- package/bin/kandown.js +247 -97
- package/dist/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,8 +91,72 @@ The web daemon stays alive after you quit the TUI so the browser keeps working.
|
|
|
91
91
|
| `kandown daemon start` | Start/reconnect this project's web daemon |
|
|
92
92
|
| `kandown daemon stop` | Stop this project's web daemon |
|
|
93
93
|
| `kandown update` | Update `kandown.html` to latest |
|
|
94
|
+
| `kandown list` \| `show` \| `create` \| `move` \| `assign` \| `commit` | One-shot task commands (see below) |
|
|
95
|
+
| `kandown tasks` | Full help for the one-shot task commands |
|
|
96
|
+
| `kandown work` | **For AI agents:** print the agent rules + a live board digest (see below) |
|
|
94
97
|
| `kandown help` | CLI help |
|
|
95
98
|
|
|
99
|
+
### One-shot task commands (scriptable, agent-friendly)
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `kandown list` | List tasks — `[-s status] [-a assignee] [-t tag] [-p priority] [--archived] [--json]` |
|
|
106
|
+
| `kandown show <id>` | Print a task file's raw content |
|
|
107
|
+
| `kandown create "title"` | Create a task — `[-p priority] [-a assignee] [-t tag ...] [--to status] [--id custom-id] [--json]` |
|
|
108
|
+
| `kandown move <id> <status>` | Move a task — `<status>` is a column name or `"archived"` |
|
|
109
|
+
| `kandown assign <id> [name]` | Assign a task (omit name to unassign) |
|
|
110
|
+
| `kandown commit [-m "message"]` | `git add tasks/ .kandown/kandown.json` + `git commit` |
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
kandown list --json | jq '.[] | select(.priority=="P1")'
|
|
114
|
+
kandown create "Refactor auth middleware" -p P1 -t backend
|
|
115
|
+
kandown move t42 Done
|
|
116
|
+
kandown assign t42 alice
|
|
117
|
+
kandown commit -m "tasks: add auth refactor"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**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.
|
|
121
|
+
|
|
122
|
+
**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.
|
|
123
|
+
|
|
124
|
+
### `kandown work` — the agent entrypoint
|
|
125
|
+
|
|
126
|
+
`kandown init` no longer copies a block of rules into your `AGENTS.md`/`CLAUDE.md` — it appends a single line instead:
|
|
127
|
+
|
|
128
|
+
> 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.
|
|
129
|
+
|
|
130
|
+
Running `kandown work` prints, as plain markdown on stdout:
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
2. **Global instructions** (optional) — `~/.kandown/instructions.md`, applied to every kandown project on this machine (personal conventions, team-wide rules).
|
|
134
|
+
3. **Project instructions** (optional) — `.kandown/instructions.md`, this project only (stack quirks, "always use pnpm", commit message language, etc).
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Environment variables
|
|
142
|
+
|
|
143
|
+
| Variable | Effect |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `KANDOWN_NO_UPDATE=1` | Disable the background auto-update check entirely (recommended for CI) |
|
|
146
|
+
| `KANDOWN_DEBUG=1` | Print full stack traces on unexpected errors instead of a one-line summary |
|
|
147
|
+
| `KANDOWN_AGENT_HOOK_URL` | POST endpoint that receives tasks sent via the "Send to Agent" button / TUI `g` key |
|
|
148
|
+
| `KANDOWN_AGENT_HOOK_LABEL` | Custom label for the agent hook button (default: `Agent`) |
|
|
149
|
+
| `KANDOWN_AGENT_HOOK_HEADERS` | JSON object of extra HTTP headers to send with the agent hook request |
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Security notes
|
|
156
|
+
|
|
157
|
+
- 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.
|
|
158
|
+
- `.kandown/daemon.json` and `.kandown/daemon.lock` are runtime-only files; `kandown init` adds them to `.kandown/.gitignore` automatically.
|
|
159
|
+
|
|
96
160
|
---
|
|
97
161
|
|
|
98
162
|
## 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,
|
|
@@ -383,7 +384,7 @@ function atomicWriteFileSync(path, content) {
|
|
|
383
384
|
}
|
|
384
385
|
|
|
385
386
|
// 📖 Output contract (M2): stdout carries DATA ONLY (ids, JSON, tables, help)
|
|
386
|
-
// so `$(kandown
|
|
387
|
+
// so `$(kandown create ...)` and `| jq` pipelines stay clean. Every
|
|
387
388
|
// decoration — status lines, warnings, errors, progress — goes to stderr.
|
|
388
389
|
/** Data output → stdout. Use ONLY for content the command was asked to produce. */
|
|
389
390
|
const out = (msg) => console.log(msg);
|
|
@@ -457,7 +458,14 @@ ${c.bold}Commands:${c.reset}
|
|
|
457
458
|
${c.cyan}settings${c.reset} Open the settings TUI
|
|
458
459
|
${c.cyan}daemon${c.reset} Manage the per-project web daemon (start|stop|status)
|
|
459
460
|
${c.cyan}update${c.reset} Update kandown.html to the latest version
|
|
460
|
-
${c.cyan}
|
|
461
|
+
${c.cyan}list${c.reset} List tasks ${c.dim}(-s status, -a assignee, -t tag, -p priority, --json)${c.reset}
|
|
462
|
+
${c.cyan}show${c.reset} Print a task's raw file content
|
|
463
|
+
${c.cyan}create${c.reset} Create a task ${c.dim}(-p priority, -a assignee, -t tag, --to status)${c.reset}
|
|
464
|
+
${c.cyan}move${c.reset} Move a task to a column (or "archived")
|
|
465
|
+
${c.cyan}assign${c.reset} Assign / unassign a task
|
|
466
|
+
${c.cyan}commit${c.reset} git add + commit the task files
|
|
467
|
+
${c.cyan}tasks${c.reset} Full help for the commands above
|
|
468
|
+
${c.cyan}work${c.reset} ${c.bold}For AI agents:${c.reset} print the agent rules + live board digest
|
|
461
469
|
${c.cyan}help${c.reset} Show this help
|
|
462
470
|
|
|
463
471
|
${c.bold}Options:${c.reset}
|
|
@@ -471,9 +479,10 @@ ${c.bold}Examples:${c.reset}
|
|
|
471
479
|
${c.dim}$${c.reset} npx kandown init
|
|
472
480
|
${c.dim}$${c.reset} npx kandown init --path docs/kanban
|
|
473
481
|
${c.dim}$${c.reset} npx kandown init --no-agents
|
|
474
|
-
${c.dim}$${c.reset} npx kandown
|
|
475
|
-
${c.dim}$${c.reset} npx kandown
|
|
476
|
-
${c.dim}$${c.reset} npx kandown
|
|
482
|
+
${c.dim}$${c.reset} npx kandown work ${c.dim}# what an AI agent should run first${c.reset}
|
|
483
|
+
${c.dim}$${c.reset} npx kandown list --json
|
|
484
|
+
${c.dim}$${c.reset} npx kandown create "Refactor auth" -p P1
|
|
485
|
+
${c.dim}$${c.reset} npx kandown commit -m "tasks: refactor auth"
|
|
477
486
|
`);
|
|
478
487
|
}
|
|
479
488
|
|
|
@@ -548,12 +557,7 @@ function appendAgentReference(cwd, agentsFile, kandownPath) {
|
|
|
548
557
|
${marker}
|
|
549
558
|
## Task management
|
|
550
559
|
|
|
551
|
-
**
|
|
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
|
|
560
|
+
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
561
|
`;
|
|
558
562
|
|
|
559
563
|
try {
|
|
@@ -574,12 +578,7 @@ function createAgentsFileIfMissing(cwd, kandownPath) {
|
|
|
574
578
|
<!-- kandown:agent-ref -->
|
|
575
579
|
## Task management
|
|
576
580
|
|
|
577
|
-
**
|
|
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
|
|
581
|
+
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
582
|
`;
|
|
584
583
|
writeFileSync(agentsPath, content, 'utf8');
|
|
585
584
|
return true;
|
|
@@ -883,9 +882,9 @@ function cmdUpdate(rawArgs) {
|
|
|
883
882
|
success(`Updated ${args.path}/kandown.html`);
|
|
884
883
|
}
|
|
885
884
|
|
|
886
|
-
/* ═════════════
|
|
885
|
+
/* ═════════════ One-shot task commands ═════════════ */
|
|
887
886
|
/**
|
|
888
|
-
* 📖 Self-contained minimal YAML frontmatter parser/writer for the
|
|
887
|
+
* 📖 Self-contained minimal YAML frontmatter parser/writer for the one-shot task
|
|
889
888
|
* commands. The web app uses a richer schema (parseSimpleYaml) in the
|
|
890
889
|
* browser; the CLI keeps its own because it can't import the browser bundle
|
|
891
890
|
* and these commands only need to round-trip a known small set of scalar
|
|
@@ -1028,7 +1027,7 @@ function nextTaskId(kandownDir) {
|
|
|
1028
1027
|
return 't' + (maxN + 1);
|
|
1029
1028
|
}
|
|
1030
1029
|
|
|
1031
|
-
function
|
|
1030
|
+
function padCell(str, len) {
|
|
1032
1031
|
const s = String(str);
|
|
1033
1032
|
// 📖 Only truncate when the string is strictly LONGER than the column —
|
|
1034
1033
|
// an exact fit must render as-is (`>=` chopped the last char of e.g. the
|
|
@@ -1037,8 +1036,8 @@ function shellPad(str, len) {
|
|
|
1037
1036
|
return s + ' '.repeat(len - s.length);
|
|
1038
1037
|
}
|
|
1039
1038
|
|
|
1040
|
-
function
|
|
1041
|
-
// 📖 Minimal flag parser for the
|
|
1039
|
+
function taskParseArgs(argv) {
|
|
1040
|
+
// 📖 Minimal flag parser for the task commands. Stops at the first
|
|
1042
1041
|
// positional arg so `kandown create "Some title with -dash"` works.
|
|
1043
1042
|
const out = { positional: [], flags: {} };
|
|
1044
1043
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -1058,9 +1057,9 @@ function shellParseArgs(argv) {
|
|
|
1058
1057
|
return out;
|
|
1059
1058
|
}
|
|
1060
1059
|
|
|
1061
|
-
function
|
|
1060
|
+
function resolveStatusArg(config, status) {
|
|
1062
1061
|
// 📖 Status can be a configured column name OR the reserved `archived`
|
|
1063
|
-
// sentinel. Match is case-insensitive to
|
|
1062
|
+
// sentinel. Match is case-insensitive to stay forgiving in scripts.
|
|
1064
1063
|
if (!status) return null;
|
|
1065
1064
|
const lower = status.toLowerCase();
|
|
1066
1065
|
if (lower === 'archived') return 'archived';
|
|
@@ -1071,11 +1070,11 @@ function shellResolveStatus(config, status) {
|
|
|
1071
1070
|
return null;
|
|
1072
1071
|
}
|
|
1073
1072
|
|
|
1074
|
-
function
|
|
1073
|
+
function cmdList(rawArgs) {
|
|
1075
1074
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1076
|
-
const args =
|
|
1075
|
+
const args = taskParseArgs(rawArgs);
|
|
1077
1076
|
const config = readKandownConfig(kandownDir);
|
|
1078
|
-
const statusFilter = args.flags.status ?
|
|
1077
|
+
const statusFilter = args.flags.status ? resolveStatusArg(config, args.flags.status) : null;
|
|
1079
1078
|
if (args.flags.status && !statusFilter) {
|
|
1080
1079
|
err(`Unknown status: ${args.flags.status}`);
|
|
1081
1080
|
process.exit(1);
|
|
@@ -1111,28 +1110,28 @@ function shellList(rawArgs) {
|
|
|
1111
1110
|
|
|
1112
1111
|
if (rows.length === 0) {
|
|
1113
1112
|
// 📖 Placeholder goes to stderr — an empty result on stdout stays empty
|
|
1114
|
-
// so `[ -z "$(kandown
|
|
1113
|
+
// so `[ -z "$(kandown list ...)" ]` style checks behave.
|
|
1115
1114
|
log(c.dim + '(no tasks)' + c.reset);
|
|
1116
1115
|
return;
|
|
1117
1116
|
}
|
|
1118
1117
|
|
|
1119
1118
|
const idW = Math.max(2, ...rows.map(r => r.id.length));
|
|
1120
|
-
out(`${c.dim}${
|
|
1119
|
+
out(`${c.dim}${padCell('ID', idW)} ${padCell('STATUS', 14)} ${padCell('PRI', 4)} ${padCell('ASSIGNEE', 12)} TITLE${c.reset}`);
|
|
1121
1120
|
for (const r of rows) {
|
|
1122
1121
|
const status = (r.fm.status || 'Backlog') + (r.fm.archived === true || r.fm.archived === 'true' ? ' (archived)' : '');
|
|
1123
1122
|
const pri = r.fm.priority || '';
|
|
1124
1123
|
const assignee = r.fm.assignee || '';
|
|
1125
1124
|
const title = (r.fm.title || '(untitled)').replace(/\n/g, ' ');
|
|
1126
|
-
out(`${
|
|
1125
|
+
out(`${padCell(r.id, idW)} ${padCell(status, 14)} ${padCell(pri, 4)} ${padCell(assignee, 12)} ${title}`);
|
|
1127
1126
|
}
|
|
1128
1127
|
}
|
|
1129
1128
|
|
|
1130
|
-
function
|
|
1129
|
+
function cmdShow(rawArgs) {
|
|
1131
1130
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1132
|
-
const args =
|
|
1131
|
+
const args = taskParseArgs(rawArgs);
|
|
1133
1132
|
const id = args.positional[0];
|
|
1134
1133
|
if (!id) {
|
|
1135
|
-
err('Usage: kandown
|
|
1134
|
+
err('Usage: kandown show <id>');
|
|
1136
1135
|
process.exit(1);
|
|
1137
1136
|
}
|
|
1138
1137
|
const path = findTaskFile(kandownDir, id);
|
|
@@ -1149,7 +1148,7 @@ function shellShow(rawArgs) {
|
|
|
1149
1148
|
* resolved when it lives in the terminal column OR is archived. Unknown
|
|
1150
1149
|
* ids and self-references never block.
|
|
1151
1150
|
*/
|
|
1152
|
-
function
|
|
1151
|
+
function depIsResolved(kandownDir, id, terminalLower) {
|
|
1153
1152
|
if (!/^[a-zA-Z0-9_-]+$/.test(id)) return true; // unknown / invalid → don't block
|
|
1154
1153
|
const path = findTaskFile(kandownDir, id);
|
|
1155
1154
|
if (!path) return true; // unknown id → don't block
|
|
@@ -1158,17 +1157,17 @@ function shellTaskIsResolved(kandownDir, id, terminalLower) {
|
|
|
1158
1157
|
return isArch || (parsed.frontmatter.status || '').toLowerCase() === terminalLower;
|
|
1159
1158
|
}
|
|
1160
1159
|
|
|
1161
|
-
function
|
|
1160
|
+
function cmdCreate(rawArgs) {
|
|
1162
1161
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1163
|
-
const args =
|
|
1162
|
+
const args = taskParseArgs(rawArgs);
|
|
1164
1163
|
const title = args.positional.join(' ').trim();
|
|
1165
1164
|
if (!title) {
|
|
1166
|
-
err('Usage: kandown
|
|
1165
|
+
err('Usage: kandown create "title" [-p priority] [-a assignee] [-t tag] [--to status]');
|
|
1167
1166
|
process.exit(1);
|
|
1168
1167
|
}
|
|
1169
1168
|
const config = readKandownConfig(kandownDir);
|
|
1170
1169
|
const defaultStatus = (config && config.board && config.board.columns && config.board.columns[0]) || 'Backlog';
|
|
1171
|
-
const targetStatus = args.flags.to ?
|
|
1170
|
+
const targetStatus = args.flags.to ? resolveStatusArg(config, args.flags.to) : defaultStatus;
|
|
1172
1171
|
if (args.flags.to && !targetStatus) {
|
|
1173
1172
|
err(`Unknown status: ${args.flags.to}`);
|
|
1174
1173
|
process.exit(1);
|
|
@@ -1207,17 +1206,17 @@ function shellCreate(rawArgs) {
|
|
|
1207
1206
|
}
|
|
1208
1207
|
}
|
|
1209
1208
|
|
|
1210
|
-
function
|
|
1209
|
+
function cmdMove(rawArgs) {
|
|
1211
1210
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1212
|
-
const args =
|
|
1211
|
+
const args = taskParseArgs(rawArgs);
|
|
1213
1212
|
const [id, rawStatus] = args.positional;
|
|
1214
1213
|
const targetStatus = rawStatus || args.flags.to;
|
|
1215
1214
|
if (!id || !targetStatus) {
|
|
1216
|
-
err('Usage: kandown
|
|
1215
|
+
err('Usage: kandown move <id> <status>');
|
|
1217
1216
|
process.exit(1);
|
|
1218
1217
|
}
|
|
1219
1218
|
const config = readKandownConfig(kandownDir);
|
|
1220
|
-
const resolved =
|
|
1219
|
+
const resolved = resolveStatusArg(config, targetStatus);
|
|
1221
1220
|
if (!resolved) {
|
|
1222
1221
|
err(`Unknown status: ${targetStatus}`);
|
|
1223
1222
|
process.exit(1);
|
|
@@ -1238,7 +1237,7 @@ function shellMove(rawArgs) {
|
|
|
1238
1237
|
const blocked = [];
|
|
1239
1238
|
for (const dep of deps) {
|
|
1240
1239
|
if (typeof dep !== 'string' || !dep.trim() || dep === id) continue;
|
|
1241
|
-
if (!
|
|
1240
|
+
if (!depIsResolved(kandownDir, dep, terminalLower)) blocked.push(dep);
|
|
1242
1241
|
}
|
|
1243
1242
|
if (blocked.length > 0) {
|
|
1244
1243
|
const list = blocked.length === 1 ? blocked[0] : `${blocked.slice(0, -1).join(', ')} and ${blocked[blocked.length - 1]}`;
|
|
@@ -1267,12 +1266,12 @@ function shellMove(rawArgs) {
|
|
|
1267
1266
|
log(`${c.green}✓${c.reset} ${c.bold}${id}${c.reset} → ${resolved}`);
|
|
1268
1267
|
}
|
|
1269
1268
|
|
|
1270
|
-
function
|
|
1269
|
+
function cmdAssign(rawArgs) {
|
|
1271
1270
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1272
|
-
const args =
|
|
1271
|
+
const args = taskParseArgs(rawArgs);
|
|
1273
1272
|
const [id, name] = args.positional;
|
|
1274
1273
|
if (!id) {
|
|
1275
|
-
err('Usage: kandown
|
|
1274
|
+
err('Usage: kandown assign <id> [name] (omit name to unassign)');
|
|
1276
1275
|
process.exit(1);
|
|
1277
1276
|
}
|
|
1278
1277
|
const path = findTaskFile(kandownDir, id);
|
|
@@ -1287,9 +1286,9 @@ function shellAssign(rawArgs) {
|
|
|
1287
1286
|
log(`${c.green}✓${c.reset} ${c.bold}${id}${c.reset} → ${name ? c.cyan + name : c.dim + '(unassigned)'}${c.reset}`);
|
|
1288
1287
|
}
|
|
1289
1288
|
|
|
1290
|
-
function
|
|
1289
|
+
function cmdCommit(rawArgs) {
|
|
1291
1290
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1292
|
-
const args =
|
|
1291
|
+
const args = taskParseArgs(rawArgs);
|
|
1293
1292
|
const projectRoot = getProjectRoot(kandownDir);
|
|
1294
1293
|
// 📖 Refuse to commit if not inside a git repo — silently failing here would
|
|
1295
1294
|
// let the user believe they versioned their tasks when they didn't.
|
|
@@ -1335,34 +1334,16 @@ function shellCommit(rawArgs) {
|
|
|
1335
1334
|
}
|
|
1336
1335
|
}
|
|
1337
1336
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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}
|
|
1337
|
+
/**
|
|
1338
|
+
* 📖 Prints the one-shot task-command cheatsheet. Reachable as `kandown tasks`
|
|
1339
|
+
* (or `kandown tasks help`) — the commands themselves (list/show/create/move/
|
|
1340
|
+
* assign/commit) are top-level, not nested under a "shell" prefix, since
|
|
1341
|
+
* they're the most basic operations of the product and scripts/agents should
|
|
1342
|
+
* never need to remember a wrapper word to reach them.
|
|
1343
|
+
*/
|
|
1344
|
+
function printTaskCommandsHelp() {
|
|
1345
|
+
out(`
|
|
1346
|
+
${c.bold}kandown${c.reset} ${c.dim}· one-shot task commands (scriptable, agent-friendly)${c.reset}
|
|
1366
1347
|
|
|
1367
1348
|
${c.bold}Commands:${c.reset}
|
|
1368
1349
|
${c.cyan}list${c.reset} [-s status] [-a assignee] [-t tag] [-p priority] [--archived] [--json]
|
|
@@ -1373,18 +1354,151 @@ ${c.bold}Commands:${c.reset}
|
|
|
1373
1354
|
${c.cyan}commit${c.reset} [-m "message"] (git add tasks/ + .kandown/kandown.json + git commit)
|
|
1374
1355
|
|
|
1375
1356
|
${c.bold}Examples:${c.reset}
|
|
1376
|
-
${c.dim}$${c.reset} kandown
|
|
1377
|
-
${c.dim}$${c.reset} kandown
|
|
1378
|
-
${c.dim}$${c.reset} kandown
|
|
1379
|
-
${c.dim}$${c.reset} kandown
|
|
1380
|
-
${c.dim}$${c.reset} kandown
|
|
1357
|
+
${c.dim}$${c.reset} kandown list --json | jq '.[] | select(.priority=="P1")'
|
|
1358
|
+
${c.dim}$${c.reset} kandown create "Refactor auth middleware" -p P1 -t backend
|
|
1359
|
+
${c.dim}$${c.reset} kandown move t42 Done
|
|
1360
|
+
${c.dim}$${c.reset} kandown assign t42 alice
|
|
1361
|
+
${c.dim}$${c.reset} kandown commit -m "tasks: add auth refactor"
|
|
1381
1362
|
`);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/* ═════════════ kandown work — agent entrypoint ═════════════ */
|
|
1366
|
+
/**
|
|
1367
|
+
* 📖 `kandown work` replaces the "paste the rules into AGENTS.md/CLAUDE.md"
|
|
1368
|
+
* pattern: instead of a stale copy embedded in the project's agent file at
|
|
1369
|
+
* init time, the CLI serves the rules fresh on every invocation — always in
|
|
1370
|
+
* sync with the installed version — plus a live digest of the board so the
|
|
1371
|
+
* agent gets its marching orders and its context in one call.
|
|
1372
|
+
*
|
|
1373
|
+
* Layering (base → global → project), each optional except the base:
|
|
1374
|
+
* 1. Base rules — templates/AGENT_KANDOWN.md, shipped with the package.
|
|
1375
|
+
* 2. Global — ~/.kandown/instructions.md, applies to every project
|
|
1376
|
+
* on this machine (team conventions, personal style).
|
|
1377
|
+
* 3. Project — .kandown/instructions.md, this project only.
|
|
1378
|
+
* Later layers are appended, not merged — the agent reads all of them.
|
|
1379
|
+
*/
|
|
1380
|
+
|
|
1381
|
+
const PRIORITY_RANK = { P1: 0, P2: 1, P3: 2, P4: 3 };
|
|
1382
|
+
function priorityRank(p) {
|
|
1383
|
+
return Object.prototype.hasOwnProperty.call(PRIORITY_RANK, p) ? PRIORITY_RANK[p] : 4;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* 📖 Builds the "Current board" section: column counts, tasks per column
|
|
1388
|
+
* (with blocked-by annotations), and a "Next actionable task" pick — the
|
|
1389
|
+
* task in the most-advanced non-terminal column (closest to done, so
|
|
1390
|
+
* in-flight work finishes before new work starts) with no unresolved
|
|
1391
|
+
* `depends_on`, tie-broken by priority. Mirrors the same gate logic used by
|
|
1392
|
+
* `move`/the TUI/the web store.
|
|
1393
|
+
*/
|
|
1394
|
+
function buildBoardDigest(kandownDir) {
|
|
1395
|
+
const config = readKandownConfig(kandownDir);
|
|
1396
|
+
const columns = (config && config.board && Array.isArray(config.board.columns) && config.board.columns.length > 0)
|
|
1397
|
+
? config.board.columns
|
|
1398
|
+
: ['Backlog', 'Todo', 'In Progress', 'Review', 'Done'];
|
|
1399
|
+
const terminalCol = columns[columns.length - 1];
|
|
1400
|
+
const terminalLower = terminalCol.toLowerCase();
|
|
1401
|
+
|
|
1402
|
+
const rows = [];
|
|
1403
|
+
for (const id of listAllTaskIds(kandownDir)) {
|
|
1404
|
+
const path = findTaskFile(kandownDir, id);
|
|
1405
|
+
if (!path) continue;
|
|
1406
|
+
let content;
|
|
1407
|
+
try { content = readFileSync(path, 'utf8'); } catch { continue; }
|
|
1408
|
+
const parsed = parseFrontmatter(content);
|
|
1409
|
+
const fm = parsed.frontmatter;
|
|
1410
|
+
const isArchived = fm.archived === true || fm.archived === 'true' || path.includes('/archive/');
|
|
1411
|
+
if (isArchived) continue;
|
|
1412
|
+
rows.push({ id, fm });
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (rows.length === 0) {
|
|
1416
|
+
return `## Current board\n\n(no tasks yet)`;
|
|
1387
1417
|
}
|
|
1418
|
+
|
|
1419
|
+
const resolved = new Map();
|
|
1420
|
+
for (const r of rows) {
|
|
1421
|
+
const isArch = r.fm.archived === true || r.fm.archived === 'true';
|
|
1422
|
+
resolved.set(r.id, isArch || (r.fm.status || '').toLowerCase() === terminalLower);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const byColumn = new Map(columns.map(col => [col, []]));
|
|
1426
|
+
for (const r of rows) {
|
|
1427
|
+
const status = r.fm.status || columns[0];
|
|
1428
|
+
const col = columns.find(c => c.toLowerCase() === String(status).toLowerCase()) || columns[0];
|
|
1429
|
+
const deps = Array.isArray(r.fm.depends_on) ? r.fm.depends_on : [];
|
|
1430
|
+
const blocked = deps.filter(d => typeof d === 'string' && d.trim() && d !== r.id && !resolved.get(d));
|
|
1431
|
+
// 📖 `col` always resolves to one of `columns` (falls back to columns[0]),
|
|
1432
|
+
// which the Map was seeded with above — the entry always exists.
|
|
1433
|
+
byColumn.get(col).push({ ...r, blocked });
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const lines = ['## Current board', ''];
|
|
1437
|
+
lines.push(`**Columns:** ${columns.map(col => `${col} (${(byColumn.get(col) || []).length})`).join(' · ')}`);
|
|
1438
|
+
|
|
1439
|
+
for (const col of columns) {
|
|
1440
|
+
const tasks = (byColumn.get(col) || []).sort((a, b) => priorityRank(a.fm.priority) - priorityRank(b.fm.priority));
|
|
1441
|
+
if (tasks.length === 0) continue;
|
|
1442
|
+
lines.push('', `### ${col}`);
|
|
1443
|
+
for (const t of tasks) {
|
|
1444
|
+
const pri = t.fm.priority ? `[${t.fm.priority}] ` : '';
|
|
1445
|
+
const assignee = t.fm.assignee ? ` (@${t.fm.assignee})` : '';
|
|
1446
|
+
const blockedStr = t.blocked.length > 0 ? ` ⛔ blocked by ${t.blocked.join(', ')}` : '';
|
|
1447
|
+
lines.push(`- ${t.id} ${pri}${t.fm.title || '(untitled)'}${assignee}${blockedStr}`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const actionable = rows.filter(r => {
|
|
1452
|
+
const status = r.fm.status || columns[0];
|
|
1453
|
+
if (status.toLowerCase() === terminalLower) return false;
|
|
1454
|
+
const deps = Array.isArray(r.fm.depends_on) ? r.fm.depends_on : [];
|
|
1455
|
+
return !deps.some(d => typeof d === 'string' && d.trim() && d !== r.id && !resolved.get(d));
|
|
1456
|
+
});
|
|
1457
|
+
const next = actionable.sort((a, b) => {
|
|
1458
|
+
const idxA = columns.findIndex(col => col.toLowerCase() === (a.fm.status || columns[0]).toLowerCase());
|
|
1459
|
+
const idxB = columns.findIndex(col => col.toLowerCase() === (b.fm.status || columns[0]).toLowerCase());
|
|
1460
|
+
if (idxA !== idxB) return idxB - idxA; // most-advanced non-terminal column first
|
|
1461
|
+
return priorityRank(a.fm.priority) - priorityRank(b.fm.priority);
|
|
1462
|
+
})[0];
|
|
1463
|
+
|
|
1464
|
+
lines.push('', '### Next actionable task');
|
|
1465
|
+
lines.push(next
|
|
1466
|
+
? `→ **${next.id}** — ${next.fm.title || '(untitled)'} (${next.fm.priority || 'no priority'}, ${next.fm.status || columns[0]})`
|
|
1467
|
+
: 'None — every task is done, archived, or blocked.');
|
|
1468
|
+
|
|
1469
|
+
return lines.join('\n');
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async function cmdWork(rawArgs) {
|
|
1473
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1474
|
+
|
|
1475
|
+
let baseRules = '';
|
|
1476
|
+
try {
|
|
1477
|
+
baseRules = readFileSync(join(PKG_ROOT, 'templates', 'AGENT_KANDOWN.md'), 'utf8').trim();
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
warn(`Could not read base rules (${e.message})`);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
let globalInstructions = '';
|
|
1483
|
+
const globalPath = join(homedir(), '.kandown', 'instructions.md');
|
|
1484
|
+
if (existsSync(globalPath)) {
|
|
1485
|
+
try { globalInstructions = readFileSync(globalPath, 'utf8').trim(); }
|
|
1486
|
+
catch (e) { warn(`Could not read global instructions (${e.message})`); }
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
let projectInstructions = '';
|
|
1490
|
+
const projectPath = join(kandownDir, 'instructions.md');
|
|
1491
|
+
if (existsSync(projectPath)) {
|
|
1492
|
+
try { projectInstructions = readFileSync(projectPath, 'utf8').trim(); }
|
|
1493
|
+
catch (e) { warn(`Could not read project instructions (${e.message})`); }
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const sections = [baseRules];
|
|
1497
|
+
if (globalInstructions) sections.push(`## Global instructions\n\n${globalInstructions}`);
|
|
1498
|
+
if (projectInstructions) sections.push(`## Project-specific instructions\n\n${projectInstructions}`);
|
|
1499
|
+
sections.push(buildBoardDigest(kandownDir));
|
|
1500
|
+
|
|
1501
|
+
out(sections.filter(Boolean).join('\n\n---\n\n'));
|
|
1388
1502
|
}
|
|
1389
1503
|
|
|
1390
1504
|
function parsePort(value) {
|
|
@@ -2503,11 +2617,13 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
2503
2617
|
const skipUpdate = process.argv.slice(2).includes('--no-update-check');
|
|
2504
2618
|
|
|
2505
2619
|
// 📖 Update policy (M1): the check only runs for INTERACTIVE commands.
|
|
2506
|
-
//
|
|
2507
|
-
// must never pay a network round-trip nor risk a
|
|
2508
|
-
// Inside checkForUpdate there are further guards: 24h
|
|
2509
|
-
// and the KANDOWN_NO_UPDATE=1 opt-out.
|
|
2510
|
-
const SCRIPTED_COMMANDS = new Set([
|
|
2620
|
+
// The one-shot task commands (scripts/agents) and `daemon` (spawned
|
|
2621
|
+
// children, status probes) must never pay a network round-trip nor risk a
|
|
2622
|
+
// mid-pipeline respawn. Inside checkForUpdate there are further guards: 24h
|
|
2623
|
+
// throttle, TTY-only, and the KANDOWN_NO_UPDATE=1 opt-out.
|
|
2624
|
+
const SCRIPTED_COMMANDS = new Set([
|
|
2625
|
+
'list', 'ls', 'show', 'create', 'new', 'move', 'assign', 'commit', 'tasks', 'work', 'daemon',
|
|
2626
|
+
]);
|
|
2511
2627
|
if (!skipUpdate && !SCRIPTED_COMMANDS.has(cmd)) await checkForUpdate(process.argv);
|
|
2512
2628
|
|
|
2513
2629
|
switch (cmd) {
|
|
@@ -2532,13 +2648,47 @@ switch (cmd) {
|
|
|
2532
2648
|
cmdUpdate(rest);
|
|
2533
2649
|
break;
|
|
2534
2650
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2651
|
+
// 📖 One-shot task commands — top-level, no "shell" wrapper. These are the
|
|
2652
|
+
// most basic operations of the product (list/read/create/move/assign a
|
|
2653
|
+
// task, commit the board to git); nesting them under a prefix only added
|
|
2654
|
+
// friction for scripts and AI agents, which is exactly who these are for.
|
|
2655
|
+
case 'list':
|
|
2656
|
+
case 'ls':
|
|
2657
|
+
cmdList(rest);
|
|
2658
|
+
break;
|
|
2659
|
+
|
|
2660
|
+
case 'show':
|
|
2661
|
+
cmdShow(rest);
|
|
2662
|
+
break;
|
|
2663
|
+
|
|
2664
|
+
case 'create':
|
|
2665
|
+
case 'new':
|
|
2666
|
+
cmdCreate(rest);
|
|
2667
|
+
break;
|
|
2668
|
+
|
|
2669
|
+
case 'move':
|
|
2670
|
+
cmdMove(rest);
|
|
2671
|
+
break;
|
|
2672
|
+
|
|
2673
|
+
case 'assign':
|
|
2674
|
+
cmdAssign(rest);
|
|
2675
|
+
break;
|
|
2676
|
+
|
|
2677
|
+
case 'commit':
|
|
2678
|
+
cmdCommit(rest);
|
|
2679
|
+
break;
|
|
2680
|
+
|
|
2681
|
+
case 'tasks':
|
|
2682
|
+
// 📖 `kandown tasks` — cheatsheet for the commands above (`list help`
|
|
2683
|
+
// etc. would collide with real usage, so the index lives on its own verb).
|
|
2684
|
+
printTaskCommandsHelp();
|
|
2685
|
+
break;
|
|
2686
|
+
|
|
2687
|
+
case 'work':
|
|
2688
|
+
// 📖 `kandown work` — the agent entrypoint (rules + live board digest).
|
|
2689
|
+
// See cmdWork's doc comment for the full rationale.
|
|
2690
|
+
await cmdWork(rest);
|
|
2540
2691
|
break;
|
|
2541
|
-
}
|
|
2542
2692
|
|
|
2543
2693
|
case 'help':
|
|
2544
2694
|
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.18.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.
|