Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
9869fefb
Unverified
提交
9869fefb
authored
8月 26, 2025
作者:
Will Chen
提交者:
GitHub
8月 26, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Support dyad docker (#674)
TODOs: - [ ] clean-up docker images
https://claude.ai/chat/13b2c5d3-0d46-49e3-a771-d10edf1e29f4
上级
e6c92a24
全部展开
隐藏空白字符变更
内嵌
并排
正在显示
6 个修改的文件
包含
147 行增加
和
14 行删除
+147
-14
RuntimeModeSelector.tsx
src/components/RuntimeModeSelector.tsx
+74
-0
PreviewIframe.tsx
src/components/preview_panel/PreviewIframe.tsx
+18
-13
app_handlers.ts
src/ipc/handlers/app_handlers.ts
+0
-0
process_manager.ts
src/ipc/utils/process_manager.ts
+46
-1
schemas.ts
src/lib/schemas.ts
+4
-0
settings.tsx
src/pages/settings.tsx
+5
-0
没有找到文件。
src/components/RuntimeModeSelector.tsx
0 → 100644
浏览文件 @
9869fefb
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
,
}
from
"@/components/ui/select"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
showError
}
from
"@/lib/toast"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
export
function
RuntimeModeSelector
()
{
const
{
settings
,
updateSettings
}
=
useSettings
();
if
(
!
settings
)
{
return
null
;
}
const
isDockerMode
=
settings
?.
runtimeMode2
===
"docker"
;
const
handleRuntimeModeChange
=
async
(
value
:
"host"
|
"docker"
)
=>
{
try
{
await
updateSettings
({
runtimeMode2
:
value
});
}
catch
(
error
:
any
)
{
showError
(
`Failed to update runtime mode:
${
error
.
message
}
`
);
}
};
return
(
<
div
className=
"space-y-2"
>
<
div
className=
"space-y-1"
>
<
div
className=
"flex items-center space-x-2"
>
<
Label
className=
"text-sm font-medium"
htmlFor=
"runtime-mode"
>
Runtime Mode
</
Label
>
<
Select
value=
{
settings
.
runtimeMode2
??
"host"
}
onValueChange=
{
handleRuntimeModeChange
}
>
<
SelectTrigger
className=
"w-48"
id=
"runtime-mode"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
<
SelectItem
value=
"host"
>
Local (default)
</
SelectItem
>
<
SelectItem
value=
"docker"
>
Docker (experimental)
</
SelectItem
>
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400"
>
Choose whether to run apps directly on the local machine or in Docker
containers
</
div
>
</
div
>
{
isDockerMode
&&
(
<
div
className=
"text-sm text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 p-2 rounded"
>
⚠️ Docker mode is
<
b
>
experimental
</
b
>
and requires
{
" "
}
<
button
type=
"button"
className=
"underline font-medium cursor-pointer"
onClick=
{
()
=>
IpcClient
.
getInstance
().
openExternalUrl
(
"https://www.docker.com/products/docker-desktop/"
,
)
}
>
Docker Desktop
</
button
>
{
" "
}
to be installed and running
</
div
>
)
}
</
div
>
);
}
src/components/preview_panel/PreviewIframe.tsx
浏览文件 @
9869fefb
...
...
@@ -51,10 +51,11 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
const
[
isCollapsed
,
setIsCollapsed
]
=
useState
(
true
);
const
{
isStreaming
}
=
useStreamChat
();
if
(
!
error
)
return
null
;
const
isDockerError
=
error
.
includes
(
"Cannot connect to the Docker"
);
const
getTruncatedError
=
()
=>
{
const
firstLine
=
error
.
split
(
"
\
n"
)[
0
];
const
snippetLength
=
2
0
0
;
const
snippetLength
=
2
5
0
;
const
snippet
=
error
.
substring
(
0
,
snippetLength
);
return
firstLine
.
length
<
snippet
.
length
?
firstLine
...
...
@@ -97,23 +98,27 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
<
Lightbulb
size=
{
16
}
className=
" text-red-800 dark:text-red-300"
/>
</
div
>
<
span
className=
"text-sm text-red-700 dark:text-red-200"
>
<
span
className=
"font-medium"
>
Tip:
</
span
>
Check if restarting the
app fixes the error.
<
span
className=
"font-medium"
>
Tip:
</
span
>
{
isDockerError
?
"Make sure Docker Desktop is running and try restarting the app."
:
"Check if restarting the app fixes the error."
}
</
span
>
</
div
>
</
div
>
{
/* AI Fix button at the bottom */
}
<
div
className=
"mt-2 flex justify-end"
>
<
button
disabled=
{
isStreaming
}
onClick=
{
onAIFix
}
className=
"cursor-pointer flex items-center space-x-1 px-2 py-0.5 bg-red-500 dark:bg-red-600 text-white rounded text-sm hover:bg-red-600 dark:hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<
Sparkles
size=
{
14
}
/>
<
span
>
Fix error with AI
</
span
>
</
button
>
</
div
>
{
!
isDockerError
&&
(
<
div
className=
"mt-2 flex justify-end"
>
<
button
disabled=
{
isStreaming
}
onClick=
{
onAIFix
}
className=
"cursor-pointer flex items-center space-x-1 px-2 py-0.5 bg-red-500 dark:bg-red-600 text-white rounded text-sm hover:bg-red-600 dark:hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<
Sparkles
size=
{
14
}
/>
<
span
>
Fix error with AI
</
span
>
</
button
>
</
div
>
)
}
</
div
>
);
};
...
...
src/ipc/handlers/app_handlers.ts
浏览文件 @
9869fefb
差异被折叠。
点击展开。
src/ipc/utils/process_manager.ts
浏览文件 @
9869fefb
import
{
ChildProcess
}
from
"node:child_process"
;
import
{
ChildProcess
,
spawn
}
from
"node:child_process"
;
import
treeKill
from
"tree-kill"
;
// Define a type for the value stored in runningApps
export
interface
RunningAppInfo
{
process
:
ChildProcess
;
processId
:
number
;
isDocker
:
boolean
;
containerName
?:
string
;
}
// Store running app processes
...
...
@@ -81,6 +83,49 @@ export function killProcess(process: ChildProcess): Promise<void> {
});
}
/**
* Gracefully stops a Docker container by name. Resolves even if the container doesn't exist.
*/
export
function
stopDockerContainer
(
containerName
:
string
):
Promise
<
void
>
{
return
new
Promise
<
void
>
((
resolve
)
=>
{
const
stop
=
spawn
(
"docker"
,
[
"stop"
,
containerName
],
{
stdio
:
"pipe"
});
stop
.
on
(
"close"
,
()
=>
resolve
());
stop
.
on
(
"error"
,
()
=>
resolve
());
});
}
/**
* Removes Docker named volumes used for an app's dependencies.
* Best-effort: resolves even if volumes don't exist.
*/
export
function
removeDockerVolumesForApp
(
appId
:
number
):
Promise
<
void
>
{
return
new
Promise
<
void
>
((
resolve
)
=>
{
const
pnpmVolume
=
`dyad-pnpm-
${
appId
}
`
;
const
rm
=
spawn
(
"docker"
,
[
"volume"
,
"rm"
,
"-f"
,
pnpmVolume
],
{
stdio
:
"pipe"
,
});
rm
.
on
(
"close"
,
()
=>
resolve
());
rm
.
on
(
"error"
,
()
=>
resolve
());
});
}
/**
* Stops an app based on its RunningAppInfo (container vs host) and removes it from the running map.
*/
export
async
function
stopAppByInfo
(
appId
:
number
,
appInfo
:
RunningAppInfo
,
):
Promise
<
void
>
{
if
(
appInfo
.
isDocker
)
{
const
containerName
=
appInfo
.
containerName
||
`dyad-app-
${
appId
}
`
;
await
stopDockerContainer
(
containerName
);
}
else
{
await
killProcess
(
appInfo
.
process
);
}
runningApps
.
delete
(
appId
);
}
/**
* Removes an app from the running apps map if it's the current process
* @param appId The app ID
...
...
src/lib/schemas.ts
浏览文件 @
9869fefb
...
...
@@ -69,6 +69,9 @@ export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
export
const
RuntimeModeSchema
=
z
.
enum
([
"web-sandbox"
,
"local-node"
,
"unset"
]);
export
type
RuntimeMode
=
z
.
infer
<
typeof
RuntimeModeSchema
>
;
export
const
RuntimeMode2Schema
=
z
.
enum
([
"host"
,
"docker"
]);
export
type
RuntimeMode2
=
z
.
infer
<
typeof
RuntimeMode2Schema
>
;
export
const
ChatModeSchema
=
z
.
enum
([
"build"
,
"ask"
]);
export
type
ChatMode
=
z
.
infer
<
typeof
ChatModeSchema
>
;
...
...
@@ -170,6 +173,7 @@ export const UserSettingsSchema = z.object({
enableNativeGit
:
z
.
boolean
().
optional
(),
enableAutoUpdate
:
z
.
boolean
(),
releaseChannel
:
ReleaseChannelSchema
,
runtimeMode2
:
RuntimeMode2Schema
.
optional
(),
////////////////////////////////
// E2E TESTING ONLY.
...
...
src/pages/settings.tsx
浏览文件 @
9869fefb
...
...
@@ -23,6 +23,7 @@ import { AutoFixProblemsSwitch } from "@/components/AutoFixProblemsSwitch";
import
{
AutoUpdateSwitch
}
from
"@/components/AutoUpdateSwitch"
;
import
{
ReleaseChannelSelector
}
from
"@/components/ReleaseChannelSelector"
;
import
{
NeonIntegration
}
from
"@/components/NeonIntegration"
;
import
{
RuntimeModeSelector
}
from
"@/components/RuntimeModeSelector"
;
export
default
function
SettingsPage
()
{
const
[
isResetDialogOpen
,
setIsResetDialogOpen
]
=
useState
(
false
);
...
...
@@ -256,6 +257,10 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
<
ReleaseChannelSelector
/>
</
div
>
<
div
className=
"mt-4"
>
<
RuntimeModeSelector
/>
</
div
>
<
div
className=
"flex items-center text-sm text-gray-500 dark:text-gray-400 mt-4"
>
<
span
className=
"mr-2 font-medium"
>
App Version:
</
span
>
<
span
className=
"bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono"
>
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论