Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
35b459d8
Unverified
提交
35b459d8
authored
5月 15, 2025
作者:
Will Chen
提交者:
GitHub
5月 15, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Support turbo edits (pro) (#166)
上级
d545babb
隐藏空白字符变更
内嵌
并排
正在显示
12 个修改的文件
包含
400 行增加
和
26 行删除
+400
-26
package.json
package.json
+1
-0
ProModeSelector.tsx
src/components/ProModeSelector.tsx
+30
-0
DyadEdit.tsx
src/components/chat/DyadEdit.tsx
+110
-0
DyadMarkdownParser.tsx
src/components/chat/DyadMarkdownParser.tsx
+18
-0
chat_stream_handlers.ts
src/ipc/handlers/chat_stream_handlers.ts
+9
-5
debug_handlers.ts
src/ipc/handlers/debug_handlers.ts
+1
-1
proposal_handlers.ts
src/ipc/handlers/proposal_handlers.ts
+2
-1
token_count_handlers.ts
src/ipc/handlers/token_count_handlers.ts
+1
-1
get_model_client.ts
src/ipc/utils/get_model_client.ts
+36
-10
llm_engine_provider.ts
src/ipc/utils/llm_engine_provider.ts
+159
-0
schemas.ts
src/lib/schemas.ts
+1
-0
codebase.ts
src/utils/codebase.ts
+32
-8
没有找到文件。
package.json
浏览文件 @
35b459d8
...
...
@@ -14,6 +14,7 @@
"scripts"
:
{
"clean"
:
"rm -rf out && rm -rf scaffold/node_modules"
,
"start"
:
"electron-forge start"
,
"dev:engine"
:
"DYAD_LOCAL_ENGINE=http://localhost:8080/v1 npm start"
,
"package"
:
"npm run clean && electron-forge package"
,
"make"
:
"npm run clean && electron-forge make"
,
"publish"
:
"npm run clean && electron-forge publish"
,
...
...
src/components/ProModeSelector.tsx
浏览文件 @
35b459d8
...
...
@@ -20,6 +20,13 @@ export function ProModeSelector() {
const
toggleSaverMode
=
()
=>
{
updateSettings
({
enableProSaverMode
:
!
settings
?.
enableProSaverMode
});
};
const
toggleLazyEdits
=
()
=>
{
updateSettings
({
enableProLazyEditsMode
:
!
settings
?.
enableProLazyEditsMode
,
});
};
if
(
!
settings
?.
enableDyadPro
)
{
return
null
;
}
...
...
@@ -75,6 +82,29 @@ export function ProModeSelector() {
onCheckedChange=
{
toggleSaverMode
}
/>
</
div
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"space-y-2"
>
<
Label
htmlFor=
"lazy-edits"
>
Turbo Edits
</
Label
>
<
div
className=
"flex items-center gap-1"
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
Info
className=
"h-4 w-4 text-muted-foreground cursor-help"
/>
</
TooltipTrigger
>
<
TooltipContent
side=
"right"
className=
"max-w-72"
>
Edits files faster.
</
TooltipContent
>
</
Tooltip
>
<
p
className=
"text-xs text-muted-foreground max-w-55"
>
Makes editing files faster and cheaper.
</
p
>
</
div
>
</
div
>
<
Switch
id=
"lazy-edits"
checked=
{
Boolean
(
settings
?.
enableProLazyEditsMode
)
}
onCheckedChange=
{
toggleLazyEdits
}
/>
</
div
>
</
div
>
</
PopoverContent
>
</
Popover
>
...
...
src/components/chat/DyadEdit.tsx
0 → 100644
浏览文件 @
35b459d8
import
type
React
from
"react"
;
import
type
{
ReactNode
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
ChevronsDownUp
,
ChevronsUpDown
,
Loader
,
CircleX
,
Rabbit
,
}
from
"lucide-react"
;
import
{
CodeHighlight
}
from
"./CodeHighlight"
;
import
{
CustomTagState
}
from
"./stateTypes"
;
interface
DyadEditProps
{
children
?:
ReactNode
;
node
?:
any
;
path
?:
string
;
description
?:
string
;
}
export
const
DyadEdit
:
React
.
FC
<
DyadEditProps
>
=
({
children
,
node
,
path
:
pathProp
,
description
:
descriptionProp
,
})
=>
{
const
[
isContentVisible
,
setIsContentVisible
]
=
useState
(
false
);
// Use props directly if provided, otherwise extract from node
const
path
=
pathProp
||
node
?.
properties
?.
path
||
""
;
const
description
=
descriptionProp
||
node
?.
properties
?.
description
||
""
;
const
state
=
node
?.
properties
?.
state
as
CustomTagState
;
const
inProgress
=
state
===
"pending"
;
const
aborted
=
state
===
"aborted"
;
// Extract filename from path
const
fileName
=
path
?
path
.
split
(
"/"
).
pop
()
:
""
;
return
(
<
div
className=
{
`bg-(--background-lightest) hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
inProgress
? "border-amber-500"
: aborted
? "border-red-500"
: "border-border"
}`
}
onClick=
{
()
=>
setIsContentVisible
(
!
isContentVisible
)
}
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center"
>
<
Rabbit
size=
{
16
}
/>
<
span
className=
"bg-blue-500 text-white text-xs px-1.5 py-0.5 rounded ml-1 font-medium"
>
Turbo Edit
</
span
>
</
div
>
{
fileName
&&
(
<
span
className=
"text-gray-700 dark:text-gray-300 font-medium text-sm"
>
{
fileName
}
</
span
>
)
}
{
inProgress
&&
(
<
div
className=
"flex items-center text-amber-600 text-xs"
>
<
Loader
size=
{
14
}
className=
"mr-1 animate-spin"
/>
<
span
>
Editing...
</
span
>
</
div
>
)
}
{
aborted
&&
(
<
div
className=
"flex items-center text-red-600 text-xs"
>
<
CircleX
size=
{
14
}
className=
"mr-1"
/>
<
span
>
Did not finish
</
span
>
</
div
>
)
}
</
div
>
<
div
className=
"flex items-center"
>
{
isContentVisible
?
(
<
ChevronsDownUp
size=
{
20
}
className=
"text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
/>
)
:
(
<
ChevronsUpDown
size=
{
20
}
className=
"text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
/>
)
}
</
div
>
</
div
>
{
path
&&
(
<
div
className=
"text-xs text-gray-500 dark:text-gray-400 font-medium mb-1"
>
{
path
}
</
div
>
)
}
{
description
&&
(
<
div
className=
"text-sm text-gray-600 dark:text-gray-300"
>
<
span
className=
"font-medium"
>
Summary:
</
span
>
{
description
}
</
div
>
)
}
{
isContentVisible
&&
(
<
div
className=
"text-xs"
>
<
CodeHighlight
className=
"language-typescript"
>
{
children
}
</
CodeHighlight
>
</
div
>
)
}
</
div
>
);
};
src/components/chat/DyadMarkdownParser.tsx
浏览文件 @
35b459d8
...
...
@@ -7,6 +7,7 @@ import { DyadDelete } from "./DyadDelete";
import
{
DyadAddDependency
}
from
"./DyadAddDependency"
;
import
{
DyadExecuteSql
}
from
"./DyadExecuteSql"
;
import
{
DyadAddIntegration
}
from
"./DyadAddIntegration"
;
import
{
DyadEdit
}
from
"./DyadEdit"
;
import
{
CodeHighlight
}
from
"./CodeHighlight"
;
import
{
useAtomValue
}
from
"jotai"
;
import
{
isStreamingAtom
}
from
"@/atoms/chatAtoms"
;
...
...
@@ -115,6 +116,7 @@ function preprocessUnclosedTags(content: string): {
"dyad-add-integration"
,
"dyad-output"
,
"dyad-chat-summary"
,
"dyad-edit"
,
];
let
processedContent
=
content
;
...
...
@@ -177,6 +179,7 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-add-integration"
,
"dyad-output"
,
"dyad-chat-summary"
,
"dyad-edit"
,
];
const
tagPattern
=
new
RegExp
(
...
...
@@ -344,6 +347,21 @@ function renderCustomTag(
</
DyadAddIntegration
>
);
case
"dyad-edit"
:
return
(
<
DyadEdit
node=
{
{
properties
:
{
path
:
attributes
.
path
||
""
,
description
:
attributes
.
description
||
""
,
state
:
getState
({
isStreaming
,
inProgress
}),
},
}
}
>
{
content
}
</
DyadEdit
>
);
case
"dyad-output"
:
return
(
<
DyadOutput
...
...
src/ipc/handlers/chat_stream_handlers.ts
浏览文件 @
35b459d8
...
...
@@ -215,17 +215,16 @@ export function registerChatStreamHandlers() {
}
else
{
// Normal AI processing for non-test prompts
const
settings
=
readSettings
();
const
{
modelClient
,
backupModelClients
}
=
await
getModelClient
(
settings
.
selectedModel
,
settings
,
);
// Extract codebase information if app is associated with the chat
let
codebaseInfo
=
""
;
let
files
:
{
path
:
string
;
content
:
string
}[]
=
[];
if
(
updatedChat
.
app
)
{
const
appPath
=
getDyadAppPath
(
updatedChat
.
app
.
path
);
try
{
codebaseInfo
=
await
extractCodebase
(
appPath
);
const
out
=
await
extractCodebase
(
appPath
);
codebaseInfo
=
out
.
formattedOutput
;
files
=
out
.
files
;
logger
.
log
(
`Extracted codebase information from
${
appPath
}
`
);
}
catch
(
error
)
{
logger
.
error
(
"Error extracting codebase:"
,
error
);
...
...
@@ -237,6 +236,11 @@ export function registerChatStreamHandlers() {
"estimated tokens"
,
codebaseInfo
.
length
/
4
,
);
const
{
modelClient
,
backupModelClients
}
=
await
getModelClient
(
settings
.
selectedModel
,
settings
,
files
,
);
// Prepare message history for the AI
const
messageHistory
=
updatedChat
.
messages
.
map
((
message
)
=>
({
...
...
src/ipc/handlers/debug_handlers.ts
浏览文件 @
35b459d8
...
...
@@ -140,7 +140,7 @@ export function registerDebugHandlers() {
// Extract codebase
const
appPath
=
getDyadAppPath
(
app
.
path
);
const
codebase
=
await
extractCodebase
(
appPath
)
;
const
codebase
=
(
await
extractCodebase
(
appPath
)).
formattedOutput
;
return
{
debugInfo
,
...
...
src/ipc/handlers/proposal_handlers.ts
浏览文件 @
35b459d8
...
...
@@ -92,7 +92,8 @@ async function getCodebaseTokenCount(
// Calculate and cache the token count
logger
.
log
(
`Calculating codebase token count for chatId:
${
chatId
}
`
);
const
codebase
=
await
extractCodebase
(
getDyadAppPath
(
appPath
));
const
codebase
=
(
await
extractCodebase
(
getDyadAppPath
(
appPath
)))
.
formattedOutput
;
const
tokenCount
=
estimateTokens
(
codebase
);
// Store in cache
...
...
src/ipc/handlers/token_count_handlers.ts
浏览文件 @
35b459d8
...
...
@@ -68,7 +68,7 @@ export function registerTokenCountHandlers() {
if
(
chat
.
app
)
{
const
appPath
=
getDyadAppPath
(
chat
.
app
.
path
);
codebaseInfo
=
await
extractCodebase
(
appPath
)
;
codebaseInfo
=
(
await
extractCodebase
(
appPath
)).
formattedOutput
;
codebaseTokens
=
estimateTokens
(
codebaseInfo
);
logger
.
log
(
`Extracted codebase information from
${
appPath
}
, tokens:
${
codebaseTokens
}
`
,
...
...
src/ipc/utils/get_model_client.ts
浏览文件 @
35b459d8
...
...
@@ -11,6 +11,9 @@ import log from "electron-log";
import
{
getLanguageModelProviders
}
from
"../shared/language_model_helpers"
;
import
{
LanguageModelProvider
}
from
"../ipc_types"
;
import
{
llmErrorStore
}
from
"@/main/llm_error_store"
;
import
{
createDyadEngine
}
from
"./llm_engine_provider"
;
const
dyadLocalEngine
=
process
.
env
.
DYAD_LOCAL_ENGINE
;
const
AUTO_MODELS
=
[
{
...
...
@@ -32,10 +35,16 @@ export interface ModelClient {
builtinProviderId
?:
string
;
}
interface
File
{
path
:
string
;
content
:
string
;
}
const
logger
=
log
.
scope
(
"getModelClient"
);
export
async
function
getModelClient
(
model
:
LargeLanguageModel
,
settings
:
UserSettings
,
files
?:
File
[],
):
Promise
<
{
modelClient
:
ModelClient
;
backupModelClients
:
ModelClient
[];
...
...
@@ -65,8 +74,9 @@ export async function getModelClient(
{
provider
:
autoModel
.
provider
,
name
:
autoModel
.
name
,
}
as
LargeLanguageModel
,
},
settings
,
files
,
);
}
}
...
...
@@ -85,17 +95,33 @@ export async function getModelClient(
// Handle Dyad Pro override
if
(
dyadApiKey
&&
settings
.
enableDyadPro
)
{
// Check if the selected provider supports Dyad Pro (has a gateway prefix)
if
(
providerConfig
.
gatewayPrefix
)
{
const
provider
=
createOpenAI
({
apiKey
:
dyadApiKey
,
baseURL
:
"https://llm-gateway.dyad.sh/v1"
,
});
logger
.
info
(
"Using Dyad Pro API key via Gateway"
);
// Check if the selected provider supports Dyad Pro (has a gateway prefix) OR
// we're using local engine.
if
(
providerConfig
.
gatewayPrefix
||
dyadLocalEngine
)
{
const
provider
=
settings
.
enableProLazyEditsMode
?
createDyadEngine
({
apiKey
:
dyadApiKey
,
baseURL
:
dyadLocalEngine
??
"https://engine.dyad.sh/v1"
,
})
:
createOpenAI
({
apiKey
:
dyadApiKey
,
baseURL
:
"https://llm-gateway.dyad.sh/v1"
,
});
logger
.
info
(
`Using Dyad Pro API key. engine_enabled=
${
settings
.
enableProLazyEditsMode
}
`
,
);
// Do not use free variant (for openrouter).
const
modelName
=
model
.
name
.
split
(
":free"
)[
0
];
const
autoModelClient
=
{
model
:
provider
(
`
${
providerConfig
.
gatewayPrefix
}${
modelName
}
`
),
model
:
provider
(
`
${
providerConfig
.
gatewayPrefix
||
""
}${
modelName
}
`
,
settings
.
enableProLazyEditsMode
?
{
files
,
}
:
undefined
,
),
builtinProviderId
:
"auto"
,
};
const
googleSettings
=
settings
.
providerSettings
?.
google
;
...
...
@@ -235,7 +261,7 @@ function getRegularModelClient(
const
provider
=
createOpenAICompatible
({
name
:
providerConfig
.
id
,
baseURL
:
providerConfig
.
apiBaseUrl
,
apiKey
:
apiKey
,
apiKey
,
});
return
{
modelClient
:
{
...
...
src/ipc/utils/llm_engine_provider.ts
0 → 100644
浏览文件 @
35b459d8
import
{
LanguageModelV1
,
LanguageModelV1ObjectGenerationMode
,
}
from
"@ai-sdk/provider"
;
import
{
OpenAICompatibleChatLanguageModel
}
from
"@ai-sdk/openai-compatible"
;
import
{
FetchFunction
,
loadApiKey
,
withoutTrailingSlash
,
}
from
"@ai-sdk/provider-utils"
;
import
{
OpenAICompatibleChatSettings
}
from
"@ai-sdk/openai-compatible"
;
import
log
from
"electron-log"
;
const
logger
=
log
.
scope
(
"llm_engine_provider"
);
export
type
ExampleChatModelId
=
string
&
{};
export
interface
ExampleChatSettings
extends
OpenAICompatibleChatSettings
{
files
?:
{
path
:
string
;
content
:
string
}[];
}
export
interface
ExampleProviderSettings
{
/**
Example API key.
*/
apiKey
?:
string
;
/**
Base URL for the API calls.
*/
baseURL
?:
string
;
/**
Custom headers to include in the requests.
*/
headers
?:
Record
<
string
,
string
>
;
/**
Optional custom url query parameters to include in request urls.
*/
queryParams
?:
Record
<
string
,
string
>
;
/**
Custom fetch implementation. You can use it as a middleware to intercept requests,
or to provide a custom fetch implementation for e.g. testing.
*/
fetch
?:
FetchFunction
;
}
export
interface
DyadEngineProvider
{
/**
Creates a model for text generation.
*/
(
modelId
:
ExampleChatModelId
,
settings
?:
ExampleChatSettings
,
):
LanguageModelV1
;
/**
Creates a chat model for text generation.
*/
chatModel
(
modelId
:
ExampleChatModelId
,
settings
?:
ExampleChatSettings
,
):
LanguageModelV1
;
}
export
function
createDyadEngine
(
options
:
ExampleProviderSettings
=
{},
):
DyadEngineProvider
{
const
baseURL
=
withoutTrailingSlash
(
options
.
baseURL
??
"https://api.example.com/v1"
,
);
const
getHeaders
=
()
=>
({
Authorization
:
`Bearer
${
loadApiKey
({
apiKey
:
options
.
apiKey
,
environmentVariableName
:
"DYAD_PRO_API_KEY"
,
description
:
"Example API key"
,
})}
`
,
...
options
.
headers
,
});
interface
CommonModelConfig
{
provider
:
string
;
url
:
({
path
}:
{
path
:
string
})
=>
string
;
headers
:
()
=>
Record
<
string
,
string
>
;
fetch
?:
FetchFunction
;
}
const
getCommonModelConfig
=
(
modelType
:
string
):
CommonModelConfig
=>
({
provider
:
`example.
${
modelType
}
`
,
url
:
({
path
})
=>
{
const
url
=
new
URL
(
`
${
baseURL
}${
path
}
`
);
if
(
options
.
queryParams
)
{
url
.
search
=
new
URLSearchParams
(
options
.
queryParams
).
toString
();
}
return
url
.
toString
();
},
headers
:
getHeaders
,
fetch
:
options
.
fetch
,
});
const
createChatModel
=
(
modelId
:
ExampleChatModelId
,
settings
:
ExampleChatSettings
=
{},
)
=>
{
// Extract files from settings to process them appropriately
const
{
files
,
...
restSettings
}
=
settings
;
// Create configuration with file handling
const
config
=
{
...
getCommonModelConfig
(
"chat"
),
defaultObjectGenerationMode
:
"tool"
as
LanguageModelV1ObjectGenerationMode
,
// Custom fetch implementation that adds files to the request
fetch
:
files
?.
length
?
(
input
:
RequestInfo
|
URL
,
init
?:
RequestInit
)
=>
{
// Use default fetch if no init or body
if
(
!
init
||
!
init
.
body
||
typeof
init
.
body
!==
"string"
)
{
return
(
options
.
fetch
||
fetch
)(
input
,
init
);
}
try
{
// Parse the request body to manipulate it
const
parsedBody
=
JSON
.
parse
(
init
.
body
);
// Add files to the request if they exist
if
(
files
?.
length
)
{
parsedBody
.
dyad_options
=
{
files
,
enable_lazy_edits
:
true
,
};
}
// Return modified request with files included
const
modifiedInit
=
{
...
init
,
body
:
JSON
.
stringify
(
parsedBody
),
};
// Use the provided fetch or default fetch
return
(
options
.
fetch
||
fetch
)(
input
,
modifiedInit
);
}
catch
(
e
)
{
logger
.
error
(
"Error parsing request body"
,
e
);
// If parsing fails, use original request
return
(
options
.
fetch
||
fetch
)(
input
,
init
);
}
}
:
options
.
fetch
,
};
return
new
OpenAICompatibleChatLanguageModel
(
modelId
,
restSettings
,
config
);
};
const
provider
=
(
modelId
:
ExampleChatModelId
,
settings
?:
ExampleChatSettings
,
)
=>
createChatModel
(
modelId
,
settings
);
provider
.
chatModel
=
createChatModel
;
return
provider
;
}
src/lib/schemas.ts
浏览文件 @
35b459d8
...
...
@@ -119,6 +119,7 @@ export const UserSettingsSchema = z.object({
lastShownReleaseNotesVersion
:
z
.
string
().
optional
(),
maxChatTurnsInContext
:
z
.
number
().
optional
(),
enableProSaverMode
:
z
.
boolean
().
optional
(),
enableProLazyEditsMode
:
z
.
boolean
().
optional
(),
// DEPRECATED.
runtimeMode
:
RuntimeModeSchema
.
optional
(),
});
...
...
src/utils/codebase.ts
浏览文件 @
35b459d8
...
...
@@ -275,13 +275,19 @@ ${content}
/**
* Extract and format codebase files as a string to be included in prompts
* @param appPath - Path to the codebase to extract
* @returns
A string containing formatted file content
s
* @returns
Object containing formatted output and individual file
s
*/
export
async
function
extractCodebase
(
appPath
:
string
):
Promise
<
string
>
{
export
async
function
extractCodebase
(
appPath
:
string
):
Promise
<
{
formattedOutput
:
string
;
files
:
{
path
:
string
;
content
:
string
}[];
}
>
{
try
{
await
fsAsync
.
access
(
appPath
);
}
catch
{
return
`# Error: Directory
${
appPath
}
does not exist or is not accessible`
;
return
{
formattedOutput
:
`# Error: Directory
${
appPath
}
does not exist or is not accessible`
,
files
:
[],
};
}
const
startTime
=
Date
.
now
();
...
...
@@ -292,15 +298,33 @@ export async function extractCodebase(appPath: string): Promise<string> {
// This is important for cache-ability.
const
sortedFiles
=
await
sortFilesByModificationTime
(
files
);
// Format files
let
output
=
""
;
const
formatPromises
=
sortedFiles
.
map
((
file
)
=>
formatFile
(
file
,
appPath
));
// Format files and collect individual file contents
const
filesArray
:
{
path
:
string
;
content
:
string
}[]
=
[];
const
formatPromises
=
sortedFiles
.
map
(
async
(
file
)
=>
{
const
formattedContent
=
await
formatFile
(
file
,
appPath
);
// Get raw content for the files array
const
relativePath
=
path
.
relative
(
appPath
,
file
);
const
rawContent
=
await
readFileWithCache
(
file
);
if
(
rawContent
!==
null
)
{
filesArray
.
push
({
path
:
relativePath
,
content
:
rawContent
,
});
}
return
formattedContent
;
});
const
formattedFiles
=
await
Promise
.
all
(
formatPromises
);
o
utput
=
formattedFiles
.
join
(
""
);
const
formattedO
utput
=
formattedFiles
.
join
(
""
);
const
endTime
=
Date
.
now
();
logger
.
log
(
"extractCodebase: time taken"
,
endTime
-
startTime
);
return
output
;
return
{
formattedOutput
,
files
:
filesArray
,
};
}
/**
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论