Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
30415638
Unverified
提交
30415638
authored
6月 24, 2025
作者:
Will Chen
提交者:
GitHub
6月 24, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Finish incomplete dyad write (#475)
Fixes #452 Fixes #456 Fixes #195
上级
fa29488b
全部展开
隐藏空白字符变更
内嵌
并排
正在显示
11 个修改的文件
包含
781 行增加
和
95 行删除
+781
-95
generate-supabase-client.md
e2e-tests/fixtures/generate-supabase-client.md
+5
-0
partial-write.md
e2e-tests/fixtures/partial-write.md
+3
-0
partial_response.spec.ts
e2e-tests/partial_response.spec.ts
+12
-0
partial_response.spec.ts_partial-message-is-resumed-1.aria.yml
...al_response.spec.ts_partial-message-is-resumed-1.aria.yml
+22
-0
partial_response.spec.ts_partial-message-is-resumed-1.txt
...partial_response.spec.ts_partial-message-is-resumed-1.txt
+0
-0
partial_response.spec.ts_partial-message-is-resumed-2.txt
...partial_response.spec.ts_partial-message-is-resumed-2.txt
+192
-0
supabase_client.spec.ts_supabase-client-is-generated-1.txt
...upabase_client.spec.ts_supabase-client-is-generated-1.txt
+200
-0
supabase_client.spec.ts
e2e-tests/supabase_client.spec.ts
+15
-0
chat_stream_handlers.test.ts
src/__tests__/chat_stream_handlers.test.ts
+147
-2
chat_stream_handlers.ts
src/ipc/handlers/chat_stream_handlers.ts
+142
-64
chatCompletionHandler.ts
testing/fake-llm-server/chatCompletionHandler.ts
+43
-29
没有找到文件。
e2e-tests/fixtures/generate-supabase-client.md
0 → 100644
浏览文件 @
30415638
BEGIN
<dyad-write
path=
"src/integrations/supabase/client.ts"
description=
"Creating a supabase client."
>
$$SUPABASE_CLIENT_CODE$$
</dyad-write>
END
e2e-tests/fixtures/partial-write.md
0 → 100644
浏览文件 @
30415638
START OF MESSAGE
<dyad-write
path=
"src/new-file.ts"
description=
"this file will be partially written"
>
const a = "
[
[STRING_TO_BE_FINISHED
]
]
e2e-tests/partial_response.spec.ts
0 → 100644
浏览文件 @
30415638
import
{
test
}
from
"./helpers/test_helper"
;
test
(
"partial message is resumed"
,
async
({
po
})
=>
{
await
po
.
setUp
({
autoApprove
:
true
});
await
po
.
importApp
(
"minimal"
);
await
po
.
sendPrompt
(
"tc=partial-write"
);
// This is a special test case which triggers a dump.
await
po
.
snapshotServerDump
(
"all-messages"
);
await
po
.
snapshotMessages
({
replaceDumpPath
:
true
});
await
po
.
snapshotAppFiles
();
});
e2e-tests/snapshots/partial_response.spec.ts_partial-message-is-resumed-1.aria.yml
0 → 100644
浏览文件 @
30415638
-
paragraph
:
/Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
-
img
-
text
:
file1.txt
-
img
-
text
:
file1.txt
-
paragraph
:
More EOM
-
img
-
text
:
Approved
-
paragraph
:
tc=partial-write
-
paragraph
:
START OF MESSAGE
-
img
-
text
:
new-file.ts
-
img
-
text
:
"
src/new-file.ts
Summary:
this
file
will
be
partially
written"
-
paragraph
:
"
[[dyad-dump-path=*]]"
-
img
-
text
:
Approved
-
button "Undo"
:
-
img
-
button "Retry"
:
-
img
\ No newline at end of file
e2e-tests/snapshots/partial_response.spec.ts_partial-message-is-resumed-1.txt
0 → 100644
浏览文件 @
30415638
差异被折叠。
点击展开。
e2e-tests/snapshots/partial_response.spec.ts_partial-message-is-resumed-2.txt
0 → 100644
浏览文件 @
30415638
=== .gitignore ===
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
=== file1.txt ===
A file (2)
=== index.html ===
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dyad-generated-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
=== package.json ===
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
},
"packageManager": "<scrubbed>"
}
=== src/App.tsx ===
const App = () => <div>Minimal imported app</div>;
export default App;
=== src/main.tsx ===
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(<App />);
=== src/new-file.ts ===
const a = "[[STRING_TO_BE_FINISHED]]
[[STRING_IS_FINISHED]]";
=== src/vite-env.d.ts ===
/// <reference types="vite/client" />
=== tsconfig.app.json ===
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
=== tsconfig.json ===
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"noImplicitAny": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
}
}
=== tsconfig.node.json ===
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
=== vite.config.ts ===
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig(() => ({
server: {
host: "::",
port: 8080,
},
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));
e2e-tests/snapshots/supabase_client.spec.ts_supabase-client-is-generated-1.txt
0 → 100644
浏览文件 @
30415638
=== .gitignore ===
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
=== file1.txt ===
A file (2)
=== index.html ===
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dyad-generated-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
=== package.json ===
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"typescript": "^5.5.3",
"vite": "^6.3.4"
},
"packageManager": "<scrubbed>"
}
=== src/App.tsx ===
const App = () => <div>Minimal imported app</div>;
export default App;
=== src/integrations/supabase/client.ts ===
// This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js';
const SUPABASE_URL = "https://fake-project-id.supabase.co";
const SUPABASE_PUBLISHABLE_KEY = "test-publishable-key";
// Import the supabase client like this:
// import { supabase } from "@/integrations/supabase/client";
export const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
=== src/main.tsx ===
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(<App />);
=== src/vite-env.d.ts ===
/// <reference types="vite/client" />
=== tsconfig.app.json ===
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
=== tsconfig.json ===
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"noImplicitAny": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
}
}
=== tsconfig.node.json ===
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
=== vite.config.ts ===
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
export default defineConfig(() => ({
server: {
host: "::",
port: 8080,
},
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
}));
e2e-tests/supabase_client.spec.ts
0 → 100644
浏览文件 @
30415638
import
{
testSkipIfWindows
}
from
"./helpers/test_helper"
;
testSkipIfWindows
(
"supabase client is generated"
,
async
({
po
})
=>
{
await
po
.
setUp
({
autoApprove
:
true
});
await
po
.
importApp
(
"minimal"
);
await
po
.
sendPrompt
(
"tc=add-supabase"
);
// Connect to Supabase
await
po
.
page
.
getByText
(
"Set up supabase"
).
click
();
await
po
.
clickConnectSupabaseButton
();
await
po
.
clickBackButton
();
await
po
.
sendPrompt
(
"tc=generate-supabase-client"
);
await
po
.
snapshotAppFiles
();
});
src/__tests__/chat_stream_handlers.test.ts
浏览文件 @
30415638
...
@@ -6,7 +6,10 @@ import {
...
@@ -6,7 +6,10 @@ import {
processFullResponseActions
,
processFullResponseActions
,
getDyadAddDependencyTags
,
getDyadAddDependencyTags
,
}
from
"../ipc/processors/response_processor"
;
}
from
"../ipc/processors/response_processor"
;
import
{
removeDyadTags
}
from
"../ipc/handlers/chat_stream_handlers"
;
import
{
removeDyadTags
,
hasUnclosedDyadWrite
,
}
from
"../ipc/handlers/chat_stream_handlers"
;
import
fs
from
"node:fs"
;
import
fs
from
"node:fs"
;
import
git
from
"isomorphic-git"
;
import
git
from
"isomorphic-git"
;
import
{
db
}
from
"../db"
;
import
{
db
}
from
"../db"
;
...
@@ -1040,7 +1043,7 @@ const component = <Component />;
...
@@ -1040,7 +1043,7 @@ const component = <Component />;
it
(
"should handle dyad tags with special characters in content"
,
()
=>
{
it
(
"should handle dyad tags with special characters in content"
,
()
=>
{
const
text
=
`<dyad-write path="file.js">
const
text
=
`<dyad-write path="file.js">
const regex = /<div[^>]*>.*?<
\
/div>/g;
const regex = /<div[^>]*>.*?</div>/g;
const special = "Special chars: @#$%^&*()[]{}|\\";
const special = "Special chars: @#$%^&*()[]{}|\\";
</dyad-write>`
;
</dyad-write>`
;
const
result
=
removeDyadTags
(
text
);
const
result
=
removeDyadTags
(
text
);
...
@@ -1059,3 +1062,145 @@ const special = "Special chars: @#$%^&*()[]{}|\\";
...
@@ -1059,3 +1062,145 @@ const special = "Special chars: @#$%^&*()[]{}|\\";
expect
(
result
).
toBe
(
"Before After"
);
expect
(
result
).
toBe
(
"Before After"
);
});
});
});
});
describe
(
"hasUnclosedDyadWrite"
,
()
=>
{
it
(
"should return false when there are no dyad-write tags"
,
()
=>
{
const
text
=
"This is just regular text without any dyad tags."
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should return false when dyad-write tag is properly closed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js">console.log('hello');</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should return true when dyad-write tag is not closed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js">console.log('hello');`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should return false when dyad-write tag with attributes is properly closed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js" description="A test file">console.log('hello');</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should return true when dyad-write tag with attributes is not closed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js" description="A test file">console.log('hello');`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should return false when there are multiple closed dyad-write tags"
,
()
=>
{
const
text
=
`<dyad-write path="src/file1.js">code1</dyad-write>
Some text in between
<dyad-write path="src/file2.js">code2</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should return true when the last dyad-write tag is unclosed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file1.js">code1</dyad-write>
Some text in between
<dyad-write path="src/file2.js">code2`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should return false when first tag is unclosed but last tag is closed"
,
()
=>
{
const
text
=
`<dyad-write path="src/file1.js">code1
Some text in between
<dyad-write path="src/file2.js">code2</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle multiline content correctly"
,
()
=>
{
const
text
=
`<dyad-write path="src/component.tsx" description="React component">
import React from 'react';
const Component = () => {
return (
<div>
<h1>Hello World</h1>
</div>
);
};
export default Component;
</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle multiline unclosed content correctly"
,
()
=>
{
const
text
=
`<dyad-write path="src/component.tsx" description="React component">
import React from 'react';
const Component = () => {
return (
<div>
<h1>Hello World</h1>
</div>
);
};
export default Component;`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should handle complex attributes correctly"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js" description="File with quotes and special chars" version="1.0" author="test">
const message = "Hello 'world'";
const regex = /<div[^>]*>/g;
</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle text before and after dyad-write tags"
,
()
=>
{
const
text
=
`Some text before the tag
<dyad-write path="src/file.js">console.log('hello');</dyad-write>
Some text after the tag`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle unclosed tag with text after"
,
()
=>
{
const
text
=
`Some text before the tag
<dyad-write path="src/file.js">console.log('hello');
Some text after the unclosed tag`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should handle empty dyad-write tags"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js"></dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle unclosed empty dyad-write tags"
,
()
=>
{
const
text
=
`<dyad-write path="src/file.js">`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
true
);
});
it
(
"should focus on the last opening tag when there are mixed states"
,
()
=>
{
const
text
=
`<dyad-write path="src/file1.js">completed content</dyad-write>
<dyad-write path="src/file2.js">unclosed content
<dyad-write path="src/file3.js">final content</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
it
(
"should handle tags with special characters in attributes"
,
()
=>
{
const
text
=
`<dyad-write path="src/file-name_with.special@chars.js" description="File with special chars in path">content</dyad-write>`
;
const
result
=
hasUnclosedDyadWrite
(
text
);
expect
(
result
).
toBe
(
false
);
});
});
src/ipc/handlers/chat_stream_handlers.ts
浏览文件 @
30415638
...
@@ -451,41 +451,86 @@ This conversation includes one or more image attachments. When the user uploads
...
@@ -451,41 +451,86 @@ This conversation includes one or more image attachments. When the user uploads
];
];
}
}
// When calling streamText, the messages need to be properly formatted for mixed content
const
simpleStreamText
=
async
({
const
{
fullStream
}
=
streamText
({
chatMessages
,
maxTokens
:
await
getMaxTokens
(
settings
.
selectedModel
),
}:
{
temperature
:
0
,
chatMessages
:
CoreMessage
[];
maxRetries
:
2
,
})
=>
{
model
:
modelClient
.
model
,
return
streamText
({
providerOptions
:
{
maxTokens
:
await
getMaxTokens
(
settings
.
selectedModel
),
"dyad-gateway"
:
getExtraProviderOptions
(
temperature
:
0
,
modelClient
.
builtinProviderId
,
maxRetries
:
2
,
),
model
:
modelClient
.
model
,
google
:
{
providerOptions
:
{
thinkingConfig
:
{
"dyad-gateway"
:
getExtraProviderOptions
(
includeThoughts
:
true
,
modelClient
.
builtinProviderId
,
},
),
}
satisfies
GoogleGenerativeAIProviderOptions
,
google
:
{
},
thinkingConfig
:
{
system
:
systemPrompt
,
includeThoughts
:
true
,
messages
:
chatMessages
.
filter
((
m
)
=>
m
.
content
),
},
onError
:
(
error
:
any
)
=>
{
}
satisfies
GoogleGenerativeAIProviderOptions
,
logger
.
error
(
"Error streaming text:"
,
error
);
},
let
errorMessage
=
(
error
as
any
)?.
error
?.
message
;
system
:
systemPrompt
,
const
responseBody
=
error
?.
error
?.
responseBody
;
messages
:
chatMessages
.
filter
((
m
)
=>
m
.
content
),
if
(
errorMessage
&&
responseBody
)
{
onError
:
(
error
:
any
)
=>
{
errorMessage
+=
"
\
n
\
nDetails: "
+
responseBody
;
logger
.
error
(
"Error streaming text:"
,
error
);
}
let
errorMessage
=
(
error
as
any
)?.
error
?.
message
;
const
message
=
errorMessage
||
JSON
.
stringify
(
error
);
const
responseBody
=
error
?.
error
?.
responseBody
;
event
.
sender
.
send
(
if
(
errorMessage
&&
responseBody
)
{
"chat:response:error"
,
errorMessage
+=
"
\
n
\
nDetails: "
+
responseBody
;
`Sorry, there was an error from the AI:
${
message
}
`
,
}
const
message
=
errorMessage
||
JSON
.
stringify
(
error
);
event
.
sender
.
send
(
"chat:response:error"
,
`Sorry, there was an error from the AI:
${
message
}
`
,
);
// Clean up the abort controller
activeStreams
.
delete
(
req
.
chatId
);
},
abortSignal
:
abortController
.
signal
,
});
};
const
processResponseChunkUpdate
=
async
({
fullResponse
,
}:
{
fullResponse
:
string
;
})
=>
{
if
(
fullResponse
.
includes
(
"$$SUPABASE_CLIENT_CODE$$"
)
&&
updatedChat
.
app
?.
supabaseProjectId
)
{
const
supabaseClientCode
=
await
getSupabaseClientCode
({
projectId
:
updatedChat
.
app
?.
supabaseProjectId
,
});
fullResponse
=
fullResponse
.
replace
(
"$$SUPABASE_CLIENT_CODE$$"
,
supabaseClientCode
,
);
);
// Clean up the abort controller
}
activeStreams
.
delete
(
req
.
chatId
);
// Store the current partial response
},
partialResponses
.
set
(
req
.
chatId
,
fullResponse
);
abortSignal
:
abortController
.
signal
,
});
// Update the placeholder assistant message content in the messages array
const
currentMessages
=
[...
updatedChat
.
messages
];
if
(
currentMessages
.
length
>
0
&&
currentMessages
[
currentMessages
.
length
-
1
].
role
===
"assistant"
)
{
currentMessages
[
currentMessages
.
length
-
1
].
content
=
fullResponse
;
}
// Update the assistant message in the database
safeSend
(
event
.
sender
,
"chat:response:chunk"
,
{
chatId
:
req
.
chatId
,
messages
:
currentMessages
,
});
return
fullResponse
;
};
// When calling streamText, the messages need to be properly formatted for mixed content
const
{
fullStream
}
=
await
simpleStreamText
({
chatMessages
});
// Process the stream as before
// Process the stream as before
let
inThinkingBlock
=
false
;
let
inThinkingBlock
=
false
;
...
@@ -520,36 +565,8 @@ This conversation includes one or more image attachments. When the user uploads
...
@@ -520,36 +565,8 @@ This conversation includes one or more image attachments. When the user uploads
fullResponse
+=
chunk
;
fullResponse
+=
chunk
;
fullResponse
=
cleanFullResponse
(
fullResponse
);
fullResponse
=
cleanFullResponse
(
fullResponse
);
fullResponse
=
await
processResponseChunkUpdate
({
if
(
fullResponse
,
fullResponse
.
includes
(
"$$SUPABASE_CLIENT_CODE$$"
)
&&
updatedChat
.
app
?.
supabaseProjectId
)
{
const
supabaseClientCode
=
await
getSupabaseClientCode
({
projectId
:
updatedChat
.
app
?.
supabaseProjectId
,
});
fullResponse
=
fullResponse
.
replace
(
"$$SUPABASE_CLIENT_CODE$$"
,
supabaseClientCode
,
);
}
// Store the current partial response
partialResponses
.
set
(
req
.
chatId
,
fullResponse
);
// Update the placeholder assistant message content in the messages array
const
currentMessages
=
[...
updatedChat
.
messages
];
if
(
currentMessages
.
length
>
0
&&
currentMessages
[
currentMessages
.
length
-
1
].
role
===
"assistant"
)
{
currentMessages
[
currentMessages
.
length
-
1
].
content
=
fullResponse
;
}
// Update the assistant message in the database
safeSend
(
event
.
sender
,
"chat:response:chunk"
,
{
chatId
:
req
.
chatId
,
messages
:
currentMessages
,
});
});
// If the stream was aborted, exit early
// If the stream was aborted, exit early
...
@@ -558,6 +575,45 @@ This conversation includes one or more image attachments. When the user uploads
...
@@ -558,6 +575,45 @@ This conversation includes one or more image attachments. When the user uploads
break
;
break
;
}
}
}
}
if
(
!
abortController
.
signal
.
aborted
&&
settings
.
selectedChatMode
!==
"ask"
&&
hasUnclosedDyadWrite
(
fullResponse
)
)
{
let
continuationAttempts
=
0
;
while
(
hasUnclosedDyadWrite
(
fullResponse
)
&&
continuationAttempts
<
2
&&
!
abortController
.
signal
.
aborted
)
{
logger
.
warn
(
`Received unclosed dyad-write tag, attempting to continue, attempt #
${
continuationAttempts
+
1
}
`
,
);
continuationAttempts
++
;
const
{
fullStream
:
contStream
}
=
await
simpleStreamText
({
// Build messages: replay history then pre-fill assistant with current partial.
chatMessages
:
[
...
chatMessages
,
{
role
:
"assistant"
,
content
:
fullResponse
},
],
});
for
await
(
const
part
of
contStream
)
{
// If the stream was aborted, exit early
if
(
abortController
.
signal
.
aborted
)
{
logger
.
log
(
`Stream for chat
${
req
.
chatId
}
was aborted`
);
break
;
}
if
(
part
.
type
!==
"text-delta"
)
continue
;
// ignore reasoning for continuation
fullResponse
+=
part
.
textDelta
;
fullResponse
=
cleanFullResponse
(
fullResponse
);
fullResponse
=
await
processResponseChunkUpdate
({
fullResponse
,
});
}
}
}
}
catch
(
streamError
)
{
}
catch
(
streamError
)
{
// Check if this was an abort error
// Check if this was an abort error
if
(
abortController
.
signal
.
aborted
)
{
if
(
abortController
.
signal
.
aborted
)
{
...
@@ -832,3 +888,25 @@ export function removeDyadTags(text: string): string {
...
@@ -832,3 +888,25 @@ export function removeDyadTags(text: string): string {
const
dyadRegex
=
/<dyad-
[^
>
]
*>
[\s\S]
*
?
<
\/
dyad-
[^
>
]
*>/g
;
const
dyadRegex
=
/<dyad-
[^
>
]
*>
[\s\S]
*
?
<
\/
dyad-
[^
>
]
*>/g
;
return
text
.
replace
(
dyadRegex
,
""
).
trim
();
return
text
.
replace
(
dyadRegex
,
""
).
trim
();
}
}
export
function
hasUnclosedDyadWrite
(
text
:
string
):
boolean
{
// Find the last opening dyad-write tag
const
openRegex
=
/<dyad-write
[^
>
]
*>/g
;
let
lastOpenIndex
=
-
1
;
let
match
;
while
((
match
=
openRegex
.
exec
(
text
))
!==
null
)
{
lastOpenIndex
=
match
.
index
;
}
// If no opening tag found, there's nothing unclosed
if
(
lastOpenIndex
===
-
1
)
{
return
false
;
}
// Look for a closing tag after the last opening tag
const
textAfterLastOpen
=
text
.
substring
(
lastOpenIndex
);
const
hasClosingTag
=
/<
\/
dyad-write>/
.
test
(
textAfterLastOpen
);
return
!
hasClosingTag
;
}
testing/fake-llm-server/chatCompletionHandler.ts
浏览文件 @
30415638
...
@@ -64,35 +64,7 @@ export default Index;
...
@@ -64,35 +64,7 @@ export default Index;
)
)
:
lastMessage
.
content
.
includes
(
"[dump]"
))
:
lastMessage
.
content
.
includes
(
"[dump]"
))
)
{
)
{
const
timestamp
=
Date
.
now
();
messageContent
=
generateDump
(
req
);
const
generatedDir
=
path
.
join
(
__dirname
,
"generated"
);
// Create generated directory if it doesn't exist
if
(
!
fs
.
existsSync
(
generatedDir
))
{
fs
.
mkdirSync
(
generatedDir
,
{
recursive
:
true
});
}
const
dumpFilePath
=
path
.
join
(
generatedDir
,
`
${
timestamp
}
.json`
);
try
{
fs
.
writeFileSync
(
dumpFilePath
,
JSON
.
stringify
(
{
body
:
req
.
body
,
headers
:
{
authorization
:
req
.
headers
[
"authorization"
]
},
},
null
,
2
,
).
replace
(
/
\r\n
/g
,
"
\
n"
),
"utf-8"
,
);
console
.
log
(
`* Dumped messages to:
${
dumpFilePath
}
`
);
messageContent
=
`[[dyad-dump-path=
${
dumpFilePath
}
]]`
;
}
catch
(
error
)
{
console
.
error
(
`* Error writing dump file:
${
error
}
`
);
messageContent
=
`Error: Could not write dump file:
${
error
}
`
;
}
}
}
if
(
lastMessage
&&
lastMessage
.
content
===
"[increment]"
)
{
if
(
lastMessage
&&
lastMessage
.
content
===
"[increment]"
)
{
...
@@ -133,6 +105,16 @@ export default Index;
...
@@ -133,6 +105,16 @@ export default Index;
}
}
}
}
if
(
lastMessage
&&
lastMessage
.
content
&&
typeof
lastMessage
.
content
===
"string"
&&
lastMessage
.
content
.
trim
().
endsWith
(
"[[STRING_TO_BE_FINISHED]]"
)
)
{
messageContent
=
`[[STRING_IS_FINISHED]]";</dyad-write>\nFinished writing file.`
;
messageContent
+=
"
\
n
\
n"
+
generateDump
(
req
);
}
// Non-streaming response
// Non-streaming response
if
(
!
stream
)
{
if
(
!
stream
)
{
return
res
.
json
({
return
res
.
json
({
...
@@ -183,3 +165,35 @@ export default Index;
...
@@ -183,3 +165,35 @@ export default Index;
}
}
},
10
);
},
10
);
};
};
function
generateDump
(
req
:
Request
)
{
const
timestamp
=
Date
.
now
();
const
generatedDir
=
path
.
join
(
__dirname
,
"generated"
);
// Create generated directory if it doesn't exist
if
(
!
fs
.
existsSync
(
generatedDir
))
{
fs
.
mkdirSync
(
generatedDir
,
{
recursive
:
true
});
}
const
dumpFilePath
=
path
.
join
(
generatedDir
,
`
${
timestamp
}
.json`
);
try
{
fs
.
writeFileSync
(
dumpFilePath
,
JSON
.
stringify
(
{
body
:
req
.
body
,
headers
:
{
authorization
:
req
.
headers
[
"authorization"
]
},
},
null
,
2
,
).
replace
(
/
\r\n
/g
,
"
\
n"
),
"utf-8"
,
);
console
.
log
(
`* Dumped messages to:
${
dumpFilePath
}
`
);
return
`[[dyad-dump-path=
${
dumpFilePath
}
]]`
;
}
catch
(
error
)
{
console
.
error
(
`* Error writing dump file:
${
error
}
`
);
return
`Error: Could not write dump file:
${
error
}
`
;
}
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论