jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,317 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useCallback } from 'react';
4
+ import { openDialog } from '@/lib/tauri';
5
+ import { dataBridge } from '@/lib/data-bridge';
6
+ import type { ContextDocument } from '@/lib/data-bridge';
7
+
8
+ interface ContextDocumentsSectionProps {
9
+ initialDocuments: ContextDocument[];
10
+ }
11
+
12
+ export function ContextDocumentsSection({ initialDocuments }: ContextDocumentsSectionProps) {
13
+ const [documents, setDocuments] = useState<ContextDocument[]>(initialDocuments);
14
+ const [dropdownOpen, setDropdownOpen] = useState(false);
15
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
16
+ const [editingDoc, setEditingDoc] = useState<{ name: string; content: string }>({ name: '', content: '' });
17
+ const [isAdding, setIsAdding] = useState(false);
18
+ const [dragOver, setDragOver] = useState(false);
19
+ const [validationError, setValidationError] = useState<string | null>(null);
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+ const dropdownRef = useRef<HTMLDivElement>(null);
22
+
23
+ const handleAddText = () => {
24
+ setDropdownOpen(false);
25
+ setIsAdding(true);
26
+ setEditingDoc({ name: '', content: '' });
27
+ setValidationError(null);
28
+ };
29
+
30
+ const handleAddFile = async () => {
31
+ setDropdownOpen(false);
32
+ const selected = await openDialog({ multiple: true, title: 'Select context files' });
33
+ if (!selected) return;
34
+ const paths = Array.isArray(selected) ? selected : [selected];
35
+ for (const filePath of paths) {
36
+ if (typeof filePath === 'string') {
37
+ const name = filePath.split('/').pop() || filePath;
38
+ // Skip duplicates by name
39
+ if (documents.some(d => d.name === name)) continue;
40
+ const doc: ContextDocument = { type: 'file', name, path: filePath };
41
+ await dataBridge.addContextDocument(doc);
42
+ setDocuments(prev => [...prev, doc]);
43
+ }
44
+ }
45
+ };
46
+
47
+ const handleSaveNew = async () => {
48
+ if (!editingDoc.name.trim()) {
49
+ setValidationError('Name is required');
50
+ return;
51
+ }
52
+ if (!editingDoc.content.trim()) {
53
+ setValidationError('Content is required');
54
+ return;
55
+ }
56
+ setValidationError(null);
57
+ const doc: ContextDocument = { type: 'text', name: editingDoc.name.trim(), content: editingDoc.content };
58
+ await dataBridge.addContextDocument(doc);
59
+ setDocuments(prev => [...prev, doc]);
60
+ setIsAdding(false);
61
+ setEditingDoc({ name: '', content: '' });
62
+ };
63
+
64
+ const handleCancelNew = () => {
65
+ setIsAdding(false);
66
+ setEditingDoc({ name: '', content: '' });
67
+ setValidationError(null);
68
+ };
69
+
70
+ const handleEdit = (index: number) => {
71
+ const doc = documents[index];
72
+ setEditingIndex(index);
73
+ setEditingDoc({ name: doc.name, content: doc.content || '' });
74
+ setValidationError(null);
75
+ };
76
+
77
+ const handleSaveEdit = async () => {
78
+ if (editingIndex === null) return;
79
+ if (!editingDoc.name.trim()) {
80
+ setValidationError('Name is required');
81
+ return;
82
+ }
83
+ if (!editingDoc.content.trim()) {
84
+ setValidationError('Content is required');
85
+ return;
86
+ }
87
+ setValidationError(null);
88
+ const doc: ContextDocument = { type: 'text', name: editingDoc.name.trim(), content: editingDoc.content };
89
+ await dataBridge.updateContextDocument(editingIndex, doc);
90
+ setDocuments(prev => prev.map((d, i) => i === editingIndex ? doc : d));
91
+ setEditingIndex(null);
92
+ setEditingDoc({ name: '', content: '' });
93
+ };
94
+
95
+ const handleCancelEdit = () => {
96
+ setEditingIndex(null);
97
+ setEditingDoc({ name: '', content: '' });
98
+ setValidationError(null);
99
+ };
100
+
101
+ const handleRemove = async (index: number) => {
102
+ await dataBridge.removeContextDocument(index);
103
+ setDocuments(prev => prev.filter((_, i) => i !== index));
104
+ };
105
+
106
+ const handleDragOver = useCallback((e: React.DragEvent) => {
107
+ e.preventDefault();
108
+ setDragOver(true);
109
+ }, []);
110
+
111
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
112
+ if (containerRef.current && !containerRef.current.contains(e.relatedTarget as Node)) {
113
+ setDragOver(false);
114
+ }
115
+ }, []);
116
+
117
+ const handleDrop = useCallback(async (e: React.DragEvent) => {
118
+ e.preventDefault();
119
+ setDragOver(false);
120
+ const files = Array.from(e.dataTransfer.files);
121
+ for (const file of files) {
122
+ const filePath = (file as unknown as { path?: string }).path || file.name;
123
+ const name = file.name;
124
+ // Skip duplicates by name
125
+ if (documents.some(d => d.name === name)) continue;
126
+ const doc: ContextDocument = { type: 'file', name, path: filePath };
127
+ await dataBridge.addContextDocument(doc);
128
+ setDocuments(prev => [...prev, doc]);
129
+ }
130
+ }, [documents]);
131
+
132
+ return (
133
+ <div className="mt-8">
134
+ <div className="flex items-center justify-between mb-3">
135
+ <h3 className="text-[15px] font-medium text-zinc-900 dark:text-zinc-100">
136
+ Context Documents
137
+ </h3>
138
+ <div className="relative" ref={dropdownRef}>
139
+ <button
140
+ onClick={() => setDropdownOpen(!dropdownOpen)}
141
+ className="w-8 h-8 rounded-lg border-2 border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500 text-lg flex items-center justify-center transition-all duration-200 hover:border-[#819D9F] hover:text-zinc-900 dark:hover:text-zinc-100 active:scale-95"
142
+ >
143
+ +
144
+ </button>
145
+ {dropdownOpen && (
146
+ <>
147
+ <div className="fixed inset-0 z-10" onClick={() => setDropdownOpen(false)} />
148
+ <div className="absolute top-[calc(100%+6px)] right-0 z-20 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl p-1.5 min-w-[220px] shadow-lg">
149
+ <button
150
+ onClick={handleAddFile}
151
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg w-full text-left text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
152
+ >
153
+ <svg className="w-[18px] h-[18px] text-zinc-400" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
154
+ <path d="M10 4v12M15 9.5c0 2.5-2.5 4.5-5 4.5S5 12 5 9.5V6a3 3 0 016 0v4a1.5 1.5 0 01-3 0V6.5" />
155
+ </svg>
156
+ Upload from device
157
+ </button>
158
+ <button
159
+ onClick={handleAddText}
160
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg w-full text-left text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
161
+ >
162
+ <svg className="w-[18px] h-[18px] text-zinc-400" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
163
+ <path d="M4 5h12M4 10h8M4 15h5" />
164
+ <path d="M15 12v6M12 15h6" />
165
+ </svg>
166
+ Add text content
167
+ </button>
168
+ </div>
169
+ </>
170
+ )}
171
+ </div>
172
+ </div>
173
+
174
+ <div
175
+ ref={containerRef}
176
+ onDragOver={handleDragOver}
177
+ onDragLeave={handleDragLeave}
178
+ onDrop={handleDrop}
179
+ className={`bg-zinc-50 dark:bg-zinc-800/50 rounded-xl border-2 transition-all duration-200 overflow-hidden ${
180
+ dragOver
181
+ ? 'border-[#819D9F] shadow-[0_0_0_3px_rgba(129,157,159,0.15)]'
182
+ : 'border-zinc-200 dark:border-zinc-700'
183
+ }`}
184
+ >
185
+ {documents.length === 0 && !isAdding ? (
186
+ <div className="py-10 text-center text-zinc-400 dark:text-zinc-500 text-sm">
187
+ Drop files here or use + to add context
188
+ </div>
189
+ ) : (
190
+ <>
191
+ {documents.map((doc, index) => (
192
+ editingIndex === index ? (
193
+ <div key={index} className="bg-zinc-100 dark:bg-zinc-900/50 border-b border-zinc-200/50 dark:border-zinc-700/40">
194
+ <div className="flex items-center gap-2.5 px-3.5 pt-3">
195
+ <Grip />
196
+ <TypeIcon type={doc.type} />
197
+ <input
198
+ type="text"
199
+ value={editingDoc.name}
200
+ onChange={e => setEditingDoc(prev => ({ ...prev, name: e.target.value }))}
201
+ placeholder="Name this snippet..."
202
+ className="flex-1 bg-transparent border-none text-[13px] font-medium text-zinc-900 dark:text-zinc-100 outline-none"
203
+ />
204
+ </div>
205
+ <div className="px-3.5 mt-2">
206
+ <textarea
207
+ value={editingDoc.content}
208
+ onChange={e => setEditingDoc(prev => ({ ...prev, content: e.target.value }))}
209
+ placeholder="Type or paste your context text here..."
210
+ className="w-full bg-white dark:bg-zinc-800 border-2 border-zinc-200 dark:border-zinc-700 rounded-lg px-3 py-2.5 text-[13px] text-zinc-700 dark:text-zinc-200 leading-relaxed resize-y min-h-[72px] focus:outline-none focus:border-[#819D9F] focus:shadow-[0_0_0_3px_rgba(129,157,159,0.15)] transition-all"
211
+ rows={3}
212
+ />
213
+ </div>
214
+ <div className="flex items-center justify-end gap-2 px-3.5 py-2">
215
+ {validationError && (
216
+ <span className="text-[12px] text-red-500 dark:text-red-400 mr-auto">{validationError}</span>
217
+ )}
218
+ <button onClick={handleCancelEdit} className="px-3.5 py-1.5 rounded-lg border-2 border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-500 text-[13px] font-medium hover:border-zinc-300 dark:hover:border-zinc-600 hover:text-zinc-700 dark:hover:text-zinc-300 transition-all">Cancel</button>
219
+ <button onClick={handleSaveEdit} className="px-3.5 py-1.5 rounded-lg border-none bg-[#819D9F] text-zinc-900 text-[13px] font-medium hover:brightness-105 transition-all active:scale-[0.98]">Save</button>
220
+ </div>
221
+ </div>
222
+ ) : (
223
+ <div
224
+ key={index}
225
+ className="group flex items-center px-3.5 py-2.5 border-b border-zinc-200/50 dark:border-zinc-700/40 last:border-b-0 hover:bg-white/50 dark:hover:bg-white/[0.02] transition-colors"
226
+ >
227
+ <Grip />
228
+ <TypeIcon type={doc.type} />
229
+ <div className="flex-1 min-w-0 ml-2.5">
230
+ <div className="text-[13px] font-medium text-zinc-900 dark:text-zinc-100 truncate">{doc.name}</div>
231
+ <div className="text-[11px] text-zinc-400 dark:text-zinc-500 truncate font-mono mt-0.5">
232
+ {doc.type === 'file' ? doc.path : (doc.content || '').slice(0, 80) + ((doc.content || '').length > 80 ? '...' : '')}
233
+ </div>
234
+ </div>
235
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
236
+ {doc.type === 'text' && (
237
+ <button onClick={() => handleEdit(index)} className="w-[26px] h-[26px] rounded-md flex items-center justify-center text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-200 transition-all text-[13px]" title="Edit">
238
+
239
+ </button>
240
+ )}
241
+ <button onClick={() => handleRemove(index)} className="w-[26px] h-[26px] rounded-md flex items-center justify-center text-zinc-400 hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 transition-all text-[13px]" title="Remove">
242
+ ×
243
+ </button>
244
+ </div>
245
+ </div>
246
+ )
247
+ ))}
248
+
249
+ {isAdding && (
250
+ <div className="bg-zinc-100 dark:bg-zinc-900/50 border-b border-zinc-200/50 dark:border-zinc-700/40">
251
+ <div className="flex items-center gap-2.5 px-3.5 pt-3">
252
+ <Grip />
253
+ <TypeIcon type="text" />
254
+ <input
255
+ type="text"
256
+ value={editingDoc.name}
257
+ onChange={e => setEditingDoc(prev => ({ ...prev, name: e.target.value }))}
258
+ placeholder="Name this snippet..."
259
+ className="flex-1 bg-transparent border-none text-[13px] font-medium text-zinc-900 dark:text-zinc-100 outline-none"
260
+ autoFocus
261
+ />
262
+ </div>
263
+ <div className="px-3.5 mt-2">
264
+ <textarea
265
+ value={editingDoc.content}
266
+ onChange={e => setEditingDoc(prev => ({ ...prev, content: e.target.value }))}
267
+ placeholder="Type or paste your context text here..."
268
+ className="w-full bg-white dark:bg-zinc-800 border-2 border-zinc-200 dark:border-zinc-700 rounded-lg px-3 py-2.5 text-[13px] text-zinc-700 dark:text-zinc-200 leading-relaxed resize-y min-h-[72px] focus:outline-none focus:border-[#819D9F] focus:shadow-[0_0_0_3px_rgba(129,157,159,0.15)] transition-all"
269
+ rows={3}
270
+ />
271
+ </div>
272
+ <div className="flex items-center justify-end gap-2 px-3.5 py-2">
273
+ {validationError && (
274
+ <span className="text-[12px] text-red-500 dark:text-red-400 mr-auto">{validationError}</span>
275
+ )}
276
+ <button onClick={handleCancelNew} className="px-3.5 py-1.5 rounded-lg border-2 border-zinc-200 dark:border-zinc-700 bg-transparent text-zinc-500 text-[13px] font-medium hover:border-zinc-300 dark:hover:border-zinc-600 hover:text-zinc-700 dark:hover:text-zinc-300 transition-all">Cancel</button>
277
+ <button onClick={handleSaveNew} className="px-3.5 py-1.5 rounded-lg border-none bg-[#819D9F] text-zinc-900 text-[13px] font-medium hover:brightness-105 transition-all active:scale-[0.98]">Save</button>
278
+ </div>
279
+ </div>
280
+ )}
281
+ </>
282
+ )}
283
+
284
+ {dragOver && (
285
+ <div className="py-5 text-center border-t border-dashed border-zinc-300 dark:border-zinc-600">
286
+ <span className="text-[13px] font-medium text-[#819D9F]">Drop files to add as context</span>
287
+ </div>
288
+ )}
289
+ </div>
290
+ </div>
291
+ );
292
+ }
293
+
294
+ function Grip() {
295
+ return (
296
+ <div className="flex flex-col gap-[1.5px] cursor-grab opacity-20 hover:opacity-60 transition-opacity p-1">
297
+ <span className="block w-2.5 h-[1.5px] bg-zinc-500 rounded-sm" />
298
+ <span className="block w-2.5 h-[1.5px] bg-zinc-500 rounded-sm" />
299
+ <span className="block w-2.5 h-[1.5px] bg-zinc-500 rounded-sm" />
300
+ </div>
301
+ );
302
+ }
303
+
304
+ function TypeIcon({ type }: { type: 'file' | 'text' }) {
305
+ if (type === 'file') {
306
+ return (
307
+ <div className="w-[26px] h-[26px] rounded-md flex items-center justify-center text-[11px] font-bold shrink-0 bg-[#819D9F]/15 text-[#819D9F]">
308
+ F
309
+ </div>
310
+ );
311
+ }
312
+ return (
313
+ <div className="w-[26px] h-[26px] rounded-md flex items-center justify-center text-[11px] font-bold shrink-0 bg-[#e57a44]/12 text-[#e57a44]">
314
+ T
315
+ </div>
316
+ );
317
+ }