Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
a1aee5c2
Unverified
提交
a1aee5c2
authored
7月 08, 2025
作者:
Will Chen
提交者:
GitHub
7月 08, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Graduate file editing from experimental (#599)
上级
dfdd267f
隐藏空白字符变更
内嵌
并排
正在显示
9 个修改的文件
包含
184 行增加
和
42 行删除
+184
-42
edit_code.spec.ts
e2e-tests/edit_code.spec.ts
+84
-0
edit_code.spec.ts_edited-mde-with-dyad.txt
...ests/snapshots/edit_code.spec.ts_edited-mde-with-dyad.txt
+0
-0
FileEditor.tsx
src/components/preview_panel/FileEditor.tsx
+64
-9
app_handlers.ts
src/ipc/handlers/app_handlers.ts
+23
-4
ipc_client.ts
src/ipc/ipc_client.ts
+3
-2
ipc_types.ts
src/ipc/ipc_types.ts
+4
-0
file_utils.ts
src/ipc/utils/file_utils.ts
+5
-2
schemas.ts
src/lib/schemas.ts
+1
-1
settings.tsx
src/pages/settings.tsx
+0
-24
没有找到文件。
e2e-tests/edit_code.spec.ts
0 → 100644
浏览文件 @
a1aee5c2
import
{
test
}
from
"./helpers/test_helper"
;
import
{
expect
}
from
"@playwright/test"
;
import
fs
from
"fs"
;
import
path
from
"path"
;
test
(
"edit code"
,
async
({
po
})
=>
{
const
editedFilePath
=
path
.
join
(
"src"
,
"components"
,
"made-with-dyad.tsx"
);
await
po
.
sendPrompt
(
"foo"
);
const
appPath
=
await
po
.
getCurrentAppPath
();
await
po
.
clickTogglePreviewPanel
();
await
po
.
selectPreviewMode
(
"code"
);
await
po
.
page
.
getByText
(
"made-with-dyad.tsx"
).
click
();
await
po
.
page
.
getByRole
(
"code"
)
.
locator
(
"div"
)
.
filter
({
hasText
:
"export const"
})
.
nth
(
4
)
.
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Editor content"
})
.
fill
(
"export const MadeWithDyad = ;"
);
// Save the file
await
po
.
page
.
getByTestId
(
"save-file-button"
).
click
();
// Expect toast to be visible
await
expect
(
po
.
page
.
getByText
(
"File saved"
)).
toBeVisible
();
// We are NOT snapshotting the app files because the Monaco UI edit
// is not deterministic.
const
editedFile
=
fs
.
readFileSync
(
path
.
join
(
appPath
,
editedFilePath
),
"utf8"
,
);
expect
(
editedFile
).
toContain
(
"export const MadeWithDyad = ;"
);
});
test
(
"edit code edits the right file"
,
async
({
po
})
=>
{
const
editedFilePath
=
path
.
join
(
"src"
,
"components"
,
"made-with-dyad.tsx"
);
const
robotsFilePath
=
path
.
join
(
"public"
,
"robots.txt"
);
await
po
.
sendPrompt
(
"foo"
);
const
appPath
=
await
po
.
getCurrentAppPath
();
const
originalRobotsFile
=
fs
.
readFileSync
(
path
.
join
(
appPath
,
robotsFilePath
),
"utf8"
,
);
await
po
.
clickTogglePreviewPanel
();
await
po
.
selectPreviewMode
(
"code"
);
await
po
.
page
.
getByText
(
"made-with-dyad.tsx"
).
click
();
await
po
.
page
.
getByRole
(
"code"
)
.
locator
(
"div"
)
.
filter
({
hasText
:
"export const"
})
.
nth
(
4
)
.
click
();
await
po
.
page
.
getByRole
(
"textbox"
,
{
name
:
"Editor content"
})
.
fill
(
"export const MadeWithDyad = ;"
);
// Save the file by switching files
await
po
.
page
.
getByText
(
"robots.txt"
).
click
();
// Expect toast to be visible
await
expect
(
po
.
page
.
getByText
(
"File saved"
)).
toBeVisible
();
// We are NOT snapshotting the app files because the Monaco UI edit
// is not deterministic.
const
editedFile
=
fs
.
readFileSync
(
path
.
join
(
appPath
,
editedFilePath
),
"utf8"
,
);
expect
(
editedFile
).
toContain
(
"export const MadeWithDyad = ;"
);
// Make sure the robots.txt file is not edited
const
editedRobotsFile
=
fs
.
readFileSync
(
path
.
join
(
appPath
,
robotsFilePath
),
"utf8"
,
);
expect
(
editedRobotsFile
).
toEqual
(
originalRobotsFile
);
});
e2e-tests/snapshots/edit_code.spec.ts_edited-mde-with-dyad.txt
0 → 100644
浏览文件 @
a1aee5c2
This source diff could not be displayed because it is too large. You can
view the blob
instead.
src/components/preview_panel/FileEditor.tsx
浏览文件 @
a1aee5c2
...
...
@@ -2,11 +2,19 @@ import React, { useState, useRef, useEffect } from "react";
import
Editor
,
{
OnMount
}
from
"@monaco-editor/react"
;
import
{
useLoadAppFile
}
from
"@/hooks/useLoadAppFile"
;
import
{
useTheme
}
from
"@/contexts/ThemeContext"
;
import
{
ChevronRight
,
Circle
}
from
"lucide-react"
;
import
{
ChevronRight
,
Circle
,
Save
}
from
"lucide-react"
;
import
"@/components/chat/monaco"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
showError
,
showSuccess
,
showWarning
}
from
"@/lib/toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Tooltip
,
TooltipContent
,
TooltipTrigger
,
}
from
"@/components/ui/tooltip"
;
import
{
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
showError
}
from
"@/lib/toast
"
;
import
{
useCheckProblems
}
from
"@/hooks/useCheckProblems
"
;
interface
FileEditorProps
{
appId
:
number
|
null
;
...
...
@@ -16,9 +24,16 @@ interface FileEditorProps {
interface
BreadcrumbProps
{
path
:
string
;
hasUnsavedChanges
:
boolean
;
onSave
:
()
=>
void
;
isSaving
:
boolean
;
}
const
Breadcrumb
:
React
.
FC
<
BreadcrumbProps
>
=
({
path
,
hasUnsavedChanges
})
=>
{
const
Breadcrumb
:
React
.
FC
<
BreadcrumbProps
>
=
({
path
,
hasUnsavedChanges
,
onSave
,
isSaving
,
})
=>
{
const
segments
=
path
.
split
(
"/"
).
filter
(
Boolean
);
return
(
...
...
@@ -39,7 +54,24 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ path, hasUnsavedChanges }) => {
</
React
.
Fragment
>
))
}
</
div
>
<
div
className=
"flex-shrink-0 ml-2"
>
<
div
className=
"flex items-center gap-2 flex-shrink-0 ml-2"
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
onSave
}
disabled=
{
!
hasUnsavedChanges
||
isSaving
}
className=
"h-6 w-6 p-0"
data
-
testid=
"save-file-button"
>
<
Save
size=
{
12
}
/>
</
Button
>
</
TooltipTrigger
>
<
TooltipContent
>
{
hasUnsavedChanges
?
"Save changes"
:
"No unsaved changes"
}
</
TooltipContent
>
</
Tooltip
>
{
hasUnsavedChanges
&&
(
<
Circle
size=
{
8
}
...
...
@@ -56,10 +88,10 @@ const Breadcrumb: React.FC<BreadcrumbProps> = ({ path, hasUnsavedChanges }) => {
export
const
FileEditor
=
({
appId
,
filePath
}:
FileEditorProps
)
=>
{
const
{
content
,
loading
,
error
}
=
useLoadAppFile
(
appId
,
filePath
);
const
{
theme
}
=
useTheme
();
const
{
settings
}
=
useSettings
();
const
[
value
,
setValue
]
=
useState
<
string
|
undefined
>
(
undefined
);
const
[
displayUnsavedChanges
,
setDisplayUnsavedChanges
]
=
useState
(
false
);
const
[
isSaving
,
setIsSaving
]
=
useState
(
false
);
const
{
settings
}
=
useSettings
();
// Use refs for values that need to be current in event handlers
const
originalValueRef
=
useRef
<
string
|
undefined
>
(
undefined
);
const
editorRef
=
useRef
<
any
>
(
null
);
...
...
@@ -67,6 +99,9 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
const
needsSaveRef
=
useRef
<
boolean
>
(
false
);
const
currentValueRef
=
useRef
<
string
|
undefined
>
(
undefined
);
const
queryClient
=
useQueryClient
();
const
{
checkProblems
}
=
useCheckProblems
(
appId
);
// Update state when content loads
useEffect
(()
=>
{
if
(
content
!==
null
)
{
...
...
@@ -75,6 +110,7 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
currentValueRef
.
current
=
content
;
needsSaveRef
.
current
=
false
;
setDisplayUnsavedChanges
(
false
);
setIsSaving
(
false
);
}
},
[
content
,
filePath
]);
...
...
@@ -125,9 +161,23 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
try
{
isSavingRef
.
current
=
true
;
setIsSaving
(
true
);
const
ipcClient
=
IpcClient
.
getInstance
();
await
ipcClient
.
editAppFile
(
appId
,
filePath
,
currentValueRef
.
current
);
const
{
warning
}
=
await
ipcClient
.
editAppFile
(
appId
,
filePath
,
currentValueRef
.
current
,
);
await
queryClient
.
invalidateQueries
({
queryKey
:
[
"versions"
,
appId
]
});
if
(
settings
?.
enableAutoFixProblems
)
{
checkProblems
();
}
if
(
warning
)
{
showWarning
(
warning
);
}
else
{
showSuccess
(
"File saved"
);
}
originalValueRef
.
current
=
currentValueRef
.
current
;
needsSaveRef
.
current
=
false
;
...
...
@@ -136,6 +186,7 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
showError
(
error
);
}
finally
{
isSavingRef
.
current
=
false
;
setIsSaving
(
false
);
}
};
...
...
@@ -182,7 +233,12 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
return
(
<
div
className=
"h-full flex flex-col"
>
<
Breadcrumb
path=
{
filePath
}
hasUnsavedChanges=
{
displayUnsavedChanges
}
/>
<
Breadcrumb
path=
{
filePath
}
hasUnsavedChanges=
{
displayUnsavedChanges
}
onSave=
{
saveFile
}
isSaving=
{
isSaving
}
/>
<
div
className=
"flex-1 overflow-hidden"
>
<
Editor
height=
"100%"
...
...
@@ -199,7 +255,6 @@ export const FileEditor = ({ appId, filePath }: FileEditorProps) => {
fontFamily
:
"monospace"
,
fontSize
:
13
,
lineNumbers
:
"on"
,
readOnly
:
!
settings
?.
experiments
?.
enableFileEditing
,
}
}
/>
</
div
>
...
...
src/ipc/handlers/app_handlers.ts
浏览文件 @
a1aee5c2
...
...
@@ -7,6 +7,7 @@ import type {
CreateAppParams
,
RenameBranchParams
,
CopyAppParams
,
EditAppFileReturnType
,
}
from
"../ipc_types"
;
import
fs
from
"node:fs"
;
import
path
from
"node:path"
;
...
...
@@ -32,7 +33,10 @@ import fixPath from "fix-path";
import
killPort
from
"kill-port"
;
import
util
from
"util"
;
import
log
from
"electron-log"
;
import
{
getSupabaseProjectName
}
from
"../../supabase_admin/supabase_management_client"
;
import
{
deploySupabaseFunctions
,
getSupabaseProjectName
,
}
from
"../../supabase_admin/supabase_management_client"
;
import
{
createLoggedHandler
}
from
"./safe_handle"
;
import
{
getLanguageModelProviders
}
from
"../shared/language_model_helpers"
;
import
{
startProxy
}
from
"../utils/start_proxy_server"
;
...
...
@@ -41,6 +45,7 @@ import { createFromTemplate } from "./createFromTemplate";
import
{
gitCommit
}
from
"../utils/git_utils"
;
import
{
safeSend
}
from
"../utils/safe_sender"
;
import
{
normalizePath
}
from
"../../../shared/normalizePath"
;
import
{
isServerFunction
}
from
"@/supabase_admin/supabase_utils"
;
async
function
copyDir
(
source
:
string
,
...
...
@@ -604,7 +609,7 @@ export function registerAppHandlers() {
filePath
,
content
,
}:
{
appId
:
number
;
filePath
:
string
;
content
:
string
},
):
Promise
<
void
>
=>
{
):
Promise
<
EditAppFileReturnType
>
=>
{
const
app
=
await
db
.
query
.
apps
.
findFirst
({
where
:
eq
(
apps
.
id
,
appId
),
});
...
...
@@ -641,12 +646,26 @@ export function registerAppHandlers() {
message
:
`Updated
${
filePath
}
`
,
});
}
return
;
}
catch
(
error
:
any
)
{
logger
.
error
(
`Error writing file
${
filePath
}
for app
${
appId
}
:`
,
error
);
throw
new
Error
(
`Failed to write file:
${
error
.
message
}
`
);
}
if
(
isServerFunction
(
filePath
)
&&
app
.
supabaseProjectId
)
{
try
{
await
deploySupabaseFunctions
({
supabaseProjectId
:
app
.
supabaseProjectId
,
functionName
:
path
.
basename
(
path
.
dirname
(
filePath
)),
content
:
content
,
});
}
catch
(
error
)
{
logger
.
error
(
`Error deploying Supabase function
${
filePath
}
:`
,
error
);
return
{
warning
:
`File saved, but failed to deploy Supabase function:
${
filePath
}
:
${
error
}
`
,
};
}
}
return
{};
},
);
...
...
src/ipc/ipc_client.ts
浏览文件 @
a1aee5c2
...
...
@@ -37,6 +37,7 @@ import type {
ComponentSelection
,
AppUpgrade
,
ProblemReport
,
EditAppFileReturnType
,
}
from
"./ipc_types"
;
import
type
{
AppChatContext
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
...
...
@@ -220,8 +221,8 @@ export class IpcClient {
appId
:
number
,
filePath
:
string
,
content
:
string
,
):
Promise
<
void
>
{
await
this
.
ipcRenderer
.
invoke
(
"edit-app-file"
,
{
):
Promise
<
EditAppFileReturnType
>
{
return
this
.
ipcRenderer
.
invoke
(
"edit-app-file"
,
{
appId
,
filePath
,
content
,
...
...
src/ipc/ipc_types.ts
浏览文件 @
a1aee5c2
...
...
@@ -248,3 +248,7 @@ export interface AppUpgrade {
manualUpgradeUrl
:
string
;
isNeeded
:
boolean
;
}
export
interface
EditAppFileReturnType
{
warning
?:
string
;
}
src/ipc/utils/file_utils.ts
浏览文件 @
a1aee5c2
...
...
@@ -4,6 +4,9 @@ import path from "node:path";
import
fsExtra
from
"fs-extra"
;
import
{
generateCuteAppName
}
from
"../../lib/utils"
;
// Directories to exclude when scanning files
const
EXCLUDED_DIRS
=
[
"node_modules"
,
".git"
,
".next"
];
/**
* Recursively gets all files in a directory, excluding node_modules and .git
* @param dir The directory to scan
...
...
@@ -22,8 +25,8 @@ export function getFilesRecursively(dir: string, baseDir: string): string[] {
const
res
=
path
.
join
(
dir
,
dirent
.
name
);
if
(
dirent
.
isDirectory
())
{
// For directories, concat the results of recursive call
// Exclude
node_modules and .git
directories
if
(
dirent
.
name
!==
"node_modules"
&&
dirent
.
name
!==
".git"
)
{
// Exclude
specified
directories
if
(
!
EXCLUDED_DIRS
.
includes
(
dirent
.
name
)
)
{
files
.
push
(...
getFilesRecursively
(
res
,
baseDir
));
}
}
else
{
...
...
src/lib/schemas.ts
浏览文件 @
a1aee5c2
...
...
@@ -93,7 +93,7 @@ export type Supabase = z.infer<typeof SupabaseSchema>;
export
const
ExperimentsSchema
=
z
.
object
({
// Deprecated
enableSupabaseIntegration
:
z
.
boolean
().
describe
(
"DEPRECATED"
).
optional
(),
enableFileEditing
:
z
.
boolean
().
optional
(),
enableFileEditing
:
z
.
boolean
().
describe
(
"DEPRECATED"
).
optional
(),
});
export
type
Experiments
=
z
.
infer
<
typeof
ExperimentsSchema
>
;
...
...
src/pages/settings.tsx
浏览文件 @
a1aee5c2
...
...
@@ -150,30 +150,6 @@ export default function SettingsPage() {
.
</
div
>
</
div
>
{
/* Enable File Editing Experiment */
}
<
div
className=
"space-y-1"
>
<
div
className=
"flex items-center space-x-2"
>
<
Switch
id=
"enable-file-editing"
checked=
{
!!
settings
?.
experiments
?.
enableFileEditing
}
onCheckedChange=
{
(
checked
)
=>
updateSettings
({
experiments
:
{
...
settings
?.
experiments
,
enableFileEditing
:
checked
,
},
})
}
/>
<
Label
htmlFor=
"enable-file-editing"
>
Enable File Editing
</
Label
>
</
div
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400"
>
File editing is not reliable and requires you to manually
commit changes and update Supabase edge functions.
</
div
>
</
div
>
</
div
>
</
div
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论