claudeship 0.2.12 → 0.2.15

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 (128) hide show
  1. package/README.md +18 -0
  2. package/apps/server/dist/app.module.js +10 -0
  3. package/apps/server/dist/app.module.js.map +1 -1
  4. package/apps/server/dist/chat/prompts/fullstack-express-prompt.d.ts +1 -1
  5. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js +109 -1
  6. package/apps/server/dist/chat/prompts/fullstack-express-prompt.js.map +1 -1
  7. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.d.ts +1 -1
  8. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js +109 -1
  9. package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js.map +1 -1
  10. package/apps/server/dist/chat/prompts/web-system-prompt.d.ts +1 -1
  11. package/apps/server/dist/chat/prompts/web-system-prompt.js +156 -0
  12. package/apps/server/dist/chat/prompts/web-system-prompt.js.map +1 -1
  13. package/apps/server/dist/checkpoint/checkpoint.controller.d.ts +19 -0
  14. package/apps/server/dist/checkpoint/checkpoint.controller.js +93 -0
  15. package/apps/server/dist/checkpoint/checkpoint.controller.js.map +1 -0
  16. package/apps/server/dist/checkpoint/checkpoint.module.d.ts +2 -0
  17. package/apps/server/dist/checkpoint/checkpoint.module.js +25 -0
  18. package/apps/server/dist/checkpoint/checkpoint.module.js.map +1 -0
  19. package/apps/server/dist/checkpoint/checkpoint.service.d.ts +41 -0
  20. package/apps/server/dist/checkpoint/checkpoint.service.js +261 -0
  21. package/apps/server/dist/checkpoint/checkpoint.service.js.map +1 -0
  22. package/apps/server/dist/database/database.controller.d.ts +23 -0
  23. package/apps/server/dist/database/database.controller.js +109 -0
  24. package/apps/server/dist/database/database.controller.js.map +1 -0
  25. package/apps/server/dist/database/database.module.d.ts +2 -0
  26. package/apps/server/dist/database/database.module.js +25 -0
  27. package/apps/server/dist/database/database.module.js.map +1 -0
  28. package/apps/server/dist/database/database.service.d.ts +32 -0
  29. package/apps/server/dist/database/database.service.js +238 -0
  30. package/apps/server/dist/database/database.service.js.map +1 -0
  31. package/apps/server/dist/env/env.controller.d.ts +14 -0
  32. package/apps/server/dist/env/env.controller.js +84 -0
  33. package/apps/server/dist/env/env.controller.js.map +1 -0
  34. package/apps/server/dist/env/env.module.d.ts +2 -0
  35. package/apps/server/dist/env/env.module.js +25 -0
  36. package/apps/server/dist/env/env.module.js.map +1 -0
  37. package/apps/server/dist/env/env.service.d.ts +21 -0
  38. package/apps/server/dist/env/env.service.js +194 -0
  39. package/apps/server/dist/env/env.service.js.map +1 -0
  40. package/apps/server/dist/preview/preview.controller.d.ts +5 -0
  41. package/apps/server/dist/preview/preview.controller.js +41 -0
  42. package/apps/server/dist/preview/preview.controller.js.map +1 -1
  43. package/apps/server/dist/preview/preview.service.d.ts +20 -0
  44. package/apps/server/dist/preview/preview.service.js +51 -2
  45. package/apps/server/dist/preview/preview.service.js.map +1 -1
  46. package/apps/server/dist/project/project.controller.d.ts +10 -1
  47. package/apps/server/dist/project/project.controller.js +57 -0
  48. package/apps/server/dist/project/project.controller.js.map +1 -1
  49. package/apps/server/dist/project/project.service.d.ts +15 -0
  50. package/apps/server/dist/project/project.service.js +111 -0
  51. package/apps/server/dist/project/project.service.js.map +1 -1
  52. package/apps/server/dist/project-context/project-context.controller.d.ts +42 -0
  53. package/apps/server/dist/project-context/project-context.controller.js +127 -0
  54. package/apps/server/dist/project-context/project-context.controller.js.map +1 -0
  55. package/apps/server/dist/project-context/project-context.module.d.ts +2 -0
  56. package/apps/server/dist/project-context/project-context.module.js +25 -0
  57. package/apps/server/dist/project-context/project-context.module.js.map +1 -0
  58. package/apps/server/dist/project-context/project-context.service.d.ts +36 -0
  59. package/apps/server/dist/project-context/project-context.service.js +260 -0
  60. package/apps/server/dist/project-context/project-context.service.js.map +1 -0
  61. package/apps/server/dist/testing/testing.controller.d.ts +24 -0
  62. package/apps/server/dist/testing/testing.controller.js +126 -0
  63. package/apps/server/dist/testing/testing.controller.js.map +1 -0
  64. package/apps/server/dist/testing/testing.module.d.ts +2 -0
  65. package/apps/server/dist/testing/testing.module.js +26 -0
  66. package/apps/server/dist/testing/testing.module.js.map +1 -0
  67. package/apps/server/dist/testing/testing.service.d.ts +62 -0
  68. package/apps/server/dist/testing/testing.service.js +269 -0
  69. package/apps/server/dist/testing/testing.service.js.map +1 -0
  70. package/apps/server/dist/tsconfig.tsbuildinfo +1 -1
  71. package/apps/server/package.json +1 -1
  72. package/apps/web/.next/BUILD_ID +1 -1
  73. package/apps/web/.next/app-build-manifest.json +5 -5
  74. package/apps/web/.next/build-manifest.json +2 -2
  75. package/apps/web/.next/cache/.previewinfo +1 -1
  76. package/apps/web/.next/cache/.rscinfo +1 -1
  77. package/apps/web/.next/cache/.tsbuildinfo +1 -1
  78. package/apps/web/.next/cache/config.json +3 -3
  79. package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
  80. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  81. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  82. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  83. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  84. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  85. package/apps/web/.next/prerender-manifest.json +10 -10
  86. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  87. package/apps/web/.next/server/app/_not-found.html +1 -1
  88. package/apps/web/.next/server/app/_not-found.rsc +2 -2
  89. package/apps/web/.next/server/app/index.html +1 -1
  90. package/apps/web/.next/server/app/index.rsc +3 -3
  91. package/apps/web/.next/server/app/page.js +2 -2
  92. package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/apps/web/.next/server/app/project/[id]/page.js +2 -2
  94. package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  95. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  96. package/apps/web/.next/server/app/settings.html +1 -1
  97. package/apps/web/.next/server/app/settings.rsc +3 -3
  98. package/apps/web/.next/server/chunks/392.js +1 -1
  99. package/apps/web/.next/server/pages/404.html +1 -1
  100. package/apps/web/.next/server/pages/500.html +1 -1
  101. package/apps/web/.next/server/server-reference-manifest.json +1 -1
  102. package/apps/web/.next/static/chunks/574-1fe2bcd6cfb41646.js +1 -0
  103. package/apps/web/.next/static/chunks/app/page-f19cfa58541ca83d.js +1 -0
  104. package/apps/web/.next/static/chunks/app/project/[id]/page-dffaa1d02f012216.js +1 -0
  105. package/apps/web/.next/static/chunks/app/settings/page-d1318c2fd58729a5.js +1 -0
  106. package/apps/web/.next/static/css/0a24552d9794f8c8.css +3 -0
  107. package/apps/web/.next/trace +18 -17
  108. package/apps/web/node_modules/.bin/eslint +2 -2
  109. package/apps/web/package.json +2 -1
  110. package/apps/web/src/components/checkpoint/CheckpointPanel.tsx +384 -0
  111. package/apps/web/src/components/database/DatabasePanel.tsx +405 -0
  112. package/apps/web/src/components/env/EnvPanel.tsx +356 -0
  113. package/apps/web/src/components/preview/ConsoleViewer.tsx +270 -0
  114. package/apps/web/src/components/preview/ErrorOverlay.tsx +189 -0
  115. package/apps/web/src/components/preview/PreviewPanel.tsx +148 -6
  116. package/apps/web/src/components/testing/TestRunner.tsx +481 -0
  117. package/apps/web/src/components/ui/tabs.tsx +55 -0
  118. package/apps/web/src/components/visual-editor/VisualEditor.tsx +382 -0
  119. package/apps/web/src/components/workspace/WorkspaceLayout.tsx +66 -4
  120. package/apps/web/src/lib/api.ts +5 -2
  121. package/package.json +1 -1
  122. package/apps/web/.next/static/chunks/298-6f3d6b321c288cd3.js +0 -1
  123. package/apps/web/.next/static/chunks/app/page-3d093f7f480a8599.js +0 -1
  124. package/apps/web/.next/static/chunks/app/project/[id]/page-e5cda6f9050b0a52.js +0 -1
  125. package/apps/web/.next/static/chunks/app/settings/page-92d28565c3d8c755.js +0 -1
  126. package/apps/web/.next/static/css/8f946046a2047594.css +0 -3
  127. /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_buildManifest.js +0 -0
  128. /package/apps/web/.next/static/{aXT20mSdxaem1-z8VH2F1 → mkY_TTl_ho_ehDKiX10AN}/_ssgManifest.js +0 -0
@@ -6,9 +6,9 @@ case `uname` in
6
6
  esac
7
7
 
8
8
  if [ -z "$NODE_PATH" ]; then
9
- export NODE_PATH="/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules/eslint/bin/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules/eslint/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/node_modules"
9
+ export NODE_PATH="/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules/eslint/bin/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules/eslint/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/node_modules"
10
10
  else
11
- export NODE_PATH="/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules/eslint/bin/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules/eslint/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@2.6.1/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/node_modules:$NODE_PATH"
11
+ export NODE_PATH="/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules/eslint/bin/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules/eslint/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/eslint@9.39.2_jiti@1.21.7/node_modules:/home/runner/work/ClaudeShip/ClaudeShip/node_modules/.pnpm/node_modules:$NODE_PATH"
12
12
  fi
13
13
  if [ -x "$basedir/node" ]; then
14
14
  exec "$basedir/node" "$basedir/../eslint/bin/eslint.js" "$@"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudeship/web",
3
- "version": "0.2.12",
3
+ "version": "0.2.15",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "node scripts/dev-with-recovery.mjs",
@@ -16,6 +16,7 @@
16
16
  "@radix-ui/react-checkbox": "^1.3.3",
17
17
  "@radix-ui/react-label": "^2.1.8",
18
18
  "@radix-ui/react-radio-group": "^1.3.8",
19
+ "@radix-ui/react-tabs": "^1.1.13",
19
20
  "class-variance-authority": "^0.7.1",
20
21
  "clsx": "^2.1.1",
21
22
  "lucide-react": "^0.460.0",
@@ -0,0 +1,384 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import {
5
+ GitBranch,
6
+ RotateCcw,
7
+ Plus,
8
+ RefreshCw,
9
+ AlertCircle,
10
+ Clock,
11
+ FileText,
12
+ ChevronDown,
13
+ ChevronRight,
14
+ } from "lucide-react";
15
+ import { Button } from "@/components/ui/button";
16
+ import { Input } from "@/components/ui/input";
17
+ import { api } from "@/lib/api";
18
+
19
+ interface Checkpoint {
20
+ id: string;
21
+ hash: string;
22
+ message: string;
23
+ author: string;
24
+ timestamp: number;
25
+ filesChanged: number;
26
+ insertions: number;
27
+ deletions: number;
28
+ }
29
+
30
+ interface FileDiff {
31
+ path: string;
32
+ status: "added" | "modified" | "deleted";
33
+ additions: number;
34
+ deletions: number;
35
+ diff: string;
36
+ }
37
+
38
+ interface CheckpointPanelProps {
39
+ projectId: string;
40
+ }
41
+
42
+ export function CheckpointPanel({ projectId }: CheckpointPanelProps) {
43
+ const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([]);
44
+ const [selectedCheckpoint, setSelectedCheckpoint] = useState<Checkpoint | null>(null);
45
+ const [diff, setDiff] = useState<FileDiff[] | null>(null);
46
+ const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const [error, setError] = useState<string | null>(null);
49
+ const [newMessage, setNewMessage] = useState("");
50
+ const [hasChanges, setHasChanges] = useState(false);
51
+
52
+ // Fetch checkpoints
53
+ const fetchCheckpoints = useCallback(async () => {
54
+ setIsLoading(true);
55
+ setError(null);
56
+ try {
57
+ const result = await api.get<Checkpoint[]>(
58
+ `/projects/${projectId}/checkpoints`
59
+ );
60
+ setCheckpoints(result);
61
+ } catch (e) {
62
+ setError(e instanceof Error ? e.message : "Failed to load checkpoints");
63
+ } finally {
64
+ setIsLoading(false);
65
+ }
66
+ }, [projectId]);
67
+
68
+ // Check for changes
69
+ const checkStatus = useCallback(async () => {
70
+ try {
71
+ const result = await api.get<{ hasChanges: boolean }>(
72
+ `/projects/${projectId}/checkpoints/status`
73
+ );
74
+ setHasChanges(result.hasChanges);
75
+ } catch {
76
+ // Ignore
77
+ }
78
+ }, [projectId]);
79
+
80
+ useEffect(() => {
81
+ fetchCheckpoints();
82
+ checkStatus();
83
+ }, [fetchCheckpoints, checkStatus]);
84
+
85
+ // Poll for changes
86
+ useEffect(() => {
87
+ const interval = setInterval(checkStatus, 5000);
88
+ return () => clearInterval(interval);
89
+ }, [checkStatus]);
90
+
91
+ const handleSelectCheckpoint = async (checkpoint: Checkpoint) => {
92
+ setSelectedCheckpoint(checkpoint);
93
+ setExpandedFiles(new Set());
94
+
95
+ try {
96
+ const result = await api.get<{ files: FileDiff[] }>(
97
+ `/projects/${projectId}/checkpoints/diff?from=${checkpoint.hash}`
98
+ );
99
+ setDiff(result.files);
100
+ } catch {
101
+ setDiff(null);
102
+ }
103
+ };
104
+
105
+ const handleCreateCheckpoint = async () => {
106
+ if (!newMessage.trim()) {
107
+ setError("Please enter a checkpoint message");
108
+ return;
109
+ }
110
+
111
+ setIsLoading(true);
112
+ setError(null);
113
+ try {
114
+ await api.post(`/projects/${projectId}/checkpoints`, {
115
+ message: newMessage,
116
+ });
117
+ setNewMessage("");
118
+ await fetchCheckpoints();
119
+ await checkStatus();
120
+ } catch (e) {
121
+ setError(e instanceof Error ? e.message : "Failed to create checkpoint");
122
+ } finally {
123
+ setIsLoading(false);
124
+ }
125
+ };
126
+
127
+ const handleRestore = async (checkpoint: Checkpoint) => {
128
+ if (
129
+ !confirm(
130
+ `Restore to "${checkpoint.message}"? This will discard current changes.`
131
+ )
132
+ ) {
133
+ return;
134
+ }
135
+
136
+ setIsLoading(true);
137
+ setError(null);
138
+ try {
139
+ await api.post(`/projects/${projectId}/checkpoints/${checkpoint.hash}/restore`);
140
+ await fetchCheckpoints();
141
+ await checkStatus();
142
+ setSelectedCheckpoint(null);
143
+ setDiff(null);
144
+ } catch (e) {
145
+ setError(e instanceof Error ? e.message : "Failed to restore checkpoint");
146
+ } finally {
147
+ setIsLoading(false);
148
+ }
149
+ };
150
+
151
+ const toggleFileExpand = (path: string) => {
152
+ const newExpanded = new Set(expandedFiles);
153
+ if (newExpanded.has(path)) {
154
+ newExpanded.delete(path);
155
+ } else {
156
+ newExpanded.add(path);
157
+ }
158
+ setExpandedFiles(newExpanded);
159
+ };
160
+
161
+ const formatTime = (timestamp: number) => {
162
+ const date = new Date(timestamp);
163
+ const now = new Date();
164
+ const diff = now.getTime() - date.getTime();
165
+
166
+ if (diff < 60000) return "Just now";
167
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
168
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
169
+ return date.toLocaleDateString();
170
+ };
171
+
172
+ if (isLoading && checkpoints.length === 0) {
173
+ return (
174
+ <div className="flex h-full items-center justify-center text-muted-foreground">
175
+ <RefreshCw className="h-5 w-5 animate-spin mr-2" />
176
+ Loading...
177
+ </div>
178
+ );
179
+ }
180
+
181
+ return (
182
+ <div className="flex h-full flex-col">
183
+ {/* Header */}
184
+ <div className="flex items-center justify-between border-b px-4 py-3">
185
+ <h3 className="font-medium flex items-center gap-2">
186
+ <GitBranch className="h-4 w-4" />
187
+ Checkpoints
188
+ </h3>
189
+ <Button
190
+ variant="ghost"
191
+ size="sm"
192
+ onClick={fetchCheckpoints}
193
+ disabled={isLoading}
194
+ className="h-8 w-8 p-0"
195
+ >
196
+ <RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
197
+ </Button>
198
+ </div>
199
+
200
+ {/* Error */}
201
+ {error && (
202
+ <div className="flex items-center gap-2 px-4 py-2 bg-destructive/10 text-destructive text-sm">
203
+ <AlertCircle className="h-4 w-4" />
204
+ {error}
205
+ </div>
206
+ )}
207
+
208
+ {/* Create Checkpoint */}
209
+ {hasChanges && (
210
+ <div className="border-b p-4 space-y-2">
211
+ <div className="flex gap-2">
212
+ <Input
213
+ value={newMessage}
214
+ onChange={(e) => setNewMessage(e.target.value)}
215
+ placeholder="Checkpoint message..."
216
+ className="flex-1"
217
+ onKeyDown={(e) => {
218
+ if (e.key === "Enter") handleCreateCheckpoint();
219
+ }}
220
+ />
221
+ <Button onClick={handleCreateCheckpoint} disabled={isLoading} size="sm">
222
+ <Plus className="h-4 w-4 mr-1" />
223
+ Save
224
+ </Button>
225
+ </div>
226
+ <p className="text-xs text-muted-foreground">
227
+ You have unsaved changes
228
+ </p>
229
+ </div>
230
+ )}
231
+
232
+ <div className="flex flex-1 overflow-hidden">
233
+ {/* Timeline */}
234
+ <div className="w-72 border-r overflow-auto">
235
+ <div className="p-2 space-y-1">
236
+ {/* Current state */}
237
+ <div className="flex items-start gap-3 p-3 rounded-lg bg-primary/5 border border-primary/20">
238
+ <div className="w-3 h-3 rounded-full bg-primary mt-1" />
239
+ <div className="flex-1 min-w-0">
240
+ <p className="font-medium text-sm">Current State</p>
241
+ <p className="text-xs text-muted-foreground">
242
+ {hasChanges ? "Unsaved changes" : "No changes"}
243
+ </p>
244
+ </div>
245
+ </div>
246
+
247
+ {/* Checkpoints */}
248
+ {checkpoints.map((checkpoint, i) => (
249
+ <button
250
+ key={checkpoint.hash}
251
+ onClick={() => handleSelectCheckpoint(checkpoint)}
252
+ className={`w-full flex items-start gap-3 p-3 rounded-lg text-left transition-colors ${
253
+ selectedCheckpoint?.hash === checkpoint.hash
254
+ ? "bg-muted"
255
+ : "hover:bg-muted/50"
256
+ }`}
257
+ >
258
+ <div className="relative">
259
+ <div className="w-3 h-3 rounded-full bg-muted-foreground/30 mt-1" />
260
+ {i < checkpoints.length - 1 && (
261
+ <div className="absolute top-4 left-1.5 w-px h-full bg-muted-foreground/20" />
262
+ )}
263
+ </div>
264
+ <div className="flex-1 min-w-0">
265
+ <p className="font-medium text-sm truncate">{checkpoint.message}</p>
266
+ <div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
267
+ <Clock className="h-3 w-3" />
268
+ {formatTime(checkpoint.timestamp)}
269
+ </div>
270
+ {checkpoint.filesChanged > 0 && (
271
+ <div className="flex items-center gap-2 text-xs mt-1">
272
+ <span className="text-green-500">+{checkpoint.insertions}</span>
273
+ <span className="text-red-500">-{checkpoint.deletions}</span>
274
+ <span className="text-muted-foreground">
275
+ {checkpoint.filesChanged} files
276
+ </span>
277
+ </div>
278
+ )}
279
+ </div>
280
+ </button>
281
+ ))}
282
+
283
+ {checkpoints.length === 0 && (
284
+ <p className="text-sm text-muted-foreground px-3 py-2">
285
+ No checkpoints yet
286
+ </p>
287
+ )}
288
+ </div>
289
+ </div>
290
+
291
+ {/* Diff Viewer */}
292
+ <div className="flex-1 overflow-auto">
293
+ {selectedCheckpoint && diff ? (
294
+ <div className="p-4 space-y-4">
295
+ <div className="flex items-center justify-between">
296
+ <div>
297
+ <h4 className="font-medium">{selectedCheckpoint.message}</h4>
298
+ <p className="text-sm text-muted-foreground">
299
+ {new Date(selectedCheckpoint.timestamp).toLocaleString()}
300
+ </p>
301
+ </div>
302
+ <Button
303
+ variant="outline"
304
+ size="sm"
305
+ onClick={() => handleRestore(selectedCheckpoint)}
306
+ >
307
+ <RotateCcw className="h-4 w-4 mr-1" />
308
+ Restore
309
+ </Button>
310
+ </div>
311
+
312
+ {diff.length === 0 ? (
313
+ <p className="text-sm text-muted-foreground">
314
+ No differences from current state
315
+ </p>
316
+ ) : (
317
+ <div className="space-y-2">
318
+ {diff.map((file) => (
319
+ <div key={file.path} className="border rounded-lg overflow-hidden">
320
+ <button
321
+ onClick={() => toggleFileExpand(file.path)}
322
+ className="w-full flex items-center gap-2 p-3 hover:bg-muted/50 text-left"
323
+ >
324
+ {expandedFiles.has(file.path) ? (
325
+ <ChevronDown className="h-4 w-4" />
326
+ ) : (
327
+ <ChevronRight className="h-4 w-4" />
328
+ )}
329
+ <FileText className="h-4 w-4" />
330
+ <span className="flex-1 font-mono text-sm truncate">
331
+ {file.path}
332
+ </span>
333
+ <span
334
+ className={`text-xs px-2 py-0.5 rounded ${
335
+ file.status === "added"
336
+ ? "bg-green-500/20 text-green-500"
337
+ : file.status === "deleted"
338
+ ? "bg-red-500/20 text-red-500"
339
+ : "bg-yellow-500/20 text-yellow-500"
340
+ }`}
341
+ >
342
+ {file.status}
343
+ </span>
344
+ <span className="text-xs text-green-500">+{file.additions}</span>
345
+ <span className="text-xs text-red-500">-{file.deletions}</span>
346
+ </button>
347
+ {expandedFiles.has(file.path) && (
348
+ <pre className="bg-zinc-950 p-4 text-xs font-mono overflow-x-auto max-h-96">
349
+ {file.diff.split("\n").map((line, i) => (
350
+ <div
351
+ key={i}
352
+ className={
353
+ line.startsWith("+") && !line.startsWith("+++")
354
+ ? "text-green-400 bg-green-500/10"
355
+ : line.startsWith("-") && !line.startsWith("---")
356
+ ? "text-red-400 bg-red-500/10"
357
+ : line.startsWith("@@")
358
+ ? "text-blue-400"
359
+ : "text-zinc-400"
360
+ }
361
+ >
362
+ {line}
363
+ </div>
364
+ ))}
365
+ </pre>
366
+ )}
367
+ </div>
368
+ ))}
369
+ </div>
370
+ )}
371
+ </div>
372
+ ) : (
373
+ <div className="flex h-full items-center justify-center text-muted-foreground">
374
+ <div className="text-center">
375
+ <GitBranch className="h-8 w-8 mx-auto mb-2 opacity-50" />
376
+ <p>Select a checkpoint to view changes</p>
377
+ </div>
378
+ </div>
379
+ )}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ );
384
+ }