@assistkick/create 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,187 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import type { Project } from '../hooks/useProjects';
3
+
4
+ interface ProjectSelectorProps {
5
+ projects: Project[];
6
+ selectedProjectId: string | null;
7
+ onSelect: (id: string) => void;
8
+ onCreate: (name: string) => Promise<any>;
9
+ onRename: (id: string, name: string) => Promise<void>;
10
+ onArchive: (id: string) => Promise<void>;
11
+ }
12
+
13
+ export function ProjectSelector({
14
+ projects, selectedProjectId, onSelect, onCreate, onRename, onArchive,
15
+ }: ProjectSelectorProps) {
16
+ const [open, setOpen] = useState(false);
17
+ const [creating, setCreating] = useState(false);
18
+ const [newName, setNewName] = useState('');
19
+ const [renamingId, setRenamingId] = useState<string | null>(null);
20
+ const [renameValue, setRenameValue] = useState('');
21
+ const dropdownRef = useRef<HTMLDivElement>(null);
22
+ const createInputRef = useRef<HTMLInputElement>(null);
23
+ const renameInputRef = useRef<HTMLInputElement>(null);
24
+
25
+ const selectedProject = projects.find(p => p.id === selectedProjectId);
26
+
27
+ // Close dropdown on outside click
28
+ useEffect(() => {
29
+ if (!open) return;
30
+ const handler = (e: MouseEvent) => {
31
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
32
+ setOpen(false);
33
+ setCreating(false);
34
+ setRenamingId(null);
35
+ }
36
+ };
37
+ document.addEventListener('mousedown', handler);
38
+ return () => document.removeEventListener('mousedown', handler);
39
+ }, [open]);
40
+
41
+ // Focus create input when shown
42
+ useEffect(() => {
43
+ if (creating) createInputRef.current?.focus();
44
+ }, [creating]);
45
+
46
+ // Focus rename input when shown
47
+ useEffect(() => {
48
+ if (renamingId) renameInputRef.current?.focus();
49
+ }, [renamingId]);
50
+
51
+ const handleCreate = async () => {
52
+ const trimmed = newName.trim();
53
+ if (!trimmed) return;
54
+ try {
55
+ await onCreate(trimmed);
56
+ setNewName('');
57
+ setCreating(false);
58
+ setOpen(false);
59
+ } catch {
60
+ // Error is handled by the caller
61
+ }
62
+ };
63
+
64
+ const handleRename = async () => {
65
+ const trimmed = renameValue.trim();
66
+ if (!trimmed || !renamingId) return;
67
+ try {
68
+ await onRename(renamingId, trimmed);
69
+ setRenamingId(null);
70
+ setRenameValue('');
71
+ } catch {
72
+ // Error is handled by the caller
73
+ }
74
+ };
75
+
76
+ const handleArchive = async (id: string) => {
77
+ try {
78
+ await onArchive(id);
79
+ } catch {
80
+ // Error is handled by the caller
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div className="project-selector" ref={dropdownRef}>
86
+ <button
87
+ className="project-selector-trigger"
88
+ onClick={() => setOpen(v => !v)}
89
+ title="Select project"
90
+ >
91
+ <span className="project-selector-label">
92
+ {selectedProject?.name || 'No project'}
93
+ </span>
94
+ <span className="project-selector-chevron">{open ? '\u25B4' : '\u25BE'}</span>
95
+ </button>
96
+
97
+ {open && (
98
+ <div className="project-selector-dropdown">
99
+ <div className="project-selector-list">
100
+ {projects.map(project => (
101
+ <div
102
+ key={project.id}
103
+ className={`project-selector-item${project.id === selectedProjectId ? ' active' : ''}`}
104
+ >
105
+ {renamingId === project.id ? (
106
+ <input
107
+ ref={renameInputRef}
108
+ className="project-selector-input"
109
+ value={renameValue}
110
+ onChange={e => setRenameValue(e.target.value)}
111
+ onKeyDown={e => {
112
+ if (e.key === 'Enter') handleRename();
113
+ if (e.key === 'Escape') { setRenamingId(null); setRenameValue(''); }
114
+ }}
115
+ onBlur={handleRename}
116
+ />
117
+ ) : (
118
+ <>
119
+ <button
120
+ className="project-selector-item-name"
121
+ onClick={() => { onSelect(project.id); setOpen(false); }}
122
+ >
123
+ {project.name}
124
+ </button>
125
+ <div className="project-selector-item-actions">
126
+ <button
127
+ className="project-selector-action-btn"
128
+ title="Rename"
129
+ onClick={(e) => {
130
+ e.stopPropagation();
131
+ setRenamingId(project.id);
132
+ setRenameValue(project.name);
133
+ }}
134
+ >
135
+ &#9998;
136
+ </button>
137
+ {!project.isDefault && (
138
+ <button
139
+ className="project-selector-action-btn project-selector-archive-btn"
140
+ title="Archive"
141
+ onClick={(e) => {
142
+ e.stopPropagation();
143
+ handleArchive(project.id);
144
+ }}
145
+ >
146
+ &#10005;
147
+ </button>
148
+ )}
149
+ </div>
150
+ </>
151
+ )}
152
+ </div>
153
+ ))}
154
+ </div>
155
+
156
+ <div className="project-selector-footer">
157
+ {creating ? (
158
+ <div className="project-selector-create-form">
159
+ <input
160
+ ref={createInputRef}
161
+ className="project-selector-input"
162
+ placeholder="Project name..."
163
+ value={newName}
164
+ onChange={e => setNewName(e.target.value)}
165
+ onKeyDown={e => {
166
+ if (e.key === 'Enter') handleCreate();
167
+ if (e.key === 'Escape') { setCreating(false); setNewName(''); }
168
+ }}
169
+ />
170
+ <button className="project-selector-create-confirm" onClick={handleCreate}>
171
+ +
172
+ </button>
173
+ </div>
174
+ ) : (
175
+ <button
176
+ className="project-selector-new-btn"
177
+ onClick={() => setCreating(true)}
178
+ >
179
+ + New Project
180
+ </button>
181
+ )}
182
+ </div>
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,192 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { apiClient } from '../api/client';
3
+
4
+ interface QaIssueSheetProps {
5
+ isOpen: boolean;
6
+ featureId: string | null;
7
+ featureName: string;
8
+ column: string;
9
+ notes: any[];
10
+ reviews: any[];
11
+ onClose: () => void;
12
+ onNotesChanged: () => void;
13
+ }
14
+
15
+ export function QaIssueSheet({
16
+ isOpen, featureId, featureName, column, notes: initialNotes, reviews: initialReviews,
17
+ onClose, onNotesChanged,
18
+ }: QaIssueSheetProps) {
19
+ const sheetRef = useRef<HTMLDivElement>(null);
20
+ const [notes, setNotes] = useState<any[]>(initialNotes);
21
+ const [reviews, setReviews] = useState<any[]>(initialReviews);
22
+ const [newNoteText, setNewNoteText] = useState('');
23
+ const [editingNoteId, setEditingNoteId] = useState<string | null>(null);
24
+ const [editText, setEditText] = useState('');
25
+
26
+ const isEditable = column === 'qa';
27
+
28
+ // Sync when props change
29
+ useEffect(() => {
30
+ setNotes(initialNotes);
31
+ setReviews(initialReviews);
32
+ setEditingNoteId(null);
33
+ setNewNoteText('');
34
+ }, [featureId, initialNotes, initialReviews]);
35
+
36
+ // Close on outside click
37
+ useEffect(() => {
38
+ if (!isOpen) return;
39
+ const handleClick = (e: MouseEvent) => {
40
+ if (sheetRef.current && !sheetRef.current.contains(e.target as Node)) {
41
+ if ((e.target as HTMLElement).closest('.kanban-issues-btn')) return;
42
+ onClose();
43
+ }
44
+ };
45
+ document.addEventListener('click', handleClick);
46
+ return () => document.removeEventListener('click', handleClick);
47
+ }, [isOpen, onClose]);
48
+
49
+ const refreshNotes = useCallback(async () => {
50
+ if (!featureId) return;
51
+ try {
52
+ const kanbanData = await apiClient.fetchKanban();
53
+ const entry = kanbanData[featureId];
54
+ if (entry) {
55
+ setNotes(entry.notes || []);
56
+ setReviews(entry.reviews || []);
57
+ }
58
+ onNotesChanged();
59
+ } catch (err) {
60
+ console.error('Failed to refresh notes:', err);
61
+ }
62
+ }, [featureId, onNotesChanged]);
63
+
64
+ const handleAddNote = async () => {
65
+ const text = newNoteText.trim();
66
+ if (!text || !featureId) return;
67
+ try {
68
+ await apiClient.createNote(featureId, text);
69
+ setNewNoteText('');
70
+ await refreshNotes();
71
+ } catch (err) {
72
+ console.error('Failed to create note:', err);
73
+ }
74
+ };
75
+
76
+ const handleUpdateNote = async (noteId: string) => {
77
+ const text = editText.trim();
78
+ if (!text || !featureId) return;
79
+ try {
80
+ await apiClient.updateNote(featureId, noteId, text);
81
+ setEditingNoteId(null);
82
+ await refreshNotes();
83
+ } catch (err) {
84
+ console.error('Failed to update note:', err);
85
+ }
86
+ };
87
+
88
+ const handleDeleteNote = async (noteId: string) => {
89
+ if (!featureId) return;
90
+ try {
91
+ await apiClient.deleteNote(featureId, noteId);
92
+ await refreshNotes();
93
+ } catch (err) {
94
+ console.error('Failed to delete note:', err);
95
+ }
96
+ };
97
+
98
+ const formatDate = (isoString: string) => {
99
+ if (!isoString) return '';
100
+ try {
101
+ return new Date(isoString).toLocaleDateString('en-US', {
102
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
103
+ });
104
+ } catch { return ''; }
105
+ };
106
+
107
+ return (
108
+ <div className={`qa-issue-sheet${isOpen ? ' open' : ''}`} ref={sheetRef}>
109
+ <div className="qa-sheet-header">
110
+ <span className="qa-sheet-title">{featureId} — {featureName}</span>
111
+ <button className="qa-sheet-close" onClick={onClose}>&times;</button>
112
+ </div>
113
+ <div className="qa-sheet-body">
114
+ <div className="qa-sheet-subtitle">
115
+ {isEditable ? 'Report and manage QA issues' : 'Issues (read-only)'}
116
+ </div>
117
+
118
+ {reviews.length > 0 && (
119
+ <div className="qa-sheet-review-history">
120
+ <div className="qa-sheet-section-title">Review History</div>
121
+ {reviews.map((review: any, i: number) => (
122
+ <div key={i} className="qa-sheet-review-item">
123
+ <div className="qa-sheet-review-header">
124
+ <span className="qa-sheet-review-cycle">Cycle {review.cycle}</span>
125
+ <span className={`qa-sheet-review-badge ${review.status}`}>
126
+ {review.status === 'approved' ? 'Approved' : 'Rejected'}
127
+ </span>
128
+ <span className="qa-sheet-review-time">{formatDate(review.timestamp)}</span>
129
+ </div>
130
+ {review.reason && <div className="qa-sheet-review-reason">{review.reason}</div>}
131
+ </div>
132
+ ))}
133
+ </div>
134
+ )}
135
+
136
+ {notes.length === 0 ? (
137
+ <div className="qa-sheet-empty">No issues reported yet.</div>
138
+ ) : (
139
+ <div className="qa-sheet-notes-list">
140
+ {notes.map((note: any) => (
141
+ <div key={note.id} className="qa-sheet-note">
142
+ {editingNoteId === note.id ? (
143
+ <div className="qa-sheet-edit-form">
144
+ <textarea
145
+ className="qa-sheet-textarea"
146
+ value={editText}
147
+ onChange={e => setEditText(e.target.value)}
148
+ rows={4}
149
+ autoFocus
150
+ />
151
+ <div className="qa-sheet-form-btns">
152
+ <button className="qa-sheet-add-btn" onClick={() => handleUpdateNote(note.id)}>Save</button>
153
+ <button className="qa-sheet-cancel-btn" onClick={() => setEditingNoteId(null)}>Cancel</button>
154
+ </div>
155
+ </div>
156
+ ) : (
157
+ <>
158
+ <div className="qa-sheet-note-text">{note.text}</div>
159
+ <div className="qa-sheet-note-meta">
160
+ <span className="qa-sheet-note-date">{formatDate(note.created_at || note.timestamp)}</span>
161
+ {isEditable && (
162
+ <span className="qa-sheet-note-actions">
163
+ <button className="qa-sheet-action-btn" onClick={() => { setEditingNoteId(note.id); setEditText(note.text); }}>Edit</button>
164
+ <button className="qa-sheet-action-btn delete" onClick={() => handleDeleteNote(note.id)}>Delete</button>
165
+ </span>
166
+ )}
167
+ </div>
168
+ </>
169
+ )}
170
+ </div>
171
+ ))}
172
+ </div>
173
+ )}
174
+
175
+ {isEditable && (
176
+ <div className="qa-sheet-add-form">
177
+ <textarea
178
+ className="qa-sheet-textarea"
179
+ placeholder="Describe the issue..."
180
+ rows={4}
181
+ value={newNoteText}
182
+ onChange={e => setNewNoteText(e.target.value)}
183
+ />
184
+ <div className="qa-sheet-form-btns">
185
+ <button className="qa-sheet-add-btn" onClick={handleAddNote}>Add Issue</button>
186
+ </div>
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,231 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { marked } from 'marked';
3
+ import { apiClient } from '../api/client';
4
+ import { getTaskIcon, getTaskCssClass, shouldShowTaskList } from '../utils/task_status';
5
+
6
+ interface WorkSummary {
7
+ cycle: number;
8
+ filesCreated: string[];
9
+ filesUpdated: string[];
10
+ filesDeleted: string[];
11
+ approach: string;
12
+ decisions: string[];
13
+ timestamp: string;
14
+ }
15
+
16
+ interface SidePanelProps {
17
+ graphData: any;
18
+ onEdgeClick: (neighborId: string) => void;
19
+ }
20
+
21
+ export function SidePanel({ graphData, onEdgeClick }: SidePanelProps) {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+ const [node, setNode] = useState<any>(null);
24
+ const [content, setContent] = useState('');
25
+ const [workSummaries, setWorkSummaries] = useState<WorkSummary[]>([]);
26
+ const [expandedSummaries, setExpandedSummaries] = useState(false);
27
+ const [tasks, setTasks] = useState<{ total: number; completed: number; items: any[] } | null>(null);
28
+ const [expandedTasks, setExpandedTasks] = useState(false);
29
+
30
+ const open = useCallback(async (n: any) => {
31
+ setNode(n);
32
+ setWorkSummaries([]);
33
+ setExpandedSummaries(false);
34
+ setTasks(null);
35
+ setExpandedTasks(false);
36
+ try {
37
+ const detail = await apiClient.fetchNode(n.id);
38
+ setContent(detail.content);
39
+ setIsOpen(true);
40
+
41
+ // Fetch pipeline status for feature nodes to get work summaries
42
+ if (n.type === 'feature' || n.id?.startsWith('feat_')) {
43
+ try {
44
+ const pStatus = await apiClient.getPipelineStatus(n.id);
45
+ if (pStatus?.workSummaries?.length > 0) {
46
+ setWorkSummaries(pStatus.workSummaries);
47
+ }
48
+ if (pStatus?.tasks) {
49
+ setTasks(pStatus.tasks);
50
+ }
51
+ } catch {
52
+ // Pipeline status may not exist for all features
53
+ }
54
+ }
55
+ } catch (err) {
56
+ console.error('Failed to fetch node:', err);
57
+ }
58
+ }, []);
59
+
60
+ const close = useCallback(() => setIsOpen(false), []);
61
+
62
+ const renderMarkdown = (md: string) => {
63
+ const stripped = md.replace(/^---[\s\S]*?---\n*/m, '');
64
+ try {
65
+ return marked.parse(stripped) as string;
66
+ } catch {
67
+ return stripped;
68
+ }
69
+ };
70
+
71
+ const findEdges = (nodeId: string) => {
72
+ if (!graphData) return [];
73
+ const edges: any[] = [];
74
+ graphData.edges.forEach((e: any) => {
75
+ if (e.from === nodeId) edges.push({ neighborId: e.to, relation: e.relation, direction: 'outgoing' });
76
+ else if (e.to === nodeId) edges.push({ neighborId: e.from, relation: e.relation, direction: 'incoming' });
77
+ });
78
+ return edges;
79
+ };
80
+
81
+ // Expose open/close via ref-like approach on the component instance
82
+ // We'll use a callback pattern instead
83
+ (SidePanel as any).__open = open;
84
+ (SidePanel as any).__close = close;
85
+
86
+ if (!node) return (
87
+ <div className={`side-panel${isOpen ? ' open' : ''}`} id="side-panel">
88
+ <div className="panel-header">
89
+ <span className="panel-title" />
90
+ <button className="panel-close" onClick={close}>&times;</button>
91
+ </div>
92
+ <div className="panel-body" />
93
+ </div>
94
+ );
95
+
96
+ const statusLabel = (node.status || 'draft').replace(/_/g, ' ');
97
+ const completeness = Math.round((node.completeness || 0) * 100);
98
+ const typeLabel = node.type ? node.type.replace(/_/g, ' ') : '';
99
+ const edges = findEdges(node.id);
100
+
101
+ return (
102
+ <div className={`side-panel${isOpen ? ' open' : ''}`} id="side-panel">
103
+ <div className="panel-header">
104
+ <span className="panel-title">{node.name} ({node.id})</span>
105
+ <button className="panel-close" onClick={close}>&times;</button>
106
+ </div>
107
+ <div className="panel-body">
108
+ <div className="panel-status-section">
109
+ <span className="panel-type-badge">{typeLabel}</span>
110
+ <span className={`panel-status-badge panel-status-${node.status || 'draft'}`}>{statusLabel}</span>
111
+ <span className="panel-completeness">{completeness}% complete</span>
112
+ </div>
113
+ {shouldShowTaskList(tasks) && (
114
+ <div className="panel-task-list">
115
+ <button
116
+ className="panel-task-list-toggle"
117
+ onClick={() => setExpandedTasks(prev => !prev)}
118
+ >
119
+ <span className={`panel-task-list-chevron${expandedTasks ? ' expanded' : ''}`}>{'\u25B6'}</span>
120
+ Tasks ({tasks!.completed}/{tasks!.total})
121
+ </button>
122
+ {expandedTasks && tasks!.items?.length > 0 && (
123
+ <div className="panel-task-list-items">
124
+ {tasks!.items.map((task: any, idx: number) => (
125
+ <div key={idx} className={getTaskCssClass(task.status)}>
126
+ <span className="kanban-task-icon">
127
+ {getTaskIcon(task.status)}
128
+ </span>
129
+ <span className="kanban-task-name">{task.name}</span>
130
+ </div>
131
+ ))}
132
+ </div>
133
+ )}
134
+ </div>
135
+ )}
136
+ <div dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} />
137
+ {workSummaries.length > 0 && (
138
+ <div className="panel-work-summary">
139
+ <button
140
+ className="panel-work-summary-toggle"
141
+ onClick={() => setExpandedSummaries(prev => !prev)}
142
+ >
143
+ {expandedSummaries ? '\u25BC' : '\u25B6'} Work Summary ({workSummaries.length} cycle{workSummaries.length !== 1 ? 's' : ''})
144
+ </button>
145
+ {expandedSummaries && (
146
+ <div className="panel-work-summary-details">
147
+ {workSummaries.map((ws, idx) => (
148
+ <div key={idx} className="panel-work-summary-cycle">
149
+ <div className="panel-work-summary-cycle-header">Cycle {ws.cycle}</div>
150
+ {ws.filesCreated?.length > 0 && (
151
+ <div className="panel-work-summary-files panel-work-summary-files-created">
152
+ <span className="panel-work-summary-label">Created:</span>
153
+ <ul>
154
+ {ws.filesCreated.map((f, fi) => (
155
+ <li key={fi}>{f}</li>
156
+ ))}
157
+ </ul>
158
+ </div>
159
+ )}
160
+ {ws.filesUpdated?.length > 0 && (
161
+ <div className="panel-work-summary-files panel-work-summary-files-updated">
162
+ <span className="panel-work-summary-label">Updated:</span>
163
+ <ul>
164
+ {ws.filesUpdated.map((f, fi) => (
165
+ <li key={fi}>{f}</li>
166
+ ))}
167
+ </ul>
168
+ </div>
169
+ )}
170
+ {ws.filesDeleted?.length > 0 && (
171
+ <div className="panel-work-summary-files panel-work-summary-files-deleted">
172
+ <span className="panel-work-summary-label">Deleted:</span>
173
+ <ul>
174
+ {ws.filesDeleted.map((f, fi) => (
175
+ <li key={fi}>{f}</li>
176
+ ))}
177
+ </ul>
178
+ </div>
179
+ )}
180
+ {ws.approach && (
181
+ <div className="panel-work-summary-approach">
182
+ <span className="panel-work-summary-label">Approach:</span>
183
+ <p>{ws.approach}</p>
184
+ </div>
185
+ )}
186
+ {ws.decisions?.length > 0 && (
187
+ <div className="panel-work-summary-decisions">
188
+ <span className="panel-work-summary-label">Decisions:</span>
189
+ <ul>
190
+ {ws.decisions.map((d, di) => (
191
+ <li key={di}>{d}</li>
192
+ ))}
193
+ </ul>
194
+ </div>
195
+ )}
196
+ </div>
197
+ ))}
198
+ </div>
199
+ )}
200
+ </div>
201
+ )}
202
+ {edges.length > 0 && (
203
+ <div className="panel-edges-section">
204
+ <h3>Relationships</h3>
205
+ <ul className="panel-edge-list">
206
+ {edges.map((edge, i) => {
207
+ const neighborNode = graphData?.nodes.find((n: any) => n.id === edge.neighborId);
208
+ const name = neighborNode ? neighborNode.name : edge.neighborId;
209
+ const direction = edge.direction === 'outgoing' ? '\u2192' : '\u2190';
210
+ return (
211
+ <li key={i} className="panel-edge-item">
212
+ <span className="panel-edge-direction">{direction}</span>
213
+ <span className="panel-edge-relation">{edge.relation.replace(/_/g, ' ')}</span>
214
+ <a className="panel-edge-link" href="#" onClick={(e) => { e.preventDefault(); onEdgeClick(edge.neighborId); }}>
215
+ {name}
216
+ </a>
217
+ <span className="panel-edge-id">{edge.neighborId}</span>
218
+ </li>
219
+ );
220
+ })}
221
+ </ul>
222
+ </div>
223
+ )}
224
+ </div>
225
+ </div>
226
+ );
227
+ }
228
+
229
+ // Export the static methods for imperative use
230
+ export const openSidePanel = (node: any) => (SidePanel as any).__open?.(node);
231
+ export const closeSidePanel = () => (SidePanel as any).__close?.();