jettypod 4.4.120 → 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 (208) hide show
  1. package/.env +2 -1
  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 +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  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 +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  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 +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -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
+ }
@@ -1,8 +1,9 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
3
+ import { invoke } from '@/lib/tauri';
4
4
  import { Input } from '@/components/ui/Input';
5
5
  import { Button } from '@/components/ui/Button';
6
+ import { dataBridge } from '@/lib/data-bridge';
6
7
 
7
8
  interface EnvVar {
8
9
  name: string;
@@ -43,22 +44,8 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
43
44
  setCurrentFile(filename);
44
45
  setErrorMessage(null);
45
46
  try {
46
- // Persist selection
47
- await fetch('/api/settings/env-vars', {
48
- method: 'POST',
49
- headers: { 'Content-Type': 'application/json' },
50
- body: JSON.stringify({ action: 'select', file: filename }),
51
- });
52
- // Load vars from selected file
53
- const res = await fetch(`/api/settings/env-vars?file=${encodeURIComponent(filename)}`);
54
- if (!res.ok) {
55
- const data = await res.json();
56
- setErrorMessage(data.error || 'Failed to load environment variables');
57
- setEnvVars([]);
58
- return;
59
- }
60
- const vars = await res.json();
61
- setEnvVars(vars);
47
+ const vars = await dataBridge.getEnvVars(filename);
48
+ setEnvVars(vars.map(v => ({ name: v.key, value: v.value })));
62
49
  } catch {
63
50
  setErrorMessage('Failed to load environment variables');
64
51
  setEnvVars([]);
@@ -67,22 +54,16 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
67
54
 
68
55
  const refreshFiles = async () => {
69
56
  try {
70
- const res = await fetch('/api/settings/env-vars?action=discover');
71
- if (res.ok) {
72
- const data = await res.json();
73
- setEnvFiles(data.files || []);
74
- // If selected file no longer exists, fall back
75
- if (currentFile && !data.files.includes(currentFile)) {
76
- const fallback = data.files[0] || null;
77
- setCurrentFile(fallback);
78
- if (fallback) {
79
- const varsRes = await fetch(`/api/settings/env-vars?file=${encodeURIComponent(fallback)}`);
80
- if (varsRes.ok) {
81
- setEnvVars(await varsRes.json());
82
- }
83
- } else {
84
- setEnvVars([]);
85
- }
57
+ const files = await dataBridge.discoverEnvFiles();
58
+ setEnvFiles(files);
59
+ if (currentFile && !files.includes(currentFile)) {
60
+ const fallback = files[0] || null;
61
+ setCurrentFile(fallback);
62
+ if (fallback) {
63
+ const vars = await dataBridge.getEnvVars(fallback);
64
+ setEnvVars(vars.map(v => ({ name: v.key, value: v.value })));
65
+ } else {
66
+ setEnvVars([]);
86
67
  }
87
68
  }
88
69
  } catch {
@@ -93,16 +74,9 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
93
74
  const handleCreateEnvFile = async () => {
94
75
  setErrorMessage(null);
95
76
  try {
96
- const res = await fetch('/api/settings/env-vars', {
97
- method: 'POST',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ action: 'create', file: '.env' }),
100
- });
101
- if (!res.ok) {
102
- setErrorMessage('Failed to create .env file');
103
- return;
104
- }
105
- setEnvFiles(['.env']);
77
+ await invoke('db_create_env_file', { filename: '.env' });
78
+ const files = await dataBridge.discoverEnvFiles();
79
+ setEnvFiles(files);
106
80
  setCurrentFile('.env');
107
81
  setEnvVars([]);
108
82
  showSuccess('.env file created');
@@ -128,16 +102,7 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
128
102
 
129
103
  setErrorMessage(null);
130
104
  try {
131
- const res = await fetch('/api/settings/env-vars', {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({ action: 'add', name: formName, value: formValue, file: currentFile }),
135
- });
136
- if (!res.ok) {
137
- const data = await res.json();
138
- setErrorMessage(data.error || 'Failed to add variable');
139
- return;
140
- }
105
+ await invoke('db_create_env_var', { file: currentFile, key: formName, value: formValue });
141
106
  setEnvVars([...envVars, { name: formName, value: formValue }]);
142
107
  setFormName('');
143
108
  setFormValue('');
@@ -152,16 +117,7 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
152
117
  if (!editingName) return;
153
118
  setErrorMessage(null);
154
119
  try {
155
- const res = await fetch('/api/settings/env-vars', {
156
- method: 'POST',
157
- headers: { 'Content-Type': 'application/json' },
158
- body: JSON.stringify({ name: editingName, value: formValue, file: currentFile }),
159
- });
160
- if (!res.ok) {
161
- const data = await res.json();
162
- setErrorMessage(data.error || 'Failed to update variable');
163
- return;
164
- }
120
+ await invoke('db_update_env_var', { file: currentFile, key: editingName, value: formValue });
165
121
  setEnvVars(envVars.map(v => v.name === editingName ? { ...v, value: formValue } : v));
166
122
  setFormValue('');
167
123
  setEditingName(null);
@@ -175,16 +131,7 @@ export function EnvVarsSection({ initialEnvVars, envFiles: initialEnvFiles, sele
175
131
  if (!deletingName) return;
176
132
  setErrorMessage(null);
177
133
  try {
178
- const res = await fetch('/api/settings/env-vars', {
179
- method: 'DELETE',
180
- headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify({ name: deletingName, file: currentFile }),
182
- });
183
- if (!res.ok) {
184
- const data = await res.json();
185
- setErrorMessage(data.error || 'Failed to delete variable');
186
- return;
187
- }
134
+ await invoke('db_delete_env_var', { file: currentFile, key: deletingName });
188
135
  setEnvVars(envVars.filter(v => v.name !== deletingName));
189
136
  setDeletingName(null);
190
137
  showSuccess('Variable deleted successfully');
@@ -1,24 +1,37 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
3
+ import { invoke } from '@/lib/tauri';
4
4
  import { Input } from '@/components/ui/Input';
5
5
  import { Button } from '@/components/ui/Button';
6
+ import { dataBridge } from '@/lib/data-bridge';
6
7
 
7
8
  interface MainBranchInfo {
8
9
  branch: string;
9
10
  source: 'configured' | 'detected';
10
11
  }
11
12
 
13
+ const CLAUDE_MODELS = [
14
+ { id: 'default', label: 'Default', description: 'Opus 4.6', value: null },
15
+ { id: 'sonnet', label: 'Sonnet', description: 'Sonnet 4.6', value: 'sonnet' },
16
+ { id: 'haiku', label: 'Haiku', description: 'Haiku 4.5', value: 'haiku' },
17
+ ];
18
+
12
19
  interface GeneralSectionProps {
13
20
  initialMainBranch: MainBranchInfo;
21
+ initialClaudeModel: string | null;
14
22
  }
15
23
 
16
- export function GeneralSection({ initialMainBranch }: GeneralSectionProps) {
24
+ export function GeneralSection({ initialMainBranch, initialClaudeModel }: GeneralSectionProps) {
17
25
  const [mainBranch, setMainBranch] = useState(initialMainBranch);
18
26
  const [inputValue, setInputValue] = useState(initialMainBranch.branch);
19
27
  const [isEditing, setIsEditing] = useState(false);
20
28
  const [successMessage, setSuccessMessage] = useState<string | null>(null);
21
29
 
30
+ // Claude model state
31
+ const [claudeModel, setClaudeModel] = useState<string | null>(initialClaudeModel);
32
+ const [selectedModel, setSelectedModel] = useState<string | null>(initialClaudeModel);
33
+ const [isEditingModel, setIsEditingModel] = useState(false);
34
+
22
35
  const showSuccess = (message: string) => {
23
36
  setSuccessMessage(message);
24
37
  setTimeout(() => setSuccessMessage(null), 3000);
@@ -28,19 +41,16 @@ export function GeneralSection({ initialMainBranch }: GeneralSectionProps) {
28
41
  const trimmed = inputValue.trim();
29
42
  const value = trimmed === '' ? null : trimmed;
30
43
 
31
- const res = await fetch('/api/settings/general', {
32
- method: 'POST',
33
- headers: { 'Content-Type': 'application/json' },
34
- body: JSON.stringify({ mainBranch: value }),
35
- });
36
-
37
- if (res.ok) {
38
- const data = await res.json();
39
- setMainBranch(data.mainBranch);
40
- setInputValue(data.mainBranch.branch);
41
- setIsEditing(false);
42
- showSuccess(value ? 'Main branch updated' : 'Reset to auto-detect');
44
+ if (value) {
45
+ await invoke('db_set_main_branch', { branch: value });
46
+ } else {
47
+ await invoke('db_reset_main_branch');
43
48
  }
49
+ const branch = await dataBridge.getMainBranch();
50
+ setMainBranch({ branch, source: value ? 'configured' : 'detected' });
51
+ setInputValue(branch);
52
+ setIsEditing(false);
53
+ showSuccess(value ? 'Main branch updated' : 'Reset to auto-detect');
44
54
  };
45
55
 
46
56
  const handleCancel = () => {
@@ -49,21 +59,42 @@ export function GeneralSection({ initialMainBranch }: GeneralSectionProps) {
49
59
  };
50
60
 
51
61
  const handleReset = async () => {
52
- const res = await fetch('/api/settings/general', {
53
- method: 'POST',
54
- headers: { 'Content-Type': 'application/json' },
55
- body: JSON.stringify({ mainBranch: null }),
56
- });
57
-
58
- if (res.ok) {
59
- const data = await res.json();
60
- setMainBranch(data.mainBranch);
61
- setInputValue(data.mainBranch.branch);
62
- setIsEditing(false);
63
- showSuccess('Reset to auto-detect');
62
+ await invoke('db_reset_main_branch');
63
+ const branch = await dataBridge.getMainBranch();
64
+ setMainBranch({ branch, source: 'detected' });
65
+ setInputValue(branch);
66
+ setIsEditing(false);
67
+ showSuccess('Reset to auto-detect');
68
+ };
69
+
70
+ // Claude model handlers
71
+ const handleModelSave = async () => {
72
+ if (selectedModel) {
73
+ await dataBridge.setClaudeModel(selectedModel);
74
+ } else {
75
+ await dataBridge.resetClaudeModel();
64
76
  }
77
+ setClaudeModel(selectedModel);
78
+ setIsEditingModel(false);
79
+ const modelLabel = CLAUDE_MODELS.find(m => m.value === selectedModel)?.label || 'Default';
80
+ showSuccess(`Model set to ${modelLabel}`);
65
81
  };
66
82
 
83
+ const handleModelCancel = () => {
84
+ setSelectedModel(claudeModel);
85
+ setIsEditingModel(false);
86
+ };
87
+
88
+ const handleModelReset = async () => {
89
+ await dataBridge.resetClaudeModel();
90
+ setClaudeModel(null);
91
+ setSelectedModel(null);
92
+ setIsEditingModel(false);
93
+ showSuccess('Model reset to default');
94
+ };
95
+
96
+ const currentModel = CLAUDE_MODELS.find(m => m.value === claudeModel) || CLAUDE_MODELS[0];
97
+
67
98
  return (
68
99
  <section id="general">
69
100
  <div className="flex items-center justify-between mb-6">
@@ -136,6 +167,86 @@ export function GeneralSection({ initialMainBranch }: GeneralSectionProps) {
136
167
  </div>
137
168
  )}
138
169
  </div>
170
+
171
+ {/* Claude Code Model Setting */}
172
+ <div className="p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg mt-4">
173
+ <div className="flex items-center justify-between">
174
+ <div>
175
+ <label className="block text-base font-medium text-zinc-900 dark:text-zinc-100">
176
+ Claude Code Model
177
+ </label>
178
+ <p className="text-base text-zinc-500 dark:text-zinc-400 mt-1">
179
+ The model used for Claude Code sessions in this project.
180
+ </p>
181
+ </div>
182
+ {!isEditingModel && (
183
+ <div className="flex items-center gap-3">
184
+ <span className="text-base text-zinc-900 dark:text-zinc-100">
185
+ {currentModel.label}
186
+ </span>
187
+ <span className="px-2 py-1 text-xs rounded bg-zinc-200 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400">
188
+ {claudeModel ? 'configured' : 'default'}
189
+ </span>
190
+ <Button
191
+ onClick={() => {
192
+ setSelectedModel(claudeModel);
193
+ setIsEditingModel(true);
194
+ }}
195
+ variant="ghost"
196
+ size="sm"
197
+ >
198
+ Edit
199
+ </Button>
200
+ </div>
201
+ )}
202
+ </div>
203
+
204
+ {isEditingModel && (
205
+ <div className="mt-4 space-y-4">
206
+ <div className="space-y-2">
207
+ {CLAUDE_MODELS.map((model) => (
208
+ <label
209
+ key={model.id}
210
+ className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors duration-200 ${
211
+ selectedModel === model.value
212
+ ? 'bg-zinc-200 dark:bg-zinc-700'
213
+ : 'hover:bg-zinc-100 dark:hover:bg-zinc-700/50'
214
+ }`}
215
+ >
216
+ <input
217
+ type="radio"
218
+ name="claude-model"
219
+ checked={selectedModel === model.value}
220
+ onChange={() => setSelectedModel(model.value)}
221
+ className="w-4 h-4 accent-[#819D9F]"
222
+ />
223
+ <div>
224
+ <span className="text-base font-medium text-zinc-900 dark:text-zinc-100">
225
+ {model.label}
226
+ </span>
227
+ <span className="text-sm text-zinc-500 dark:text-zinc-400 ml-2">
228
+ {model.description}
229
+ </span>
230
+ </div>
231
+ </label>
232
+ ))}
233
+ </div>
234
+ <div className="flex gap-3">
235
+ <Button onClick={handleModelSave} size="sm">
236
+ Save
237
+ </Button>
238
+ {claudeModel && (
239
+ <Button onClick={handleModelReset} variant="ghost" size="sm">
240
+ Reset to Default
241
+ </Button>
242
+ )}
243
+ <Button onClick={handleModelCancel} variant="ghost" size="sm">
244
+ Cancel
245
+ </Button>
246
+ </div>
247
+ </div>
248
+ )}
249
+ </div>
139
250
  </section>
140
251
  );
141
252
  }