Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
f9f33596
Unverified
提交
f9f33596
authored
5月 17, 2025
作者:
Will Chen
提交者:
GitHub
5月 17, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Smart files context (#184)
上级
2455c554
显示空白字符变更
内嵌
并排
正在显示
8 个修改的文件
包含
217 行增加
和
24 行删除
+217
-24
ProModeSelector.tsx
src/components/ProModeSelector.tsx
+30
-0
DyadCodebaseContext.tsx
src/components/chat/DyadCodebaseContext.tsx
+117
-0
DyadMarkdownParser.tsx
src/components/chat/DyadMarkdownParser.tsx
+17
-0
chat_stream_handlers.ts
src/ipc/handlers/chat_stream_handlers.ts
+10
-6
get_model_client.ts
src/ipc/utils/get_model_client.ts
+11
-2
llm_engine_provider.ts
src/ipc/utils/llm_engine_provider.ts
+9
-2
schemas.ts
src/lib/schemas.ts
+1
-0
codebase.ts
src/utils/codebase.ts
+22
-14
没有找到文件。
src/components/ProModeSelector.tsx
浏览文件 @
f9f33596
...
...
@@ -27,6 +27,12 @@ export function ProModeSelector() {
});
};
const
toggleSmartContext
=
()
=>
{
updateSettings
({
enableProSmartFilesContextMode
:
!
settings
?.
enableProSmartFilesContextMode
,
});
};
if
(
!
settings
?.
enableDyadPro
)
{
return
null
;
}
...
...
@@ -105,6 +111,30 @@ export function ProModeSelector() {
onCheckedChange=
{
toggleLazyEdits
}
/>
</
div
>
<
div
className=
"flex items-center justify-between"
>
<
div
className=
"space-y-2"
>
<
Label
htmlFor=
"smart-context"
>
Smart Context
</
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"
>
Improve efficiency and save credits working on large
codebases.
</
TooltipContent
>
</
Tooltip
>
<
p
className=
"text-xs text-muted-foreground max-w-55"
>
Automatically detects the most relevant files for your chat
</
p
>
</
div
>
</
div
>
<
Switch
id=
"smart-context"
checked=
{
Boolean
(
settings
?.
enableProSmartFilesContextMode
)
}
onCheckedChange=
{
toggleSmartContext
}
/>
</
div
>
</
div
>
</
PopoverContent
>
</
Popover
>
...
...
src/components/chat/DyadCodebaseContext.tsx
0 → 100644
浏览文件 @
f9f33596
import
React
,
{
useState
,
useEffect
}
from
"react"
;
import
{
ChevronUp
,
ChevronDown
,
Code2
,
FileText
}
from
"lucide-react"
;
import
{
CustomTagState
}
from
"./stateTypes"
;
interface
DyadCodebaseContextProps
{
children
:
React
.
ReactNode
;
node
?:
{
properties
?:
{
files
?:
string
;
state
?:
CustomTagState
;
};
};
}
export
const
DyadCodebaseContext
:
React
.
FC
<
DyadCodebaseContextProps
>
=
({
node
,
})
=>
{
const
state
=
node
?.
properties
?.
state
as
CustomTagState
;
const
inProgress
=
state
===
"pending"
;
const
[
isExpanded
,
setIsExpanded
]
=
useState
(
inProgress
);
const
files
=
node
?.
properties
?.
files
?.
split
(
","
)
||
[];
// Collapse when transitioning from in-progress to not-in-progress
useEffect
(()
=>
{
if
(
!
inProgress
&&
isExpanded
)
{
setIsExpanded
(
false
);
}
},
[
inProgress
]);
return
(
<
div
className=
{
`relative bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
inProgress ? "border-blue-500" : "border-border"
}`
}
onClick=
{
()
=>
setIsExpanded
(
!
isExpanded
)
}
role=
"button"
aria
-
expanded=
{
isExpanded
}
tabIndex=
{
0
}
onKeyDown=
{
(
e
)
=>
{
if
(
e
.
key
===
"Enter"
||
e
.
key
===
" "
)
{
e
.
preventDefault
();
setIsExpanded
(
!
isExpanded
);
}
}
}
>
{
/* Top-left label badge */
}
<
div
className=
"absolute top-2 left-2 flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold text-blue-500 bg-white dark:bg-zinc-900"
style=
{
{
zIndex
:
1
}
}
>
<
Code2
size=
{
16
}
className=
"text-blue-500"
/>
<
span
>
Codebase Context
</
span
>
</
div
>
{
/* File count when collapsed */
}
{
files
.
length
>
0
&&
(
<
div
className=
"absolute top-2 left-40 flex items-center"
>
<
span
className=
"px-1.5 py-0.5 bg-gray-100 dark:bg-zinc-800 text-xs rounded text-gray-600 dark:text-gray-300"
>
Using
{
files
.
length
}
file
{
files
.
length
!==
1
?
"s"
:
""
}
</
span
>
</
div
>
)
}
{
/* Indicator icon */
}
<
div
className=
"absolute top-2 right-2 p-1 text-gray-500"
>
{
isExpanded
?
<
ChevronUp
size=
{
16
}
/>
:
<
ChevronDown
size=
{
16
}
/>
}
</
div
>
{
/* Main content with smooth transition */
}
<
div
className=
"pt-6 overflow-hidden transition-all duration-300 ease-in-out"
style=
{
{
maxHeight
:
isExpanded
?
"1000px"
:
"0px"
,
opacity
:
isExpanded
?
1
:
0
,
marginBottom
:
isExpanded
?
"0"
:
"-6px"
,
// Compensate for padding
}
}
>
{
/* File list when expanded */
}
{
files
.
length
>
0
&&
(
<
div
className=
"mb-3"
>
<
div
className=
"flex flex-wrap gap-2 mt-2"
>
{
files
.
map
((
file
,
index
)
=>
{
const
filePath
=
file
.
trim
();
const
fileName
=
filePath
.
split
(
"/"
).
pop
()
||
filePath
;
const
pathPart
=
filePath
.
substring
(
0
,
filePath
.
length
-
fileName
.
length
)
||
""
;
return
(
<
div
key=
{
index
}
className=
"px-2 py-1 bg-gray-100 dark:bg-zinc-800 rounded-lg"
>
<
div
className=
"flex items-center gap-1.5"
>
<
FileText
size=
{
14
}
className=
"text-gray-500 dark:text-gray-400 flex-shrink-0"
/>
<
div
className=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{
fileName
}
</
div
>
</
div
>
{
pathPart
&&
(
<
div
className=
"text-xs text-gray-500 dark:text-gray-400 ml-5"
>
{
pathPart
}
</
div
>
)
}
</
div
>
);
})
}
</
div
>
</
div
>
)
}
</
div
>
</
div
>
);
};
src/components/chat/DyadMarkdownParser.tsx
浏览文件 @
f9f33596
...
...
@@ -8,6 +8,7 @@ import { DyadAddDependency } from "./DyadAddDependency";
import
{
DyadExecuteSql
}
from
"./DyadExecuteSql"
;
import
{
DyadAddIntegration
}
from
"./DyadAddIntegration"
;
import
{
DyadEdit
}
from
"./DyadEdit"
;
import
{
DyadCodebaseContext
}
from
"./DyadCodebaseContext"
;
import
{
CodeHighlight
}
from
"./CodeHighlight"
;
import
{
useAtomValue
}
from
"jotai"
;
import
{
isStreamingAtom
}
from
"@/atoms/chatAtoms"
;
...
...
@@ -117,6 +118,7 @@ function preprocessUnclosedTags(content: string): {
"dyad-output"
,
"dyad-chat-summary"
,
"dyad-edit"
,
"dyad-codebase-context"
,
];
let
processedContent
=
content
;
...
...
@@ -180,6 +182,7 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-output"
,
"dyad-chat-summary"
,
"dyad-edit"
,
"dyad-codebase-context"
,
];
const
tagPattern
=
new
RegExp
(
...
...
@@ -362,6 +365,20 @@ function renderCustomTag(
</
DyadEdit
>
);
case
"dyad-codebase-context"
:
return
(
<
DyadCodebaseContext
node=
{
{
properties
:
{
files
:
attributes
.
files
||
""
,
state
:
getState
({
isStreaming
,
inProgress
}),
},
}
}
>
{
content
}
</
DyadCodebaseContext
>
);
case
"dyad-output"
:
return
(
<
DyadOutput
...
...
src/ipc/handlers/chat_stream_handlers.ts
浏览文件 @
f9f33596
...
...
@@ -236,11 +236,8 @@ export function registerChatStreamHandlers() {
"estimated tokens"
,
codebaseInfo
.
length
/
4
,
);
const
{
modelClient
,
backupModelClients
}
=
await
getModelClient
(
settings
.
selectedModel
,
settings
,
files
,
);
const
{
modelClient
,
backupModelClients
,
isEngineEnabled
}
=
await
getModelClient
(
settings
.
selectedModel
,
settings
,
files
);
// Prepare message history for the AI
const
messageHistory
=
updatedChat
.
messages
.
map
((
message
)
=>
({
...
...
@@ -328,7 +325,10 @@ This conversation includes one or more image attachments. When the user uploads
`
;
}
let
chatMessages
:
CoreMessage
[]
=
[
const
codebasePrefix
=
isEngineEnabled
?
// No codebase prefix if engine is set, we will take of it there.
[]
:
([
{
role
:
"user"
,
content
:
"This is my codebase. "
+
codebaseInfo
,
...
...
@@ -337,6 +337,10 @@ This conversation includes one or more image attachments. When the user uploads
role
:
"assistant"
,
content
:
"OK, got it. I'm ready to help"
,
},
]
as
const
);
let
chatMessages
:
CoreMessage
[]
=
[
...
codebasePrefix
,
...
limitedMessageHistory
.
map
((
msg
)
=>
({
role
:
msg
.
role
as
"user"
|
"assistant"
|
"system"
,
content
:
msg
.
content
,
...
...
src/ipc/utils/get_model_client.ts
浏览文件 @
f9f33596
...
...
@@ -50,6 +50,7 @@ export async function getModelClient(
):
Promise
<
{
modelClient
:
ModelClient
;
backupModelClients
:
ModelClient
[];
isEngineEnabled
?:
boolean
;
}
>
{
const
allProviders
=
await
getLanguageModelProviders
();
...
...
@@ -103,9 +104,12 @@ export async function getModelClient(
// so we do a nullish and not a truthy check here.
if
(
providerConfig
.
gatewayPrefix
!=
null
||
dyadLocalEngine
)
{
const
languageModel
=
await
findLanguageModel
(
model
);
const
engineProMode
=
settings
.
enableProSmartFilesContextMode
||
settings
.
enableProLazyEditsMode
;
// Currently engine is only used for turbo edits.
const
isEngineEnabled
=
Boolean
(
settings
.
enableProLazyEdits
Mode
&&
enginePro
Mode
&&
languageModel
?.
type
===
"cloud"
&&
languageModel
?.
supportsTurboEdits
,
);
...
...
@@ -113,6 +117,10 @@ export async function getModelClient(
?
createDyadEngine
({
apiKey
:
dyadApiKey
,
baseURL
:
dyadLocalEngine
??
"https://engine.dyad.sh/v1"
,
dyadOptions
:
{
enableLazyEdits
:
settings
.
enableProLazyEditsMode
,
enableSmartFilesContext
:
settings
.
enableProSmartFilesContextMode
,
},
})
:
createOpenAICompatible
({
name
:
"dyad-gateway"
,
...
...
@@ -126,7 +134,7 @@ export async function getModelClient(
const
autoModelClient
=
{
model
:
provider
(
`
${
providerConfig
.
gatewayPrefix
||
""
}${
modelName
}
`
,
settings
.
enableProLazyEdits
Mode
enginePro
Mode
?
{
files
,
}
...
...
@@ -161,6 +169,7 @@ export async function getModelClient(
providerConfig
,
).
modelClient
,
backupModelClients
:
[
autoModelClient
],
isEngineEnabled
,
};
}
else
{
return
{
...
...
src/ipc/utils/llm_engine_provider.ts
浏览文件 @
f9f33596
...
...
@@ -41,6 +41,11 @@ Custom fetch implementation. You can use it as a middleware to intercept request
or to provide a custom fetch implementation for e.g. testing.
*/
fetch
?:
FetchFunction
;
dyadOptions
:
{
enableLazyEdits
?:
boolean
;
enableSmartFilesContext
?:
boolean
;
};
}
export
interface
DyadEngineProvider
{
...
...
@@ -62,7 +67,7 @@ Creates a chat model for text generation.
}
export
function
createDyadEngine
(
options
:
ExampleProviderSettings
=
{}
,
options
:
ExampleProviderSettings
,
):
DyadEngineProvider
{
const
baseURL
=
withoutTrailingSlash
(
options
.
baseURL
??
"https://api.example.com/v1"
,
...
...
@@ -124,7 +129,9 @@ export function createDyadEngine(
if
(
files
?.
length
)
{
parsedBody
.
dyad_options
=
{
files
,
enable_lazy_edits
:
true
,
enable_lazy_edits
:
options
.
dyadOptions
.
enableLazyEdits
,
enable_smart_files_context
:
options
.
dyadOptions
.
enableSmartFilesContext
,
};
}
...
...
src/lib/schemas.ts
浏览文件 @
f9f33596
...
...
@@ -120,6 +120,7 @@ export const UserSettingsSchema = z.object({
maxChatTurnsInContext
:
z
.
number
().
optional
(),
enableProSaverMode
:
z
.
boolean
().
optional
(),
enableProLazyEditsMode
:
z
.
boolean
().
optional
(),
enableProSmartFilesContextMode
:
z
.
boolean
().
optional
(),
// DEPRECATED.
runtimeMode
:
RuntimeModeSchema
.
optional
(),
});
...
...
src/utils/codebase.ts
浏览文件 @
f9f33596
...
...
@@ -223,15 +223,9 @@ async function collectFiles(dir: string, baseDir: string): Promise<string[]> {
return
files
;
}
/**
* Format a file for inclusion in the codebase extract
*/
async
function
formatFile
(
filePath
:
string
,
baseDir
:
string
):
Promise
<
string
>
{
try
{
const
relativePath
=
path
.
relative
(
baseDir
,
filePath
);
// Skip large configuration files or generated code (just include the path)
if
(
// Skip large configuration files or generated code (just include the path)
function
isOmittedFile
(
relativePath
:
string
):
boolean
{
return
(
relativePath
.
includes
(
path
.
join
(
"src"
,
"components"
,
"ui"
))
||
relativePath
.
includes
(
"eslint.config"
)
||
relativePath
.
includes
(
"tsconfig.json"
)
||
...
...
@@ -239,9 +233,21 @@ async function formatFile(filePath: string, baseDir: string): Promise<string> {
// These should already be excluded based on file type, but
// just in case, we'll redact the contents here.
relativePath
.
includes
(
".env"
)
)
{
);
}
const
OMITTED_FILE_CONTENT
=
"// Contents omitted for brevity"
;
/**
* Format a file for inclusion in the codebase extract
*/
async
function
formatFile
(
filePath
:
string
,
baseDir
:
string
):
Promise
<
string
>
{
try
{
const
relativePath
=
path
.
relative
(
baseDir
,
filePath
);
if
(
isOmittedFile
(
relativePath
))
{
return
`<dyad-file path="
${
relativePath
}
">
// Contents omitted for brevity
${
OMITTED_FILE_CONTENT
}
</dyad-file>
`
;
...
...
@@ -305,11 +311,13 @@ export async function extractCodebase(appPath: string): Promise<{
// Get raw content for the files array
const
relativePath
=
path
.
relative
(
appPath
,
file
);
const
rawContent
=
await
readFileWithCache
(
file
);
if
(
rawContent
!==
null
)
{
const
fileContent
=
isOmittedFile
(
relativePath
)
?
OMITTED_FILE_CONTENT
:
await
readFileWithCache
(
file
);
if
(
fileContent
!==
null
)
{
filesArray
.
push
({
path
:
relativePath
,
content
:
raw
Content
,
content
:
file
Content
,
});
}
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论