superintent 0.0.1

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/bin/superintent.js +2 -0
  4. package/dist/commands/extract.d.ts +2 -0
  5. package/dist/commands/extract.js +66 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +56 -0
  8. package/dist/commands/knowledge.d.ts +2 -0
  9. package/dist/commands/knowledge.js +647 -0
  10. package/dist/commands/search.d.ts +2 -0
  11. package/dist/commands/search.js +153 -0
  12. package/dist/commands/spec.d.ts +2 -0
  13. package/dist/commands/spec.js +283 -0
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +43 -0
  16. package/dist/commands/ticket.d.ts +4 -0
  17. package/dist/commands/ticket.js +942 -0
  18. package/dist/commands/ui.d.ts +2 -0
  19. package/dist/commands/ui.js +954 -0
  20. package/dist/db/client.d.ts +4 -0
  21. package/dist/db/client.js +26 -0
  22. package/dist/db/init-schema.d.ts +2 -0
  23. package/dist/db/init-schema.js +28 -0
  24. package/dist/db/parsers.d.ts +24 -0
  25. package/dist/db/parsers.js +79 -0
  26. package/dist/db/schema.d.ts +7 -0
  27. package/dist/db/schema.js +64 -0
  28. package/dist/db/usage.d.ts +8 -0
  29. package/dist/db/usage.js +24 -0
  30. package/dist/embed/model.d.ts +5 -0
  31. package/dist/embed/model.js +34 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/types.d.ts +120 -0
  35. package/dist/types.js +1 -0
  36. package/dist/ui/components/index.d.ts +6 -0
  37. package/dist/ui/components/index.js +13 -0
  38. package/dist/ui/components/knowledge.d.ts +33 -0
  39. package/dist/ui/components/knowledge.js +238 -0
  40. package/dist/ui/components/layout.d.ts +1 -0
  41. package/dist/ui/components/layout.js +323 -0
  42. package/dist/ui/components/search.d.ts +15 -0
  43. package/dist/ui/components/search.js +114 -0
  44. package/dist/ui/components/spec.d.ts +11 -0
  45. package/dist/ui/components/spec.js +253 -0
  46. package/dist/ui/components/ticket.d.ts +90 -0
  47. package/dist/ui/components/ticket.js +604 -0
  48. package/dist/ui/components/utils.d.ts +26 -0
  49. package/dist/ui/components/utils.js +34 -0
  50. package/dist/ui/styles.css +2 -0
  51. package/dist/utils/cli.d.ts +21 -0
  52. package/dist/utils/cli.js +31 -0
  53. package/dist/utils/config.d.ts +12 -0
  54. package/dist/utils/config.js +116 -0
  55. package/dist/utils/id.d.ts +6 -0
  56. package/dist/utils/id.js +13 -0
  57. package/dist/utils/io.d.ts +8 -0
  58. package/dist/utils/io.js +15 -0
  59. package/package.json +60 -0
@@ -0,0 +1,604 @@
1
+ // Ticket-related UI components
2
+ import { escapeHtml, renderMarkdownEditor } from './utils.js';
3
+ // Helper to render a ticket card
4
+ export function renderTicketCard(ticket, options) {
5
+ const isBacklog = options?.isBacklog ?? false;
6
+ const taskCount = ticket.tasks?.length || 0;
7
+ const doneCount = ticket.tasks?.filter(t => t.done).length || 0;
8
+ const progress = taskCount > 0 ? Math.round((doneCount / taskCount) * 100) : 0;
9
+ const remaining = taskCount - doneCount;
10
+ const isComplete = taskCount > 0 && progress === 100;
11
+ const classColors = {
12
+ 'A': { bg: 'bg-green-100', text: 'text-green-700' },
13
+ 'B': { bg: 'bg-yellow-100', text: 'text-yellow-700' },
14
+ 'C': { bg: 'bg-red-100', text: 'text-red-700' },
15
+ };
16
+ const classStyle = ticket.change_class ? classColors[ticket.change_class] || { bg: 'bg-gray-100', text: 'text-gray-600' } : null;
17
+ const typeColors = {
18
+ 'feature': { bg: 'bg-purple-100', text: 'text-purple-700' },
19
+ 'bugfix': { bg: 'bg-red-100', text: 'text-red-700' },
20
+ 'refactor': { bg: 'bg-blue-100', text: 'text-blue-700' },
21
+ 'docs': { bg: 'bg-cyan-100', text: 'text-cyan-700' },
22
+ 'chore': { bg: 'bg-gray-100', text: 'text-gray-700' },
23
+ 'test': { bg: 'bg-emerald-100', text: 'text-emerald-700' },
24
+ };
25
+ const typeStyle = ticket.type ? typeColors[ticket.type] || { bg: 'bg-gray-100', text: 'text-gray-600' } : null;
26
+ // Use title if available, otherwise use intent for display
27
+ const displayTitle = ticket.title || ticket.intent;
28
+ return `
29
+ <div class="bg-white rounded-lg shadow-card p-3 cursor-pointer hover:shadow-card-hover transition-shadow group"
30
+ draggable="true"
31
+ ondragstart="onDragStart(event, '${ticket.id}')"
32
+ ondragend="onDragEnd(event)"
33
+ hx-get="/partials/ticket-modal/${encodeURIComponent(ticket.id)}"
34
+ hx-target="#modal-content"
35
+ hx-trigger="click"
36
+ onclick="showModal()">
37
+ <div class="flex items-start justify-between mb-1">
38
+ <div class="text-xs font-mono text-gray-400">${escapeHtml(ticket.id)}</div>
39
+ ${isBacklog ? `
40
+ <button class="opacity-0 group-hover:opacity-100 p-1 -mt-1 -mr-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-all"
41
+ title="Edit ticket"
42
+ hx-get="/partials/edit-ticket-modal/${encodeURIComponent(ticket.id)}"
43
+ hx-target="#modal-content"
44
+ hx-trigger="click"
45
+ onclick="event.stopPropagation(); showModal()">
46
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
48
+ </svg>
49
+ </button>
50
+ ` : ''}
51
+ </div>
52
+ <div class="text-sm font-medium text-gray-800 line-clamp-2">${escapeHtml(displayTitle)}</div>
53
+ ${taskCount > 0 ? `
54
+ <div class="mt-2">
55
+ <div class="flex items-center justify-between text-xs mb-1">
56
+ <span class="${isComplete ? 'text-green-600 font-medium' : 'text-gray-500'}">${isComplete ? 'Complete' : `${remaining} remaining`}</span>
57
+ <span class="${isComplete ? 'text-green-600' : 'text-gray-400'}">${doneCount}/${taskCount}</span>
58
+ </div>
59
+ <div class="w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
60
+ <div class="h-full bg-${isComplete ? 'green' : 'blue'}-500 rounded-full transition-all" style="width: ${progress}%"></div>
61
+ </div>
62
+ </div>
63
+ ` : ''}
64
+ <div class="mt-2 flex gap-1 flex-wrap">
65
+ ${ticket.type && typeStyle ? `<span class="px-2 py-0.5 text-xs font-medium rounded ${typeStyle.bg} ${typeStyle.text}">${ticket.type}</span>` : ''}
66
+ ${ticket.change_class && classStyle ? `<span class="px-2 py-0.5 text-xs font-medium rounded ${classStyle.bg} ${classStyle.text}">Class ${ticket.change_class}</span>` : ''}
67
+ </div>
68
+ </div>
69
+ `;
70
+ }
71
+ // Helper to render kanban view
72
+ export function renderKanbanView() {
73
+ return `
74
+ <div id="kanban-columns" hx-get="/partials/kanban-columns" hx-trigger="load, refresh">
75
+ </div>
76
+ `;
77
+ }
78
+ // Helper to render kanban columns with pagination
79
+ export function renderKanbanColumns(columns) {
80
+ const columnStyles = {
81
+ 'Backlog': { color: 'gray', bg: 'bg-gray-50' },
82
+ 'In Progress': { color: 'yellow', bg: 'bg-yellow-50' },
83
+ 'In Review': { color: 'blue', bg: 'bg-blue-50' },
84
+ 'Done': { color: 'green', bg: 'bg-green-50' },
85
+ 'Blocked': { color: 'red', bg: 'bg-red-50' },
86
+ 'Paused': { color: 'orange', bg: 'bg-orange-50' },
87
+ 'Abandoned': { color: 'gray', bg: 'bg-gray-100' },
88
+ 'Superseded': { color: 'purple', bg: 'bg-purple-50' },
89
+ 'Archive': { color: 'slate', bg: 'bg-slate-50' },
90
+ };
91
+ return `
92
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-4">
93
+ ${columns.map(col => {
94
+ const style = columnStyles[col.status] || { color: 'gray', bg: 'bg-gray-50' };
95
+ const statusSlug = col.status.toLowerCase().replace(/ /g, '-');
96
+ const isBacklog = col.status === 'Backlog';
97
+ const isArchive = col.status === 'Archive';
98
+ // Archive column is not a drop target (can't drag to Archive directly)
99
+ const dragHandlers = isArchive ? '' : `ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event, '${col.status}')"`;
100
+ return `
101
+ <div class="rounded-lg ${style.bg} p-4 flex flex-col max-h-[calc(100vh-120px)]"
102
+ ${dragHandlers}>
103
+ <div class="flex items-center justify-between mb-3 shrink-0">
104
+ <h2 class="font-semibold text-${style.color}-700">${col.status}</h2>
105
+ <div class="flex items-center gap-2">
106
+ ${isBacklog ? `
107
+ <button class="text-xs px-2 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded transition-colors"
108
+ hx-get="/partials/new-ticket-modal"
109
+ hx-target="#modal-content"
110
+ hx-trigger="click"
111
+ onclick="showModal()">
112
+ + Add
113
+ </button>
114
+ ` : ''}
115
+ <span class="text-xs text-${style.color}-500 bg-${style.color}-100 px-2 py-0.5 rounded-full">${col.tickets.length}${col.hasMore ? '+' : ''}</span>
116
+ </div>
117
+ </div>
118
+ <div id="tickets-${statusSlug}" class="space-y-3 overflow-y-auto flex-1 min-h-0 -mx-1 px-1 py-1">
119
+ ${col.tickets.map(t => renderTicketCard(t, { isBacklog })).join('')}
120
+ </div>
121
+ ${col.hasMore ? `
122
+ <button class="w-full mt-3 py-2 text-sm text-gray-600 bg-white rounded border hover:bg-gray-50 transition-colors shrink-0"
123
+ hx-get="/partials/kanban-column/${encodeURIComponent(col.status)}?offset=20"
124
+ hx-swap="outerHTML">
125
+ Load More
126
+ </button>
127
+ ` : ''}
128
+ </div>
129
+ `;
130
+ }).join('')}
131
+ </div>
132
+ `;
133
+ }
134
+ // Helper to render more tickets for a column (pagination)
135
+ export function renderColumnMore(tickets, status, nextOffset, hasMore) {
136
+ const isBacklog = status === 'Backlog';
137
+ const cards = tickets.map(t => renderTicketCard(t, { isBacklog })).join('');
138
+ if (!hasMore) {
139
+ return cards;
140
+ }
141
+ return `
142
+ ${cards}
143
+ <button class="w-full mt-3 py-2 text-sm text-gray-600 bg-white rounded border hover:bg-gray-50 transition-colors"
144
+ hx-get="/partials/kanban-column/${encodeURIComponent(status)}?offset=${nextOffset}"
145
+ hx-swap="outerHTML">
146
+ Load More
147
+ </button>
148
+ `;
149
+ }
150
+ // Helper to render ticket modal
151
+ export function renderTicketModal(ticket) {
152
+ const statusColors = {
153
+ 'Backlog': 'gray',
154
+ 'In Progress': 'yellow',
155
+ 'In Review': 'blue',
156
+ 'Done': 'green',
157
+ 'Blocked': 'red',
158
+ 'Paused': 'orange',
159
+ 'Abandoned': 'gray',
160
+ 'Superseded': 'purple',
161
+ };
162
+ const color = statusColors[ticket.status] || 'gray';
163
+ const typeColors = {
164
+ 'feature': { bg: 'bg-purple-100', text: 'text-purple-700' },
165
+ 'bugfix': { bg: 'bg-red-100', text: 'text-red-700' },
166
+ 'refactor': { bg: 'bg-blue-100', text: 'text-blue-700' },
167
+ 'docs': { bg: 'bg-cyan-100', text: 'text-cyan-700' },
168
+ 'chore': { bg: 'bg-gray-100', text: 'text-gray-700' },
169
+ 'test': { bg: 'bg-emerald-100', text: 'text-emerald-700' },
170
+ };
171
+ const typeStyle = ticket.type ? typeColors[ticket.type] || { bg: 'bg-gray-100', text: 'text-gray-600' } : null;
172
+ return `
173
+ <div class="p-6">
174
+ <div class="flex items-start justify-between mb-4">
175
+ <div>
176
+ <div class="flex items-center gap-2 mb-1">
177
+ <span class="text-xs font-mono text-gray-400">${escapeHtml(ticket.id)}</span>
178
+ <button type="button"
179
+ class="p-0.5 text-gray-400 hover:text-blue-600 rounded transition-colors"
180
+ title="Copy ticket ID"
181
+ onclick="navigator.clipboard.writeText('${escapeHtml(ticket.id)}').then(() => { const svg = this.querySelector('svg'); const originalPath = svg.innerHTML; svg.innerHTML = '<path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M5 13l4 4L19 7&quot;></path>'; this.classList.add('text-green-600'); setTimeout(() => { svg.innerHTML = originalPath; this.classList.remove('text-green-600'); }, 1500); })">
182
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
183
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
184
+ </svg>
185
+ </button>
186
+ ${ticket.type && typeStyle ? `<span class="px-2 py-0.5 text-xs font-medium rounded ${typeStyle.bg} ${typeStyle.text}">${ticket.type}</span>` : ''}
187
+ </div>
188
+ ${ticket.title ? `<h2 class="text-xl font-bold text-gray-800 mb-2">${escapeHtml(ticket.title)}</h2>` : ''}
189
+ <div class="text-sm text-gray-600 markdown-content" data-markdown>${escapeHtml(ticket.intent)}</div>
190
+ </div>
191
+ <button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 p-1">
192
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
193
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
194
+ </svg>
195
+ </button>
196
+ </div>
197
+
198
+ <div class="mb-6">
199
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Status</h3>
200
+ <select id="status-select"
201
+ class="px-3 py-1.5 rounded-lg border text-sm font-medium bg-${color}-50 text-${color}-700 border-${color}-200"
202
+ hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}/status"
203
+ hx-trigger="change"
204
+ hx-swap="none"
205
+ name="status"
206
+ onchange="updateStatusColor(this)">
207
+ <optgroup label="Normal Flow">
208
+ <option value="Backlog" ${ticket.status === 'Backlog' ? 'selected' : ''}>Backlog</option>
209
+ <option value="In Progress" ${ticket.status === 'In Progress' ? 'selected' : ''}>In Progress</option>
210
+ <option value="In Review" ${ticket.status === 'In Review' ? 'selected' : ''}>In Review</option>
211
+ <option value="Done" ${ticket.status === 'Done' ? 'selected' : ''}>Done</option>
212
+ </optgroup>
213
+ <optgroup label="Exception States">
214
+ <option value="Blocked" ${ticket.status === 'Blocked' ? 'selected' : ''}>Blocked</option>
215
+ <option value="Paused" ${ticket.status === 'Paused' ? 'selected' : ''}>Paused</option>
216
+ <option value="Abandoned" ${ticket.status === 'Abandoned' ? 'selected' : ''}>Abandoned</option>
217
+ <option value="Superseded" ${ticket.status === 'Superseded' ? 'selected' : ''}>Superseded</option>
218
+ </optgroup>
219
+ </select>
220
+ <script>
221
+ function updateStatusColor(el) {
222
+ const colors = {
223
+ 'Backlog': 'gray', 'In Progress': 'yellow', 'In Review': 'blue', 'Done': 'green',
224
+ 'Blocked': 'red', 'Paused': 'orange', 'Abandoned': 'gray', 'Superseded': 'purple'
225
+ };
226
+ const c = colors[el.value] || 'gray';
227
+ el.className = el.className.replace(/bg-[a-z]+-50/g, 'bg-' + c + '-50')
228
+ .replace(/text-[a-z]+-700/g, 'text-' + c + '-700')
229
+ .replace(/border-[a-z]+-200/g, 'border-' + c + '-200');
230
+ }
231
+ </script>
232
+ </div>
233
+
234
+ ${ticket.context ? `
235
+ <div class="mb-4">
236
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Context</h3>
237
+ <div class="text-sm text-gray-600 bg-gray-50 rounded-lg p-3 whitespace-pre-wrap">${escapeHtml(ticket.context)}</div>
238
+ </div>
239
+ ` : ''}
240
+
241
+ ${ticket.constraints_use?.length || ticket.constraints_avoid?.length ? `
242
+ <div class="mb-4">
243
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Constraints</h3>
244
+ <div class="text-sm space-y-1">
245
+ ${ticket.constraints_use?.length ? `<p><span class="text-green-600 font-medium">Use:</span> ${ticket.constraints_use.map(c => escapeHtml(c)).join(', ')}</p>` : ''}
246
+ ${ticket.constraints_avoid?.length ? `<p><span class="text-red-600 font-medium">Avoid:</span> ${ticket.constraints_avoid.map(c => escapeHtml(c)).join(', ')}</p>` : ''}
247
+ </div>
248
+ </div>
249
+ ` : ''}
250
+
251
+ ${ticket.assumptions?.length ? `
252
+ <div class="mb-4">
253
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Assumptions</h3>
254
+ <ul class="text-sm text-gray-600 list-disc list-inside">
255
+ ${ticket.assumptions.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
256
+ </ul>
257
+ </div>
258
+ ` : ''}
259
+
260
+ ${ticket.change_class ? `
261
+ <div class="mb-4">
262
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Change Class</h3>
263
+ <div class="text-sm text-gray-600">Class ${ticket.change_class}${ticket.change_class_reason ? ` - ${escapeHtml(ticket.change_class_reason)}` : ''}</div>
264
+ </div>
265
+ ` : ''}
266
+
267
+ ${ticket.origin_spec_id ? `
268
+ <div class="mb-4">
269
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Origin Spec</h3>
270
+ <span class="inline-flex items-center px-2 py-1 text-xs rounded-lg bg-indigo-50 text-indigo-700 hover:bg-indigo-100 cursor-pointer font-mono font-medium"
271
+ hx-get="/partials/spec-modal/${encodeURIComponent(ticket.origin_spec_id)}"
272
+ hx-target="#modal-content"
273
+ hx-trigger="click">
274
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
275
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
276
+ </svg>
277
+ ${escapeHtml(ticket.origin_spec_id)}
278
+ </span>
279
+ </div>
280
+ ` : ''}
281
+
282
+ ${ticket.plan ? `
283
+ <div class="mb-4">
284
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Plan</h3>
285
+ <div class="text-sm space-y-3 bg-blue-50 rounded-lg p-3">
286
+ ${ticket.plan.files?.length ? `
287
+ <div>
288
+ <span class="font-medium text-blue-700">Files to Edit:</span>
289
+ <span class="text-gray-600">${ticket.plan.files.map(f => escapeHtml(f)).join(', ')}</span>
290
+ </div>
291
+ ` : ''}
292
+ ${ticket.plan.taskSteps?.length ? `
293
+ <div>
294
+ <span class="font-medium text-blue-700">Tasks → Steps:</span>
295
+ <div class="mt-1 space-y-2">
296
+ ${ticket.plan.taskSteps.map((ts, i) => {
297
+ const taskItem = ticket.tasks?.[i];
298
+ const isDone = taskItem?.done ?? false;
299
+ return `
300
+ <div class="ml-1">
301
+ <div class="flex items-center gap-2">
302
+ <input type="checkbox" ${isDone ? 'checked' : ''}
303
+ class="rounded border-gray-300 shrink-0"
304
+ hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}/task/${i}"
305
+ hx-swap="none">
306
+ <span class="${isDone ? 'line-through text-gray-400' : 'font-medium text-gray-700'}">${escapeHtml(ts.task)}</span>
307
+ </div>
308
+ ${ts.steps?.length ? `
309
+ <ol class="ml-8 mt-1 list-decimal text-gray-500 text-xs">
310
+ ${ts.steps.map(s => `<li>${escapeHtml(s)}</li>`).join('')}
311
+ </ol>
312
+ ` : ''}
313
+ </div>
314
+ `;
315
+ }).join('')}
316
+ </div>
317
+ </div>
318
+ ` : ''}
319
+ ${ticket.plan.dodVerification?.length ? `
320
+ <div>
321
+ <span class="font-medium text-blue-700">Definition of Done → Verification:</span>
322
+ <div class="mt-1 space-y-1">
323
+ ${ticket.plan.dodVerification.map((dv, i) => {
324
+ const dodItem = ticket.definition_of_done?.[i];
325
+ const isDone = dodItem?.done ?? false;
326
+ return `
327
+ <div class="flex items-start gap-2 ml-1">
328
+ <input type="checkbox" ${isDone ? 'checked' : ''}
329
+ class="rounded border-gray-300 shrink-0 mt-0.5"
330
+ hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}/dod/${i}"
331
+ hx-swap="none">
332
+ <span class="${isDone ? 'line-through text-gray-400' : 'text-gray-600'}"><strong>${escapeHtml(dv.dod)}</strong> → ${escapeHtml(dv.verify)}</span>
333
+ </div>
334
+ `;
335
+ }).join('')}
336
+ </div>
337
+ </div>
338
+ ` : ''}
339
+ ${ticket.plan.decisions?.length ? `
340
+ <div>
341
+ <span class="font-medium text-blue-700">Decisions:</span>
342
+ <ul class="mt-1 ml-4 list-disc text-gray-600">
343
+ ${ticket.plan.decisions.map(d => `<li><strong>${escapeHtml(d.choice)}</strong>${d.reason ? ` — ${escapeHtml(d.reason)}` : ''}</li>`).join('')}
344
+ </ul>
345
+ </div>
346
+ ` : ''}
347
+ ${ticket.plan.tradeOffs?.length ? `
348
+ <div>
349
+ <span class="font-medium text-blue-700">Trade-offs:</span>
350
+ <ul class="mt-1 ml-4 list-disc text-gray-600">
351
+ ${ticket.plan.tradeOffs.map(t => `<li>${escapeHtml(t.considered)}${t.rejected ? ` — ${escapeHtml(t.rejected)}` : ''}</li>`).join('')}
352
+ </ul>
353
+ </div>
354
+ ` : ''}
355
+ ${ticket.plan.rollback ? `
356
+ <div>
357
+ <span class="font-medium text-blue-700">Rollback:</span>
358
+ <div class="mt-1 ml-4 text-gray-600">
359
+ <div class="text-xs mb-1">
360
+ <span class="font-medium">Reversibility:</span>
361
+ <span class="${ticket.plan.rollback.reversibility === 'full' ? 'text-green-600' : ticket.plan.rollback.reversibility === 'partial' ? 'text-yellow-600' : 'text-red-600'}">${ticket.plan.rollback.reversibility}</span>
362
+ </div>
363
+ ${ticket.plan.rollback.steps?.length ? `
364
+ <ul class="list-disc ml-4">
365
+ ${ticket.plan.rollback.steps.map(s => `<li>${escapeHtml(s)}</li>`).join('')}
366
+ </ul>
367
+ ` : ''}
368
+ </div>
369
+ </div>
370
+ ` : ''}
371
+ ${ticket.plan.irreversibleActions?.length ? `
372
+ <div>
373
+ <span class="font-medium text-blue-700">Irreversible Actions:</span>
374
+ <ul class="mt-1 ml-4 list-disc text-gray-600">
375
+ ${ticket.plan.irreversibleActions.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
376
+ </ul>
377
+ </div>
378
+ ` : ''}
379
+ ${ticket.plan.edgeCases?.length ? `
380
+ <div>
381
+ <span class="font-medium text-blue-700">Edge Cases:</span>
382
+ <ul class="mt-1 ml-4 list-disc text-gray-600">
383
+ ${ticket.plan.edgeCases.map(e => `<li>${escapeHtml(e)}</li>`).join('')}
384
+ </ul>
385
+ </div>
386
+ ` : ''}
387
+ </div>
388
+ </div>
389
+ ` : /* Fallback: show tasks/DoD without plan */ `
390
+ ${ticket.tasks?.length ? `
391
+ <div class="mb-4">
392
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Tasks</h3>
393
+ <ul class="space-y-1">
394
+ ${ticket.tasks.map((t, i) => `
395
+ <li class="flex items-center gap-2">
396
+ <input type="checkbox" ${t.done ? 'checked' : ''}
397
+ class="rounded border-gray-300"
398
+ hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}/task/${i}"
399
+ hx-swap="none">
400
+ <span class="${t.done ? 'line-through text-gray-400' : 'text-gray-700'} text-sm">${escapeHtml(t.text)}</span>
401
+ </li>
402
+ `).join('')}
403
+ </ul>
404
+ </div>
405
+ ` : ''}
406
+ ${ticket.definition_of_done?.length ? `
407
+ <div class="mb-4">
408
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Definition of Done</h3>
409
+ <ul class="space-y-1">
410
+ ${ticket.definition_of_done.map((d, i) => `
411
+ <li class="flex items-center gap-2">
412
+ <input type="checkbox" ${d.done ? 'checked' : ''}
413
+ class="rounded border-gray-300"
414
+ hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}/dod/${i}"
415
+ hx-swap="none">
416
+ <span class="${d.done ? 'line-through text-gray-400' : 'text-gray-700'} text-sm">${escapeHtml(d.text)}</span>
417
+ </li>
418
+ `).join('')}
419
+ </ul>
420
+ </div>
421
+ ` : ''}
422
+ `}
423
+
424
+ ${ticket.derived_knowledge?.length ? `
425
+ <div class="mb-4">
426
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Derived Knowledge</h3>
427
+ <div class="flex flex-wrap gap-2">
428
+ ${ticket.derived_knowledge.map(kid => `
429
+ <span class="inline-flex items-center px-2 py-1 text-xs rounded-lg bg-purple-50 text-purple-700 hover:bg-purple-100 cursor-pointer"
430
+ hx-get="/partials/knowledge-modal/${encodeURIComponent(kid)}"
431
+ hx-target="#modal-content"
432
+ hx-trigger="click">
433
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
434
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
435
+ </svg>
436
+ ${escapeHtml(kid)}
437
+ </span>
438
+ `).join('')}
439
+ </div>
440
+ </div>
441
+ ` : ''}
442
+
443
+ ${ticket.comments?.length ? `
444
+ <div class="mb-4">
445
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Comments</h3>
446
+ <div class="space-y-2">
447
+ ${ticket.comments.map(c => `
448
+ <div class="bg-gray-50 rounded-lg p-3">
449
+ <div class="text-xs text-gray-400 mb-1">${new Date(c.timestamp).toLocaleString()}</div>
450
+ <div class="text-sm text-gray-700">${escapeHtml(c.text)}</div>
451
+ </div>
452
+ `).join('')}
453
+ </div>
454
+ </div>
455
+ ` : ''}
456
+
457
+ <div class="mt-6 pt-4 border-t flex items-center justify-between">
458
+ <div class="text-xs text-gray-400">
459
+ <span>Created: ${ticket.created_at || 'N/A'}</span>
460
+ <span class="ml-4">Updated: ${ticket.updated_at || 'N/A'}</span>
461
+ </div>
462
+ <div class="flex gap-2">
463
+ ${ticket.status === 'Backlog' ? `
464
+ <button type="button"
465
+ class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 rounded hover:bg-blue-100 transition-colors"
466
+ hx-get="/partials/edit-ticket-modal/${encodeURIComponent(ticket.id)}"
467
+ hx-target="#modal-content"
468
+ hx-trigger="click">
469
+ Edit
470
+ </button>
471
+ ` : ''}
472
+ <button type="button"
473
+ class="px-3 py-1.5 text-xs font-medium text-red-600 bg-red-50 rounded hover:bg-red-100 transition-colors"
474
+ onclick="if(confirm('Delete this ticket?${ticket.derived_knowledge?.length ? ' Derived knowledge will be orphaned but preserved.' : ''}')) { fetch('/api/tickets/${encodeURIComponent(ticket.id)}', {method:'DELETE'}).then(r=>r.json()).then(d=>{if(d.success){hideModal();htmx.trigger('#kanban-columns','refresh');}else{alert(d.error||'Delete failed');}}).catch(e=>alert('Error: '+e)); }">
475
+ Delete
476
+ </button>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ `;
481
+ }
482
+ // Helper to render new ticket modal
483
+ export function renderNewTicketModal() {
484
+ const ticketTypes = [
485
+ { value: 'feature', label: 'Feature', desc: 'New functionality' },
486
+ { value: 'bugfix', label: 'Bugfix', desc: 'Fix an issue' },
487
+ { value: 'refactor', label: 'Refactor', desc: 'Code improvement' },
488
+ { value: 'docs', label: 'Docs', desc: 'Documentation' },
489
+ { value: 'chore', label: 'Chore', desc: 'Maintenance' },
490
+ { value: 'test', label: 'Test', desc: 'Add tests' },
491
+ ];
492
+ return `
493
+ <div class="p-6">
494
+ <div class="flex items-center justify-between mb-6">
495
+ <h2 class="text-xl font-bold text-gray-800">New Ticket</h2>
496
+ <button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 p-1">
497
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
498
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
499
+ </svg>
500
+ </button>
501
+ </div>
502
+
503
+ <form hx-post="/api/tickets/quick"
504
+ hx-target="#kanban-columns"
505
+ hx-on::after-request="hideModal()">
506
+ <div class="space-y-4">
507
+ <div>
508
+ <label class="block text-sm font-medium text-gray-700 mb-1">Title <span class="text-red-500">*</span></label>
509
+ <input type="text" name="title" required
510
+ placeholder="Brief summary of what needs to be done"
511
+ class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
512
+ </div>
513
+
514
+ <div>
515
+ <label class="block text-sm font-medium text-gray-700 mb-1">Type <span class="text-red-500">*</span></label>
516
+ <select name="type" required
517
+ class="w-full px-3 py-2 border rounded-lg text-sm bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
518
+ ${ticketTypes.map(t => `<option value="${t.value}">${t.label} - ${t.desc}</option>`).join('')}
519
+ </select>
520
+ </div>
521
+
522
+ <div>
523
+ <label class="block text-sm font-medium text-gray-700 mb-1">Intent <span class="text-red-500">*</span></label>
524
+ ${renderMarkdownEditor({ name: 'intent', id: 'ticket-intent-editor', placeholder: 'What do you want to achieve? Be specific about the desired outcome. Supports **markdown**.', rows: 4, required: true })}
525
+ </div>
526
+ </div>
527
+
528
+ <div class="flex justify-end gap-3 mt-6 pt-4 border-t">
529
+ <button type="button" onclick="hideModal()"
530
+ class="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded hover:bg-gray-200 transition-colors">
531
+ Cancel
532
+ </button>
533
+ <button type="submit"
534
+ class="px-3 py-1.5 text-xs font-medium text-white bg-blue-500 rounded hover:bg-blue-600 transition-colors">
535
+ Create Ticket
536
+ </button>
537
+ </div>
538
+ </form>
539
+ </div>
540
+ `;
541
+ }
542
+ // Helper to render edit ticket modal
543
+ export function renderEditTicketModal(ticket) {
544
+ const ticketTypes = [
545
+ { value: 'feature', label: 'Feature', desc: 'New functionality' },
546
+ { value: 'bugfix', label: 'Bugfix', desc: 'Fix an issue' },
547
+ { value: 'refactor', label: 'Refactor', desc: 'Code improvement' },
548
+ { value: 'docs', label: 'Docs', desc: 'Documentation' },
549
+ { value: 'chore', label: 'Chore', desc: 'Maintenance' },
550
+ { value: 'test', label: 'Test', desc: 'Add tests' },
551
+ ];
552
+ return `
553
+ <div class="p-6">
554
+ <div class="flex items-center justify-between mb-6">
555
+ <div>
556
+ <h2 class="text-xl font-bold text-gray-800">Edit Ticket</h2>
557
+ <span class="text-xs font-mono text-gray-400">${escapeHtml(ticket.id)}</span>
558
+ </div>
559
+ <button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 p-1">
560
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
561
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
562
+ </svg>
563
+ </button>
564
+ </div>
565
+
566
+ <form hx-patch="/api/tickets/${encodeURIComponent(ticket.id)}"
567
+ hx-target="#modal-content">
568
+ <div class="space-y-4">
569
+ <div>
570
+ <label class="block text-sm font-medium text-gray-700 mb-1">Title <span class="text-red-500">*</span></label>
571
+ <input type="text" name="title" required
572
+ value="${escapeHtml(ticket.title || '')}"
573
+ placeholder="Brief summary of what needs to be done"
574
+ class="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
575
+ </div>
576
+
577
+ <div>
578
+ <label class="block text-sm font-medium text-gray-700 mb-1">Type <span class="text-red-500">*</span></label>
579
+ <select name="type" required
580
+ class="w-full px-3 py-2 border rounded-lg text-sm bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
581
+ ${ticketTypes.map(t => `<option value="${t.value}"${ticket.type === t.value ? ' selected' : ''}>${t.label} - ${t.desc}</option>`).join('')}
582
+ </select>
583
+ </div>
584
+
585
+ <div>
586
+ <label class="block text-sm font-medium text-gray-700 mb-1">Intent <span class="text-red-500">*</span></label>
587
+ ${renderMarkdownEditor({ name: 'intent', id: 'ticket-edit-intent-editor', placeholder: 'What do you want to achieve? Be specific about the desired outcome. Supports **markdown**.', rows: 12, required: true, value: escapeHtml(ticket.intent) })}
588
+ </div>
589
+ </div>
590
+
591
+ <div class="flex justify-end gap-3 mt-6 pt-4 border-t">
592
+ <button type="button" onclick="hideModal()"
593
+ class="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded hover:bg-gray-200 transition-colors">
594
+ Cancel
595
+ </button>
596
+ <button type="submit"
597
+ class="px-3 py-1.5 text-xs font-medium text-white bg-blue-500 rounded hover:bg-blue-600 transition-colors">
598
+ Save Changes
599
+ </button>
600
+ </div>
601
+ </form>
602
+ </div>
603
+ `;
604
+ }
@@ -0,0 +1,26 @@
1
+ export declare function escapeHtml(str: string): string;
2
+ export declare function renderMarkdownEditor(opts: {
3
+ name: string;
4
+ id: string;
5
+ placeholder?: string;
6
+ rows?: number;
7
+ required?: boolean;
8
+ value?: string;
9
+ }): string;
10
+ export interface ColumnData {
11
+ status: string;
12
+ tickets: {
13
+ id: string;
14
+ type?: string;
15
+ title?: string;
16
+ status: string;
17
+ intent: string;
18
+ change_class?: string;
19
+ change_class_reason?: string;
20
+ tasks?: {
21
+ text: string;
22
+ done: boolean;
23
+ }[];
24
+ }[];
25
+ hasMore: boolean;
26
+ }