@hacksmith/doraval 0.2.35 → 0.2.43
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/bin/doraval.js +1387 -453
- package/bin/ui/index.html +732 -0
- package/package.json +2 -2
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
6
|
+
<title>dora • local dashboard</title>
|
|
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>
|
|
19
|
+
<style>
|
|
20
|
+
:root { color-scheme: dark; }
|
|
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
|
+
|
|
45
|
+
.pb-strong { border-color: #f87171; }
|
|
46
|
+
.pb-friction { border-color: #facc15; }
|
|
47
|
+
.pb-nudge { border-color: #4ade80; }
|
|
48
|
+
|
|
49
|
+
.push { font-variant-numeric: tabular-nums; }
|
|
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
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body class="bg-zinc-950 text-zinc-200">
|
|
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>
|
|
131
|
+
<div>
|
|
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>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
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>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="grid grid-cols-1 lg:grid-cols-5 gap-5">
|
|
153
|
+
<!-- Capture -->
|
|
154
|
+
<div class="lg:col-span-2 section">
|
|
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 -->
|
|
172
|
+
<div>
|
|
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>
|
|
176
|
+
</div>
|
|
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>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
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">
|
|
192
|
+
|
|
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>
|
|
198
|
+
|
|
199
|
+
<button onclick="addDecision()"
|
|
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>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- What agents see -->
|
|
209
|
+
<div class="lg:col-span-3 section">
|
|
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>
|
|
220
|
+
</div>
|
|
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>
|
|
223
|
+
</div>
|
|
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>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Hooks -->
|
|
229
|
+
<div class="lg:col-span-2 section">
|
|
230
|
+
<div class="text-sm font-semibold tracking-tight mb-3">Journal hooks</div>
|
|
231
|
+
<div id="hooks" class="space-y-2 text-sm"></div>
|
|
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>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- Journal -->
|
|
238
|
+
<div class="lg:col-span-3 section">
|
|
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>
|
|
247
|
+
</div>
|
|
248
|
+
<div id="entries" class="space-y-2 text-sm max-h-[340px] overflow-auto pr-2 -mr-1"></div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
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
|
+
|
|
267
|
+
<div class="flex-1"></div>
|
|
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>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<!-- Toasts -->
|
|
297
|
+
<div id="toasts" class="fixed bottom-5 right-5 flex flex-col gap-2 z-[70]"></div>
|
|
298
|
+
|
|
299
|
+
<script>
|
|
300
|
+
const $ = (id) => document.getElementById(id);
|
|
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
|
+
|
|
325
|
+
async function fetchJSON(url, opts = {}) {
|
|
326
|
+
const res = await fetch(url, opts);
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
const txt = await res.text();
|
|
329
|
+
throw new Error(txt || res.statusText);
|
|
330
|
+
}
|
|
331
|
+
return res.json();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function setProjectBadge(project) {
|
|
335
|
+
const el = $('project-badge');
|
|
336
|
+
el.textContent = project ? project : 'no project';
|
|
337
|
+
el.className = `px-3 py-1 rounded-full bg-zinc-900 border border-zinc-800 text-xs ${project ? '' : 'text-amber-400'}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function loadStatus() {
|
|
341
|
+
const s = await fetchJSON('/api/status');
|
|
342
|
+
currentProject = s.project;
|
|
343
|
+
setProjectBadge(currentProject);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function loadContext() {
|
|
347
|
+
const c = await fetchJSON('/api/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`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function loadEntries() {
|
|
425
|
+
const data = await fetchJSON('/api/entries');
|
|
426
|
+
allEntriesCache = [...(data.staged || []), ...(data.committed || [])];
|
|
427
|
+
|
|
428
|
+
$('entry-count').textContent = allEntriesCache.length + ' entries';
|
|
429
|
+
const container = $('entries');
|
|
430
|
+
container.innerHTML = '';
|
|
431
|
+
|
|
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>';
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// show all initially
|
|
438
|
+
allEntriesCache.forEach(e => renderEntry(e, container));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function loadHooks() {
|
|
442
|
+
const h = await fetchJSON('/api/hooks/status');
|
|
443
|
+
const el = $('hooks');
|
|
444
|
+
el.innerHTML = '';
|
|
445
|
+
|
|
446
|
+
const mkToggle = (label, info, isGlobal) => {
|
|
447
|
+
const row = document.createElement('div');
|
|
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;
|
|
451
|
+
row.innerHTML = `
|
|
452
|
+
<div class="min-w-0">
|
|
453
|
+
<div class="font-medium">${label}</div>
|
|
454
|
+
<div class="text-[10px] text-zinc-500 font-mono truncate">${info.path}</div>
|
|
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>
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
const input = row.querySelector('input');
|
|
463
|
+
input.onchange = async () => {
|
|
464
|
+
const action = input.checked ? 'enable' : 'disable';
|
|
465
|
+
try {
|
|
466
|
+
await fetchJSON('/api/hooks/' + action, {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: {'content-type': 'application/json'},
|
|
469
|
+
body: JSON.stringify({ global: isGlobal })
|
|
470
|
+
});
|
|
471
|
+
showToast(`${action === 'enable' ? 'Enabled' : 'Disabled'} ${label}`, 'info');
|
|
472
|
+
await loadHooks();
|
|
473
|
+
} catch (e) {
|
|
474
|
+
showToast('Hook change failed: ' + e.message, 'error');
|
|
475
|
+
input.checked = !input.checked; // revert
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
el.appendChild(row);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
mkToggle('Claude (local)', h.local, false);
|
|
483
|
+
mkToggle('Claude (global)', h.global, true);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function addDecision() {
|
|
487
|
+
const title = $('title').value.trim();
|
|
488
|
+
if (!title) {
|
|
489
|
+
showToast('Title is required', 'error');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const pushback = parseInt($('pushback').value, 10);
|
|
493
|
+
const tags = $('tags').value.split(',').map(t => t.trim()).filter(Boolean);
|
|
494
|
+
const rationale = $('rationale').value.trim();
|
|
495
|
+
|
|
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
|
+
|
|
501
|
+
try {
|
|
502
|
+
await fetchJSON('/api/add', {
|
|
503
|
+
method: 'POST',
|
|
504
|
+
headers: {'content-type': 'application/json'},
|
|
505
|
+
body: JSON.stringify({ title, pushback, tags, rationale })
|
|
506
|
+
});
|
|
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
|
+
|
|
522
|
+
await Promise.all([loadEntries(), loadContext()]);
|
|
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
|
+
|
|
534
|
+
} catch (e) {
|
|
535
|
+
showToast('Failed: ' + e.message, 'error');
|
|
536
|
+
if (primary) {
|
|
537
|
+
primary.innerHTML = originalText;
|
|
538
|
+
primary.style.background = '';
|
|
539
|
+
primary.style.color = '';
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function copyContext() {
|
|
545
|
+
const text = $('context').textContent;
|
|
546
|
+
if (!text || text.includes('no active')) {
|
|
547
|
+
showToast('Nothing to copy yet', 'error');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
await navigator.clipboard.writeText(text);
|
|
551
|
+
const orig = event.currentTarget?.textContent;
|
|
552
|
+
showToast('Copied to clipboard');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function refreshAll() {
|
|
556
|
+
await Promise.all([loadEntries(), loadContext(), loadHooks()]);
|
|
557
|
+
showToast('Refreshed');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function doAction(name) {
|
|
561
|
+
if (name === 'open-dir') {
|
|
562
|
+
const s = await fetchJSON('/api/status');
|
|
563
|
+
showToast('Path: ' + s.doravalDir);
|
|
564
|
+
// optionally try to open, but browser security limits it
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (name === 'sync' || name === 'update') {
|
|
568
|
+
showToast('Use CLI: dora journal ' + name);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function setupSlider() {
|
|
574
|
+
const slider = $('pushback');
|
|
575
|
+
const val = $('pb-value');
|
|
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
|
+
|
|
584
|
+
slider.addEventListener('input', update);
|
|
585
|
+
update();
|
|
586
|
+
}
|
|
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
|
+
|
|
695
|
+
async function boot() {
|
|
696
|
+
setupSlider();
|
|
697
|
+
setupCaptureKeys();
|
|
698
|
+
|
|
699
|
+
await loadStatus();
|
|
700
|
+
await Promise.all([loadContext(), loadEntries(), loadHooks()]);
|
|
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)
|
|
713
|
+
setInterval(() => {
|
|
714
|
+
if (!document.hidden) {
|
|
715
|
+
loadEntries().catch(() => {});
|
|
716
|
+
loadContext().catch(() => {});
|
|
717
|
+
}
|
|
718
|
+
}, 9000);
|
|
719
|
+
}
|
|
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
|
+
|
|
729
|
+
boot().catch(console.error);
|
|
730
|
+
</script>
|
|
731
|
+
</body>
|
|
732
|
+
</html>
|