@hacksmith/doraval 0.2.42 → 0.2.44
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 +1 -1
- package/bin/doraval.js +30 -11
- package/bin/ui/index.html +565 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ Exits with code `1` when errors are found. Pipe `--format json` output to `jq` o
|
|
|
168
168
|
|
|
169
169
|
## Providers
|
|
170
170
|
|
|
171
|
-
Claude Code
|
|
171
|
+
Claude Code, Cursor, Codex, and Copilot CLI validators and scaffolding built in. OpenCode support is experimental.
|
|
172
172
|
|
|
173
173
|
## Links
|
|
174
174
|
|
package/bin/doraval.js
CHANGED
|
@@ -599,7 +599,7 @@ var init_dist = __esm(() => {
|
|
|
599
599
|
var require_package = __commonJS((exports, module) => {
|
|
600
600
|
module.exports = {
|
|
601
601
|
name: "@hacksmith/doraval",
|
|
602
|
-
version: "0.2.
|
|
602
|
+
version: "0.2.44",
|
|
603
603
|
author: "Saif",
|
|
604
604
|
repository: {
|
|
605
605
|
type: "git",
|
|
@@ -1719,19 +1719,17 @@ var init_list = __esm(() => {
|
|
|
1719
1719
|
if (!args.all) {
|
|
1720
1720
|
allEntries = allEntries.filter((e) => e.status === "active");
|
|
1721
1721
|
}
|
|
1722
|
-
|
|
1722
|
+
let staged = [];
|
|
1723
1723
|
try {
|
|
1724
1724
|
const pdir = getPendingProjectDir(project);
|
|
1725
1725
|
if (existsSync4(pdir)) {
|
|
1726
1726
|
const files = readdirSync(pdir).filter((f) => f.endsWith(".md") && f !== ".gitkeep");
|
|
1727
|
-
|
|
1727
|
+
const stagedResults = await Promise.all(files.map(async (f) => {
|
|
1728
1728
|
const txt = await Bun.file(join3(pdir, f)).text();
|
|
1729
1729
|
const parsed = parseJournalEntries(txt);
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1730
|
+
return parsed.map((e) => ({ ...e, _staged: true }));
|
|
1731
|
+
}));
|
|
1732
|
+
staged = stagedResults.flat();
|
|
1735
1733
|
}
|
|
1736
1734
|
} catch {}
|
|
1737
1735
|
if (args.format === "json") {
|
|
@@ -4407,6 +4405,7 @@ async function loadAllEntries(project) {
|
|
|
4407
4405
|
parsed.forEach((e) => {
|
|
4408
4406
|
e._staged = true;
|
|
4409
4407
|
e._source = "staged";
|
|
4408
|
+
e._filename = f;
|
|
4410
4409
|
staged.push(e);
|
|
4411
4410
|
});
|
|
4412
4411
|
}
|
|
@@ -4563,6 +4562,25 @@ var init_ui = __esm(() => {
|
|
|
4563
4562
|
const { committed, staged } = await loadAllEntries(project || null);
|
|
4564
4563
|
return Response.json({ ok: true, committed, staged });
|
|
4565
4564
|
}
|
|
4565
|
+
if (url2.pathname === "/api/delete-staged" && req.method === "POST") {
|
|
4566
|
+
if (!project) {
|
|
4567
|
+
return Response.json({ error: "No project" }, { status: 400 });
|
|
4568
|
+
}
|
|
4569
|
+
const body = await req.json().catch(() => ({}));
|
|
4570
|
+
const filename = body.filename;
|
|
4571
|
+
if (!filename) {
|
|
4572
|
+
return Response.json({ error: "filename required" }, { status: 400 });
|
|
4573
|
+
}
|
|
4574
|
+
const pdir = getPendingProjectDir(project);
|
|
4575
|
+
const filePath = join18(pdir, filename);
|
|
4576
|
+
if (existsSync19(filePath)) {
|
|
4577
|
+
try {
|
|
4578
|
+
await Bun.file(filePath).unlink();
|
|
4579
|
+
} catch {}
|
|
4580
|
+
return Response.json({ ok: true });
|
|
4581
|
+
}
|
|
4582
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
4583
|
+
}
|
|
4566
4584
|
if (url2.pathname.startsWith("/api/")) {
|
|
4567
4585
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
4568
4586
|
}
|
|
@@ -4571,7 +4589,7 @@ var init_ui = __esm(() => {
|
|
|
4571
4589
|
});
|
|
4572
4590
|
const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${server.port}`;
|
|
4573
4591
|
const msg = `
|
|
4574
|
-
${import_picocolors16.default.blue("\u25C9")}
|
|
4592
|
+
${import_picocolors16.default.blue("\u25C9")} dora local dashboard
|
|
4575
4593
|
${import_picocolors16.default.dim("Project:")} ${project ? import_picocolors16.default.white(project) : import_picocolors16.default.yellow("none (run dora init)")}
|
|
4576
4594
|
${import_picocolors16.default.dim("URL:")} ${import_picocolors16.default.underline(import_picocolors16.default.cyan(url))}
|
|
4577
4595
|
|
|
@@ -7726,6 +7744,7 @@ var init_completion = __esm(() => {
|
|
|
7726
7744
|
"providers",
|
|
7727
7745
|
"skill",
|
|
7728
7746
|
"journal",
|
|
7747
|
+
"ui",
|
|
7729
7748
|
"claude",
|
|
7730
7749
|
"codex",
|
|
7731
7750
|
"cursor",
|
|
@@ -7781,7 +7800,7 @@ complete -F _doraval_completions doraval
|
|
|
7781
7800
|
|
|
7782
7801
|
_doraval() {
|
|
7783
7802
|
local -a commands sub
|
|
7784
|
-
commands=(validate init bump update providers skill journal claude codex cursor copilot)
|
|
7803
|
+
commands=(validate init bump update providers skill journal ui claude codex cursor copilot)
|
|
7785
7804
|
_arguments -C \\
|
|
7786
7805
|
'1: :->cmd' \\
|
|
7787
7806
|
'*::arg:->args'
|
|
@@ -7814,7 +7833,7 @@ _doraval "$@"
|
|
|
7814
7833
|
} else if (shell === "fish") {
|
|
7815
7834
|
console.log(`# doraval fish completion
|
|
7816
7835
|
complete -c doraval -f
|
|
7817
|
-
complete -c doraval -n '__fish_use_subcommand' -a 'validate init bump update providers skill journal claude codex cursor copilot'
|
|
7836
|
+
complete -c doraval -n '__fish_use_subcommand' -a 'validate init bump update providers skill journal ui claude codex cursor copilot'
|
|
7818
7837
|
|
|
7819
7838
|
complete -c doraval -n '__fish_seen_subcommand_from skill' -a 'validate drift judge'
|
|
7820
7839
|
complete -c doraval -n '__fish_seen_subcommand_from journal' -a 'init list context hook update add sync'
|
package/bin/ui/index.html
CHANGED
|
@@ -5,120 +5,338 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
6
|
<title>dora • local dashboard</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
fontFamily: {
|
|
13
|
+
sans: ['system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif']
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
8
19
|
<style>
|
|
9
20
|
:root { color-scheme: dark; }
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mono, pre, code, .font-mono {
|
|
27
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.section {
|
|
31
|
+
@apply bg-zinc-900/70 border border-zinc-800/60 rounded-3xl p-6 backdrop-blur-sm;
|
|
32
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
33
|
+
}
|
|
34
|
+
.section:hover {
|
|
35
|
+
border-color: #27272a;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.entry {
|
|
39
|
+
@apply border-l-[3px] pl-4 py-2.5 transition-all duration-150;
|
|
40
|
+
}
|
|
41
|
+
.entry:hover {
|
|
42
|
+
transform: translateX(1px);
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
.pb-strong { border-color: #f87171; }
|
|
14
46
|
.pb-friction { border-color: #facc15; }
|
|
15
47
|
.pb-nudge { border-color: #4ade80; }
|
|
16
|
-
|
|
48
|
+
|
|
17
49
|
.push { font-variant-numeric: tabular-nums; }
|
|
18
|
-
|
|
19
|
-
.
|
|
50
|
+
|
|
51
|
+
pre.context {
|
|
52
|
+
white-space: pre-wrap;
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.staged {
|
|
58
|
+
background: #27251f;
|
|
59
|
+
border-color: #854d0e !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Emil-inspired polish */
|
|
63
|
+
input, textarea, button {
|
|
64
|
+
transition: all 150ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
input:focus, textarea:focus {
|
|
68
|
+
outline: none;
|
|
69
|
+
border-color: #52525b;
|
|
70
|
+
box-shadow: 0 0 0 3px rgba(63, 66, 71, 0.2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.btn-primary {
|
|
74
|
+
transition: transform 120ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
75
|
+
background-color 150ms ease;
|
|
76
|
+
}
|
|
77
|
+
.btn-primary:active {
|
|
78
|
+
transform: scale(0.975);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.card-hover {
|
|
82
|
+
transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
83
|
+
box-shadow 180ms ease,
|
|
84
|
+
border-color 150ms ease;
|
|
85
|
+
}
|
|
86
|
+
.card-hover:hover {
|
|
87
|
+
transform: translateY(-1px);
|
|
88
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.modal {
|
|
92
|
+
animation: modalEnter 180ms cubic-bezier(0.23, 1, 0.32, 1) backwards;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes modalEnter {
|
|
96
|
+
from {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transform: scale(0.96) translateY(6px);
|
|
99
|
+
}
|
|
100
|
+
to {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
transform: scale(1) translateY(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.toast {
|
|
107
|
+
animation: toastEnter 160ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.subtle-scale {
|
|
111
|
+
transition: transform 100ms ease;
|
|
112
|
+
}
|
|
113
|
+
.subtle-scale:active {
|
|
114
|
+
transform: scale(0.97);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Playful but restrained accent */
|
|
118
|
+
.accent {
|
|
119
|
+
color: #60a5fa;
|
|
120
|
+
}
|
|
20
121
|
</style>
|
|
21
122
|
</head>
|
|
22
123
|
<body class="bg-zinc-950 text-zinc-200">
|
|
23
|
-
<div class="max-w-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
124
|
+
<div class="max-w-5xl mx-auto p-8">
|
|
125
|
+
<!-- Header -->
|
|
126
|
+
<div class="flex items-center justify-between mb-8">
|
|
127
|
+
<div class="flex items-center gap-4">
|
|
128
|
+
<div class="flex items-center justify-center w-9 h-9 rounded-2xl bg-zinc-900 border border-zinc-800 text-xl">
|
|
129
|
+
🌀
|
|
130
|
+
</div>
|
|
27
131
|
<div>
|
|
28
|
-
<div class="font-semibold tracking-
|
|
29
|
-
<div class="text-
|
|
132
|
+
<div class="font-semibold tracking-tighter text-2xl">dora</div>
|
|
133
|
+
<div class="text-[10px] text-zinc-500 -mt-1 tracking-[1px] font-mono">LOCAL DASHBOARD</div>
|
|
30
134
|
</div>
|
|
31
135
|
</div>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
136
|
+
|
|
137
|
+
<div class="flex items-center gap-2 text-sm">
|
|
138
|
+
<div id="project-badge" class="px-3.5 py-1 rounded-full bg-zinc-900 border border-zinc-800 text-xs font-medium"></div>
|
|
139
|
+
|
|
140
|
+
<button onclick="refreshAll()"
|
|
141
|
+
class="px-4 py-1.5 rounded-2xl bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 text-xs font-medium active:scale-[0.97] transition flex items-center gap-1.5">
|
|
142
|
+
<span>Refresh</span>
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<button onclick="window.open('https://github.com/saif-shines/doraval','_blank')"
|
|
146
|
+
class="px-4 py-1.5 rounded-2xl bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 text-xs font-medium active:scale-[0.97] transition">
|
|
147
|
+
GitHub
|
|
148
|
+
</button>
|
|
36
149
|
</div>
|
|
37
150
|
</div>
|
|
38
151
|
|
|
39
|
-
<div class="grid grid-cols-1 lg:grid-cols-5 gap-
|
|
152
|
+
<div class="grid grid-cols-1 lg:grid-cols-5 gap-5">
|
|
40
153
|
<!-- Capture -->
|
|
41
154
|
<div class="lg:col-span-2 section">
|
|
42
|
-
<div class="
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
155
|
+
<div class="flex items-baseline justify-between mb-4">
|
|
156
|
+
<div>
|
|
157
|
+
<div class="text-sm font-semibold tracking-tight">Capture decision</div>
|
|
158
|
+
<div class="text-[10px] text-zinc-500 mt-0.5">Add to your journal</div>
|
|
159
|
+
</div>
|
|
160
|
+
<button onclick="clearForm()"
|
|
161
|
+
class="text-[10px] text-zinc-500 hover:text-zinc-300 px-2 py-0.5 rounded-lg hover:bg-zinc-950 transition">clear</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="space-y-4">
|
|
165
|
+
<div>
|
|
166
|
+
<input id="title"
|
|
167
|
+
class="w-full bg-zinc-950 border border-zinc-800 focus:border-zinc-600 rounded-2xl px-4 py-3 text-[15px] placeholder:text-zinc-600"
|
|
168
|
+
placeholder="What did you decide?">
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Playful pushback control -->
|
|
46
172
|
<div>
|
|
47
|
-
<div class="flex justify-between text-xs mb-
|
|
48
|
-
<div>
|
|
49
|
-
<div id="pb-value" class="push font-
|
|
173
|
+
<div class="flex items-center justify-between text-xs mb-2 px-0.5">
|
|
174
|
+
<div class="text-zinc-400">How strongly do you feel?</div>
|
|
175
|
+
<div id="pb-value" class="push font-semibold tabular-nums text-base">4</div>
|
|
50
176
|
</div>
|
|
51
|
-
|
|
52
|
-
<div class="
|
|
53
|
-
<
|
|
177
|
+
|
|
178
|
+
<div class="relative">
|
|
179
|
+
<input id="pushback" type="range" min="1" max="10" step="1" value="4"
|
|
180
|
+
class="w-full accent-blue-400 cursor-pointer">
|
|
181
|
+
<div class="flex justify-between text-[10px] mt-1.5 px-0.5 text-zinc-500">
|
|
182
|
+
<div class="text-emerald-400/90">gentle nudge</div>
|
|
183
|
+
<div class="text-yellow-400/90">real friction</div>
|
|
184
|
+
<div class="text-red-400/90">non-negotiable</div>
|
|
185
|
+
</div>
|
|
54
186
|
</div>
|
|
55
187
|
</div>
|
|
56
188
|
|
|
57
|
-
<input id="tags"
|
|
189
|
+
<input id="tags"
|
|
190
|
+
class="w-full bg-zinc-950 border border-zinc-800 focus:border-zinc-600 rounded-2xl px-4 py-2.5 text-sm placeholder:text-zinc-600"
|
|
191
|
+
placeholder="tags: architecture, naming, cli">
|
|
58
192
|
|
|
59
|
-
<
|
|
193
|
+
<div>
|
|
194
|
+
<textarea id="rationale" rows="2"
|
|
195
|
+
class="w-full bg-zinc-950 border border-zinc-800 focus:border-zinc-600 rounded-2xl px-4 py-3 text-sm placeholder:text-zinc-600 resize-y"
|
|
196
|
+
placeholder="Why does this matter? (helps future you and agents)"></textarea>
|
|
197
|
+
</div>
|
|
60
198
|
|
|
61
199
|
<button onclick="addDecision()"
|
|
62
|
-
class="w-full py-
|
|
63
|
-
|
|
200
|
+
class="w-full py-3 rounded-2xl bg-white hover:bg-zinc-100 active:scale-[0.975] text-black font-semibold text-[15px] shadow-sm flex items-center justify-center gap-2 transition">
|
|
201
|
+
Add to pending
|
|
202
|
+
<span class="text-xs font-normal opacity-50">⌘⏎</span>
|
|
203
|
+
</button>
|
|
204
|
+
<div id="add-status" class="text-xs h-4 text-emerald-400/90 px-1"></div>
|
|
64
205
|
</div>
|
|
65
206
|
</div>
|
|
66
207
|
|
|
67
208
|
<!-- What agents see -->
|
|
68
209
|
<div class="lg:col-span-3 section">
|
|
69
|
-
<div class="flex items-center justify-between mb-
|
|
70
|
-
<div>
|
|
71
|
-
<
|
|
210
|
+
<div class="flex items-center justify-between mb-3">
|
|
211
|
+
<div class="flex items-center gap-2">
|
|
212
|
+
<div class="text-sm font-semibold tracking-tight">What agents will see</div>
|
|
213
|
+
<select id="agent-select" onchange="updateAgentPreview()" class="text-xs bg-zinc-950 border border-zinc-800 rounded-lg px-2 py-0.5 focus:outline-none">
|
|
214
|
+
<option value="claude">Claude (SessionStart)</option>
|
|
215
|
+
<option value="cursor">Cursor (workspaceOpen)</option>
|
|
216
|
+
<option value="codex">Codex (hooks)</option>
|
|
217
|
+
<option value="copilot">Copilot CLI</option>
|
|
218
|
+
<option value="opencode">OpenCode</option>
|
|
219
|
+
</select>
|
|
72
220
|
</div>
|
|
73
|
-
<button onclick="copyContext()"
|
|
221
|
+
<button onclick="copyContext()"
|
|
222
|
+
class="text-xs px-3 py-1 rounded-xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.97] transition">Copy</button>
|
|
74
223
|
</div>
|
|
75
|
-
<pre id="context" class="context bg-
|
|
76
|
-
<div class="mt-2 text-[
|
|
224
|
+
<pre id="context" class="context bg-zinc-950 border border-zinc-800 rounded-2xl p-5 text-zinc-300 text-[13px] overflow-auto max-h-[220px] leading-relaxed"></pre>
|
|
225
|
+
<div id="agent-note" class="mt-2 text-[10px] text-zinc-500 px-1">Exact output from <span class="font-mono">dora journal context</span>. Injected via startup hook for the selected agent.</div>
|
|
77
226
|
</div>
|
|
78
227
|
|
|
79
228
|
<!-- Hooks -->
|
|
80
229
|
<div class="lg:col-span-2 section">
|
|
81
|
-
<div class="
|
|
230
|
+
<div class="text-sm font-semibold tracking-tight mb-3">Journal hooks</div>
|
|
82
231
|
<div id="hooks" class="space-y-2 text-sm"></div>
|
|
83
|
-
<div class="mt-
|
|
232
|
+
<div class="mt-4 text-[10px] text-zinc-500 leading-snug">
|
|
233
|
+
Same as <span class="font-mono text-zinc-400">dora journal hook</span>. Restart Claude after toggling.
|
|
234
|
+
</div>
|
|
84
235
|
</div>
|
|
85
236
|
|
|
86
237
|
<!-- Journal -->
|
|
87
238
|
<div class="lg:col-span-3 section">
|
|
88
|
-
<div class="flex items-
|
|
89
|
-
<div class="
|
|
90
|
-
<div class="
|
|
239
|
+
<div class="flex items-center justify-between mb-4">
|
|
240
|
+
<div class="text-sm font-semibold tracking-tight">Journal entries</div>
|
|
241
|
+
<div class="flex items-center gap-2">
|
|
242
|
+
<input id="search" oninput="filterEntries()"
|
|
243
|
+
class="bg-zinc-950 border border-zinc-800 text-xs rounded-2xl px-3 py-1.5 placeholder:text-zinc-600 w-44 focus:outline-none focus:border-zinc-600"
|
|
244
|
+
placeholder="Search...">
|
|
245
|
+
<div class="text-[10px] text-zinc-500 tabular-nums" id="entry-count"></div>
|
|
246
|
+
</div>
|
|
91
247
|
</div>
|
|
92
|
-
<div id="entries" class="space-y-2 text-sm max-h-[
|
|
248
|
+
<div id="entries" class="space-y-2 text-sm max-h-[340px] overflow-auto pr-2 -mr-1"></div>
|
|
93
249
|
</div>
|
|
94
250
|
</div>
|
|
95
251
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<button onclick="doAction('
|
|
99
|
-
|
|
252
|
+
<!-- Footer actions -->
|
|
253
|
+
<div class="mt-8 flex items-center gap-2 text-sm">
|
|
254
|
+
<button onclick="doAction('sync')"
|
|
255
|
+
class="px-4 py-2 rounded-2xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.975] transition text-sm">
|
|
256
|
+
Sync to remote
|
|
257
|
+
</button>
|
|
258
|
+
<button onclick="doAction('update')"
|
|
259
|
+
class="px-4 py-2 rounded-2xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.975] transition text-sm">
|
|
260
|
+
Pull latest
|
|
261
|
+
</button>
|
|
262
|
+
<button onclick="doAction('open-dir')"
|
|
263
|
+
class="px-4 py-2 rounded-2xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.975] transition text-sm">
|
|
264
|
+
Open ~/.doraval
|
|
265
|
+
</button>
|
|
266
|
+
|
|
100
267
|
<div class="flex-1"></div>
|
|
101
|
-
<div class="text-zinc-500
|
|
268
|
+
<div class="text-xs text-zinc-500">CLI is best for heavy lifting. This is for quick capture & review.</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- Modal -->
|
|
273
|
+
<div id="modal" class="hidden fixed inset-0 bg-black/80 backdrop-blur-sm items-center justify-center z-50 p-4" onclick="if (event.target.id === 'modal') closeModal()">
|
|
274
|
+
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl w-full max-w-lg shadow-2xl modal" onclick="event.stopImmediatePropagation()">
|
|
275
|
+
<div class="p-6">
|
|
276
|
+
<div class="flex justify-between items-start">
|
|
277
|
+
<div id="modal-pb" class="inline-flex items-center px-3 py-0.5 rounded-full text-xs font-semibold tracking-wider"></div>
|
|
278
|
+
<button onclick="closeModal()" class="text-zinc-400 hover:text-white text-2xl leading-none -mr-1">×</button>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<h3 id="modal-title" class="text-xl font-semibold tracking-tight mt-3 pr-6"></h3>
|
|
282
|
+
|
|
283
|
+
<div class="flex items-center gap-2 mt-3 text-xs text-zinc-400">
|
|
284
|
+
<span id="modal-tags"></span>
|
|
285
|
+
<span class="text-zinc-700">•</span>
|
|
286
|
+
<span id="modal-meta"></span>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div id="modal-rationale" class="mt-5 text-[14.5px] leading-relaxed text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded-2xl p-5"></div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div class="border-t border-zinc-800 px-6 py-4 flex gap-2" id="modal-actions"></div>
|
|
102
293
|
</div>
|
|
103
294
|
</div>
|
|
104
295
|
|
|
296
|
+
<!-- Toasts -->
|
|
297
|
+
<div id="toasts" class="fixed bottom-5 right-5 flex flex-col gap-2 z-[70]"></div>
|
|
298
|
+
|
|
105
299
|
<script>
|
|
106
300
|
const $ = (id) => document.getElementById(id);
|
|
107
301
|
|
|
302
|
+
let allEntriesCache = [];
|
|
303
|
+
let currentProject = null;
|
|
304
|
+
|
|
305
|
+
function showToast(msg, type = 'info') {
|
|
306
|
+
const container = $('toasts');
|
|
307
|
+
const el = document.createElement('div');
|
|
308
|
+
el.className = `toast px-4 py-2.5 rounded-2xl text-sm border flex items-center gap-2 shadow-lg ${type === 'error'
|
|
309
|
+
? 'bg-red-950/90 border-red-800 text-red-300'
|
|
310
|
+
: 'bg-zinc-900 border-zinc-800 text-zinc-200'}`;
|
|
311
|
+
el.innerHTML = `<span>${msg}</span>`;
|
|
312
|
+
container.appendChild(el);
|
|
313
|
+
|
|
314
|
+
// Fast dismiss, interruptible
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
if (el.parentNode) {
|
|
317
|
+
el.style.transition = 'opacity 160ms ease, transform 160ms ease';
|
|
318
|
+
el.style.opacity = '0';
|
|
319
|
+
el.style.transform = 'translateY(6px)';
|
|
320
|
+
setTimeout(() => el.remove(), 120);
|
|
321
|
+
}
|
|
322
|
+
}, 2200);
|
|
323
|
+
}
|
|
324
|
+
|
|
108
325
|
async function fetchJSON(url, opts = {}) {
|
|
109
326
|
const res = await fetch(url, opts);
|
|
110
|
-
if (!res.ok)
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
const txt = await res.text();
|
|
329
|
+
throw new Error(txt || res.statusText);
|
|
330
|
+
}
|
|
111
331
|
return res.json();
|
|
112
332
|
}
|
|
113
333
|
|
|
114
334
|
function setProjectBadge(project) {
|
|
115
335
|
const el = $('project-badge');
|
|
116
336
|
el.textContent = project ? project : 'no project';
|
|
117
|
-
el.className =
|
|
337
|
+
el.className = `px-3 py-1 rounded-full bg-zinc-900 border border-zinc-800 text-xs ${project ? '' : 'text-amber-400'}`;
|
|
118
338
|
}
|
|
119
339
|
|
|
120
|
-
let currentProject = null;
|
|
121
|
-
|
|
122
340
|
async function loadStatus() {
|
|
123
341
|
const s = await fetchJSON('/api/status');
|
|
124
342
|
currentProject = s.project;
|
|
@@ -127,45 +345,97 @@ async function loadStatus() {
|
|
|
127
345
|
|
|
128
346
|
async function loadContext() {
|
|
129
347
|
const c = await fetchJSON('/api/context');
|
|
130
|
-
$('context')
|
|
348
|
+
const pre = $('context');
|
|
349
|
+
pre.textContent = c.text || '(no active high-pushback decisions yet)';
|
|
350
|
+
updateAgentPreview();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function updateAgentPreview() {
|
|
354
|
+
const select = $('agent-select');
|
|
355
|
+
const note = $('agent-note');
|
|
356
|
+
if (!select || !note) return;
|
|
357
|
+
const agent = select.value;
|
|
358
|
+
const base = 'Exact output from <span class="font-mono">dora journal context</span>. ';
|
|
359
|
+
let extra = 'Injected via startup hook.';
|
|
360
|
+
if (agent === 'claude') extra = 'Injected on <b>SessionStart</b> for Claude Code.';
|
|
361
|
+
else if (agent === 'cursor') extra = 'Can be used with Cursor plugins (e.g. workspaceOpen or custom rules).';
|
|
362
|
+
else if (agent === 'codex') extra = 'Injected via Codex hooks for context.';
|
|
363
|
+
else if (agent === 'copilot') extra = 'Useful for Copilot CLI custom instructions / agents.';
|
|
364
|
+
else if (agent === 'opencode') extra = 'Pairs well with OpenCode rules, agents and skills.';
|
|
365
|
+
note.innerHTML = base + extra;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderEntry(e, container) {
|
|
369
|
+
const pb = e.pushback ?? 0;
|
|
370
|
+
let border = 'border-emerald-400';
|
|
371
|
+
let pbLabel = 'nudge';
|
|
372
|
+
if (pb >= 7) { border = 'border-red-400'; pbLabel = 'strong'; }
|
|
373
|
+
else if (pb >= 4) { border = 'border-yellow-400'; pbLabel = 'friction'; }
|
|
374
|
+
|
|
375
|
+
const div = document.createElement('div');
|
|
376
|
+
div.className = `entry ${border} bg-zinc-900/50 hover:bg-zinc-900/80 border-l-[3px] rounded-r-2xl p-3.5 cursor-pointer ${e._staged ? 'staged' : ''}`;
|
|
377
|
+
div.onclick = () => showModal(e);
|
|
378
|
+
|
|
379
|
+
const preview = (e.rationale || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
380
|
+
|
|
381
|
+
div.innerHTML = `
|
|
382
|
+
<div class="flex items-start gap-3">
|
|
383
|
+
<div class="shrink-0 w-8 text-center pt-px">
|
|
384
|
+
<div class="text-[17px] font-semibold tabular-nums leading-none">${pb}</div>
|
|
385
|
+
<div class="text-[9px] uppercase tracking-[1px] text-zinc-500 mt-0.5">${pbLabel}</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="min-w-0 flex-1 pt-0.5">
|
|
388
|
+
<div class="font-medium leading-tight pr-1">${e.title}${e._staged ? ' <span class="text-amber-400 text-[10px] align-baseline">staged</span>' : ''}</div>
|
|
389
|
+
<div class="text-xs text-zinc-400 mt-1.5 line-clamp-2">${preview || 'no rationale yet'}</div>
|
|
390
|
+
<div class="mt-2 flex items-center gap-x-2 gap-y-0.5 text-[10px] text-zinc-500">
|
|
391
|
+
<span>${(e.tags || []).join(' ') || '—'}</span>
|
|
392
|
+
<span class="text-zinc-700">·</span>
|
|
393
|
+
<span>${e.author || 'you'}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
`;
|
|
398
|
+
container.appendChild(div);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function filterEntries() {
|
|
402
|
+
const q = ($('search')?.value || '').toLowerCase().trim();
|
|
403
|
+
const container = $('entries');
|
|
404
|
+
container.innerHTML = '';
|
|
405
|
+
|
|
406
|
+
let filtered = allEntriesCache;
|
|
407
|
+
if (q) {
|
|
408
|
+
filtered = allEntriesCache.filter(e =>
|
|
409
|
+
(e.title || '').toLowerCase().includes(q) ||
|
|
410
|
+
(e.rationale || '').toLowerCase().includes(q) ||
|
|
411
|
+
(e.tags || []).some(t => t.toLowerCase().includes(q))
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (filtered.length === 0) {
|
|
416
|
+
container.innerHTML = `<div class="text-zinc-500 text-sm py-4">No matches.</div>`;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
filtered.forEach(e => renderEntry(e, container));
|
|
421
|
+
$('entry-count').textContent = `${filtered.length} / ${allEntriesCache.length} entries`;
|
|
131
422
|
}
|
|
132
423
|
|
|
133
424
|
async function loadEntries() {
|
|
134
425
|
const data = await fetchJSON('/api/entries');
|
|
426
|
+
allEntriesCache = [...(data.staged || []), ...(data.committed || [])];
|
|
427
|
+
|
|
428
|
+
$('entry-count').textContent = allEntriesCache.length + ' entries';
|
|
135
429
|
const container = $('entries');
|
|
136
430
|
container.innerHTML = '';
|
|
137
|
-
const all = [...(data.staged || []), ...(data.committed || [])];
|
|
138
|
-
|
|
139
|
-
$('entry-count').textContent = all.length + ' entries';
|
|
140
431
|
|
|
141
|
-
if (
|
|
142
|
-
container.innerHTML = '<div class="text-zinc-500 text-sm py-4">No entries.
|
|
432
|
+
if (allEntriesCache.length === 0) {
|
|
433
|
+
container.innerHTML = '<div class="text-zinc-500 text-sm py-4">No entries yet. Capture one on the left.</div>';
|
|
143
434
|
return;
|
|
144
435
|
}
|
|
145
436
|
|
|
146
|
-
all
|
|
147
|
-
|
|
148
|
-
let cls = 'pb-nudge';
|
|
149
|
-
if (pb >= 7) cls = 'pb-strong';
|
|
150
|
-
else if (pb >= 4) cls = 'pb-friction';
|
|
151
|
-
|
|
152
|
-
const div = document.createElement('div');
|
|
153
|
-
div.className = 'entry ' + cls + (e._staged ? ' staged rounded-r-xl' : '');
|
|
154
|
-
div.innerHTML = `
|
|
155
|
-
<div class="flex gap-2 items-start">
|
|
156
|
-
<div class="push w-6 text-right font-semibold text-zinc-400">${pb}</div>
|
|
157
|
-
<div class="flex-1 min-w-0">
|
|
158
|
-
<div class="font-medium">${e.title}${e._staged ? ' <span class="text-amber-400 text-xs">(staged)</span>' : ''}</div>
|
|
159
|
-
<div class="text-xs text-zinc-500 mt-0.5 line-clamp-2">${(e.rationale||'').replace(/\s+/g,' ').slice(0,160)}</div>
|
|
160
|
-
<div class="mt-1 flex gap-2 text-[10px] text-zinc-500">
|
|
161
|
-
<span>${(e.tags||[]).join(', ') || 'no tags'}</span>
|
|
162
|
-
<span class="text-zinc-600">·</span>
|
|
163
|
-
<span>${e.author || 'human'}</span>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</div>`;
|
|
167
|
-
container.appendChild(div);
|
|
168
|
-
});
|
|
437
|
+
// show all initially
|
|
438
|
+
allEntriesCache.forEach(e => renderEntry(e, container));
|
|
169
439
|
}
|
|
170
440
|
|
|
171
441
|
async function loadHooks() {
|
|
@@ -173,88 +443,129 @@ async function loadHooks() {
|
|
|
173
443
|
const el = $('hooks');
|
|
174
444
|
el.innerHTML = '';
|
|
175
445
|
|
|
176
|
-
const
|
|
446
|
+
const mkToggle = (label, info, isGlobal) => {
|
|
177
447
|
const row = document.createElement('div');
|
|
178
|
-
row.className = 'flex items-center justify-between gap-
|
|
448
|
+
row.className = 'flex items-center justify-between gap-3 bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-2 text-sm';
|
|
449
|
+
|
|
450
|
+
const checked = info.enabled;
|
|
179
451
|
row.innerHTML = `
|
|
180
|
-
<div>
|
|
452
|
+
<div class="min-w-0">
|
|
181
453
|
<div class="font-medium">${label}</div>
|
|
182
|
-
<div class="text-[10px] text-zinc-500 font-mono truncate
|
|
183
|
-
</div>
|
|
184
|
-
<div>
|
|
185
|
-
<button data-action="${info.enabled ? 'disable' : 'enable'}" data-global="${isGlobal}"
|
|
186
|
-
class="text-xs px-3 py-1 rounded-xl border ${info.enabled ? 'border-emerald-800 text-emerald-400 hover:bg-emerald-950' : 'border-zinc-700 hover:bg-zinc-900'}">
|
|
187
|
-
${info.enabled ? 'Disable' : 'Enable'}
|
|
188
|
-
</button>
|
|
454
|
+
<div class="text-[10px] text-zinc-500 font-mono truncate">${info.path}</div>
|
|
189
455
|
</div>
|
|
456
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
457
|
+
<input type="checkbox" class="sr-only peer" ${checked ? 'checked' : ''}>
|
|
458
|
+
<div class="w-9 h-5 bg-zinc-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-emerald-600"></div>
|
|
459
|
+
</label>
|
|
190
460
|
`;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
461
|
+
|
|
462
|
+
const input = row.querySelector('input');
|
|
463
|
+
input.onchange = async () => {
|
|
464
|
+
const action = input.checked ? 'enable' : 'disable';
|
|
195
465
|
try {
|
|
196
466
|
await fetchJSON('/api/hooks/' + action, {
|
|
197
467
|
method: 'POST',
|
|
198
468
|
headers: {'content-type': 'application/json'},
|
|
199
|
-
body: JSON.stringify({ global:
|
|
469
|
+
body: JSON.stringify({ global: isGlobal })
|
|
200
470
|
});
|
|
471
|
+
showToast(`${action === 'enable' ? 'Enabled' : 'Disabled'} ${label}`, 'info');
|
|
201
472
|
await loadHooks();
|
|
202
473
|
} catch (e) {
|
|
203
|
-
|
|
474
|
+
showToast('Hook change failed: ' + e.message, 'error');
|
|
475
|
+
input.checked = !input.checked; // revert
|
|
204
476
|
}
|
|
205
477
|
};
|
|
478
|
+
|
|
206
479
|
el.appendChild(row);
|
|
207
480
|
};
|
|
208
481
|
|
|
209
|
-
|
|
210
|
-
|
|
482
|
+
mkToggle('Claude (local)', h.local, false);
|
|
483
|
+
mkToggle('Claude (global)', h.global, true);
|
|
211
484
|
}
|
|
212
485
|
|
|
213
486
|
async function addDecision() {
|
|
214
487
|
const title = $('title').value.trim();
|
|
215
|
-
if (!title) {
|
|
488
|
+
if (!title) {
|
|
489
|
+
showToast('Title is required', 'error');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
216
492
|
const pushback = parseInt($('pushback').value, 10);
|
|
217
493
|
const tags = $('tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
|
218
494
|
const rationale = $('rationale').value.trim();
|
|
219
495
|
|
|
220
|
-
|
|
496
|
+
const btns = document.querySelectorAll('button');
|
|
497
|
+
// simple feedback on the primary button
|
|
498
|
+
const primary = Array.from(btns).find(b => b.textContent.includes('Add to pending'));
|
|
499
|
+
const originalText = primary ? primary.innerHTML : '';
|
|
500
|
+
|
|
221
501
|
try {
|
|
222
502
|
await fetchJSON('/api/add', {
|
|
223
503
|
method: 'POST',
|
|
224
504
|
headers: {'content-type': 'application/json'},
|
|
225
505
|
body: JSON.stringify({ title, pushback, tags, rationale })
|
|
226
506
|
});
|
|
227
|
-
|
|
228
|
-
|
|
507
|
+
|
|
508
|
+
// Playful success moment
|
|
509
|
+
if (primary) {
|
|
510
|
+
primary.style.transitionDuration = '80ms';
|
|
511
|
+
primary.innerHTML = 'Captured ✓';
|
|
512
|
+
primary.style.background = '#166534';
|
|
513
|
+
primary.style.color = '#86efac';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
showToast('Added to pending');
|
|
517
|
+
|
|
518
|
+
$('title').value = '';
|
|
519
|
+
$('tags').value = '';
|
|
520
|
+
$('rationale').value = '';
|
|
521
|
+
|
|
229
522
|
await Promise.all([loadEntries(), loadContext()]);
|
|
230
|
-
|
|
523
|
+
|
|
524
|
+
// reset button
|
|
525
|
+
setTimeout(() => {
|
|
526
|
+
if (primary) {
|
|
527
|
+
primary.innerHTML = originalText;
|
|
528
|
+
primary.style.background = '';
|
|
529
|
+
primary.style.color = '';
|
|
530
|
+
primary.style.transitionDuration = '';
|
|
531
|
+
}
|
|
532
|
+
}, 900);
|
|
533
|
+
|
|
231
534
|
} catch (e) {
|
|
232
|
-
|
|
535
|
+
showToast('Failed: ' + e.message, 'error');
|
|
536
|
+
if (primary) {
|
|
537
|
+
primary.innerHTML = originalText;
|
|
538
|
+
primary.style.background = '';
|
|
539
|
+
primary.style.color = '';
|
|
540
|
+
}
|
|
233
541
|
}
|
|
234
542
|
}
|
|
235
543
|
|
|
236
544
|
async function copyContext() {
|
|
237
545
|
const text = $('context').textContent;
|
|
546
|
+
if (!text || text.includes('no active')) {
|
|
547
|
+
showToast('Nothing to copy yet', 'error');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
238
550
|
await navigator.clipboard.writeText(text);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
t.className = 'text-emerald-400 ml-2 text-xs';
|
|
242
|
-
$('context').after(t);
|
|
243
|
-
setTimeout(() => t.remove(), 900);
|
|
551
|
+
const orig = event.currentTarget?.textContent;
|
|
552
|
+
showToast('Copied to clipboard');
|
|
244
553
|
}
|
|
245
554
|
|
|
246
555
|
async function refreshAll() {
|
|
247
556
|
await Promise.all([loadEntries(), loadContext(), loadHooks()]);
|
|
557
|
+
showToast('Refreshed');
|
|
248
558
|
}
|
|
249
559
|
|
|
250
560
|
async function doAction(name) {
|
|
251
561
|
if (name === 'open-dir') {
|
|
252
562
|
const s = await fetchJSON('/api/status');
|
|
253
|
-
|
|
563
|
+
showToast('Path: ' + s.doravalDir);
|
|
564
|
+
// optionally try to open, but browser security limits it
|
|
254
565
|
return;
|
|
255
566
|
}
|
|
256
567
|
if (name === 'sync' || name === 'update') {
|
|
257
|
-
|
|
568
|
+
showToast('Use CLI: dora journal ' + name);
|
|
258
569
|
return;
|
|
259
570
|
}
|
|
260
571
|
}
|
|
@@ -262,22 +573,159 @@ async function doAction(name) {
|
|
|
262
573
|
function setupSlider() {
|
|
263
574
|
const slider = $('pushback');
|
|
264
575
|
const val = $('pb-value');
|
|
265
|
-
|
|
576
|
+
|
|
577
|
+
const update = () => {
|
|
578
|
+
const v = parseInt(slider.value, 10);
|
|
579
|
+
val.textContent = v;
|
|
580
|
+
|
|
581
|
+
val.className = `push font-semibold tabular-nums text-lg transition-colors ${v >= 7 ? 'text-red-400' : v >= 4 ? 'text-yellow-400' : 'text-emerald-400'}`;
|
|
582
|
+
};
|
|
583
|
+
|
|
266
584
|
slider.addEventListener('input', update);
|
|
267
585
|
update();
|
|
268
586
|
}
|
|
269
587
|
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
function showModal(e) {
|
|
591
|
+
const modal = $('modal');
|
|
592
|
+
modal.classList.remove('hidden');
|
|
593
|
+
modal.classList.add('flex');
|
|
594
|
+
|
|
595
|
+
const pb = e.pushback ?? 0;
|
|
596
|
+
let pbColor = 'bg-emerald-400 text-black';
|
|
597
|
+
let label = 'NUDGE';
|
|
598
|
+
if (pb >= 7) { pbColor = 'bg-red-500 text-white'; label = 'STRONG'; }
|
|
599
|
+
else if (pb >= 4) { pbColor = 'bg-amber-400 text-black'; label = 'FRICTION'; }
|
|
600
|
+
|
|
601
|
+
$('modal-pb').className = `inline-flex items-center px-3 py-px rounded-full text-[10px] font-semibold tracking-[0.5px] ${pbColor}`;
|
|
602
|
+
$('modal-pb').textContent = `${pb} — ${label}`;
|
|
603
|
+
|
|
604
|
+
$('modal-title').textContent = e.title;
|
|
605
|
+
|
|
606
|
+
const tagsStr = (e.tags || []).length ? (e.tags || []).join(' · ') : 'no tags';
|
|
607
|
+
$('modal-tags').innerHTML = tagsStr;
|
|
608
|
+
|
|
609
|
+
$('modal-meta').innerHTML = `${e.author || 'human'} · ${e.date || ''} ${e._staged ? '<span class="text-amber-400">· staged</span>' : ''}`;
|
|
610
|
+
|
|
611
|
+
$('modal-rationale').textContent = e.rationale || '(no rationale)';
|
|
612
|
+
|
|
613
|
+
const actions = $('modal-actions');
|
|
614
|
+
actions.innerHTML = '';
|
|
615
|
+
|
|
616
|
+
if (e._staged && e._filename) {
|
|
617
|
+
const del = document.createElement('button');
|
|
618
|
+
del.className = 'px-4 py-1.5 text-sm rounded-2xl border border-red-900/60 text-red-400 hover:bg-red-950/50 active:scale-[0.975] transition';
|
|
619
|
+
del.textContent = 'Delete staged';
|
|
620
|
+
del.onclick = async () => {
|
|
621
|
+
if (!confirm('Remove this staged entry?')) return;
|
|
622
|
+
try {
|
|
623
|
+
await fetchJSON('/api/delete-staged', {
|
|
624
|
+
method: 'POST',
|
|
625
|
+
headers: { 'content-type': 'application/json' },
|
|
626
|
+
body: JSON.stringify({ filename: e._filename })
|
|
627
|
+
});
|
|
628
|
+
closeModal();
|
|
629
|
+
await Promise.all([loadEntries(), loadContext()]);
|
|
630
|
+
showToast('Removed from pending');
|
|
631
|
+
} catch (err) {
|
|
632
|
+
showToast('Delete failed', 'error');
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
actions.appendChild(del);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const closeBtn = document.createElement('button');
|
|
639
|
+
closeBtn.className = 'ml-auto px-5 py-1.5 text-sm rounded-2xl border border-zinc-700 hover:bg-zinc-800 active:scale-[0.975] transition';
|
|
640
|
+
closeBtn.textContent = 'Done';
|
|
641
|
+
closeBtn.onclick = closeModal;
|
|
642
|
+
actions.appendChild(closeBtn);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function closeModal() {
|
|
646
|
+
const m = $('modal');
|
|
647
|
+
m.classList.remove('flex');
|
|
648
|
+
m.classList.add('hidden');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function setupSlider() {
|
|
652
|
+
const slider = $('pushback');
|
|
653
|
+
const val = $('pb-value');
|
|
654
|
+
const update = () => {
|
|
655
|
+
const v = parseInt(slider.value, 10);
|
|
656
|
+
val.textContent = v;
|
|
657
|
+
val.className = `push font-semibold tabular-nums ${v >= 7 ? 'text-red-400' : v >= 4 ? 'text-yellow-400' : 'text-emerald-400'}`;
|
|
658
|
+
};
|
|
659
|
+
slider.addEventListener('input', update);
|
|
660
|
+
update();
|
|
661
|
+
|
|
662
|
+
// allow keyboard on slider
|
|
663
|
+
slider.addEventListener('keydown', (ev) => {
|
|
664
|
+
if (ev.key === 'Enter') {
|
|
665
|
+
ev.preventDefault();
|
|
666
|
+
addDecision();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function clearForm() {
|
|
672
|
+
$('title').value = '';
|
|
673
|
+
$('tags').value = '';
|
|
674
|
+
$('rationale').value = '';
|
|
675
|
+
$('pb-value').textContent = $('pushback').value = '4';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function setupCaptureKeys() {
|
|
679
|
+
const title = $('title');
|
|
680
|
+
const rationale = $('rationale');
|
|
681
|
+
|
|
682
|
+
[title, rationale].forEach(el => {
|
|
683
|
+
el.addEventListener('keydown', (e) => {
|
|
684
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
685
|
+
e.preventDefault();
|
|
686
|
+
addDecision();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// focus title on load
|
|
692
|
+
setTimeout(() => title.focus(), 300);
|
|
693
|
+
}
|
|
694
|
+
|
|
270
695
|
async function boot() {
|
|
271
696
|
setupSlider();
|
|
697
|
+
setupCaptureKeys();
|
|
698
|
+
|
|
272
699
|
await loadStatus();
|
|
273
700
|
await Promise.all([loadContext(), loadEntries(), loadHooks()]);
|
|
274
|
-
|
|
701
|
+
|
|
702
|
+
const search = $('search');
|
|
703
|
+
if (search) {
|
|
704
|
+
search.addEventListener('keydown', (e) => {
|
|
705
|
+
if (e.key === 'Escape') {
|
|
706
|
+
search.value = '';
|
|
707
|
+
filterEntries();
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Light polling (only when visible)
|
|
275
713
|
setInterval(() => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
714
|
+
if (!document.hidden) {
|
|
715
|
+
loadEntries().catch(() => {});
|
|
716
|
+
loadContext().catch(() => {});
|
|
717
|
+
}
|
|
718
|
+
}, 9000);
|
|
279
719
|
}
|
|
280
720
|
|
|
721
|
+
window.addDecision = addDecision;
|
|
722
|
+
window.refreshAll = refreshAll;
|
|
723
|
+
window.copyContext = copyContext;
|
|
724
|
+
window.doAction = doAction;
|
|
725
|
+
window.closeModal = closeModal;
|
|
726
|
+
window.clearForm = clearForm;
|
|
727
|
+
window.updateAgentPreview = updateAgentPreview;
|
|
728
|
+
|
|
281
729
|
boot().catch(console.error);
|
|
282
730
|
</script>
|
|
283
731
|
</body>
|