@tscircuit/fake-snippets 0.0.97 → 0.0.99

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.
@@ -1,4 +1,3 @@
1
- import "dotenv/config"
2
1
  import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
2
  import { z } from "zod"
4
3
  import OpenAI from "openai"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -75,7 +75,7 @@
75
75
  "@radix-ui/react-toggle-group": "^1.1.0",
76
76
  "@radix-ui/react-tooltip": "^1.1.2",
77
77
  "@tailwindcss/typography": "^0.5.16",
78
- "@tscircuit/3d-viewer": "^0.0.279",
78
+ "@tscircuit/3d-viewer": "^0.0.303",
79
79
  "@tscircuit/assembly-viewer": "^0.0.1",
80
80
  "@tscircuit/create-snippet-url": "^0.0.8",
81
81
  "@tscircuit/eval": "^0.0.244",
@@ -83,7 +83,7 @@
83
83
  "@tscircuit/mm": "^0.0.8",
84
84
  "@tscircuit/pcb-viewer": "^1.11.194",
85
85
  "@tscircuit/prompt-benchmarks": "^0.0.28",
86
- "@tscircuit/runframe": "^0.0.669",
86
+ "@tscircuit/runframe": "0.0.715",
87
87
  "@tscircuit/schematic-viewer": "^2.0.21",
88
88
  "@types/babel__standalone": "^7.1.7",
89
89
  "@types/bun": "^1.1.10",
@@ -187,6 +187,7 @@
187
187
  "wouter": "^3.3.5",
188
188
  "zod": "^3.23.8",
189
189
  "zustand": "^4.5.5",
190
- "zustand-hoist": "^2.0.1"
190
+ "zustand-hoist": "^2.0.1",
191
+ "circuit-json-to-spice": "^0.0.6"
191
192
  }
192
193
  }
package/src/App.tsx CHANGED
@@ -122,6 +122,7 @@ class ErrorBoundary extends React.Component<
122
122
  cleanup = () => {
123
123
  if (this.visibilityHandler) {
124
124
  document.removeEventListener("visibilitychange", this.visibilityHandler)
125
+ window.removeEventListener("focus", this.visibilityHandler)
125
126
  this.visibilityHandler = undefined
126
127
  }
127
128
  if (this.reloadTimeout) {
@@ -144,12 +145,20 @@ class ErrorBoundary extends React.Component<
144
145
  this.cleanup() // Clean up any existing handlers
145
146
 
146
147
  this.visibilityHandler = () => {
147
- if (!document.hidden && this.state.hasError && !this.state.reloading) {
148
+ if (
149
+ document.visibilityState === "visible" &&
150
+ this.state.hasError &&
151
+ !this.state.reloading
152
+ ) {
148
153
  this.performReload()
149
154
  }
150
155
  }
151
156
 
152
157
  document.addEventListener("visibilitychange", this.visibilityHandler)
158
+ window.addEventListener("focus", this.visibilityHandler)
159
+
160
+ // In case the tab is already visible when the error occurs
161
+ this.visibilityHandler()
153
162
  }
154
163
 
155
164
  render() {
@@ -12,6 +12,7 @@ import { downloadDsnFile } from "@/lib/download-fns/download-dsn-file-fn"
12
12
  import { downloadFabricationFiles } from "@/lib/download-fns/download-fabrication-files"
13
13
  import { downloadSchematicSvg } from "@/lib/download-fns/download-schematic-svg"
14
14
  import { downloadReadableNetlist } from "@/lib/download-fns/download-readable-netlist"
15
+ import { downloadSpiceFile } from "@/lib/download-fns/download-spice-file"
15
16
  import { downloadAssemblySvg } from "@/lib/download-fns/download-assembly-svg"
16
17
  import { usePcbDownloadDialog } from "@/components/dialogs/pcb-download-dialog"
17
18
  import { downloadKicadFiles } from "@/lib/download-fns/download-kicad-files"
@@ -214,6 +215,18 @@ export function DownloadButtonAndMenu({
214
215
  txt
215
216
  </span>
216
217
  </DropdownMenuItem>
218
+ <DropdownMenuItem
219
+ className="text-xs"
220
+ onSelect={() => {
221
+ downloadSpiceFile(circuitJson, unscopedName || "circuit")
222
+ }}
223
+ >
224
+ <Download className="mr-1 h-3 w-3" />
225
+ <span className="flex-grow mr-6">SPICE Netlist</span>
226
+ <span className="text-[0.6rem] opacity-80 bg-blue-500 text-white font-mono rounded-md px-1 text-center py-0.5 mr-1">
227
+ spice
228
+ </span>
229
+ </DropdownMenuItem>
217
230
  <DropdownMenuItem
218
231
  className="text-xs"
219
232
  onSelect={() => {
@@ -1,6 +1,14 @@
1
1
  import React, { useState } from "react"
2
2
  import { cn } from "@/lib/utils"
3
- import { File, Folder, MoreVertical, PanelRightOpen, Plus } from "lucide-react"
3
+ import {
4
+ File,
5
+ Folder,
6
+ MoreVertical,
7
+ PanelRightOpen,
8
+ Plus,
9
+ Trash2,
10
+ Pencil,
11
+ } from "lucide-react"
4
12
  import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
5
13
  import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
6
14
  import { Input } from "@/components/ui/input"
@@ -16,8 +24,12 @@ import type {
16
24
  ICreateFileResult,
17
25
  IDeleteFileProps,
18
26
  IDeleteFileResult,
27
+ IRenameFileProps,
28
+ IRenameFileResult,
19
29
  } from "@/hooks/useFileManagement"
20
30
  import { useToast } from "@/hooks/use-toast"
31
+ import { useGlobalStore } from "@/hooks/use-global-store"
32
+ import type { Package } from "fake-snippets-api/lib/db/schema"
21
33
  type FileName = string
22
34
 
23
35
  interface FileSidebarProps {
@@ -28,6 +40,10 @@ interface FileSidebarProps {
28
40
  fileSidebarState: ReturnType<typeof useState<boolean>>
29
41
  handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
30
42
  handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
43
+ handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
44
+ isCreatingFile: boolean
45
+ setIsCreatingFile: React.Dispatch<React.SetStateAction<boolean>>
46
+ pkg?: Package
31
47
  }
32
48
 
33
49
  const FileSidebar: React.FC<FileSidebarProps> = ({
@@ -38,12 +54,18 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
38
54
  fileSidebarState,
39
55
  handleCreateFile,
40
56
  handleDeleteFile,
57
+ handleRenameFile,
58
+ isCreatingFile,
59
+ setIsCreatingFile,
60
+ pkg,
41
61
  }) => {
42
62
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
43
63
  const [newFileName, setNewFileName] = useState("")
44
- const [isCreatingFile, setIsCreatingFile] = useState(false)
45
64
  const [errorMessage, setErrorMessage] = useState("")
65
+ const [renamingFile, setRenamingFile] = useState<string | null>(null)
46
66
  const { toast } = useToast()
67
+ const session = useGlobalStore((s) => s.session)
68
+ const canModifyFiles = true
47
69
 
48
70
  const transformFilesToTreeData = (
49
71
  files: Record<FileName, string>,
@@ -79,20 +101,59 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
79
101
  ) {
80
102
  currentNode[segment] = {
81
103
  id: itemId,
82
- name: isLeafNode ? segment : segment,
104
+ name: segment,
105
+ isRenaming: renamingFile === itemId,
106
+ onRename: (newFilename: string) => {
107
+ // Preserve the folder structure when renaming
108
+ const oldPath = itemId
109
+ const pathParts = oldPath.split("/").filter((part) => part !== "") // Remove empty segments
110
+ let newPath: string
111
+
112
+ if (pathParts.length > 1) {
113
+ // File is in a folder, preserve the folder structure
114
+ const folderPath = pathParts.slice(0, -1).join("/")
115
+ newPath = folderPath + "/" + newFilename
116
+ } else {
117
+ // File is in root, just use the new filename
118
+ newPath = newFilename
119
+ }
120
+
121
+ // Preserve leading slash if original path had one
122
+ if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
123
+ newPath = "/" + newPath
124
+ }
125
+
126
+ const { fileRenamed } = handleRenameFile({
127
+ oldFilename: itemId,
128
+ newFilename: newPath,
129
+ onError: (error) => {
130
+ toast({
131
+ title: `Error renaming file`,
132
+ description: error.message,
133
+ variant: "destructive",
134
+ })
135
+ },
136
+ })
137
+ if (fileRenamed) {
138
+ setRenamingFile(null)
139
+ }
140
+ },
141
+ onCancelRename: () => {
142
+ setRenamingFile(null)
143
+ },
83
144
  icon: isLeafNode ? File : Folder,
84
145
  onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
85
146
  draggable: false,
86
147
  droppable: !isLeafNode,
87
148
  children: isLeafNode ? undefined : {},
88
- actions: (
149
+ actions: canModifyFiles ? (
89
150
  <>
90
151
  <DropdownMenu key={itemId}>
91
152
  <DropdownMenuTrigger asChild>
92
153
  <MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
93
154
  </DropdownMenuTrigger>
94
155
  <DropdownMenuContent
95
- className="w-48 bg-white shadow-lg rounded-md border-4 z-[100] border-white"
156
+ className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
96
157
  style={{
97
158
  position: "absolute",
98
159
  top: "100%",
@@ -103,13 +164,24 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
103
164
  }}
104
165
  >
105
166
  <DropdownMenuGroup>
167
+ {isLeafNode && (
168
+ <DropdownMenuItem
169
+ onClick={() => {
170
+ setRenamingFile(itemId)
171
+ }}
172
+ className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
173
+ >
174
+ <Pencil className="mr-2 h-3 w-3" />
175
+ Rename
176
+ </DropdownMenuItem>
177
+ )}
106
178
  <DropdownMenuItem
107
179
  onClick={() => {
108
180
  const { fileDeleted } = handleDeleteFile({
109
- filename: relativePath,
181
+ filename: itemId,
110
182
  onError: (error) => {
111
183
  toast({
112
- title: `Error deleting file ${relativePath}`,
184
+ title: `Error deleting file ${itemId}`,
113
185
  description: error.message,
114
186
  })
115
187
  },
@@ -118,15 +190,16 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
118
190
  setErrorMessage("")
119
191
  }
120
192
  }}
121
- className="flex items-center px-4 py-1 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
193
+ className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
122
194
  >
195
+ <Trash2 className="mr-2 h-3 w-3" />
123
196
  Delete
124
197
  </DropdownMenuItem>
125
198
  </DropdownMenuGroup>
126
199
  </DropdownMenuContent>
127
200
  </DropdownMenu>
128
201
  </>
129
- ),
202
+ ) : undefined,
130
203
  }
131
204
  }
132
205
 
@@ -152,7 +225,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
152
225
  }
153
226
 
154
227
  const treeData = transformFilesToTreeData(files)
155
- // console.log("treeData", files)
228
+
156
229
  const handleCreateFileInline = () => {
157
230
  const { newFileCreated } = handleCreateFile({
158
231
  newFileName,
@@ -19,34 +19,46 @@ export const LogContent = ({
19
19
  type?: "info" | "success" | "error"
20
20
  msg?: string
21
21
  message?: string
22
- timestamp?: string
22
+ timestamp?: string | number
23
+ [key: string]: unknown
23
24
  }>
24
25
  error?: ErrorObject | string | null
25
26
  }) => {
26
27
  return (
27
28
  <div className="font-mono text-xs space-y-1 min-w-0">
28
29
  {logs.map((log, i) => {
29
- const text = log.msg ?? log.message
30
+ const { type, msg, message, timestamp, ...rest } = log
31
+ const text = msg ?? message
30
32
  if (!text) return null
31
33
 
32
34
  return (
33
35
  <div
34
36
  key={i}
35
37
  className={`break-words whitespace-pre-wrap ${
36
- log.type === "error"
38
+ type === "error"
37
39
  ? "text-red-600"
38
- : log.type === "success"
40
+ : type === "success"
39
41
  ? "text-green-600"
40
42
  : "text-gray-600"
41
43
  }`}
42
44
  >
43
- {log.timestamp && (
45
+ {timestamp !== undefined && (
44
46
  <span className="text-gray-500 whitespace-nowrap">
45
- {new Date(log.timestamp).toLocaleTimeString()}
47
+ {new Date(Number(timestamp)).toLocaleTimeString()}
46
48
  </span>
47
49
  )}
48
- {log.timestamp && " "}
50
+ {timestamp !== undefined && " "}
49
51
  <span className="break-all">{text}</span>
52
+ {Object.keys(rest).filter((k) => k !== "package_release_id")
53
+ .length > 0 && (
54
+ <span className="text-gray-500">
55
+ {" "}
56
+ {Object.entries(rest)
57
+ .filter(([key]) => key !== "package_release_id")
58
+ .map(([key, value]) => `${key}: ${String(value)}`)
59
+ .join(" ")}
60
+ </span>
61
+ )}
50
62
  </div>
51
63
  )
52
64
  })}
@@ -1,11 +1,18 @@
1
1
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
2
2
  import { useEffect, useMemo } from "react"
3
3
  import { useQuery } from "react-query"
4
+ import { Skeleton } from "@/components/ui/skeleton"
5
+
6
+ // Pre-randomized array to avoid flickering on re-renders
7
+ const SKELETON_WIDTHS = ["w-2/3", "w-1/4", "w-5/6", "w-1/3", "w-1/2", "w-3/4"]
4
8
 
5
9
  export const ShikiCodeViewer = ({
6
10
  code,
7
11
  filePath,
8
- }: { code: string; filePath: string }) => {
12
+ }: {
13
+ code: string
14
+ filePath: string
15
+ }) => {
9
16
  const { highlighter } = useShikiHighlighter()
10
17
 
11
18
  const html = useMemo(
@@ -18,7 +25,13 @@ export const ShikiCodeViewer = ({
18
25
  )
19
26
 
20
27
  if (!html) {
21
- return <div>Loading...</div>
28
+ return (
29
+ <div className="text-sm p-4">
30
+ {SKELETON_WIDTHS.map((w, i) => (
31
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
32
+ ))}
33
+ </div>
34
+ )
22
35
  }
23
36
 
24
37
  return (
@@ -1,5 +1,6 @@
1
1
  import { CadViewer } from "@tscircuit/runframe"
2
2
  import { useCurrentPackageCircuitJson } from "../../hooks/use-current-package-circuit-json"
3
+ import { Suspense } from "react"
3
4
 
4
5
  export default function ThreeDView() {
5
6
  const { circuitJson, isLoading, error } = useCurrentPackageCircuitJson()
@@ -24,7 +25,19 @@ export default function ThreeDView() {
24
25
 
25
26
  return (
26
27
  <div className="h-[620px]">
27
- <CadViewer clickToInteractEnabled circuitJson={circuitJson} />
28
+ <Suspense
29
+ fallback={
30
+ <div className="flex justify-center items-center h-full">
31
+ <div className="w-48">
32
+ <div className="loading">
33
+ <div className="loading-bar"></div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ }
38
+ >
39
+ <CadViewer clickToInteractEnabled circuitJson={circuitJson} />
40
+ </Suspense>
28
41
  </div>
29
42
  )
30
43
  }
@@ -16,6 +16,7 @@ import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
16
16
  import { ManualEditEvent } from "@tscircuit/props"
17
17
  import { useFileManagement } from "@/hooks/useFileManagement"
18
18
  import { DEFAULT_CODE } from "@/lib/utils/package-utils"
19
+ import { useGlobalStore } from "@/hooks/use-global-store"
19
20
 
20
21
  interface Props {
21
22
  pkg?: Package
@@ -38,6 +39,7 @@ export interface CodeAndPreviewState {
38
39
 
39
40
  export function CodeAndPreview({ pkg, projectUrl }: Props) {
40
41
  const { toast } = useToast()
42
+ const session = useGlobalStore((s) => s.session)
41
43
  const urlParams = useUrlParams()
42
44
 
43
45
  const templateFromUrl = useMemo(
@@ -75,6 +77,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
75
77
  setLocalFiles,
76
78
  localFiles,
77
79
  initialFiles,
80
+ renameFile,
78
81
  } = useFileManagement({
79
82
  templateCode: templateFromUrl?.code,
80
83
  currentPackage: pkg,
@@ -208,6 +211,8 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
208
211
  isSaving={isSaving}
209
212
  handleCreateFile={createFile}
210
213
  handleDeleteFile={deleteFile}
214
+ handleRenameFile={renameFile}
215
+ pkg={pkg}
211
216
  currentFile={currentFile}
212
217
  onFileSelect={onFileSelect}
213
218
  files={localFiles}
@@ -15,7 +15,10 @@ import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-uti
15
15
  import type { ATABootstrapConfig } from "@typescript/ata"
16
16
  import { setupTypeAcquisition } from "@typescript/ata"
17
17
  import { linter } from "@codemirror/lint"
18
- import { TSCI_PACKAGE_PATTERN } from "@/lib/constants"
18
+ import {
19
+ TSCI_PACKAGE_PATTERN,
20
+ LOCAL_FILE_IMPORT_PATTERN,
21
+ } from "@/lib/constants"
19
22
  import {
20
23
  createSystem,
21
24
  createVirtualTypeScriptEnvironment,
@@ -29,10 +32,10 @@ import tsModule from "typescript"
29
32
  import CodeEditorHeader, {
30
33
  FileName,
31
34
  } from "@/components/package-port/CodeEditorHeader"
32
- import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
33
35
  import FileSidebar from "../FileSidebar"
34
36
  import { findTargetFile } from "@/lib/utils/findTargetFile"
35
37
  import type { PackageFile } from "@/types/package"
38
+ import type { Package } from "fake-snippets-api/lib/db/schema"
36
39
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
37
40
  import QuickOpen from "./QuickOpen"
38
41
  import GlobalFindReplace from "./GlobalFindReplace"
@@ -41,9 +44,12 @@ import {
41
44
  ICreateFileResult,
42
45
  IDeleteFileProps,
43
46
  IDeleteFileResult,
47
+ IRenameFileProps,
48
+ IRenameFileResult,
44
49
  } from "@/hooks/useFileManagement"
45
50
  import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
46
51
  import { inlineCopilot } from "codemirror-copilot"
52
+ import { resolveRelativePath } from "@/lib/utils/resolveRelativePath"
47
53
 
48
54
  const defaultImports = `
49
55
  import React from "@types/react/jsx-runtime"
@@ -62,14 +68,18 @@ export const CodeEditor = ({
62
68
  pkgFilesLoaded,
63
69
  currentFile,
64
70
  onFileSelect,
71
+ handleRenameFile,
65
72
  handleCreateFile,
66
73
  handleDeleteFile,
74
+ pkg,
67
75
  }: {
68
76
  onCodeChange: (code: string, filename?: string) => void
69
77
  files: PackageFile[]
70
78
  isSaving?: boolean
71
79
  handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
72
80
  handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
81
+ handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
82
+ pkg?: Package
73
83
  readOnly?: boolean
74
84
  isStreaming?: boolean
75
85
  pkgFilesLoaded?: boolean
@@ -105,6 +115,9 @@ export const CodeEditor = ({
105
115
  return files.find((x) => x.path === "index.tsx")?.path || "index.tsx"
106
116
  }, [files])
107
117
 
118
+ const [sidebarOpen, setSidebarOpen] = useState(false)
119
+ const [isCreatingFile, setIsCreatingFile] = useState(false)
120
+
108
121
  // Set current file on component mount
109
122
  useEffect(() => {
110
123
  if (files.length === 0 || !pkgFilesLoaded || currentFile) return
@@ -417,11 +430,13 @@ export const CodeEditor = ({
417
430
  const lineStart = line.from
418
431
  const lineEnd = line.to
419
432
  const lineText = view.state.sliceDoc(lineStart, lineEnd)
420
- const matches = Array.from(
433
+
434
+ // Check for TSCI package imports
435
+ const packageMatches = Array.from(
421
436
  lineText.matchAll(TSCI_PACKAGE_PATTERN),
422
437
  )
423
438
 
424
- for (const match of matches) {
439
+ for (const match of packageMatches) {
425
440
  if (match.index !== undefined) {
426
441
  const start = lineStart + match.index
427
442
  const end = start + match[0].length
@@ -484,10 +499,12 @@ export const CodeEditor = ({
484
499
  const lineStart = line.from
485
500
  const lineEnd = line.to
486
501
  const lineText = view.state.sliceDoc(lineStart, lineEnd)
487
- const matches = Array.from(
502
+
503
+ // Check for TSCI package imports first
504
+ const packageMatches = Array.from(
488
505
  lineText.matchAll(TSCI_PACKAGE_PATTERN),
489
506
  )
490
- for (const match of matches) {
507
+ for (const match of packageMatches) {
491
508
  if (match.index !== undefined) {
492
509
  const start = lineStart + match.index
493
510
  const end = start + match[0].length
@@ -502,6 +519,42 @@ export const CodeEditor = ({
502
519
  }
503
520
  }
504
521
  }
522
+
523
+ // Check for local file imports
524
+ const localFileMatches = Array.from(
525
+ lineText.matchAll(LOCAL_FILE_IMPORT_PATTERN),
526
+ )
527
+ for (const match of localFileMatches) {
528
+ if (match.index !== undefined) {
529
+ const start = lineStart + match.index
530
+ const end = start + match[0].length
531
+ if (pos >= start && pos <= end) {
532
+ const relativePath = match[0]
533
+ const resolvedPath = resolveRelativePath(
534
+ relativePath,
535
+ currentFile || "",
536
+ )
537
+
538
+ // Add common extensions if not present
539
+ let targetPath = resolvedPath
540
+ if (!targetPath.includes(".")) {
541
+ const extensions = [".tsx", ".ts", ".js", ".jsx"]
542
+ for (const ext of extensions) {
543
+ if (fileMap[`${targetPath}${ext}`]) {
544
+ targetPath = `${targetPath}${ext}`
545
+ break
546
+ }
547
+ }
548
+ }
549
+
550
+ if (fileMap[targetPath]) {
551
+ onFileSelect(targetPath)
552
+ return true
553
+ }
554
+ return !!fileMap[targetPath]
555
+ }
556
+ }
557
+ }
505
558
  return false
506
559
  },
507
560
  keydown: (event) => {
@@ -529,6 +582,12 @@ export const CodeEditor = ({
529
582
  overflow: "auto",
530
583
  zIndex: "9999",
531
584
  },
585
+ ".cm-import:hover": {
586
+ textDecoration: "underline",
587
+ textDecorationColor: "#aa1111",
588
+ textUnderlineOffset: "1px",
589
+ filter: "brightness(0.7)",
590
+ },
532
591
  }),
533
592
  EditorView.decorations.of((view) => {
534
593
  const decorations = []
@@ -536,14 +595,32 @@ export const CodeEditor = ({
536
595
  for (let pos = from; pos < to; ) {
537
596
  const line = view.state.doc.lineAt(pos)
538
597
  const lineText = line.text
539
- const matches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
540
- for (const match of matches) {
598
+
599
+ // Add decorations for TSCI package imports
600
+ const packageMatches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
601
+ for (const match of packageMatches) {
541
602
  if (match.index !== undefined) {
542
603
  const start = line.from + match.index
543
604
  const end = start + match[0].length
544
605
  decorations.push(
545
606
  Decoration.mark({
546
- class: "cm-underline cursor-pointer",
607
+ class: "cm-import cursor-pointer",
608
+ }).range(start, end),
609
+ )
610
+ }
611
+ }
612
+
613
+ // Add decorations for local file imports
614
+ const localFileMatches = lineText.matchAll(
615
+ LOCAL_FILE_IMPORT_PATTERN,
616
+ )
617
+ for (const match of localFileMatches) {
618
+ if (match.index !== undefined) {
619
+ const start = line.from + match.index
620
+ const end = start + match[0].length
621
+ decorations.push(
622
+ Decoration.mark({
623
+ class: "cm-import cursor-pointer",
547
624
  }).range(start, end),
548
625
  )
549
626
  }
@@ -719,10 +796,14 @@ export const CodeEditor = ({
719
796
  }
720
797
  })
721
798
 
799
+ useHotkeyCombo("cmd+m", () => {
800
+ setSidebarOpen(true)
801
+ setIsCreatingFile(true)
802
+ })
803
+
722
804
  if (isStreaming) {
723
805
  return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
724
806
  }
725
- const [sidebarOpen, setSidebarOpen] = useState(false)
726
807
  return (
727
808
  <div className="flex h-[98vh] w-full overflow-hidden">
728
809
  <FileSidebar
@@ -733,7 +814,11 @@ export const CodeEditor = ({
733
814
  }
734
815
  onFileSelect={(path) => handleFileChange(path)}
735
816
  handleCreateFile={handleCreateFile}
817
+ handleRenameFile={handleRenameFile}
736
818
  handleDeleteFile={handleDeleteFile}
819
+ isCreatingFile={isCreatingFile}
820
+ setIsCreatingFile={setIsCreatingFile}
821
+ pkg={pkg}
737
822
  />
738
823
  <div className="flex flex-col flex-1 w-full min-w-0 h-full">
739
824
  {showImportAndFormatButtons && (