create-stylus-ide 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 (135) hide show
  1. package/Readme.MD +1515 -0
  2. package/cli.js +28 -0
  3. package/frontend/.vscode/settings.json +9 -0
  4. package/frontend/app/api/chat/route.ts +101 -0
  5. package/frontend/app/api/check-setup/route.ts +93 -0
  6. package/frontend/app/api/cleanup/route.ts +14 -0
  7. package/frontend/app/api/compile/route.ts +95 -0
  8. package/frontend/app/api/compile-stream/route.ts +98 -0
  9. package/frontend/app/api/complete/route.ts +86 -0
  10. package/frontend/app/api/deploy/route.ts +118 -0
  11. package/frontend/app/api/export-abi/route.ts +58 -0
  12. package/frontend/app/favicon.ico +0 -0
  13. package/frontend/app/globals.css +177 -0
  14. package/frontend/app/layout.tsx +29 -0
  15. package/frontend/app/ml/page.tsx +694 -0
  16. package/frontend/app/page.tsx +1132 -0
  17. package/frontend/app/providers.tsx +18 -0
  18. package/frontend/app/qlearning/page.tsx +188 -0
  19. package/frontend/app/raytracing/page.tsx +268 -0
  20. package/frontend/components/abi/ABIDialog.tsx +132 -0
  21. package/frontend/components/ai/AICompletionPopup.tsx +76 -0
  22. package/frontend/components/ai/ChatPanel.tsx +292 -0
  23. package/frontend/components/ai/QuickActions.tsx +128 -0
  24. package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
  25. package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
  26. package/frontend/components/deploy/DeployDialog.tsx +334 -0
  27. package/frontend/components/editor/FileTabs.tsx +181 -0
  28. package/frontend/components/editor/MonacoEditor.tsx +306 -0
  29. package/frontend/components/file-tree/ContextMenu.tsx +110 -0
  30. package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
  31. package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
  32. package/frontend/components/file-tree/FileNode.tsx +60 -0
  33. package/frontend/components/file-tree/FileTree.tsx +259 -0
  34. package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
  35. package/frontend/components/file-tree/FolderNode.tsx +105 -0
  36. package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
  37. package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
  38. package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
  39. package/frontend/components/github/URLCopyButton.tsx +60 -0
  40. package/frontend/components/interact/ContractInteraction.tsx +323 -0
  41. package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
  42. package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
  43. package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
  44. package/frontend/components/project/ProjectActions.tsx +176 -0
  45. package/frontend/components/q-learning/ContractConfig.tsx +172 -0
  46. package/frontend/components/q-learning/MazeGrid.tsx +346 -0
  47. package/frontend/components/q-learning/PathAnimation.tsx +384 -0
  48. package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
  49. package/frontend/components/q-learning/TrainingForm.tsx +349 -0
  50. package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
  51. package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
  52. package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
  53. package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
  54. package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
  55. package/frontend/components/setup/SetupGuide.tsx +290 -0
  56. package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
  57. package/frontend/components/ui/alert-dialog.tsx +157 -0
  58. package/frontend/components/ui/alert.tsx +66 -0
  59. package/frontend/components/ui/badge.tsx +46 -0
  60. package/frontend/components/ui/button.tsx +62 -0
  61. package/frontend/components/ui/card.tsx +92 -0
  62. package/frontend/components/ui/context-menu.tsx +252 -0
  63. package/frontend/components/ui/dialog.tsx +143 -0
  64. package/frontend/components/ui/dropdown-menu.tsx +257 -0
  65. package/frontend/components/ui/input.tsx +21 -0
  66. package/frontend/components/ui/label.tsx +24 -0
  67. package/frontend/components/ui/progress.tsx +31 -0
  68. package/frontend/components/ui/scroll-area.tsx +58 -0
  69. package/frontend/components/ui/select.tsx +190 -0
  70. package/frontend/components/ui/separator.tsx +28 -0
  71. package/frontend/components/ui/sheet.tsx +139 -0
  72. package/frontend/components/ui/skeleton.tsx +13 -0
  73. package/frontend/components/ui/slider.tsx +63 -0
  74. package/frontend/components/ui/sonner.tsx +40 -0
  75. package/frontend/components/ui/tabs.tsx +66 -0
  76. package/frontend/components/ui/textarea.tsx +18 -0
  77. package/frontend/components/wallet/ConnectButton.tsx +167 -0
  78. package/frontend/components/wallet/FaucetButton.tsx +256 -0
  79. package/frontend/components.json +22 -0
  80. package/frontend/eslint.config.mjs +18 -0
  81. package/frontend/hooks/useAICompletion.ts +75 -0
  82. package/frontend/hooks/useBlockchainLoader.ts +58 -0
  83. package/frontend/hooks/useChats.ts +137 -0
  84. package/frontend/hooks/useCompilation.ts +173 -0
  85. package/frontend/hooks/useFileTabs.ts +178 -0
  86. package/frontend/hooks/useGitHubLoader.ts +50 -0
  87. package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
  88. package/frontend/hooks/usePanelState.ts +115 -0
  89. package/frontend/hooks/useProjectState.ts +276 -0
  90. package/frontend/hooks/useResponsive.ts +29 -0
  91. package/frontend/lib/abi-parser.ts +58 -0
  92. package/frontend/lib/blockchain-api.ts +374 -0
  93. package/frontend/lib/blockchain-explorers.ts +75 -0
  94. package/frontend/lib/blockchain-loader.ts +112 -0
  95. package/frontend/lib/cargo-template.ts +64 -0
  96. package/frontend/lib/compilation.ts +529 -0
  97. package/frontend/lib/constants.ts +31 -0
  98. package/frontend/lib/deployment.ts +176 -0
  99. package/frontend/lib/file-utils.ts +83 -0
  100. package/frontend/lib/github-api.ts +246 -0
  101. package/frontend/lib/github-loader.ts +369 -0
  102. package/frontend/lib/ml-contract-template.txt +900 -0
  103. package/frontend/lib/orbit-chains.ts +181 -0
  104. package/frontend/lib/output-formatter.ts +68 -0
  105. package/frontend/lib/project-manager.ts +632 -0
  106. package/frontend/lib/ray-tracing-abi.ts +206 -0
  107. package/frontend/lib/storage.ts +189 -0
  108. package/frontend/lib/templates.ts +1662 -0
  109. package/frontend/lib/url-parser.ts +188 -0
  110. package/frontend/lib/utils.ts +6 -0
  111. package/frontend/lib/wagmi-config.ts +24 -0
  112. package/frontend/next.config.ts +7 -0
  113. package/frontend/package-lock.json +16259 -0
  114. package/frontend/package.json +60 -0
  115. package/frontend/postcss.config.mjs +7 -0
  116. package/frontend/public/file.svg +1 -0
  117. package/frontend/public/globe.svg +1 -0
  118. package/frontend/public/ml-weights/.gitkeep +0 -0
  119. package/frontend/public/ml-weights/model.pkl +0 -0
  120. package/frontend/public/ml-weights/model_weights.json +27102 -0
  121. package/frontend/public/ml-weights/test_samples.json +7888 -0
  122. package/frontend/public/next.svg +1 -0
  123. package/frontend/public/vercel.svg +1 -0
  124. package/frontend/public/window.svg +1 -0
  125. package/frontend/scripts/check-env.js +52 -0
  126. package/frontend/scripts/setup.js +285 -0
  127. package/frontend/tailwind.config.ts +64 -0
  128. package/frontend/tsconfig.json +34 -0
  129. package/frontend/types/blockchain.ts +63 -0
  130. package/frontend/types/github.ts +54 -0
  131. package/frontend/types/project.ts +106 -0
  132. package/ml-training/README.md +56 -0
  133. package/ml-training/train_tiny_model.py +325 -0
  134. package/ml-training/update_template.py +59 -0
  135. package/package.json +30 -0
@@ -0,0 +1,334 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@/components/ui/dialog';
11
+ import { Button } from '@/components/ui/button';
12
+ import { Input } from '@/components/ui/input';
13
+ import { Label } from '@/components/ui/label';
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from '@/components/ui/select';
21
+ import { Loader2, ExternalLink, Copy, Check } from 'lucide-react';
22
+ import { useChainId } from 'wagmi';
23
+ import { arbitrum, arbitrumSepolia } from 'wagmi/chains';
24
+ import { orbitChains } from '@/lib/orbit-chains';
25
+
26
+ interface DeployDialogProps {
27
+ open: boolean;
28
+ onOpenChange: (open: boolean) => void;
29
+ sessionId: string | null;
30
+ onDeploySuccess?: (contractAddress: string, txHash?: string) => void;
31
+ }
32
+
33
+ type RpcOption = { name: string; url: string };
34
+
35
+ // Arbitrum RPCs
36
+ const ARBITRUM_SEPOLIA_RPCS: RpcOption[] = [
37
+ { name: 'Arbitrum Official', url: 'https://sepolia-rollup.arbitrum.io/rpc' },
38
+ // ⚠️ demo is rate-limited — replace with your own key
39
+ { name: 'Alchemy (demo - rate limited)', url: 'https://arb-sepolia.g.alchemy.com/v2/demo' },
40
+ { name: 'Chainstack', url: 'https://arbitrum-sepolia.core.chainstack.com/rpc/demo' },
41
+ { name: 'Public Node', url: 'https://arbitrum-sepolia-rpc.publicnode.com' },
42
+ ];
43
+
44
+ const ARBITRUM_ONE_RPCS: RpcOption[] = [
45
+ { name: 'Arbitrum Official', url: 'https://arb1.arbitrum.io/rpc' },
46
+ { name: 'Alchemy (demo - rate limited)', url: 'https://arb-mainnet.g.alchemy.com/v2/demo' },
47
+ ];
48
+
49
+ // Orbit RPCs (public/free where available)
50
+ const ORBIT_RPCS_BY_CHAIN_ID: Record<number, RpcOption[]> = {
51
+ // Xai Testnet v2 (37714555429)
52
+ 37714555429: [
53
+ { name: 'Xai Official', url: 'https://testnet-v2.xai-chain.net/rpc' },
54
+ { name: 'Ankr (public)', url: 'https://rpc.ankr.com/xai_testnet' },
55
+ { name: 'thirdweb (public)', url: 'https://37714555429.rpc.thirdweb.com' },
56
+ // Note: this endpoint may be rate-limited depending on provider policy
57
+ { name: 'QuickNode (public)', url: 'https://xai-testnet.rpc.quicknode.com' },
58
+ ],
59
+
60
+ // ApeChain Curtis Testnet (33111)
61
+ 33111: [
62
+ { name: 'Caldera (official)', url: 'https://curtis.rpc.caldera.xyz/http' },
63
+ { name: 'ApeChain (Chainlist RPC)', url: 'https://rpc.curtis.apechain.com' },
64
+ { name: 'dRPC (public)', url: 'https://apechain-curtis.drpc.org' },
65
+ { name: 'thirdweb (public)', url: 'https://33111.rpc.thirdweb.com' },
66
+ // Keeping Tenderly as optional; if it fails for writes, use Caldera/ApeChain RPC
67
+ { name: 'Tenderly Gateway (may be limited)', url: 'https://curtis.gateway.tenderly.co' },
68
+ ],
69
+
70
+ // Nitrogen (Orbit Celestia) Testnet (96384675468)
71
+ 96384675468: [
72
+ { name: 'AltLayer (official)', url: 'https://nitrogen-rpc.altlayer.io' },
73
+ ],
74
+ };
75
+
76
+ export function DeployDialog({
77
+ open,
78
+ onOpenChange,
79
+ sessionId,
80
+ onDeploySuccess,
81
+ }: DeployDialogProps) {
82
+ const [privateKey, setPrivateKey] = useState('');
83
+ const [selectedRpc, setSelectedRpc] = useState<string>('');
84
+ const [isDeploying, setIsDeploying] = useState(false);
85
+ const [deployResult, setDeployResult] = useState<{
86
+ contractAddress?: string;
87
+ txHash?: string;
88
+ error?: string;
89
+ } | null>(null);
90
+ const [copiedAddress, setCopiedAddress] = useState(false);
91
+ const [copiedTx, setCopiedTx] = useState(false);
92
+
93
+ const chainId = useChainId();
94
+
95
+ const orbitInfo = useMemo(() => orbitChains.find((c) => c.id === chainId), [chainId]);
96
+
97
+ const chainDisplayName = useMemo(() => {
98
+ if (chainId === arbitrumSepolia.id) return 'Arbitrum Sepolia';
99
+ if (chainId === arbitrum.id) return 'Arbitrum One';
100
+ return orbitInfo?.name ?? `Chain ${chainId}`;
101
+ }, [chainId, orbitInfo]);
102
+
103
+ const availableRpcs: RpcOption[] = useMemo(() => {
104
+ if (chainId === arbitrumSepolia.id) return ARBITRUM_SEPOLIA_RPCS;
105
+ if (chainId === arbitrum.id) return ARBITRUM_ONE_RPCS;
106
+
107
+ // Orbit chain specific list
108
+ const orbitRpcs = ORBIT_RPCS_BY_CHAIN_ID[chainId];
109
+ if (orbitRpcs?.length) return orbitRpcs;
110
+
111
+ // Fallback: if chain is in orbitChains, at least show its default rpcUrls
112
+ if (orbitInfo?.chain?.rpcUrls?.default?.http?.length) {
113
+ return orbitInfo.chain.rpcUrls.default.http.map((url, idx) => ({
114
+ name: idx === 0 ? 'Default (from chain config)' : `RPC ${idx + 1}`,
115
+ url,
116
+ }));
117
+ }
118
+
119
+ return [];
120
+ }, [chainId, orbitInfo]);
121
+
122
+ // ✅ Set default RPC safely (no setState during render)
123
+ useEffect(() => {
124
+ setSelectedRpc((prev) => {
125
+ if (prev && availableRpcs.some((r) => r.url === prev)) return prev;
126
+ return availableRpcs[0]?.url ?? '';
127
+ });
128
+ }, [availableRpcs]);
129
+
130
+ const explorerBase = useMemo(() => {
131
+ if (chainId === arbitrumSepolia.id) return 'https://sepolia.arbiscan.io';
132
+ if (chainId === arbitrum.id) return 'https://arbiscan.io';
133
+
134
+ const url = orbitInfo?.chain?.blockExplorers?.default?.url;
135
+ return url ? url.replace(/\/$/, '') : null;
136
+ }, [chainId, orbitInfo]);
137
+
138
+ const getExplorerUrl = (address: string) => {
139
+ const base = explorerBase ?? 'https://sepolia.arbiscan.io';
140
+ return `${base}/address/${address}`;
141
+ };
142
+
143
+ const getTxExplorerUrl = (txHash: string) => {
144
+ const base = explorerBase ?? 'https://sepolia.arbiscan.io';
145
+ return `${base}/tx/${txHash}`;
146
+ };
147
+
148
+ const reset = () => {
149
+ setPrivateKey('');
150
+ setDeployResult(null);
151
+ setCopiedAddress(false);
152
+ setCopiedTx(false);
153
+ };
154
+
155
+ // ✅ Dialog expects (open:boolean). Reset only on close.
156
+ const handleOpenChange = (nextOpen: boolean) => {
157
+ if (!nextOpen) reset();
158
+ onOpenChange(nextOpen);
159
+ };
160
+
161
+ const handleDeploy = async () => {
162
+ if (!sessionId || !privateKey || !selectedRpc) {
163
+ alert('Please provide a private key and select an RPC endpoint');
164
+ return;
165
+ }
166
+
167
+ setIsDeploying(true);
168
+ setDeployResult(null);
169
+
170
+ try {
171
+ const response = await fetch('/api/deploy', {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ sessionId, privateKey, rpcUrl: selectedRpc }),
175
+ });
176
+
177
+ const result = await response.json();
178
+
179
+ if (result.success && result.contractAddress) {
180
+ setDeployResult({ contractAddress: result.contractAddress, txHash: result.txHash });
181
+ onDeploySuccess?.(result.contractAddress, result.txHash);
182
+ } else {
183
+ setDeployResult({ error: result.error || 'Deployment failed' });
184
+ }
185
+ } catch (error) {
186
+ setDeployResult({ error: error instanceof Error ? error.message : 'Deployment failed' });
187
+ } finally {
188
+ setIsDeploying(false);
189
+ }
190
+ };
191
+
192
+ const copyToClipboard = async (text: string, type: 'address' | 'tx') => {
193
+ await navigator.clipboard.writeText(text);
194
+ if (type === 'address') {
195
+ setCopiedAddress(true);
196
+ setTimeout(() => setCopiedAddress(false), 2000);
197
+ } else {
198
+ setCopiedTx(true);
199
+ setTimeout(() => setCopiedTx(false), 2000);
200
+ }
201
+ };
202
+
203
+ return (
204
+ <Dialog open={open} onOpenChange={handleOpenChange}>
205
+ <DialogContent className="sm:max-w-md">
206
+ <DialogHeader>
207
+ <DialogTitle>Deploy Contract</DialogTitle>
208
+ <DialogDescription>
209
+ Deploy your compiled contract to <strong>{chainDisplayName}</strong>
210
+ </DialogDescription>
211
+ </DialogHeader>
212
+
213
+ {!deployResult ? (
214
+ <div className="space-y-4">
215
+ <div className="space-y-2">
216
+ <Label htmlFor="rpc-select">RPC Endpoint</Label>
217
+ <Select value={selectedRpc} onValueChange={setSelectedRpc} disabled={availableRpcs.length === 0}>
218
+ <SelectTrigger>
219
+ <SelectValue placeholder={availableRpcs.length ? 'Select RPC endpoint' : 'No RPCs available'} />
220
+ </SelectTrigger>
221
+ <SelectContent>
222
+ {availableRpcs.map((rpc) => (
223
+ <SelectItem key={rpc.url} value={rpc.url}>
224
+ {rpc.name}
225
+ </SelectItem>
226
+ ))}
227
+ </SelectContent>
228
+ </Select>
229
+
230
+ <p className="text-xs text-muted-foreground">
231
+ Tip: If deployment fails, switch RPC and retry (some public RPCs can rate-limit writes).
232
+ </p>
233
+ </div>
234
+
235
+ <div className="space-y-2">
236
+ <Label htmlFor="private-key">Private Key</Label>
237
+ <Input
238
+ id="private-key"
239
+ type="password"
240
+ placeholder="0x..."
241
+ value={privateKey}
242
+ onChange={(e) => setPrivateKey(e.target.value)}
243
+ disabled={isDeploying}
244
+ />
245
+ </div>
246
+
247
+ <Button
248
+ onClick={handleDeploy}
249
+ disabled={isDeploying || !privateKey || !selectedRpc || !sessionId}
250
+ className="w-full"
251
+ >
252
+ {isDeploying ? (
253
+ <>
254
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
255
+ Deploying...
256
+ </>
257
+ ) : (
258
+ 'Deploy Contract'
259
+ )}
260
+ </Button>
261
+ </div>
262
+ ) : deployResult.error ? (
263
+ <div className="space-y-4">
264
+ <div className="bg-destructive/10 border border-destructive/20 p-4 rounded-md">
265
+ <p className="text-sm text-destructive font-medium mb-2">Deployment Failed</p>
266
+ <p className="text-xs text-destructive/80 whitespace-pre-wrap wrap-break-words">
267
+ {deployResult.error}
268
+ </p>
269
+ </div>
270
+ <Button onClick={() => setDeployResult(null)} variant="outline" className="w-full">
271
+ Try Again
272
+ </Button>
273
+ </div>
274
+ ) : (
275
+ <div className="space-y-4">
276
+ <div className="bg-green-500/10 border border-green-500/20 p-4 rounded-md space-y-3">
277
+ <p className="text-sm text-green-500 font-medium">✓ Deployment Successful!</p>
278
+
279
+ {deployResult.contractAddress && (
280
+ <div className="space-y-2">
281
+ <Label className="text-xs">Contract Address</Label>
282
+ <div className="flex items-center gap-2">
283
+ <code className="flex-1 text-xs bg-muted p-2 rounded font-mono break-all">
284
+ {deployResult.contractAddress}
285
+ </code>
286
+ <Button
287
+ size="sm"
288
+ variant="ghost"
289
+ onClick={() => copyToClipboard(deployResult.contractAddress!, 'address')}
290
+ >
291
+ {copiedAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
292
+ </Button>
293
+ <Button size="sm" variant="ghost" asChild>
294
+ <a href={getExplorerUrl(deployResult.contractAddress)} target="_blank" rel="noopener noreferrer">
295
+ <ExternalLink className="h-4 w-4" />
296
+ </a>
297
+ </Button>
298
+ </div>
299
+ </div>
300
+ )}
301
+
302
+ {deployResult.txHash && (
303
+ <div className="space-y-2">
304
+ <Label className="text-xs">Transaction Hash</Label>
305
+ <div className="flex items-center gap-2">
306
+ <code className="flex-1 text-xs bg-muted p-2 rounded font-mono break-all">
307
+ {deployResult.txHash}
308
+ </code>
309
+ <Button
310
+ size="sm"
311
+ variant="ghost"
312
+ onClick={() => copyToClipboard(deployResult.txHash!, 'tx')}
313
+ >
314
+ {copiedTx ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
315
+ </Button>
316
+ <Button size="sm" variant="ghost" asChild>
317
+ <a href={getTxExplorerUrl(deployResult.txHash)} target="_blank" rel="noopener noreferrer">
318
+ <ExternalLink className="h-4 w-4" />
319
+ </a>
320
+ </Button>
321
+ </div>
322
+ </div>
323
+ )}
324
+ </div>
325
+
326
+ <Button onClick={() => handleOpenChange(false)} className="w-full">
327
+ Done
328
+ </Button>
329
+ </div>
330
+ )}
331
+ </DialogContent>
332
+ </Dialog>
333
+ );
334
+ }
@@ -0,0 +1,181 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { X, Plus, FileCode, Edit2, MoreHorizontal } from 'lucide-react';
5
+ import type { FileTab } from '@/hooks/useFileTabs';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ DropdownMenuSeparator,
12
+ } from '@/components/ui/dropdown-menu';
13
+ import { useState } from 'react';
14
+
15
+ interface FileTabsProps {
16
+ tabs: FileTab[];
17
+ activeTabId: string | null;
18
+ onTabClick: (id: string) => void;
19
+ onTabClose: (id: string) => void;
20
+ onNewFile: (type: 'rust' | 'toml' | 'markdown') => void;
21
+ onRenameTab?: (id: string, newName: string) => void;
22
+ }
23
+
24
+ export function FileTabs({
25
+ tabs,
26
+ activeTabId,
27
+ onTabClick,
28
+ onTabClose,
29
+ onNewFile,
30
+ onRenameTab,
31
+ }: FileTabsProps) {
32
+ const [renamingTabId, setRenamingTabId] = useState<string | null>(null);
33
+ const [renameValue, setRenameValue] = useState('');
34
+
35
+ const handleRename = (tabId: string, currentName: string) => {
36
+ setRenamingTabId(tabId);
37
+ setRenameValue(currentName);
38
+ };
39
+
40
+ const handleRenameSubmit = (tabId: string) => {
41
+ if (onRenameTab && renameValue.trim() && renameValue !== tabs.find(t => t.id === tabId)?.name) {
42
+ onRenameTab(tabId, renameValue.trim());
43
+ }
44
+ setRenamingTabId(null);
45
+ setRenameValue('');
46
+ };
47
+
48
+ const handleRenameCancel = () => {
49
+ setRenamingTabId(null);
50
+ setRenameValue('');
51
+ };
52
+
53
+ const isRenameable = (tab: FileTab) => {
54
+ // Allow renaming for new files (not the main lib.rs)
55
+ return tab.id !== 'main' && onRenameTab;
56
+ };
57
+
58
+ return (
59
+ <div className="h-12 border-b border-border flex items-center gap-1 px-2 overflow-x-auto">
60
+ {tabs.map((tab) => (
61
+ <div
62
+ key={tab.id}
63
+ className={`
64
+ flex items-center gap-2 px-3 py-1.5 rounded-t-md
65
+ transition-colors group relative
66
+ ${activeTabId === tab.id
67
+ ? 'bg-card text-foreground'
68
+ : 'bg-muted/50 text-muted-foreground hover:bg-muted'
69
+ }
70
+ `}
71
+ >
72
+ <div
73
+ className="flex items-center gap-1.5 cursor-pointer"
74
+ onClick={() => onTabClick(tab.id)}
75
+ >
76
+ <FileCode className="h-3 w-3" />
77
+ {renamingTabId === tab.id ? (
78
+ <input
79
+ type="text"
80
+ value={renameValue}
81
+ onChange={(e) => setRenameValue(e.target.value)}
82
+ onBlur={() => handleRenameSubmit(tab.id)}
83
+ onKeyDown={(e) => {
84
+ if (e.key === 'Enter') {
85
+ handleRenameSubmit(tab.id);
86
+ } else if (e.key === 'Escape') {
87
+ handleRenameCancel();
88
+ }
89
+ }}
90
+ className="text-sm bg-transparent border-none outline-none focus:ring-1 focus:ring-primary rounded px-1 min-w-0 w-20"
91
+ autoFocus
92
+ onClick={(e) => e.stopPropagation()}
93
+ />
94
+ ) : (
95
+ <span className="text-sm whitespace-nowrap">
96
+ {tab.name}
97
+ {tab.isModified && <span className="text-blue-400 ml-1">•</span>}
98
+ </span>
99
+ )}
100
+ </div>
101
+
102
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
103
+ {isRenameable(tab) && renamingTabId !== tab.id && (
104
+ <DropdownMenu>
105
+ <DropdownMenuTrigger asChild>
106
+ <Button
107
+ variant="ghost"
108
+ size="sm"
109
+ className="h-5 w-5 p-0"
110
+ onClick={(e) => e.stopPropagation()}
111
+ >
112
+ <MoreHorizontal className="h-3 w-3" />
113
+ </Button>
114
+ </DropdownMenuTrigger>
115
+ <DropdownMenuContent align="end">
116
+ <DropdownMenuItem onClick={() => handleRename(tab.id, tab.name)}>
117
+ <Edit2 className="h-3 w-3 mr-2" />
118
+ Rename
119
+ </DropdownMenuItem>
120
+ {tabs.length > 1 && (
121
+ <>
122
+ <DropdownMenuSeparator />
123
+ <DropdownMenuItem
124
+ onClick={() => onTabClose(tab.id)}
125
+ className="text-destructive focus:text-destructive"
126
+ >
127
+ <X className="h-3 w-3 mr-2" />
128
+ Close
129
+ </DropdownMenuItem>
130
+ </>
131
+ )}
132
+ </DropdownMenuContent>
133
+ </DropdownMenu>
134
+ )}
135
+
136
+ {tabs.length > 1 && !isRenameable(tab) && (
137
+ <Button
138
+ variant="ghost"
139
+ size="sm"
140
+ className="h-5 w-5 p-0"
141
+ onClick={(e) => {
142
+ e.stopPropagation();
143
+ onTabClose(tab.id);
144
+ }}
145
+ >
146
+ <X className="h-3 w-3" />
147
+ </Button>
148
+ )}
149
+ </div>
150
+ </div>
151
+ ))}
152
+
153
+ {/* New File Button */}
154
+ <DropdownMenu>
155
+ <DropdownMenuTrigger asChild>
156
+ <Button
157
+ variant="ghost"
158
+ size="sm"
159
+ className="h-8 w-8 p-0 ml-2"
160
+ >
161
+ <Plus className="h-4 w-4" />
162
+ </Button>
163
+ </DropdownMenuTrigger>
164
+ <DropdownMenuContent>
165
+ <DropdownMenuItem onClick={() => onNewFile('rust')}>
166
+ <FileCode className="h-4 w-4 mr-2" />
167
+ New Rust File
168
+ </DropdownMenuItem>
169
+ <DropdownMenuItem onClick={() => onNewFile('toml')}>
170
+ <FileCode className="h-4 w-4 mr-2" />
171
+ New TOML File
172
+ </DropdownMenuItem>
173
+ <DropdownMenuItem onClick={() => onNewFile('markdown')}>
174
+ <FileCode className="h-4 w-4 mr-2" />
175
+ New Markdown File
176
+ </DropdownMenuItem>
177
+ </DropdownMenuContent>
178
+ </DropdownMenu>
179
+ </div>
180
+ );
181
+ }