kandown 0.7.5 → 0.8.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 +15 -1
- package/bin/kandown.js +255 -30
- package/bin/tui.js +233 -39
- package/dist/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,10 +64,12 @@ This creates a `.kandown/` folder with:
|
|
|
64
64
|
kandown
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
This opens:
|
|
67
|
+
This starts a per-project local web daemon, then opens:
|
|
68
68
|
- A **web UI** in your browser (board view, list view, task editor)
|
|
69
69
|
- A **terminal TUI** for keyboard-driven workflow (works over SSH, no browser needed)
|
|
70
70
|
|
|
71
|
+
The web daemon stays alive after you quit the TUI so the browser keeps working. Stop or restart it anytime from the TUI with `d`, or from the CLI with `kandown daemon stop`.
|
|
72
|
+
|
|
71
73
|
### CLI Commands
|
|
72
74
|
|
|
73
75
|
| Command | Description |
|
|
@@ -76,6 +78,9 @@ This opens:
|
|
|
76
78
|
| `kandown init` | Initialize in current project |
|
|
77
79
|
| `kandown board` | TUI only (no browser) |
|
|
78
80
|
| `kandown settings` | Terminal settings editor |
|
|
81
|
+
| `kandown daemon status` | Show this project's web daemon status |
|
|
82
|
+
| `kandown daemon start` | Start/reconnect this project's web daemon |
|
|
83
|
+
| `kandown daemon stop` | Stop this project's web daemon |
|
|
79
84
|
| `kandown update` | Update `kandown.html` to latest |
|
|
80
85
|
| `kandown help` | CLI help |
|
|
81
86
|
|
|
@@ -142,6 +147,15 @@ Press `a` in the board TUI to launch an AI agent on any task. Supported agents:
|
|
|
142
147
|
| `⌘S` / `Ctrl+S` | Save task |
|
|
143
148
|
| `⌘⌫` / `Ctrl+Backspace` | Delete task (with confirmation) |
|
|
144
149
|
|
|
150
|
+
### TUI extras
|
|
151
|
+
|
|
152
|
+
| Shortcut / gesture | Action |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `d` | Start/stop the per-project web daemon |
|
|
155
|
+
| Mouse drag task | Move a task between columns in the terminal |
|
|
156
|
+
| `m` | Open the focused task context menu |
|
|
157
|
+
| `r` | Reload board from disk |
|
|
158
|
+
|
|
145
159
|
### Data Model
|
|
146
160
|
|
|
147
161
|
Each task is a standalone Markdown file:
|
package/bin/kandown.js
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
* → cmdUpdate — refreshes installed kandown.html
|
|
24
24
|
* → injectServerRoot — injects the CLI server root into single-file HTML
|
|
25
25
|
* → createServeServer — creates the local zero-dependency HTTP server
|
|
26
|
-
* →
|
|
26
|
+
* → readDaemonMetadata — reads per-project daemon status metadata
|
|
27
|
+
* → startDaemon — starts/reconnects the per-project web daemon
|
|
28
|
+
* → stopDaemon — stops the per-project web daemon
|
|
29
|
+
* → cmdDaemon — daemon lifecycle command router
|
|
30
|
+
* → cmdServe — starts/reconnects the daemon, opens web UI, and launches the board TUI
|
|
27
31
|
* → main — dispatches CLI commands
|
|
28
32
|
*
|
|
29
33
|
* @exports none
|
|
@@ -63,6 +67,7 @@ const DEFAULT_SERVE_PORT = 2048;
|
|
|
63
67
|
const MAX_SERVE_PORT = 2060;
|
|
64
68
|
const START_PORT_RANGE = 2048;
|
|
65
69
|
const END_PORT_RANGE = 2060;
|
|
70
|
+
const DAEMON_FILE = 'daemon.json';
|
|
66
71
|
|
|
67
72
|
// 📖 Get current CLI version from package.json at PKG_ROOT
|
|
68
73
|
function getCurrentVersion() {
|
|
@@ -290,6 +295,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
290
295
|
${c.cyan}board${c.reset} Open the interactive kanban board in the terminal
|
|
291
296
|
${c.cyan}init${c.reset} Initialize .kandown/ in the current directory
|
|
292
297
|
${c.cyan}settings${c.reset} Open the settings TUI
|
|
298
|
+
${c.cyan}daemon${c.reset} Manage the per-project web daemon (start|stop|status)
|
|
293
299
|
${c.cyan}update${c.reset} Update kandown.html to the latest version
|
|
294
300
|
${c.cyan}help${c.reset} Show this help
|
|
295
301
|
|
|
@@ -300,6 +306,7 @@ ${c.bold}Examples:${c.reset}
|
|
|
300
306
|
${c.dim}$${c.reset} npx kandown ${c.dim}# local web server + board TUI${c.reset}
|
|
301
307
|
${c.dim}$${c.reset} npx kandown --port 3000 ${c.dim}# use a specific web UI port${c.reset}
|
|
302
308
|
${c.dim}$${c.reset} npx kandown board ${c.dim}# board TUI only${c.reset}
|
|
309
|
+
${c.dim}$${c.reset} npx kandown daemon stop ${c.dim}# stop this project's web daemon${c.reset}
|
|
303
310
|
${c.dim}$${c.reset} npx kandown init
|
|
304
311
|
${c.dim}$${c.reset} npx kandown init --path docs/kanban
|
|
305
312
|
${c.dim}$${c.reset} npx kandown init --no-agents
|
|
@@ -448,6 +455,12 @@ function doInit(args, cwd, kandownPath, kandownDir) {
|
|
|
448
455
|
info('kandown.json already exists (kept)');
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
const kandownGitignore = join(kandownDir, '.gitignore');
|
|
459
|
+
if (!existsSync(kandownGitignore)) {
|
|
460
|
+
writeFileSync(kandownGitignore, `${DAEMON_FILE}\n`, 'utf8');
|
|
461
|
+
success('.gitignore (daemon runtime metadata ignored)');
|
|
462
|
+
}
|
|
463
|
+
|
|
451
464
|
const agentKandownSrc = join(templatesDir, 'AGENT_KANDOWN.md');
|
|
452
465
|
const agentKandownDest = join(kandownDir, 'AGENT_KANDOWN.md');
|
|
453
466
|
if (!existsSync(agentKandownDest)) {
|
|
@@ -541,6 +554,151 @@ function parsePort(value) {
|
|
|
541
554
|
return port;
|
|
542
555
|
}
|
|
543
556
|
|
|
557
|
+
function daemonMetadataPath(kandownDir) {
|
|
558
|
+
return join(kandownDir, DAEMON_FILE);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function readDaemonMetadata(kandownDir) {
|
|
562
|
+
const metadataPath = daemonMetadataPath(kandownDir);
|
|
563
|
+
if (!existsSync(metadataPath)) return null;
|
|
564
|
+
try {
|
|
565
|
+
const raw = JSON.parse(readFileSync(metadataPath, 'utf8'));
|
|
566
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
567
|
+
if (!Number.isInteger(raw.pid) || !Number.isInteger(raw.port)) return null;
|
|
568
|
+
if (typeof raw.url !== 'string' || typeof raw.kandownDir !== 'string') return null;
|
|
569
|
+
return raw;
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function writeDaemonMetadata(kandownDir, metadata) {
|
|
576
|
+
writeFileSync(daemonMetadataPath(kandownDir), JSON.stringify(metadata, null, 2) + '\n', 'utf8');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function removeDaemonMetadata(kandownDir) {
|
|
580
|
+
try {
|
|
581
|
+
if (existsSync(daemonMetadataPath(kandownDir))) unlinkSync(daemonMetadataPath(kandownDir));
|
|
582
|
+
} catch { /* ignore cleanup failure */ }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function ensureDaemonGitignore(kandownDir) {
|
|
586
|
+
const gitignorePath = join(kandownDir, '.gitignore');
|
|
587
|
+
try {
|
|
588
|
+
if (!existsSync(gitignorePath)) {
|
|
589
|
+
writeFileSync(gitignorePath, `${DAEMON_FILE}\n`, 'utf8');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const existing = readFileSync(gitignorePath, 'utf8');
|
|
593
|
+
if (!existing.split(/\r?\n/).includes(DAEMON_FILE)) {
|
|
594
|
+
writeFileSync(gitignorePath, `${existing.trimEnd()}\n${DAEMON_FILE}\n`, 'utf8');
|
|
595
|
+
}
|
|
596
|
+
} catch { /* ignore gitignore best-effort failure */ }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function isProcessAlive(pid) {
|
|
600
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
601
|
+
try {
|
|
602
|
+
process.kill(pid, 0);
|
|
603
|
+
return true;
|
|
604
|
+
} catch {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function fetchDaemonInfo(port) {
|
|
610
|
+
try {
|
|
611
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
|
|
612
|
+
signal: AbortSignal.timeout(700),
|
|
613
|
+
});
|
|
614
|
+
if (!res.ok) return null;
|
|
615
|
+
return await res.json();
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function getDaemonStatus(kandownDir) {
|
|
622
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
623
|
+
if (!metadata) return { running: false, metadata: null };
|
|
624
|
+
if (!isProcessAlive(metadata.pid)) {
|
|
625
|
+
removeDaemonMetadata(kandownDir);
|
|
626
|
+
return { running: false, metadata: null };
|
|
627
|
+
}
|
|
628
|
+
const remote = await fetchDaemonInfo(metadata.port);
|
|
629
|
+
if (!remote || remote.kandownDir !== kandownDir || remote.pid !== metadata.pid) {
|
|
630
|
+
removeDaemonMetadata(kandownDir);
|
|
631
|
+
return { running: false, metadata: null };
|
|
632
|
+
}
|
|
633
|
+
return { running: true, metadata };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function refreshKandownHtml(kandownDir) {
|
|
637
|
+
const htmlDest = join(kandownDir, 'kandown.html');
|
|
638
|
+
const htmlSrc = join(PKG_ROOT, 'dist', 'index.html');
|
|
639
|
+
if (existsSync(htmlDest) && existsSync(htmlSrc)) {
|
|
640
|
+
copyFileSync(htmlSrc, htmlDest);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function waitForDaemon(kandownDir, timeoutMs = 5000) {
|
|
647
|
+
const started = Date.now();
|
|
648
|
+
while (Date.now() - started < timeoutMs) {
|
|
649
|
+
const status = await getDaemonStatus(kandownDir);
|
|
650
|
+
if (status.running) return status;
|
|
651
|
+
await new Promise(r => setTimeout(r, 150));
|
|
652
|
+
}
|
|
653
|
+
return { running: false, metadata: null };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function startDaemon(kandownDir, preferredPort) {
|
|
657
|
+
const current = await getDaemonStatus(kandownDir);
|
|
658
|
+
if (current.running) return current;
|
|
659
|
+
|
|
660
|
+
removeDaemonMetadata(kandownDir);
|
|
661
|
+
ensureDaemonGitignore(kandownDir);
|
|
662
|
+
const daemonArgs = [
|
|
663
|
+
process.argv[1],
|
|
664
|
+
'--no-update-check',
|
|
665
|
+
'daemon',
|
|
666
|
+
'run',
|
|
667
|
+
'--path',
|
|
668
|
+
kandownDir,
|
|
669
|
+
];
|
|
670
|
+
if (preferredPort !== null) daemonArgs.push('--port', String(preferredPort));
|
|
671
|
+
|
|
672
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
673
|
+
cwd: dirname(kandownDir),
|
|
674
|
+
detached: true,
|
|
675
|
+
stdio: 'ignore',
|
|
676
|
+
env: { ...process.env, KANDOWN_DAEMON: '1' },
|
|
677
|
+
});
|
|
678
|
+
child.unref();
|
|
679
|
+
|
|
680
|
+
return waitForDaemon(kandownDir);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function stopDaemon(kandownDir) {
|
|
684
|
+
const status = await getDaemonStatus(kandownDir);
|
|
685
|
+
if (!status.running || !status.metadata) {
|
|
686
|
+
removeDaemonMetadata(kandownDir);
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
process.kill(status.metadata.pid, 'SIGTERM');
|
|
692
|
+
} catch { /* already stopped */ }
|
|
693
|
+
|
|
694
|
+
const started = Date.now();
|
|
695
|
+
while (Date.now() - started < 2500 && isProcessAlive(status.metadata.pid)) {
|
|
696
|
+
await new Promise(r => setTimeout(r, 100));
|
|
697
|
+
}
|
|
698
|
+
removeDaemonMetadata(kandownDir);
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
544
702
|
function apiHeaders() {
|
|
545
703
|
return {
|
|
546
704
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -720,6 +878,18 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
720
878
|
const resource = parts[0];
|
|
721
879
|
const id = parts[1];
|
|
722
880
|
|
|
881
|
+
if (resource === 'daemon') {
|
|
882
|
+
if (req.method === 'GET') {
|
|
883
|
+
return writeJson(res, 200, {
|
|
884
|
+
ok: true,
|
|
885
|
+
pid: process.pid,
|
|
886
|
+
kandownDir,
|
|
887
|
+
version: getCurrentVersion(),
|
|
888
|
+
startedAt: daemonStartedAt,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
723
893
|
if (resource === 'config') {
|
|
724
894
|
if (req.method === 'GET') return getConfig(res, kandownDir);
|
|
725
895
|
if (req.method === 'PUT') return putConfig(req, res, kandownDir);
|
|
@@ -740,6 +910,8 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
740
910
|
writeJson(res, 404, { error: 'Not found' });
|
|
741
911
|
}
|
|
742
912
|
|
|
913
|
+
const daemonStartedAt = new Date().toISOString();
|
|
914
|
+
|
|
743
915
|
function serveApp(res, kandownDir) {
|
|
744
916
|
const htmlPath = join(kandownDir, 'kandown.html');
|
|
745
917
|
if (!existsSync(htmlPath)) {
|
|
@@ -915,47 +1087,96 @@ function detectStaleKandown(port, currentKandownDir) {
|
|
|
915
1087
|
};
|
|
916
1088
|
}
|
|
917
1089
|
|
|
1090
|
+
async function cmdDaemon(rawArgs) {
|
|
1091
|
+
const [subcommand = 'status', ...rest] = rawArgs;
|
|
1092
|
+
const { kandownDir } = ensureKandownDir(rest);
|
|
1093
|
+
const preferredPort = parsePort(parseArgs(rest).port);
|
|
1094
|
+
|
|
1095
|
+
if (subcommand === 'run') {
|
|
1096
|
+
const { server, port } = await listenOnAvailablePort(kandownDir, preferredPort);
|
|
1097
|
+
const url = `http://localhost:${port}`;
|
|
1098
|
+
ensureDaemonGitignore(kandownDir);
|
|
1099
|
+
writeDaemonMetadata(kandownDir, {
|
|
1100
|
+
pid: process.pid,
|
|
1101
|
+
port,
|
|
1102
|
+
url,
|
|
1103
|
+
kandownDir,
|
|
1104
|
+
startedAt: daemonStartedAt,
|
|
1105
|
+
version: getCurrentVersion(),
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const shutdown = () => {
|
|
1109
|
+
server.close(() => {
|
|
1110
|
+
removeDaemonMetadata(kandownDir);
|
|
1111
|
+
process.exit(0);
|
|
1112
|
+
});
|
|
1113
|
+
};
|
|
1114
|
+
process.once('SIGINT', shutdown);
|
|
1115
|
+
process.once('SIGTERM', shutdown);
|
|
1116
|
+
await new Promise(() => {});
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (subcommand === 'start') {
|
|
1121
|
+
refreshKandownHtml(kandownDir);
|
|
1122
|
+
const status = await startDaemon(kandownDir, preferredPort);
|
|
1123
|
+
if (!status.running || !status.metadata) {
|
|
1124
|
+
err('Daemon failed to start');
|
|
1125
|
+
process.exit(1);
|
|
1126
|
+
}
|
|
1127
|
+
success(`Daemon running: ${status.metadata.url}`);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (subcommand === 'stop') {
|
|
1132
|
+
const stopped = await stopDaemon(kandownDir);
|
|
1133
|
+
if (stopped) success('Daemon stopped');
|
|
1134
|
+
else info('Daemon already stopped');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (subcommand === 'status') {
|
|
1139
|
+
const status = await getDaemonStatus(kandownDir);
|
|
1140
|
+
if (status.running && status.metadata) {
|
|
1141
|
+
success(`Daemon ON ${status.metadata.url} PID ${status.metadata.pid}`);
|
|
1142
|
+
} else {
|
|
1143
|
+
info('Daemon OFF');
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
err(`Unknown daemon command: ${subcommand}`);
|
|
1149
|
+
log(` Use ${c.cyan}kandown daemon start|stop|status${c.reset}`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
918
1153
|
/**
|
|
919
|
-
* 📖 Starts the
|
|
920
|
-
* terminal to the board TUI. The
|
|
921
|
-
*
|
|
1154
|
+
* 📖 Starts/reconnects the per-project web daemon, opens it in the browser,
|
|
1155
|
+
* then hands the terminal to the board TUI. The daemon intentionally survives
|
|
1156
|
+
* TUI exit so the web UI keeps working until the user stops it.
|
|
922
1157
|
*/
|
|
923
1158
|
async function cmdServe(rawArgs) {
|
|
924
1159
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
925
1160
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const htmlDest = join(kandownDir, 'kandown.html');
|
|
929
|
-
const htmlSrc = join(PKG_ROOT, 'dist', 'index.html');
|
|
930
|
-
if (existsSync(htmlDest) && existsSync(htmlSrc)) {
|
|
931
|
-
try {
|
|
932
|
-
copyFileSync(htmlSrc, htmlDest);
|
|
1161
|
+
try {
|
|
1162
|
+
if (refreshKandownHtml(kandownDir)) {
|
|
933
1163
|
info(`Refreshed kandown.html (CLI v${getCurrentVersion()})`);
|
|
934
|
-
} catch (e) {
|
|
935
|
-
warn(`Could not refresh kandown.html: ${e.message}`);
|
|
936
1164
|
}
|
|
937
|
-
}
|
|
938
|
-
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
warn(`Could not refresh kandown.html: ${e.message}`);
|
|
939
1167
|
}
|
|
940
1168
|
|
|
941
1169
|
const preferredPort = parsePort(parseArgs(rawArgs).port);
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
};
|
|
948
|
-
process.once('SIGINT', shutdown);
|
|
949
|
-
process.once('SIGTERM', shutdown);
|
|
1170
|
+
const status = await startDaemon(kandownDir, preferredPort);
|
|
1171
|
+
if (!status.running || !status.metadata) {
|
|
1172
|
+
err('Failed to start web daemon');
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
950
1175
|
|
|
951
|
-
success(`Web
|
|
1176
|
+
success(`Web daemon: ${status.metadata.url}`);
|
|
952
1177
|
info(`Project: ${kandownDir}`);
|
|
953
|
-
openInBrowser(url);
|
|
954
|
-
|
|
955
|
-
await cmdTui('board', rawArgs);
|
|
956
|
-
} finally {
|
|
957
|
-
server.close();
|
|
958
|
-
}
|
|
1178
|
+
openInBrowser(status.metadata.url);
|
|
1179
|
+
await cmdTui('board', rawArgs);
|
|
959
1180
|
}
|
|
960
1181
|
|
|
961
1182
|
/**
|
|
@@ -1044,6 +1265,10 @@ switch (cmd) {
|
|
|
1044
1265
|
await cmdTui('settings', rest);
|
|
1045
1266
|
break;
|
|
1046
1267
|
|
|
1268
|
+
case 'daemon':
|
|
1269
|
+
await cmdDaemon(rest);
|
|
1270
|
+
break;
|
|
1271
|
+
|
|
1047
1272
|
case 'update':
|
|
1048
1273
|
cmdUpdate(rest);
|
|
1049
1274
|
break;
|
package/bin/tui.js
CHANGED
|
@@ -54589,10 +54589,137 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
|
54589
54589
|
return true;
|
|
54590
54590
|
}
|
|
54591
54591
|
|
|
54592
|
+
// src/cli/lib/daemon.ts
|
|
54593
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
54594
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
54595
|
+
import { spawn } from "child_process";
|
|
54596
|
+
function metadataPath(kandownDir) {
|
|
54597
|
+
return join3(kandownDir, "daemon.json");
|
|
54598
|
+
}
|
|
54599
|
+
function isRecord(value) {
|
|
54600
|
+
return typeof value === "object" && value !== null;
|
|
54601
|
+
}
|
|
54602
|
+
function parseMetadata(value) {
|
|
54603
|
+
if (!isRecord(value)) return null;
|
|
54604
|
+
const { pid, port, url, kandownDir, startedAt, version } = value;
|
|
54605
|
+
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
|
|
54606
|
+
if (typeof port !== "number" || !Number.isInteger(port)) return null;
|
|
54607
|
+
if (typeof url !== "string" || typeof kandownDir !== "string") return null;
|
|
54608
|
+
if (typeof startedAt !== "string") return null;
|
|
54609
|
+
if (version !== null && typeof version !== "string" && version !== void 0) return null;
|
|
54610
|
+
return { pid, port, url, kandownDir, startedAt, version: version ?? null };
|
|
54611
|
+
}
|
|
54612
|
+
function readDaemonMetadata(kandownDir) {
|
|
54613
|
+
const path = metadataPath(kandownDir);
|
|
54614
|
+
if (!existsSync4(path)) return null;
|
|
54615
|
+
try {
|
|
54616
|
+
return parseMetadata(JSON.parse(readFileSync4(path, "utf8")));
|
|
54617
|
+
} catch {
|
|
54618
|
+
return null;
|
|
54619
|
+
}
|
|
54620
|
+
}
|
|
54621
|
+
function removeDaemonMetadata(kandownDir) {
|
|
54622
|
+
try {
|
|
54623
|
+
const path = metadataPath(kandownDir);
|
|
54624
|
+
if (existsSync4(path)) unlinkSync(path);
|
|
54625
|
+
} catch {
|
|
54626
|
+
}
|
|
54627
|
+
}
|
|
54628
|
+
function isProcessAlive(pid) {
|
|
54629
|
+
try {
|
|
54630
|
+
process.kill(pid, 0);
|
|
54631
|
+
return true;
|
|
54632
|
+
} catch {
|
|
54633
|
+
return false;
|
|
54634
|
+
}
|
|
54635
|
+
}
|
|
54636
|
+
function parseRemoteDaemonInfo(value) {
|
|
54637
|
+
if (!isRecord(value)) return null;
|
|
54638
|
+
const { ok, pid, kandownDir } = value;
|
|
54639
|
+
if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
|
|
54640
|
+
return { ok, pid, kandownDir };
|
|
54641
|
+
}
|
|
54642
|
+
async function fetchDaemonInfo(port) {
|
|
54643
|
+
try {
|
|
54644
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
|
|
54645
|
+
signal: AbortSignal.timeout(700)
|
|
54646
|
+
});
|
|
54647
|
+
if (!response.ok) return null;
|
|
54648
|
+
return parseRemoteDaemonInfo(await response.json());
|
|
54649
|
+
} catch {
|
|
54650
|
+
return null;
|
|
54651
|
+
}
|
|
54652
|
+
}
|
|
54653
|
+
async function getDaemonStatus(kandownDir) {
|
|
54654
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
54655
|
+
if (!metadata) return { running: false, metadata: null };
|
|
54656
|
+
if (!isProcessAlive(metadata.pid)) {
|
|
54657
|
+
removeDaemonMetadata(kandownDir);
|
|
54658
|
+
return { running: false, metadata: null };
|
|
54659
|
+
}
|
|
54660
|
+
const remote = await fetchDaemonInfo(metadata.port);
|
|
54661
|
+
if (!remote || remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
|
|
54662
|
+
removeDaemonMetadata(kandownDir);
|
|
54663
|
+
return { running: false, metadata: null };
|
|
54664
|
+
}
|
|
54665
|
+
return { running: true, metadata };
|
|
54666
|
+
}
|
|
54667
|
+
async function waitForDaemon(kandownDir, timeoutMs = 5e3) {
|
|
54668
|
+
const started = Date.now();
|
|
54669
|
+
while (Date.now() - started < timeoutMs) {
|
|
54670
|
+
const status = await getDaemonStatus(kandownDir);
|
|
54671
|
+
if (status.running) return status;
|
|
54672
|
+
await new Promise((resolve3) => setTimeout(resolve3, 150));
|
|
54673
|
+
}
|
|
54674
|
+
return { running: false, metadata: null };
|
|
54675
|
+
}
|
|
54676
|
+
async function startProjectDaemon(kandownDir, preferredPort) {
|
|
54677
|
+
const current = await getDaemonStatus(kandownDir);
|
|
54678
|
+
if (current.running) return current;
|
|
54679
|
+
const cliPath = process.argv[1];
|
|
54680
|
+
if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
|
|
54681
|
+
const args = [
|
|
54682
|
+
cliPath,
|
|
54683
|
+
"--no-update-check",
|
|
54684
|
+
"daemon",
|
|
54685
|
+
"run",
|
|
54686
|
+
"--path",
|
|
54687
|
+
kandownDir
|
|
54688
|
+
];
|
|
54689
|
+
if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
|
|
54690
|
+
args.push("--port", String(preferredPort));
|
|
54691
|
+
}
|
|
54692
|
+
const child = spawn(process.execPath, args, {
|
|
54693
|
+
cwd: dirname2(kandownDir),
|
|
54694
|
+
detached: true,
|
|
54695
|
+
stdio: "ignore",
|
|
54696
|
+
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
54697
|
+
});
|
|
54698
|
+
child.unref();
|
|
54699
|
+
return waitForDaemon(kandownDir);
|
|
54700
|
+
}
|
|
54701
|
+
async function stopProjectDaemon(kandownDir) {
|
|
54702
|
+
const status = await getDaemonStatus(kandownDir);
|
|
54703
|
+
if (!status.running || !status.metadata) {
|
|
54704
|
+
removeDaemonMetadata(kandownDir);
|
|
54705
|
+
return false;
|
|
54706
|
+
}
|
|
54707
|
+
try {
|
|
54708
|
+
process.kill(status.metadata.pid, "SIGTERM");
|
|
54709
|
+
} catch {
|
|
54710
|
+
}
|
|
54711
|
+
const started = Date.now();
|
|
54712
|
+
while (Date.now() - started < 2500 && isProcessAlive(status.metadata.pid)) {
|
|
54713
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
54714
|
+
}
|
|
54715
|
+
removeDaemonMetadata(kandownDir);
|
|
54716
|
+
return true;
|
|
54717
|
+
}
|
|
54718
|
+
|
|
54592
54719
|
// src/cli/lib/file-watcher.ts
|
|
54593
54720
|
import { createReadStream, statSync } from "fs";
|
|
54594
54721
|
import { createHash } from "crypto";
|
|
54595
|
-
import { join as
|
|
54722
|
+
import { join as join6 } from "path";
|
|
54596
54723
|
|
|
54597
54724
|
// node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
|
|
54598
54725
|
import { stat as statcb } from "fs";
|
|
@@ -55323,9 +55450,9 @@ var NodeFsHandler = class {
|
|
|
55323
55450
|
if (this.fsw.closed) {
|
|
55324
55451
|
return;
|
|
55325
55452
|
}
|
|
55326
|
-
const
|
|
55453
|
+
const dirname5 = sysPath.dirname(file);
|
|
55327
55454
|
const basename3 = sysPath.basename(file);
|
|
55328
|
-
const parent = this.fsw._getWatchedDir(
|
|
55455
|
+
const parent = this.fsw._getWatchedDir(dirname5);
|
|
55329
55456
|
let prevStats = stats;
|
|
55330
55457
|
if (parent.has(basename3))
|
|
55331
55458
|
return;
|
|
@@ -55352,7 +55479,7 @@ var NodeFsHandler = class {
|
|
|
55352
55479
|
prevStats = newStats2;
|
|
55353
55480
|
}
|
|
55354
55481
|
} catch (error) {
|
|
55355
|
-
this.fsw._remove(
|
|
55482
|
+
this.fsw._remove(dirname5, basename3);
|
|
55356
55483
|
}
|
|
55357
55484
|
} else if (parent.has(basename3)) {
|
|
55358
55485
|
const at = newStats.atimeMs;
|
|
@@ -56314,18 +56441,18 @@ var FileWatcher = class {
|
|
|
56314
56441
|
* poll to catch any races or network-mounted file changes.
|
|
56315
56442
|
*/
|
|
56316
56443
|
start(kandownDir) {
|
|
56317
|
-
const tasksDir =
|
|
56318
|
-
const configPath =
|
|
56444
|
+
const tasksDir = join6(kandownDir, "tasks");
|
|
56445
|
+
const configPath = join6(kandownDir, "kandown.json");
|
|
56319
56446
|
const existingIds = listTaskIds(kandownDir);
|
|
56320
56447
|
for (const id of existingIds) {
|
|
56321
56448
|
this.knownTaskIds.add(id);
|
|
56322
56449
|
try {
|
|
56323
|
-
const filePath =
|
|
56450
|
+
const filePath = join6(tasksDir, `${id}.md`);
|
|
56324
56451
|
this.taskHashes.set(id, hashFileSync(filePath));
|
|
56325
56452
|
} catch {
|
|
56326
56453
|
}
|
|
56327
56454
|
}
|
|
56328
|
-
this.watcher = watch([
|
|
56455
|
+
this.watcher = watch([join6(tasksDir, "*.md"), configPath], {
|
|
56329
56456
|
ignoreInitial: true,
|
|
56330
56457
|
awaitWriteFinish: { stabilityThreshold: 25, pollInterval: 25 },
|
|
56331
56458
|
alwaysStat: true
|
|
@@ -56373,8 +56500,8 @@ var FileWatcher = class {
|
|
|
56373
56500
|
// ─── Private ───────────────────────────────────────────────────────────────
|
|
56374
56501
|
handleFsEvent(event, filePath, kandownDir) {
|
|
56375
56502
|
if (this.stopped) return;
|
|
56376
|
-
const tasksDir =
|
|
56377
|
-
const configPath =
|
|
56503
|
+
const tasksDir = join6(kandownDir, "tasks");
|
|
56504
|
+
const configPath = join6(kandownDir, "kandown.json");
|
|
56378
56505
|
if (filePath === configPath) {
|
|
56379
56506
|
const key = `config:${event}`;
|
|
56380
56507
|
const existing = this.debounceTimers.get(key);
|
|
@@ -56426,14 +56553,14 @@ var FileWatcher = class {
|
|
|
56426
56553
|
/** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
|
|
56427
56554
|
async pollHashes(kandownDir) {
|
|
56428
56555
|
if (this.stopped) return;
|
|
56429
|
-
const tasksDir =
|
|
56430
|
-
const configPath =
|
|
56556
|
+
const tasksDir = join6(kandownDir, "tasks");
|
|
56557
|
+
const configPath = join6(kandownDir, "kandown.json");
|
|
56431
56558
|
try {
|
|
56432
56559
|
const newHash = hashFileSync(configPath);
|
|
56433
56560
|
} catch {
|
|
56434
56561
|
}
|
|
56435
56562
|
for (const taskId of this.knownTaskIds) {
|
|
56436
|
-
const filePath =
|
|
56563
|
+
const filePath = join6(tasksDir, `${taskId}.md`);
|
|
56437
56564
|
try {
|
|
56438
56565
|
statSync(filePath);
|
|
56439
56566
|
const newHash = await hashFile(filePath);
|
|
@@ -56450,7 +56577,7 @@ var FileWatcher = class {
|
|
|
56450
56577
|
const currentIds = listTaskIds(kandownDir);
|
|
56451
56578
|
for (const id of currentIds) {
|
|
56452
56579
|
if (!this.knownTaskIds.has(id)) {
|
|
56453
|
-
const filePath =
|
|
56580
|
+
const filePath = join6(tasksDir, `${id}.md`);
|
|
56454
56581
|
try {
|
|
56455
56582
|
const newHash = await hashFile(filePath);
|
|
56456
56583
|
this.knownTaskIds.add(id);
|
|
@@ -56620,9 +56747,9 @@ function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
|
|
|
56620
56747
|
}
|
|
56621
56748
|
|
|
56622
56749
|
// src/cli/lib/launcher.ts
|
|
56623
|
-
import { execSync, spawn } from "child_process";
|
|
56750
|
+
import { execSync, spawn as spawn2 } from "child_process";
|
|
56624
56751
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
56625
|
-
import { join as
|
|
56752
|
+
import { join as join7 } from "path";
|
|
56626
56753
|
import { tmpdir } from "os";
|
|
56627
56754
|
function isInTmux() {
|
|
56628
56755
|
return !!process.env.TMUX;
|
|
@@ -56646,7 +56773,7 @@ function launchAgent(opts) {
|
|
|
56646
56773
|
].join("\n");
|
|
56647
56774
|
const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
|
|
56648
56775
|
moveTaskToColumn(kandownDir, taskId, "In Progress");
|
|
56649
|
-
const contextFile =
|
|
56776
|
+
const contextFile = join7(tmpdir(), `kandown-${taskId}-context.md`);
|
|
56650
56777
|
writeFileSync3(contextFile, `${systemPrompt}
|
|
56651
56778
|
|
|
56652
56779
|
---
|
|
@@ -56664,7 +56791,7 @@ ${taskPrompt}`, "utf8");
|
|
|
56664
56791
|
});
|
|
56665
56792
|
} else {
|
|
56666
56793
|
onBeforeExec?.();
|
|
56667
|
-
const child =
|
|
56794
|
+
const child = spawn2(binary, args, {
|
|
56668
56795
|
stdio: "inherit",
|
|
56669
56796
|
env: {
|
|
56670
56797
|
...process.env,
|
|
@@ -56864,6 +56991,15 @@ var RE_HEADER = /^#{1,3}\s/;
|
|
|
56864
56991
|
var RE_SUBTASK = /^\s*-\s+\[([ xX])\]/;
|
|
56865
56992
|
var RE_DONE = /^\s*-\s+\[x\]/i;
|
|
56866
56993
|
var RE_BRACKET_TAG = /^\[([^\]]+)\]\s*/;
|
|
56994
|
+
function columnAccentColor(name) {
|
|
56995
|
+
const normalized = name.toLowerCase();
|
|
56996
|
+
if (/backlog/.test(normalized)) return "magenta";
|
|
56997
|
+
if (/todo/.test(normalized)) return "cyan";
|
|
56998
|
+
if (/progress|doing/.test(normalized)) return "yellow";
|
|
56999
|
+
if (/review/.test(normalized)) return "blue";
|
|
57000
|
+
if (/done|archive|closed|complete/.test(normalized)) return "green";
|
|
57001
|
+
return "cyan";
|
|
57002
|
+
}
|
|
56867
57003
|
function TaskRow({ task, focused, dragging, colWidth }) {
|
|
56868
57004
|
const cursor = dragging ? "\u2195" : focused ? "\u25B8" : " ";
|
|
56869
57005
|
const check2 = task.checked ? "\u2713" : "\u25CB";
|
|
@@ -56920,8 +57056,9 @@ function KanbanColumn({
|
|
|
56920
57056
|
isMoveFocused,
|
|
56921
57057
|
draggedTaskId
|
|
56922
57058
|
}) {
|
|
56923
|
-
const
|
|
56924
|
-
const
|
|
57059
|
+
const accent = columnAccentColor(name);
|
|
57060
|
+
const headerBg = isFocused ? accent : void 0;
|
|
57061
|
+
const headerColor = isFocused ? "black" : accent;
|
|
56925
57062
|
const countStr = tasks.length > 0 ? ` (${tasks.length})` : "";
|
|
56926
57063
|
const rows = [];
|
|
56927
57064
|
tasks.forEach((task, idx) => {
|
|
@@ -56960,31 +57097,45 @@ function KanbanColumn({
|
|
|
56960
57097
|
showMoveTarget && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MovePlaceholder, { name, focused: !!isMoveFocused, colWidth })
|
|
56961
57098
|
] });
|
|
56962
57099
|
}
|
|
56963
|
-
function BoardHeader({ title, inTmux, modeHint, version }) {
|
|
57100
|
+
function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBusy }) {
|
|
56964
57101
|
const tmuxHint = inTmux ? " tmux" : "";
|
|
56965
57102
|
const versionTag = version ? ` v${version}` : "";
|
|
56966
|
-
const
|
|
57103
|
+
const daemonLabel = daemonBusy ? "\u25CC daemon\u2026" : daemonStatus.running ? `\u25CF web ${daemonStatus.metadata?.port ?? ""}` : "\u25CB web off";
|
|
57104
|
+
const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 d daemon \xB7 r reload \xB7 q quit";
|
|
56967
57105
|
const width = termWidth();
|
|
56968
|
-
const leftWidth = Math.min(Math.max(
|
|
56969
|
-
const
|
|
56970
|
-
const
|
|
57106
|
+
const leftWidth = Math.min(Math.max(34, Math.floor(width * 0.46)), width);
|
|
57107
|
+
const daemonWidth = Math.min(16, Math.max(0, width - leftWidth));
|
|
57108
|
+
const rightWidth = Math.max(0, width - leftWidth - daemonWidth);
|
|
57109
|
+
const left = pad(` \u25C6 KANDOWN${tmuxHint}${versionTag} ${title}`, leftWidth);
|
|
57110
|
+
const daemon = pad(daemonLabel, daemonWidth);
|
|
56971
57111
|
const right = truncate(hint, rightWidth).padStart(rightWidth, " ");
|
|
56972
57112
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
|
|
56973
57113
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { bold: true, color: "cyan", children: left }),
|
|
57114
|
+
daemonWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: daemonStatus.running ? "green" : "yellow", bold: true, children: daemon }),
|
|
56974
57115
|
rightWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", dimColor: true, children: right })
|
|
56975
57116
|
] });
|
|
56976
57117
|
}
|
|
56977
|
-
function StatusBar({ message, task }) {
|
|
56978
|
-
|
|
56979
|
-
|
|
56980
|
-
|
|
56981
|
-
|
|
56982
|
-
|
|
56983
|
-
task.
|
|
56984
|
-
|
|
56985
|
-
|
|
56986
|
-
|
|
56987
|
-
|
|
57118
|
+
function StatusBar({ message, task, daemonStatus }) {
|
|
57119
|
+
const daemonText = daemonStatus.running && daemonStatus.metadata ? `web daemon ON \xB7 ${daemonStatus.metadata.url}` : "web daemon OFF \xB7 press d to start";
|
|
57120
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginTop: 1, flexDirection: "column", children: [
|
|
57121
|
+
message ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", bold: true, children: [
|
|
57122
|
+
" \u2726 ",
|
|
57123
|
+
message
|
|
57124
|
+
] }) : task ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", children: [
|
|
57125
|
+
" ",
|
|
57126
|
+
"\u25C6 ",
|
|
57127
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: task.id }),
|
|
57128
|
+
task.progress ? ` checklist ${task.progress.done}/${task.progress.total}` : "",
|
|
57129
|
+
" ",
|
|
57130
|
+
task.checked ? "\u2713 done" : "\u25CB open"
|
|
57131
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " " }),
|
|
57132
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: daemonStatus.running ? "green" : "yellow", dimColor: !daemonStatus.running, children: [
|
|
57133
|
+
" ",
|
|
57134
|
+
daemonStatus.running ? "\u25CF" : "\u25CB",
|
|
57135
|
+
" ",
|
|
57136
|
+
daemonText
|
|
57137
|
+
] })
|
|
57138
|
+
] });
|
|
56988
57139
|
}
|
|
56989
57140
|
function TaskDetail({ task, taskId, scrollOffset }) {
|
|
56990
57141
|
const fm = task.frontmatter;
|
|
@@ -57049,6 +57200,9 @@ function Board({ kandownDir, version }) {
|
|
|
57049
57200
|
const [moveTargetCol, setMoveTargetCol] = (0, import_react37.useState)(0);
|
|
57050
57201
|
const [mousePress, setMousePress] = (0, import_react37.useState)(null);
|
|
57051
57202
|
const [taskDrag, setTaskDrag] = (0, import_react37.useState)(null);
|
|
57203
|
+
const [daemonStatus, setDaemonStatus] = (0, import_react37.useState)({ running: false, metadata: null });
|
|
57204
|
+
const [daemonBusy, setDaemonBusy] = (0, import_react37.useState)(false);
|
|
57205
|
+
const [preferredDaemonPort, setPreferredDaemonPort] = (0, import_react37.useState)(null);
|
|
57052
57206
|
const [installedAgents, setInstalledAgents] = (0, import_react37.useState)([]);
|
|
57053
57207
|
const inTmux = isInTmux();
|
|
57054
57208
|
const layoutRef = (0, import_react37.useRef)({
|
|
@@ -57122,6 +57276,42 @@ function Board({ kandownDir, version }) {
|
|
|
57122
57276
|
setStatusMsg("Board reloaded");
|
|
57123
57277
|
setTimeout(() => setStatusMsg(""), 1500);
|
|
57124
57278
|
}, [kandownDir, updateLayout]);
|
|
57279
|
+
const refreshDaemonStatus = (0, import_react37.useCallback)(async () => {
|
|
57280
|
+
const next = await getDaemonStatus(kandownDir);
|
|
57281
|
+
setDaemonStatus(next);
|
|
57282
|
+
if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
|
|
57283
|
+
}, [kandownDir]);
|
|
57284
|
+
(0, import_react37.useEffect)(() => {
|
|
57285
|
+
void refreshDaemonStatus();
|
|
57286
|
+
const timer = setInterval(() => {
|
|
57287
|
+
void refreshDaemonStatus();
|
|
57288
|
+
}, 2e3);
|
|
57289
|
+
return () => clearInterval(timer);
|
|
57290
|
+
}, [refreshDaemonStatus]);
|
|
57291
|
+
const toggleDaemon = (0, import_react37.useCallback)(async () => {
|
|
57292
|
+
if (daemonBusy) return;
|
|
57293
|
+
setDaemonBusy(true);
|
|
57294
|
+
try {
|
|
57295
|
+
const current = await getDaemonStatus(kandownDir);
|
|
57296
|
+
if (current.running) {
|
|
57297
|
+
if (current.metadata) setPreferredDaemonPort(current.metadata.port);
|
|
57298
|
+
await stopProjectDaemon(kandownDir);
|
|
57299
|
+
const next = await getDaemonStatus(kandownDir);
|
|
57300
|
+
setDaemonStatus(next);
|
|
57301
|
+
setStatusMsg("Web daemon stopped");
|
|
57302
|
+
} else {
|
|
57303
|
+
const next = await startProjectDaemon(kandownDir, preferredDaemonPort);
|
|
57304
|
+
setDaemonStatus(next);
|
|
57305
|
+
if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
|
|
57306
|
+
setStatusMsg(next.running ? "Web daemon started" : "Web daemon failed to start");
|
|
57307
|
+
}
|
|
57308
|
+
} catch (error) {
|
|
57309
|
+
setStatusMsg(`Daemon error: ${error instanceof Error ? error.message : String(error)}`);
|
|
57310
|
+
} finally {
|
|
57311
|
+
setDaemonBusy(false);
|
|
57312
|
+
setTimeout(() => setStatusMsg(""), 2500);
|
|
57313
|
+
}
|
|
57314
|
+
}, [daemonBusy, kandownDir, preferredDaemonPort]);
|
|
57125
57315
|
const getFocusedTask = (0, import_react37.useCallback)(() => {
|
|
57126
57316
|
if (!board) return null;
|
|
57127
57317
|
const col = board.columns[colIndex];
|
|
@@ -57370,6 +57560,10 @@ function Board({ kandownDir, version }) {
|
|
|
57370
57560
|
reloadBoard();
|
|
57371
57561
|
return;
|
|
57372
57562
|
}
|
|
57563
|
+
if (input === "d") {
|
|
57564
|
+
void toggleDaemon();
|
|
57565
|
+
return;
|
|
57566
|
+
}
|
|
57373
57567
|
if (input === "l" || key.rightArrow) {
|
|
57374
57568
|
const maxCol = (board?.columns.length ?? 1) - 1;
|
|
57375
57569
|
setColIndex((c) => Math.min(c + 1, maxCol));
|
|
@@ -57548,7 +57742,7 @@ function Board({ kandownDir, version }) {
|
|
|
57548
57742
|
if (mode === "agent-picker") {
|
|
57549
57743
|
const taskId = detailTaskId || focusedTask?.id || "";
|
|
57550
57744
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
|
|
57551
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version }),
|
|
57745
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version, daemonStatus, daemonBusy }),
|
|
57552
57746
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
57553
57747
|
AgentPicker,
|
|
57554
57748
|
{
|
|
@@ -57574,7 +57768,7 @@ function Board({ kandownDir, version }) {
|
|
|
57574
57768
|
] });
|
|
57575
57769
|
}
|
|
57576
57770
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
|
|
57577
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version }),
|
|
57771
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version, daemonStatus, daemonBusy }),
|
|
57578
57772
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { flexDirection: "row", children: board.columns.map((col, cIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
57579
57773
|
KanbanColumn,
|
|
57580
57774
|
{
|
|
@@ -57598,7 +57792,7 @@ function Board({ kandownDir, version }) {
|
|
|
57598
57792
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: mode === "dragging" ? "release over a column" : "click \u2193" }),
|
|
57599
57793
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: mode === "dragging" ? " \xB7 Esc cancel" : " or \u2190/\u2192 + Enter" })
|
|
57600
57794
|
] }) }),
|
|
57601
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask })
|
|
57795
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask, daemonStatus })
|
|
57602
57796
|
] });
|
|
57603
57797
|
}
|
|
57604
57798
|
|
package/dist/index.html
CHANGED
|
@@ -112,7 +112,7 @@ Error generating stack: `+k.message+`
|
|
|
112
112
|
|
|
113
113
|
## Subtasks
|
|
114
114
|
|
|
115
|
-
`}}}async function of(e,t,n,a){if(pa())return C1e(t,nR(n,a));const r=await(await e.getFileHandle(`${t}.md`,{create:!0})).createWritable();await r.write(nR(n,a)),await r.close()}async function rR(e,t){if(pa())return A1e(t);try{await e.removeEntry(`${t}.md`)}catch{}}const T1e="kanban-md",M1e=1,Vf="recentProjects";function PY(){return new Promise((e,t)=>{const n=indexedDB.open(T1e,M1e);n.onerror=()=>t(n.error),n.onsuccess=()=>e(n.result),n.onupgradeneeded=()=>{const a=n.result;a.objectStoreNames.contains(Vf)||a.createObjectStore(Vf,{keyPath:"id"})}})}async function wN(e){const t=await PY();return new Promise((n,a)=>{const i=t.transaction(Vf,"readwrite");i.objectStore(Vf).put(e),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function F$(){const e=await PY();return new Promise((t,n)=>{const i=e.transaction(Vf,"readonly").objectStore(Vf).getAll();i.onsuccess=()=>{const r=i.result||[];r.sort((s,o)=>o.lastOpened-s.lastOpened),t(r.slice(0,10))},i.onerror=()=>n(i.error)})}async function sR(e,t=!0){const n={mode:t?"readwrite":"read"};return await e.queryPermission(n)==="granted"||await e.requestPermission(n)==="granted"}class L1e{dirHandle=null;tasksDirHandle=null;intervalId=null;configHash=null;taskHashes=new Map;knownTaskIds=new Set;listeners=new Map;debounceTimers=new Map;debounceDelay=150;start(t,n){this.dirHandle=t,this.tasksDirHandle=n,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(t=>clearTimeout(t)),this.debounceTimers.clear()}on(t,n){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(n),()=>{this.listeners.get(t)?.delete(n)}}getKnownTaskIds(){return Array.from(this.knownTaskIds)}async initHashes(){if(!this.dirHandle||!this.tasksDirHandle)return;const t=await oR(this.dirHandle);t!==null&&(this.configHash=await this.hash(t)),await this.syncTaskDir(!1)}async tick(){if(!this.dirHandle||!this.tasksDirHandle)return;const t=await oR(this.dirHandle);if(t!==null){const n=await this.hash(t);this.configHash!==null&&n!==this.configHash&&(this.configHash=n,this.debouncedEmit("configChanged"))}for(const n of this.knownTaskIds){const a=await cR(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))}}await this.syncTaskDir(!0)}debouncedEmit(t,...n){const a=t+JSON.stringify(n),i=this.debounceTimers.get(a);i&&clearTimeout(i);const r=setTimeout(()=>{this.debounceTimers.delete(a),this.emit(t,...n)},this.debounceDelay);this.debounceTimers.set(a,r)}async syncTaskDir(t){if(this.tasksDirHandle){for await(const n of this.tasksDirHandle.values())if(n.kind==="file"&&n.name.endsWith(".md")){const a=n.name.replace(".md","");if(!this.knownTaskIds.has(a)){this.knownTaskIds.add(a);const i=await cR(this.tasksDirHandle,a);i!==null&&(this.taskHashes.set(a,await this.hash(i)),t&&this.debouncedEmit("newTaskDetected",a))}}}}async hash(t){const a=new TextEncoder().encode(t),i=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(i)).map(s=>s.toString(16).padStart(2,"0")).join("")}emit(t,...n){this.listeners.get(t)?.forEach(i=>{if(t==="configChanged"){i();return}if(t==="taskChanged"){const[s]=n;i(s);return}const[r]=n;i(r)})}}async function oR(e){try{return await(await(await e.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function cR(e,t){try{return await(await(await e.getFileHandle(`${t}.md`)).getFile()).text()}catch{return null}}const cf=new L1e,j1e={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 pR=null;function TY(){return"Notification"in window?Notification.permission:"unsupported"}async function D1e(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function xN({title:e,body:t,config:n}){if(n.notifications.sound&&F1e(n.notifications.soundId),!(!n.notifications.browser||TY()!=="granted"))try{new Notification(e,{body:t})}catch{}}function F1e(e){const t=j1e[e],n=window.AudioContext??window.webkitAudioContext;if(!(!n||t.length===0))try{pR??=new n;const a=pR;a.state==="suspended"&&a.resume();const i=a.currentTime+.01;t.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{}}function I1e(e){let t=-1;for(const n of e)for(const a of n.tasks){const i=a.id.match(/^t(\d+)$/);if(i){const r=parseInt(i[1],10);r>t&&(t=r)}}return"t"+(t+1)}async function R1e(){const e=await U6();return await Promise.all(e.map(async n=>{const{frontmatter:a,body:i}=await SY(n),r={...a,id:a.id||n,status:a.status||"Backlog"},{subtasks:s,bodyWithoutSubtasks:o}=Wo(i);return{id:n,frontmatter:r,body:o,subtasks:s}}))}async function O1e(e){const t=await P1e(e);return await Promise.all(t.map(async a=>{const{frontmatter:i,body:r}=await ep(e,a),s={...i,id:i.id||a,status:i.status||"Backlog"},{subtasks:o,bodyWithoutSubtasks:c}=Wo(r);return{id:a,frontmatter:s,body:c,subtasks:o}}))}async function lR(e,t,n){const a=[];for(const i of t){const r=i.name;i.tasks.forEach((s,o)=>{a.push((async()=>{const{frontmatter:c,body:p}=await ep(e,s.id);await of(e,s.id,{...c,id:s.id,status:r,order:o},p)})())})}await Promise.all(a)}function Vu(e){_1e(e.ui.theme,e.ui.skin,e.ui.font,e.ui.background)}function I$(e){return{title:e.frontmatter.title||e.id,status:e.frontmatter.status||"Backlog",body:e.body,subtasks:e.subtasks}}function CN(e){_f.clear(),e.forEach(t=>{_f.set(t.id,I$(t))})}function B1e(e){const t=e.split(/[\\/]+/).filter(Boolean),n=t[t.length-1];return n===".kandown"?t[t.length-2]??"Project":n??"Project"}function z1e(e,t){return t.reduce((n,a,i)=>{const r=e[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function H1e(e,t){const n=e.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""})),a=t.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""}));return e.title!==t.title||e.body!==t.body||JSON.stringify(n)!==JSON.stringify(a)}let q1e=0;const _f=new Map,tb=new Map;let AN=null;const Me=l1e((e,t)=>({isOpen:!1,loading:!1,dirHandle:null,projectName:null,tasksDirHandle:null,boardTitle:"Project Kanban",columns:[],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,drawerTaskId:null,drawerData:null,currentPage:"board",config:bs,recentProjects:[],toasts:[],drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{const n=await E1e();if(!n)return;const{projectHandle:a,kandownHandle:i}=n,r=await Hb(i),s=a.name;e({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`);const o=pa()?kN():null;await wN({id:a.name,name:a.name,handle:a,lastOpened:Date.now(),...o?{kandownDir:o}:{}}),await t().loadConfig(),await t().reloadBoard();const c=await F$();e({recentProjects:c}),t().setupWatcher()},openRecentProject:async n=>{if(!await sR(n.handle,!0)){t().toast("Permission denied","error");return}const i=await iR(n.handle),r=await Hb(i),s=n.handle.name;e({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`),await wN({...n,lastOpened:Date.now()}),await t().loadConfig(),await t().reloadBoard(),t().setupWatcher()},openServerProject:async()=>{e({loading:!0});try{const n=kN();if(!n)throw new Error("No server root");const a=B1e(n),i=await EY();Vu(i);const r=await U6(),s=await Promise.all(r.map(async u=>{const{frontmatter:m,body:g}=await SY(u),h={...m,id:m.id||u,status:m.status||"Backlog"},{subtasks:v,bodyWithoutSubtasks:w}=Wo(g);return{id:u,frontmatter:h,body:w,subtasks:v}}));CN(s);const o=s.map(u=>({frontmatter:u.frontmatter,body:Hm(u.body,u.subtasks)})),c=_N(o,i.board.columns),p=c.reduce((u,m)=>u+m.tasks.length,0),l=new Map;if(p<=10)for(const u of s)l.set(u.frontmatter.id,{frontmatter:u.frontmatter,subtasks:u.subtasks,body:u.body});e({loading:!1,isOpen:!0,config:i,columns:c,boardTitle:"Project Kanban",projectName:a,taskContents:l,searchMatches:new Map}),window.history.pushState({},"",`?p=${encodeURIComponent(a)}`),t().setupWatcher()}catch{e({loading:!1,isOpen:!1}),t().toast("Impossible de charger le projet. Relancez `kandown`.","error")}},tryAutoOpenServerProject:async()=>{if(!pa())return;const n=kN();if(!n)return;const a=await F$(),i=a.find(p=>p.kandownDir===n);if(!i){await t().openServerProject();return}if(!await sR(i.handle,!0)){await t().openServerProject();return}const s=await iR(i.handle),o=await Hb(s),c=i.handle.name;e({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await wN({...i,lastOpened:Date.now()}),await t().loadConfig(),await t().reloadBoard(),t().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=t();if(!(!n&&!pa()))try{const a=await S1e(n);a?(e({config:a}),Vu(a)):(e({config:bs}),Vu(bs))}catch{e({config:bs}),Vu(bs)}},updateConfig:async n=>{const{dirHandle:a,config:i}=t();if(!a&&!pa())return;const r=n(i);e({config:r}),Vu(r);try{await $1e(a,r)}catch(s){t().toast("Failed to save config: "+s.message,"error")}},reloadBoard:async()=>{const{tasksDirHandle:n,config:a}=t();if(pa())try{const i=await R1e();CN(i);const r=i.map(p=>({frontmatter:p.frontmatter,body:Hm(p.body,p.subtasks)})),s=_N(r,a.board.columns);e({boardTitle:"Project Kanban",columns:s});const o=s.reduce((p,l)=>p+l.tasks.length,0),c=new Map;if(o<=10)for(const p of i)c.set(p.frontmatter.id,{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body});e({taskContents:c,searchMatches:new Map})}catch(i){t().toast("Failed to load board: "+i.message,"error")}else if(n)try{const i=await O1e(n);CN(i);const r=i.map(p=>({frontmatter:p.frontmatter,body:Hm(p.body,p.subtasks)})),s=_N(r,a.board.columns);e({boardTitle:"Project Kanban",columns:s});const o=s.reduce((p,l)=>p+l.tasks.length,0),c=new Map;if(o<=10)for(const p of i)c.set(p.frontmatter.id,{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body});e({taskContents:c,searchMatches:new Map})}catch(i){t().toast("Failed to load board: "+i.message,"error")}},moveTask:async(n,a,i,r)=>{const{columns:s,config:o}=t(),c=pa();if(!c&&!t().tasksDirHandle)return;const p=s.find(w=>w.name===a),l=s.find(w=>w.name===i);if(!p||!l)return;const u=p.tasks.findIndex(w=>w.id===n);if(u===-1)return;const m=s.map(w=>({...w,tasks:[...w.tasks]})),g=m.find(w=>w.name===a),h=m.find(w=>w.name===i),[v]=g.tasks.splice(u,1);/done|termin|closed|complet/i.test(i)?v.checked=!0:v.checked=!1,r!==void 0?h.tasks.splice(r,0,v):h.tasks.push(v),e({columns:m});try{const{tasksDirHandle:w}=t();if(!w&&!c)return;const x=a===i?m.filter(A=>A.name===i):m.filter(A=>A.name===a||A.name===i);await lR(w??null,x,o.board.columns)}catch(w){t().toast("Failed to save: "+w.message,"error"),e({columns:s})}},reorderInColumn:async(n,a,i)=>{const{columns:r,tasksDirHandle:s,config:o}=t();if(!s&&!pa())return;const c=r.map(u=>({...u,tasks:[...u.tasks]})),p=c.find(u=>u.name===n);if(!p)return;const[l]=p.tasks.splice(a,1);p.tasks.splice(i,0,l),e({columns:c});try{const{tasksDirHandle:u}=t(),m=pa();if(!u&&!m)return;await lR(u??null,[p],o.board.columns)}catch(u){t().toast("Failed to save: "+u.message,"error"),e({columns:r})}},addColumn:async n=>{const a=n.trim();if(!a)return;const{config:i}=t();i.board.columns.some(r=>r.toLowerCase()===a.toLowerCase())||(await t().updateConfig(r=>({...r,board:{...r.board,columns:[...r.board.columns,a]}})),await t().reloadBoard())},renameColumn:async(n,a)=>{const i=a.trim(),{columns:r,tasksDirHandle:s,config:o}=t();if(!s||!i||i.toLowerCase()===n.toLowerCase())return;if(r.some(l=>l.name.toLowerCase()===i.toLowerCase())){t().toast("Column already exists","error");return}const c=r,p=r.map(l=>l.name===n?{...l,name:i}:l);e({columns:p});try{const l=c.find(u=>u.name===n);l&&await Promise.all(l.tasks.map(async(u,m)=>{const{frontmatter:g,body:h}=await ep(s,u.id);await of(s,u.id,{...g,id:u.id,status:i,order:m},h)})),await t().updateConfig(u=>{const m={...u.board.columnColors??{}},g=m[n.toLowerCase()];g&&(m[i.toLowerCase()]=g,delete m[n.toLowerCase()]);const h=u.board.columns.some(v=>v.toLowerCase()===n.toLowerCase())?u.board.columns:[...u.board.columns,n];return{...u,board:{...u.board,columns:h.map(v=>v.toLowerCase()===n.toLowerCase()?i:v),columnColors:m}}}),await t().reloadBoard()}catch(l){t().toast("Failed to rename column: "+l.message,"error"),e({columns:c})}},deleteColumn:async n=>{const{columns:a,tasksDirHandle:i}=t();if(!i&&!pa())return;const r=a.find(o=>o.name===n);if(!r)return;const s=a;e({columns:a.filter(o=>o.name!==n)});try{await Promise.all(r.tasks.map(o=>rR(i,o.id))),await t().updateConfig(o=>{const c={...o.board.columnColors??{}};return delete c[n.toLowerCase()],{...o,board:{...o.board,columns:o.board.columns.filter(p=>p.toLowerCase()!==n.toLowerCase()),columnColors:c}}}),await t().reloadBoard(),t().toast("Column deleted")}catch(o){t().toast("Failed to delete column: "+o.message,"error"),e({columns:s})}},createTask:async n=>{const{columns:a,tasksDirHandle:i,config:r,taskContents:s}=t();if(!i&&!pa()||!a.length)return null;const o=n||r.board.columns[0]||a[0].name,c=I1e(a),p=a.find(m=>m.name===o)?.tasks.length??0,l={id:c,title:"",checked:!1,tags:[],assignee:null,priority:r.fields.priority?r.board.defaultPriority:null,ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",progress:null},u=a.map(m=>m.name===o?{...m,tasks:[...m.tasks,l]}:m);e({columns:u});try{const m={id:c,title:"",status:o,order:p,priority:r.fields.priority?r.board.defaultPriority:"",tags:[],assignee:"",created:new Date().toISOString().slice(0,10),ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",tools:""},g="";await of(i||null,c,m,g);const v=new Map(s);return v.set(c,{frontmatter:m,subtasks:[],body:g}),e({taskContents:v}),t().toast(`Created ${c.replace(/^t/,"")}`),await t().openDrawer(c),c}catch(m){return t().toast("Failed to create: "+m.message,"error"),e({columns:a}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r}=t();if(!i&&!pa())return;const s=a.map(p=>({...p,tasks:p.tasks.filter(l=>l.id!==n)}));e({columns:s});const o=new Map(r);o.delete(n);const c=new Map(t().searchMatches);c.delete(n),e({taskContents:o,searchMatches:c});try{await rR(i||null,n),t().toast("Deleted")}catch(p){t().toast("Failed to delete: "+p.message,"error"),e({columns:a})}},openDrawer:async n=>{const{tasksDirHandle:a}=t();if(!(!a&&!pa()))try{const{frontmatter:i,body:r}=await ep(a,n),{subtasks:s,bodyWithoutSubtasks:o}=Wo(r),c={frontmatter:i,subtasks:s,body:o,savedAt:Date.now()};e({drawerTaskId:n,drawerData:{frontmatter:i,subtasks:s,body:o},drawerBaseVersion:c,conflictState:null,showConflictModal:!1})}catch(i){t().toast("Failed to open: "+i.message,"error")}},closeDrawer:()=>e({drawerTaskId:null,drawerData:null,drawerBaseVersion:null,conflictState:null,showConflictModal:!1}),updateDrawerData:n=>{const{drawerData:a}=t();a&&e({drawerData:n(a)})},saveDrawer:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=t();if(!n||!a)return;const s=Hm(a.body,a.subtasks),o={...a.frontmatter,id:n};try{await of(i||null,n,o,s),t().toast("Saved"),e({drawerTaskId:null,drawerData:null});const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),e({taskContents:c}),await t().reloadBoard()}catch(c){t().toast("Failed to save: "+c.message,"error")}},saveDrawerMetadata:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=t();if(!(!n||!a))try{const s=Hm(a.body,a.subtasks),o={...a.frontmatter,id:n};await of(i||null,n,o,s);const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),e({taskContents:c}),await t().reloadBoard()}catch(s){t().toast("Failed to save: "+s.message,"error")}},setViewMode:n=>{localStorage.setItem("kandown:view",n),e({viewMode:n})},setDensity:n=>{localStorage.setItem("kandown:density",n),e({density:n})},setFilter:(n,a)=>{if(e(i=>({filters:{...i.filters,[n]:a}})),n==="search"){const{columns:i,tasksDirHandle:r,taskContents:s}=t(),o=a,c=i.flatMap(p=>p.tasks.map(l=>l.id));if(r){const p=c.filter(l=>!s.has(l));p.length>0?t().loadTaskContents(p).then(()=>{t().computeSearchMatches(o)}):t().computeSearchMatches(o)}}},clearFilters:()=>e({filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},searchMatches:new Map}),setCommandOpen:n=>e({commandOpen:n}),setCurrentPage:n=>e({currentPage:n}),loadTaskContents:async n=>{const{tasksDirHandle:a}=t();if(!a)return;const i=new Map(t().taskContents);await Promise.all(n.map(async r=>{if(!i.has(r))try{const{frontmatter:s,body:o}=await ep(a,r),{subtasks:c,bodyWithoutSubtasks:p}=Wo(o);i.set(r,{frontmatter:s,subtasks:c,body:p})}catch{}})),e({taskContents:i})},computeSearchMatches:n=>{if(!n.trim()){e({searchMatches:new Map});return}const{taskContents:a}=t(),i=new Map,r=n.toLowerCase();for(const[s,o]of a){const c=NY(o,r);c.length>0&&i.set(s,c)}e({searchMatches:i})},toast:(n,a="success")=>{const i=++q1e;e(r=>({toasts:[...r.toasts,{id:i,message:n,type:a}]})),setTimeout(()=>{e(r=>({toasts:r.toasts.filter(s=>s.id!==i)}))},2500)},dismissToast:n=>e(a=>({toasts:a.toasts.filter(i=>i.id!==n)})),resolveConflict:async n=>{const{conflictState:a,drawerData:i,tasksDirHandle:r,drawerTaskId:s,drawerBaseVersion:o}=t();if(!(!a||!r||!s))if(n==="reload"){const{frontmatter:c,body:p}=await ep(r,s),{subtasks:l,bodyWithoutSubtasks:u}=Wo(p);e({drawerData:{frontmatter:c,subtasks:l,body:u},drawerBaseVersion:{frontmatter:c,subtasks:l,body:u,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),t().toast("Reloaded from disk")}else if(n==="overwrite"){if(i&&s&&o){const c=Hm(i.body,i.subtasks),p={...i.frontmatter,id:s};await of(r,s,p,c),e({drawerBaseVersion:{...i,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),t().toast("Overwritten remote changes")}}else e({conflictState:null,showConflictModal:!1})},setupWatcher:()=>{if(pa()){AN&&clearInterval(AN),AN=setInterval(()=>{t().reloadBoard()},2e3);return}const{dirHandle:n,tasksDirHandle:a}=t();if(!n||!a)return;cf.stop(),tb.forEach(s=>clearTimeout(s)),tb.clear(),cf.start(n,a);const i=(s,o)=>{const c=tb.get(s);c&&clearTimeout(c);const p=Math.max(2e3,t().config.notifications.editDebounceMs),l=setTimeout(()=>{tb.delete(s);const u=t().config;u.notifications.taskEdits&&xN({title:"Task edited",body:`${o} changed on disk.`,config:u})},p);tb.set(s,l)},r=async s=>{const{tasksDirHandle:o,config:c}=t();if(!o)return;const{frontmatter:p,body:l}=await ep(o,s),{subtasks:u,bodyWithoutSubtasks:m}=Wo(l),g={id:s,frontmatter:{...p,id:p.id||s,status:p.status||"Backlog"},body:m,subtasks:u},h=I$(g),v=_f.get(s);if(!v){_f.set(s,h);return}c.notifications.statusChanges&&v.status!==h.status&&xN({title:"Task status changed",body:`${h.title}: ${v.status} → ${h.status}`,config:c});const w=z1e(v.subtasks,h.subtasks);c.notifications.subtaskCompletions&&w>0&&xN({title:"Subtask completed",body:w===1?`${h.title}: 1 subtask completed.`:`${h.title}: ${w} subtasks completed.`,config:c}),H1e(v,h)&&i(s,h.title),_f.set(s,h)};cf.on("configChanged",()=>{t().loadConfig(),t().toast("Settings updated externally","info")}),cf.on("taskChanged",async s=>{const{drawerTaskId:o,drawerBaseVersion:c,tasksDirHandle:p}=t();if(await r(s),o===s&&c&&p){const{frontmatter:l,body:u}=await ep(p,s),{subtasks:m,bodyWithoutSubtasks:g}=Wo(u),h=c,v=JSON.stringify(h.frontmatter)!==JSON.stringify(l),w=h.body!==g,x=JSON.stringify(h.subtasks)!==JSON.stringify(m);if(!v&&!w&&!x)return;let A="none";v&&(w||x)?A="full":v?A="metadata-only":(w||x)&&(A="body-only"),e({conflictState:{taskId:s,type:A,local:h,remote:{frontmatter:l,body:g,subtasks:m}},showConflictModal:A==="full"})}else t().reloadBoard()}),cf.on("newTaskDetected",async s=>{const{tasksDirHandle:o}=t();if(o){const{frontmatter:c,body:p}=await ep(o,s),{subtasks:l,bodyWithoutSubtasks:u}=Wo(p);_f.set(s,I$({id:s,frontmatter:{...c,id:c.id||s,status:c.status||"Backlog"},body:u,subtasks:l}))}t().reloadBoard()})}}));F$().then(e=>{Me.setState({recentProjects:e})});Vu(bs);window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Vu(Me.getState().config)});function U1e(){const e=Me(o=>o.config),t=Me(o=>o.updateConfig),[n,a]=S.useState(!1);if(S.useEffect(()=>{a(!0)},[]),!n)return null;const i=e.ui.theme==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e.ui.theme,r=i==="dark",s=()=>{const o=r?"light":"dark";t(c=>({...c,ui:{...c.ui,theme:o}}))};return y.jsx($t.button,{onClick:s,className:L$("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:y.jsx($t.div,{className:L$("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:y.jsx($t.div,{initial:{rotate:-90,scale:.5,opacity:0},animate:{rotate:0,scale:1,opacity:1},transition:{duration:.25,ease:"easeOut"},children:r?y.jsx(va.Moon,{size:13,className:"text-sky-300"}):y.jsx(va.Sun,{size:13,className:"text-amber-400"})},i)})})}function V1e(e){const t=kve(e,{stiffness:180,damping:22,mass:.8});return S.useEffect(()=>{t.set(e)},[e,t]),nY(t,a=>Math.round(a).toString())}const R$="0.7.5",Z1e=({className:e})=>y.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 150 150",className:e,fill:"currentColor",children:[y.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"}),y.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"}),y.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 G1e(){const{t:e}=za(),t=Me(q=>q.dirHandle),n=Me(q=>q.isOpen);Me(q=>q.projectName);const a=Me(q=>q.columns),i=Me(q=>q.openFolder),r=Me(q=>q.reloadBoard),s=Me(q=>q.createTask),o=Me(q=>q.setCommandOpen),c=Me(q=>q.viewMode),p=Me(q=>q.setViewMode),l=Me(q=>q.density),u=Me(q=>q.setDensity),m=Me(q=>q.setCurrentPage),g=Me(q=>q.recentProjects),h=Me(q=>q.openRecentProject),v=Me(q=>q.filters),w=Me(q=>q.setFilter),x=Me(q=>q.clearFilters),A=Me(q=>q.config.fields),[N,M]=S.useState(!1),T=S.useRef(null),P=S.useRef(null),F=a.reduce((q,W)=>q+W.tasks.length,0),R=V1e(F),B=[];A.priority&&v.priority&&B.push({type:"priority",label:v.priority,value:v.priority}),A.tags&&v.tag&&B.push({type:"tag",label:"#"+v.tag,value:v.tag}),A.assignee&&v.assignee&&B.push({type:"assignee",label:"@"+v.assignee,value:v.assignee});const j=[{label:e("filterBar.ownerAll"),value:""},{label:e("filterBar.ownerHuman"),value:"human"},{label:e("filterBar.ownerAI"),value:"ai"}],V=B.length>0||v.search||A.ownerType&&v.ownerType;return S.useEffect(()=>{if(!N)return;const q=W=>{T.current&&!T.current.contains(W.target)&&M(!1)};return document.addEventListener("mousedown",q),()=>document.removeEventListener("mousedown",q)},[N]),y.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:[y.jsxs("div",{className:"flex items-center gap-4 min-w-0",children:[y.jsx("div",{className:"flex items-center gap-2.5 flex-shrink-0",children:y.jsxs("button",{onClick:()=>window.history.pushState({},"",window.location.pathname),className:"flex items-center gap-2 cursor-pointer",children:[y.jsx(Z1e,{className:"w-[34px] h-[34px] dark:text-white text-black"}),y.jsx("span",{className:"text-[15px] font-semibold tracking-tight text-fg",children:"kandown"}),y.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",R$]})]})}),t&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),y.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:[y.jsx(va.Search,{size:14,className:"text-fg-muted/60 flex-shrink-0"}),y.jsx("input",{ref:P,type:"text",placeholder:e("filterBar.searchPlaceholder"),value:v.search,onChange:q=>w("search",q.target.value),className:"bg-transparent border-none outline-none text-fg text-[13px] w-full placeholder:text-fg-muted/60"}),v.search?y.jsx("button",{onClick:()=>w("search",""),className:"text-fg-muted/60 hover:text-fg flex-shrink-0",children:y.jsx(va.X,{size:14})}):y.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"})]}),y.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap overflow-hidden",children:[y.jsx(Zr,{children:B.map(q=>y.jsxs($t.button,{initial:{opacity:0,scale:.9},animate:{opacity:1,scale:1},exit:{opacity:0,scale:.9},transition:{duration:.15},onClick:()=>w(q.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:[q.label,y.jsx(va.X,{size:10,className:"text-fg-muted/60"})]},q.type+q.value))}),A.ownerType&&y.jsx("div",{className:"flex items-center h-6 border border-black/[0.06] dark:border-white/[0.1] rounded-lg overflow-hidden",children:j.map(q=>y.jsx("button",{onClick:()=>w("ownerType",q.value),className:`h-full px-2.5 text-[12px] transition-colors ${v.ownerType===q.value?"bg-black/[0.06] dark:bg-white/[0.12] text-fg":"text-fg-muted/70 hover:text-fg"}`,children:q.label},q.value))}),V&&y.jsx("button",{onClick:x,className:"text-[12px] text-fg-muted/60 hover:text-fg transition-colors",children:e("filterBar.clearAll")})]})]}),t&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),y.jsxs("div",{className:"relative flex-shrink-0",ref:T,children:[y.jsxs("button",{onClick:()=>M(q=>!q),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:[y.jsx(va.Folder,{size:13,className:"text-fg-muted/70"}),y.jsxs("span",{className:"font-medium",children:[".",t.name]}),y.jsx(va.ChevronDown,{size:11,className:"opacity-50"})]}),y.jsx(Zr,{children:N&&y.jsx($t.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:y.jsxs("div",{className:"py-1.5",children:[g.length>0&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-muted/60",children:e("header.recentProjects")}),g.map(q=>y.jsxs("button",{onClick:()=>{M(!1),h(q)},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:[y.jsx(va.Folder,{size:12,className:"text-fg-muted/60"}),y.jsx("span",{className:"truncate",children:q.name}),q.id===t.name&&y.jsx(va.Check,{size:12,className:"ml-auto text-emerald-500"})]},q.id)),y.jsx("div",{className:"h-px bg-black/[0.06] dark:bg-white/[0.08] my-1.5 mx-2"})]}),y.jsxs("button",{onClick:()=>{M(!1),i()},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:[y.jsx(va.Plus,{size:12,className:"text-fg-muted/60"}),y.jsx("span",{children:e("header.openFolder...")})]})]})})})]})]})]}),y.jsx("div",{className:"flex items-center gap-2 flex-shrink-0",children:n||t?y.jsxs(y.Fragment,{children:[y.jsxs("div",{className:"flex items-center gap-2 mr-2 text-[12.5px] text-fg-muted/70",children:[y.jsx("span",{className:"inline-block w-1.5 h-1.5 rounded-full bg-emerald-500"}),y.jsx($t.span,{className:"tabular-nums font-medium",children:R}),y.jsx("span",{children:e("header.tasks")})]}),y.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:[y.jsx("button",{onClick:()=>p("board"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${c==="board"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:e("common.board"),children:y.jsx(va.LayoutBoard,{size:18})}),y.jsx("button",{onClick:()=>p("list"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${c==="list"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:e("common.list"),children:y.jsx(va.LayoutList,{size:18})})]}),y.jsx(dr,{variant:"icon",icon:"Density",onClick:()=>u(l==="compact"?"comfortable":"compact"),title:`Density: ${l}`}),y.jsx(dr,{variant:"icon",icon:"Settings",onClick:()=>m("settings"),title:e("common.settings")}),y.jsx(U1e,{}),y.jsx("div",{className:"w-px h-5 bg-black/[0.08] dark:bg-white/[0.08] mx-1"}),y.jsx(dr,{variant:"secondary",icon:"Search",label:e("common.search"),shortcut:"⌘K",onClick:()=>o(!0),title:"Command palette (⌘K)"}),y.jsx(dr,{variant:"icon",icon:"Refresh",onClick:r,title:`${e("common.reload")} (R)`}),y.jsx(dr,{variant:"primary",icon:"Plus",label:e("common.newTask"),shortcut:"N",onClick:()=>s()})]}):y.jsx(dr,{variant:"primary",label:e("common.openFolder"),onClick:i})})]})}/**
|
|
115
|
+
`}}}async function of(e,t,n,a){if(pa())return C1e(t,nR(n,a));const r=await(await e.getFileHandle(`${t}.md`,{create:!0})).createWritable();await r.write(nR(n,a)),await r.close()}async function rR(e,t){if(pa())return A1e(t);try{await e.removeEntry(`${t}.md`)}catch{}}const T1e="kanban-md",M1e=1,Vf="recentProjects";function PY(){return new Promise((e,t)=>{const n=indexedDB.open(T1e,M1e);n.onerror=()=>t(n.error),n.onsuccess=()=>e(n.result),n.onupgradeneeded=()=>{const a=n.result;a.objectStoreNames.contains(Vf)||a.createObjectStore(Vf,{keyPath:"id"})}})}async function wN(e){const t=await PY();return new Promise((n,a)=>{const i=t.transaction(Vf,"readwrite");i.objectStore(Vf).put(e),i.oncomplete=()=>n(),i.onerror=()=>a(i.error)})}async function F$(){const e=await PY();return new Promise((t,n)=>{const i=e.transaction(Vf,"readonly").objectStore(Vf).getAll();i.onsuccess=()=>{const r=i.result||[];r.sort((s,o)=>o.lastOpened-s.lastOpened),t(r.slice(0,10))},i.onerror=()=>n(i.error)})}async function sR(e,t=!0){const n={mode:t?"readwrite":"read"};return await e.queryPermission(n)==="granted"||await e.requestPermission(n)==="granted"}class L1e{dirHandle=null;tasksDirHandle=null;intervalId=null;configHash=null;taskHashes=new Map;knownTaskIds=new Set;listeners=new Map;debounceTimers=new Map;debounceDelay=150;start(t,n){this.dirHandle=t,this.tasksDirHandle=n,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(t=>clearTimeout(t)),this.debounceTimers.clear()}on(t,n){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(n),()=>{this.listeners.get(t)?.delete(n)}}getKnownTaskIds(){return Array.from(this.knownTaskIds)}async initHashes(){if(!this.dirHandle||!this.tasksDirHandle)return;const t=await oR(this.dirHandle);t!==null&&(this.configHash=await this.hash(t)),await this.syncTaskDir(!1)}async tick(){if(!this.dirHandle||!this.tasksDirHandle)return;const t=await oR(this.dirHandle);if(t!==null){const n=await this.hash(t);this.configHash!==null&&n!==this.configHash&&(this.configHash=n,this.debouncedEmit("configChanged"))}for(const n of this.knownTaskIds){const a=await cR(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))}}await this.syncTaskDir(!0)}debouncedEmit(t,...n){const a=t+JSON.stringify(n),i=this.debounceTimers.get(a);i&&clearTimeout(i);const r=setTimeout(()=>{this.debounceTimers.delete(a),this.emit(t,...n)},this.debounceDelay);this.debounceTimers.set(a,r)}async syncTaskDir(t){if(this.tasksDirHandle){for await(const n of this.tasksDirHandle.values())if(n.kind==="file"&&n.name.endsWith(".md")){const a=n.name.replace(".md","");if(!this.knownTaskIds.has(a)){this.knownTaskIds.add(a);const i=await cR(this.tasksDirHandle,a);i!==null&&(this.taskHashes.set(a,await this.hash(i)),t&&this.debouncedEmit("newTaskDetected",a))}}}}async hash(t){const a=new TextEncoder().encode(t),i=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(i)).map(s=>s.toString(16).padStart(2,"0")).join("")}emit(t,...n){this.listeners.get(t)?.forEach(i=>{if(t==="configChanged"){i();return}if(t==="taskChanged"){const[s]=n;i(s);return}const[r]=n;i(r)})}}async function oR(e){try{return await(await(await e.getFileHandle("kandown.json")).getFile()).text()}catch{return null}}async function cR(e,t){try{return await(await(await e.getFileHandle(`${t}.md`)).getFile()).text()}catch{return null}}const cf=new L1e,j1e={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 pR=null;function TY(){return"Notification"in window?Notification.permission:"unsupported"}async function D1e(){return"Notification"in window?await Notification.requestPermission():"unsupported"}function xN({title:e,body:t,config:n}){if(n.notifications.sound&&F1e(n.notifications.soundId),!(!n.notifications.browser||TY()!=="granted"))try{new Notification(e,{body:t})}catch{}}function F1e(e){const t=j1e[e],n=window.AudioContext??window.webkitAudioContext;if(!(!n||t.length===0))try{pR??=new n;const a=pR;a.state==="suspended"&&a.resume();const i=a.currentTime+.01;t.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{}}function I1e(e){let t=-1;for(const n of e)for(const a of n.tasks){const i=a.id.match(/^t(\d+)$/);if(i){const r=parseInt(i[1],10);r>t&&(t=r)}}return"t"+(t+1)}async function R1e(){const e=await U6();return await Promise.all(e.map(async n=>{const{frontmatter:a,body:i}=await SY(n),r={...a,id:a.id||n,status:a.status||"Backlog"},{subtasks:s,bodyWithoutSubtasks:o}=Wo(i);return{id:n,frontmatter:r,body:o,subtasks:s}}))}async function O1e(e){const t=await P1e(e);return await Promise.all(t.map(async a=>{const{frontmatter:i,body:r}=await ep(e,a),s={...i,id:i.id||a,status:i.status||"Backlog"},{subtasks:o,bodyWithoutSubtasks:c}=Wo(r);return{id:a,frontmatter:s,body:c,subtasks:o}}))}async function lR(e,t,n){const a=[];for(const i of t){const r=i.name;i.tasks.forEach((s,o)=>{a.push((async()=>{const{frontmatter:c,body:p}=await ep(e,s.id);await of(e,s.id,{...c,id:s.id,status:r,order:o},p)})())})}await Promise.all(a)}function Vu(e){_1e(e.ui.theme,e.ui.skin,e.ui.font,e.ui.background)}function I$(e){return{title:e.frontmatter.title||e.id,status:e.frontmatter.status||"Backlog",body:e.body,subtasks:e.subtasks}}function CN(e){_f.clear(),e.forEach(t=>{_f.set(t.id,I$(t))})}function B1e(e){const t=e.split(/[\\/]+/).filter(Boolean),n=t[t.length-1];return n===".kandown"?t[t.length-2]??"Project":n??"Project"}function z1e(e,t){return t.reduce((n,a,i)=>{const r=e[i]?.done??!1;return n+(a.done&&!r?1:0)},0)}function H1e(e,t){const n=e.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""})),a=t.subtasks.map(i=>({text:i.text,description:i.description??"",report:i.report??""}));return e.title!==t.title||e.body!==t.body||JSON.stringify(n)!==JSON.stringify(a)}let q1e=0;const _f=new Map,tb=new Map;let AN=null;const Me=l1e((e,t)=>({isOpen:!1,loading:!1,dirHandle:null,projectName:null,tasksDirHandle:null,boardTitle:"Project Kanban",columns:[],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,drawerTaskId:null,drawerData:null,currentPage:"board",config:bs,recentProjects:[],toasts:[],drawerBaseVersion:null,conflictState:null,showConflictModal:!1,openFolder:async()=>{const n=await E1e();if(!n)return;const{projectHandle:a,kandownHandle:i}=n,r=await Hb(i),s=a.name;e({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`);const o=pa()?kN():null;await wN({id:a.name,name:a.name,handle:a,lastOpened:Date.now(),...o?{kandownDir:o}:{}}),await t().loadConfig(),await t().reloadBoard();const c=await F$();e({recentProjects:c}),t().setupWatcher()},openRecentProject:async n=>{if(!await sR(n.handle,!0)){t().toast("Permission denied","error");return}const i=await iR(n.handle),r=await Hb(i),s=n.handle.name;e({dirHandle:i,tasksDirHandle:r,projectName:s}),window.history.pushState({},"",`?p=${encodeURIComponent(s)}`),await wN({...n,lastOpened:Date.now()}),await t().loadConfig(),await t().reloadBoard(),t().setupWatcher()},openServerProject:async()=>{e({loading:!0});try{const n=kN();if(!n)throw new Error("No server root");const a=B1e(n),i=await EY();Vu(i);const r=await U6(),s=await Promise.all(r.map(async u=>{const{frontmatter:m,body:g}=await SY(u),h={...m,id:m.id||u,status:m.status||"Backlog"},{subtasks:v,bodyWithoutSubtasks:w}=Wo(g);return{id:u,frontmatter:h,body:w,subtasks:v}}));CN(s);const o=s.map(u=>({frontmatter:u.frontmatter,body:Hm(u.body,u.subtasks)})),c=_N(o,i.board.columns),p=c.reduce((u,m)=>u+m.tasks.length,0),l=new Map;if(p<=10)for(const u of s)l.set(u.frontmatter.id,{frontmatter:u.frontmatter,subtasks:u.subtasks,body:u.body});e({loading:!1,isOpen:!0,config:i,columns:c,boardTitle:"Project Kanban",projectName:a,taskContents:l,searchMatches:new Map}),window.history.pushState({},"",`?p=${encodeURIComponent(a)}`),t().setupWatcher()}catch{e({loading:!1,isOpen:!1}),t().toast("Impossible de charger le projet. Relancez `kandown`.","error")}},tryAutoOpenServerProject:async()=>{if(!pa())return;const n=kN();if(!n)return;const a=await F$(),i=a.find(p=>p.kandownDir===n);if(!i){await t().openServerProject();return}if(!await sR(i.handle,!0)){await t().openServerProject();return}const s=await iR(i.handle),o=await Hb(s),c=i.handle.name;e({dirHandle:s,tasksDirHandle:o,projectName:c,recentProjects:a,isOpen:!0}),window.history.pushState({},"",`?p=${encodeURIComponent(c)}`),await wN({...i,lastOpened:Date.now()}),await t().loadConfig(),await t().reloadBoard(),t().setupWatcher()},loadConfig:async()=>{const{dirHandle:n}=t();if(!(!n&&!pa()))try{const a=await S1e(n);a?(e({config:a}),Vu(a)):(e({config:bs}),Vu(bs))}catch{e({config:bs}),Vu(bs)}},updateConfig:async n=>{const{dirHandle:a,config:i}=t();if(!a&&!pa())return;const r=n(i);e({config:r}),Vu(r);try{await $1e(a,r)}catch(s){t().toast("Failed to save config: "+s.message,"error")}},reloadBoard:async()=>{const{tasksDirHandle:n,config:a}=t();if(pa())try{const i=await R1e();CN(i);const r=i.map(p=>({frontmatter:p.frontmatter,body:Hm(p.body,p.subtasks)})),s=_N(r,a.board.columns);e({boardTitle:"Project Kanban",columns:s});const o=s.reduce((p,l)=>p+l.tasks.length,0),c=new Map;if(o<=10)for(const p of i)c.set(p.frontmatter.id,{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body});e({taskContents:c,searchMatches:new Map})}catch(i){t().toast("Failed to load board: "+i.message,"error")}else if(n)try{const i=await O1e(n);CN(i);const r=i.map(p=>({frontmatter:p.frontmatter,body:Hm(p.body,p.subtasks)})),s=_N(r,a.board.columns);e({boardTitle:"Project Kanban",columns:s});const o=s.reduce((p,l)=>p+l.tasks.length,0),c=new Map;if(o<=10)for(const p of i)c.set(p.frontmatter.id,{frontmatter:p.frontmatter,subtasks:p.subtasks,body:p.body});e({taskContents:c,searchMatches:new Map})}catch(i){t().toast("Failed to load board: "+i.message,"error")}},moveTask:async(n,a,i,r)=>{const{columns:s,config:o}=t(),c=pa();if(!c&&!t().tasksDirHandle)return;const p=s.find(w=>w.name===a),l=s.find(w=>w.name===i);if(!p||!l)return;const u=p.tasks.findIndex(w=>w.id===n);if(u===-1)return;const m=s.map(w=>({...w,tasks:[...w.tasks]})),g=m.find(w=>w.name===a),h=m.find(w=>w.name===i),[v]=g.tasks.splice(u,1);/done|termin|closed|complet/i.test(i)?v.checked=!0:v.checked=!1,r!==void 0?h.tasks.splice(r,0,v):h.tasks.push(v),e({columns:m});try{const{tasksDirHandle:w}=t();if(!w&&!c)return;const x=a===i?m.filter(A=>A.name===i):m.filter(A=>A.name===a||A.name===i);await lR(w??null,x,o.board.columns)}catch(w){t().toast("Failed to save: "+w.message,"error"),e({columns:s})}},reorderInColumn:async(n,a,i)=>{const{columns:r,tasksDirHandle:s,config:o}=t();if(!s&&!pa())return;const c=r.map(u=>({...u,tasks:[...u.tasks]})),p=c.find(u=>u.name===n);if(!p)return;const[l]=p.tasks.splice(a,1);p.tasks.splice(i,0,l),e({columns:c});try{const{tasksDirHandle:u}=t(),m=pa();if(!u&&!m)return;await lR(u??null,[p],o.board.columns)}catch(u){t().toast("Failed to save: "+u.message,"error"),e({columns:r})}},addColumn:async n=>{const a=n.trim();if(!a)return;const{config:i}=t();i.board.columns.some(r=>r.toLowerCase()===a.toLowerCase())||(await t().updateConfig(r=>({...r,board:{...r.board,columns:[...r.board.columns,a]}})),await t().reloadBoard())},renameColumn:async(n,a)=>{const i=a.trim(),{columns:r,tasksDirHandle:s,config:o}=t();if(!s||!i||i.toLowerCase()===n.toLowerCase())return;if(r.some(l=>l.name.toLowerCase()===i.toLowerCase())){t().toast("Column already exists","error");return}const c=r,p=r.map(l=>l.name===n?{...l,name:i}:l);e({columns:p});try{const l=c.find(u=>u.name===n);l&&await Promise.all(l.tasks.map(async(u,m)=>{const{frontmatter:g,body:h}=await ep(s,u.id);await of(s,u.id,{...g,id:u.id,status:i,order:m},h)})),await t().updateConfig(u=>{const m={...u.board.columnColors??{}},g=m[n.toLowerCase()];g&&(m[i.toLowerCase()]=g,delete m[n.toLowerCase()]);const h=u.board.columns.some(v=>v.toLowerCase()===n.toLowerCase())?u.board.columns:[...u.board.columns,n];return{...u,board:{...u.board,columns:h.map(v=>v.toLowerCase()===n.toLowerCase()?i:v),columnColors:m}}}),await t().reloadBoard()}catch(l){t().toast("Failed to rename column: "+l.message,"error"),e({columns:c})}},deleteColumn:async n=>{const{columns:a,tasksDirHandle:i}=t();if(!i&&!pa())return;const r=a.find(o=>o.name===n);if(!r)return;const s=a;e({columns:a.filter(o=>o.name!==n)});try{await Promise.all(r.tasks.map(o=>rR(i,o.id))),await t().updateConfig(o=>{const c={...o.board.columnColors??{}};return delete c[n.toLowerCase()],{...o,board:{...o.board,columns:o.board.columns.filter(p=>p.toLowerCase()!==n.toLowerCase()),columnColors:c}}}),await t().reloadBoard(),t().toast("Column deleted")}catch(o){t().toast("Failed to delete column: "+o.message,"error"),e({columns:s})}},createTask:async n=>{const{columns:a,tasksDirHandle:i,config:r,taskContents:s}=t();if(!i&&!pa()||!a.length)return null;const o=n||r.board.columns[0]||a[0].name,c=I1e(a),p=a.find(m=>m.name===o)?.tasks.length??0,l={id:c,title:"",checked:!1,tags:[],assignee:null,priority:r.fields.priority?r.board.defaultPriority:null,ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",progress:null},u=a.map(m=>m.name===o?{...m,tasks:[...m.tasks,l]}:m);e({columns:u});try{const m={id:c,title:"",status:o,order:p,priority:r.fields.priority?r.board.defaultPriority:"",tags:[],assignee:"",created:new Date().toISOString().slice(0,10),ownerType:r.fields.ownerType?r.board.defaultOwnerType:"",tools:""},g="";await of(i||null,c,m,g);const v=new Map(s);return v.set(c,{frontmatter:m,subtasks:[],body:g}),e({taskContents:v}),t().toast(`Created ${c.replace(/^t/,"")}`),await t().openDrawer(c),c}catch(m){return t().toast("Failed to create: "+m.message,"error"),e({columns:a}),null}},deleteTask:async n=>{const{columns:a,tasksDirHandle:i,taskContents:r}=t();if(!i&&!pa())return;const s=a.map(p=>({...p,tasks:p.tasks.filter(l=>l.id!==n)}));e({columns:s});const o=new Map(r);o.delete(n);const c=new Map(t().searchMatches);c.delete(n),e({taskContents:o,searchMatches:c});try{await rR(i||null,n),t().toast("Deleted")}catch(p){t().toast("Failed to delete: "+p.message,"error"),e({columns:a})}},openDrawer:async n=>{const{tasksDirHandle:a}=t();if(!(!a&&!pa()))try{const{frontmatter:i,body:r}=await ep(a,n),{subtasks:s,bodyWithoutSubtasks:o}=Wo(r),c={frontmatter:i,subtasks:s,body:o,savedAt:Date.now()};e({drawerTaskId:n,drawerData:{frontmatter:i,subtasks:s,body:o},drawerBaseVersion:c,conflictState:null,showConflictModal:!1})}catch(i){t().toast("Failed to open: "+i.message,"error")}},closeDrawer:()=>e({drawerTaskId:null,drawerData:null,drawerBaseVersion:null,conflictState:null,showConflictModal:!1}),updateDrawerData:n=>{const{drawerData:a}=t();a&&e({drawerData:n(a)})},saveDrawer:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=t();if(!n||!a)return;const s=Hm(a.body,a.subtasks),o={...a.frontmatter,id:n};try{await of(i||null,n,o,s),t().toast("Saved"),e({drawerTaskId:null,drawerData:null});const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),e({taskContents:c}),await t().reloadBoard()}catch(c){t().toast("Failed to save: "+c.message,"error")}},saveDrawerMetadata:async()=>{const{drawerTaskId:n,drawerData:a,tasksDirHandle:i,taskContents:r}=t();if(!(!n||!a))try{const s=Hm(a.body,a.subtasks),o={...a.frontmatter,id:n};await of(i||null,n,o,s);const c=new Map(r);c.set(n,{frontmatter:o,subtasks:a.subtasks,body:a.body}),e({taskContents:c}),await t().reloadBoard()}catch(s){t().toast("Failed to save: "+s.message,"error")}},setViewMode:n=>{localStorage.setItem("kandown:view",n),e({viewMode:n})},setDensity:n=>{localStorage.setItem("kandown:density",n),e({density:n})},setFilter:(n,a)=>{if(e(i=>({filters:{...i.filters,[n]:a}})),n==="search"){const{columns:i,tasksDirHandle:r,taskContents:s}=t(),o=a,c=i.flatMap(p=>p.tasks.map(l=>l.id));if(r){const p=c.filter(l=>!s.has(l));p.length>0?t().loadTaskContents(p).then(()=>{t().computeSearchMatches(o)}):t().computeSearchMatches(o)}}},clearFilters:()=>e({filters:{search:"",priority:null,tag:null,assignee:null,ownerType:null},searchMatches:new Map}),setCommandOpen:n=>e({commandOpen:n}),setCurrentPage:n=>e({currentPage:n}),loadTaskContents:async n=>{const{tasksDirHandle:a}=t();if(!a)return;const i=new Map(t().taskContents);await Promise.all(n.map(async r=>{if(!i.has(r))try{const{frontmatter:s,body:o}=await ep(a,r),{subtasks:c,bodyWithoutSubtasks:p}=Wo(o);i.set(r,{frontmatter:s,subtasks:c,body:p})}catch{}})),e({taskContents:i})},computeSearchMatches:n=>{if(!n.trim()){e({searchMatches:new Map});return}const{taskContents:a}=t(),i=new Map,r=n.toLowerCase();for(const[s,o]of a){const c=NY(o,r);c.length>0&&i.set(s,c)}e({searchMatches:i})},toast:(n,a="success")=>{const i=++q1e;e(r=>({toasts:[...r.toasts,{id:i,message:n,type:a}]})),setTimeout(()=>{e(r=>({toasts:r.toasts.filter(s=>s.id!==i)}))},2500)},dismissToast:n=>e(a=>({toasts:a.toasts.filter(i=>i.id!==n)})),resolveConflict:async n=>{const{conflictState:a,drawerData:i,tasksDirHandle:r,drawerTaskId:s,drawerBaseVersion:o}=t();if(!(!a||!r||!s))if(n==="reload"){const{frontmatter:c,body:p}=await ep(r,s),{subtasks:l,bodyWithoutSubtasks:u}=Wo(p);e({drawerData:{frontmatter:c,subtasks:l,body:u},drawerBaseVersion:{frontmatter:c,subtasks:l,body:u,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),t().toast("Reloaded from disk")}else if(n==="overwrite"){if(i&&s&&o){const c=Hm(i.body,i.subtasks),p={...i.frontmatter,id:s};await of(r,s,p,c),e({drawerBaseVersion:{...i,savedAt:Date.now()},conflictState:null,showConflictModal:!1}),t().toast("Overwritten remote changes")}}else e({conflictState:null,showConflictModal:!1})},setupWatcher:()=>{if(pa()){AN&&clearInterval(AN),AN=setInterval(()=>{t().reloadBoard()},2e3);return}const{dirHandle:n,tasksDirHandle:a}=t();if(!n||!a)return;cf.stop(),tb.forEach(s=>clearTimeout(s)),tb.clear(),cf.start(n,a);const i=(s,o)=>{const c=tb.get(s);c&&clearTimeout(c);const p=Math.max(2e3,t().config.notifications.editDebounceMs),l=setTimeout(()=>{tb.delete(s);const u=t().config;u.notifications.taskEdits&&xN({title:"Task edited",body:`${o} changed on disk.`,config:u})},p);tb.set(s,l)},r=async s=>{const{tasksDirHandle:o,config:c}=t();if(!o)return;const{frontmatter:p,body:l}=await ep(o,s),{subtasks:u,bodyWithoutSubtasks:m}=Wo(l),g={id:s,frontmatter:{...p,id:p.id||s,status:p.status||"Backlog"},body:m,subtasks:u},h=I$(g),v=_f.get(s);if(!v){_f.set(s,h);return}c.notifications.statusChanges&&v.status!==h.status&&xN({title:"Task status changed",body:`${h.title}: ${v.status} → ${h.status}`,config:c});const w=z1e(v.subtasks,h.subtasks);c.notifications.subtaskCompletions&&w>0&&xN({title:"Subtask completed",body:w===1?`${h.title}: 1 subtask completed.`:`${h.title}: ${w} subtasks completed.`,config:c}),H1e(v,h)&&i(s,h.title),_f.set(s,h)};cf.on("configChanged",()=>{t().loadConfig(),t().toast("Settings updated externally","info")}),cf.on("taskChanged",async s=>{const{drawerTaskId:o,drawerBaseVersion:c,tasksDirHandle:p}=t();if(await r(s),o===s&&c&&p){const{frontmatter:l,body:u}=await ep(p,s),{subtasks:m,bodyWithoutSubtasks:g}=Wo(u),h=c,v=JSON.stringify(h.frontmatter)!==JSON.stringify(l),w=h.body!==g,x=JSON.stringify(h.subtasks)!==JSON.stringify(m);if(!v&&!w&&!x)return;let A="none";v&&(w||x)?A="full":v?A="metadata-only":(w||x)&&(A="body-only"),e({conflictState:{taskId:s,type:A,local:h,remote:{frontmatter:l,body:g,subtasks:m}},showConflictModal:A==="full"})}else t().reloadBoard()}),cf.on("newTaskDetected",async s=>{const{tasksDirHandle:o}=t();if(o){const{frontmatter:c,body:p}=await ep(o,s),{subtasks:l,bodyWithoutSubtasks:u}=Wo(p);_f.set(s,I$({id:s,frontmatter:{...c,id:c.id||s,status:c.status||"Backlog"},body:u,subtasks:l}))}t().reloadBoard()})}}));F$().then(e=>{Me.setState({recentProjects:e})});Vu(bs);window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Vu(Me.getState().config)});function U1e(){const e=Me(o=>o.config),t=Me(o=>o.updateConfig),[n,a]=S.useState(!1);if(S.useEffect(()=>{a(!0)},[]),!n)return null;const i=e.ui.theme==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e.ui.theme,r=i==="dark",s=()=>{const o=r?"light":"dark";t(c=>({...c,ui:{...c.ui,theme:o}}))};return y.jsx($t.button,{onClick:s,className:L$("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:y.jsx($t.div,{className:L$("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:y.jsx($t.div,{initial:{rotate:-90,scale:.5,opacity:0},animate:{rotate:0,scale:1,opacity:1},transition:{duration:.25,ease:"easeOut"},children:r?y.jsx(va.Moon,{size:13,className:"text-sky-300"}):y.jsx(va.Sun,{size:13,className:"text-amber-400"})},i)})})}function V1e(e){const t=kve(e,{stiffness:180,damping:22,mass:.8});return S.useEffect(()=>{t.set(e)},[e,t]),nY(t,a=>Math.round(a).toString())}const R$="0.8.0",Z1e=({className:e})=>y.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 150 150",className:e,fill:"currentColor",children:[y.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"}),y.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"}),y.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 G1e(){const{t:e}=za(),t=Me(q=>q.dirHandle),n=Me(q=>q.isOpen);Me(q=>q.projectName);const a=Me(q=>q.columns),i=Me(q=>q.openFolder),r=Me(q=>q.reloadBoard),s=Me(q=>q.createTask),o=Me(q=>q.setCommandOpen),c=Me(q=>q.viewMode),p=Me(q=>q.setViewMode),l=Me(q=>q.density),u=Me(q=>q.setDensity),m=Me(q=>q.setCurrentPage),g=Me(q=>q.recentProjects),h=Me(q=>q.openRecentProject),v=Me(q=>q.filters),w=Me(q=>q.setFilter),x=Me(q=>q.clearFilters),A=Me(q=>q.config.fields),[N,M]=S.useState(!1),T=S.useRef(null),P=S.useRef(null),F=a.reduce((q,W)=>q+W.tasks.length,0),R=V1e(F),B=[];A.priority&&v.priority&&B.push({type:"priority",label:v.priority,value:v.priority}),A.tags&&v.tag&&B.push({type:"tag",label:"#"+v.tag,value:v.tag}),A.assignee&&v.assignee&&B.push({type:"assignee",label:"@"+v.assignee,value:v.assignee});const j=[{label:e("filterBar.ownerAll"),value:""},{label:e("filterBar.ownerHuman"),value:"human"},{label:e("filterBar.ownerAI"),value:"ai"}],V=B.length>0||v.search||A.ownerType&&v.ownerType;return S.useEffect(()=>{if(!N)return;const q=W=>{T.current&&!T.current.contains(W.target)&&M(!1)};return document.addEventListener("mousedown",q),()=>document.removeEventListener("mousedown",q)},[N]),y.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:[y.jsxs("div",{className:"flex items-center gap-4 min-w-0",children:[y.jsx("div",{className:"flex items-center gap-2.5 flex-shrink-0",children:y.jsxs("button",{onClick:()=>window.history.pushState({},"",window.location.pathname),className:"flex items-center gap-2 cursor-pointer",children:[y.jsx(Z1e,{className:"w-[34px] h-[34px] dark:text-white text-black"}),y.jsx("span",{className:"text-[15px] font-semibold tracking-tight text-fg",children:"kandown"}),y.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",R$]})]})}),t&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),y.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:[y.jsx(va.Search,{size:14,className:"text-fg-muted/60 flex-shrink-0"}),y.jsx("input",{ref:P,type:"text",placeholder:e("filterBar.searchPlaceholder"),value:v.search,onChange:q=>w("search",q.target.value),className:"bg-transparent border-none outline-none text-fg text-[13px] w-full placeholder:text-fg-muted/60"}),v.search?y.jsx("button",{onClick:()=>w("search",""),className:"text-fg-muted/60 hover:text-fg flex-shrink-0",children:y.jsx(va.X,{size:14})}):y.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"})]}),y.jsxs("div",{className:"flex items-center gap-1.5 flex-wrap overflow-hidden",children:[y.jsx(Zr,{children:B.map(q=>y.jsxs($t.button,{initial:{opacity:0,scale:.9},animate:{opacity:1,scale:1},exit:{opacity:0,scale:.9},transition:{duration:.15},onClick:()=>w(q.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:[q.label,y.jsx(va.X,{size:10,className:"text-fg-muted/60"})]},q.type+q.value))}),A.ownerType&&y.jsx("div",{className:"flex items-center h-6 border border-black/[0.06] dark:border-white/[0.1] rounded-lg overflow-hidden",children:j.map(q=>y.jsx("button",{onClick:()=>w("ownerType",q.value),className:`h-full px-2.5 text-[12px] transition-colors ${v.ownerType===q.value?"bg-black/[0.06] dark:bg-white/[0.12] text-fg":"text-fg-muted/70 hover:text-fg"}`,children:q.label},q.value))}),V&&y.jsx("button",{onClick:x,className:"text-[12px] text-fg-muted/60 hover:text-fg transition-colors",children:e("filterBar.clearAll")})]})]}),t&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"w-px h-[20px] bg-black/[0.08] dark:bg-white/[0.08] flex-shrink-0"}),y.jsxs("div",{className:"relative flex-shrink-0",ref:T,children:[y.jsxs("button",{onClick:()=>M(q=>!q),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:[y.jsx(va.Folder,{size:13,className:"text-fg-muted/70"}),y.jsxs("span",{className:"font-medium",children:[".",t.name]}),y.jsx(va.ChevronDown,{size:11,className:"opacity-50"})]}),y.jsx(Zr,{children:N&&y.jsx($t.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:y.jsxs("div",{className:"py-1.5",children:[g.length>0&&y.jsxs(y.Fragment,{children:[y.jsx("div",{className:"px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-muted/60",children:e("header.recentProjects")}),g.map(q=>y.jsxs("button",{onClick:()=>{M(!1),h(q)},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:[y.jsx(va.Folder,{size:12,className:"text-fg-muted/60"}),y.jsx("span",{className:"truncate",children:q.name}),q.id===t.name&&y.jsx(va.Check,{size:12,className:"ml-auto text-emerald-500"})]},q.id)),y.jsx("div",{className:"h-px bg-black/[0.06] dark:bg-white/[0.08] my-1.5 mx-2"})]}),y.jsxs("button",{onClick:()=>{M(!1),i()},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:[y.jsx(va.Plus,{size:12,className:"text-fg-muted/60"}),y.jsx("span",{children:e("header.openFolder...")})]})]})})})]})]})]}),y.jsx("div",{className:"flex items-center gap-2 flex-shrink-0",children:n||t?y.jsxs(y.Fragment,{children:[y.jsxs("div",{className:"flex items-center gap-2 mr-2 text-[12.5px] text-fg-muted/70",children:[y.jsx("span",{className:"inline-block w-1.5 h-1.5 rounded-full bg-emerald-500"}),y.jsx($t.span,{className:"tabular-nums font-medium",children:R}),y.jsx("span",{children:e("header.tasks")})]}),y.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:[y.jsx("button",{onClick:()=>p("board"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${c==="board"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:e("common.board"),children:y.jsx(va.LayoutBoard,{size:18})}),y.jsx("button",{onClick:()=>p("list"),className:`w-9 h-9 inline-flex items-center justify-center rounded-lg transition-all ${c==="list"?"bg-card text-fg shadow-sm":"text-fg-muted/70 hover:text-fg"}`,title:e("common.list"),children:y.jsx(va.LayoutList,{size:18})})]}),y.jsx(dr,{variant:"icon",icon:"Density",onClick:()=>u(l==="compact"?"comfortable":"compact"),title:`Density: ${l}`}),y.jsx(dr,{variant:"icon",icon:"Settings",onClick:()=>m("settings"),title:e("common.settings")}),y.jsx(U1e,{}),y.jsx("div",{className:"w-px h-5 bg-black/[0.08] dark:bg-white/[0.08] mx-1"}),y.jsx(dr,{variant:"secondary",icon:"Search",label:e("common.search"),shortcut:"⌘K",onClick:()=>o(!0),title:"Command palette (⌘K)"}),y.jsx(dr,{variant:"icon",icon:"Refresh",onClick:r,title:`${e("common.reload")} (R)`}),y.jsx(dr,{variant:"primary",icon:"Plus",label:e("common.newTask"),shortcut:"N",onClick:()=>s()})]}):y.jsx(dr,{variant:"primary",label:e("common.openFolder"),onClick:i})})]})}/**
|
|
116
116
|
* @license @tabler/icons-react v3.41.1 - MIT
|
|
117
117
|
*
|
|
118
118
|
* This source code is licensed under the MIT license.
|