Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
639b3a32
提交
639b3a32
authored
4月 18, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
basic suggested action scaffolding
上级
b2e3631a
隐藏空白字符变更
内嵌
并排
正在显示
10 个修改的文件
包含
149 行增加
和
86 行删除
+149
-86
appAtoms.ts
src/atoms/appAtoms.ts
+2
-0
proposalAtoms.ts
src/atoms/proposalAtoms.ts
+1
-1
ChatInput.tsx
src/components/chat/ChatInput.tsx
+56
-13
PreviewPanel.tsx
src/components/preview_panel/PreviewPanel.tsx
+8
-7
useProposal.ts
src/hooks/useProposal.ts
+29
-42
useRunApp.ts
src/hooks/useRunApp.ts
+15
-4
useStreamChat.ts
src/hooks/useStreamChat.ts
+4
-2
proposal_handlers.ts
src/ipc/handlers/proposal_handlers.ts
+19
-14
ipc_client.ts
src/ipc/ipc_client.ts
+1
-1
schemas.ts
src/lib/schemas.ts
+14
-2
没有找到文件。
src/atoms/appAtoms.ts
浏览文件 @
639b3a32
...
...
@@ -17,3 +17,5 @@ export const userSettingsAtom = atom<UserSettings | null>(null);
// Atom for storing allow-listed environment variables
export
const
envVarsAtom
=
atom
<
Record
<
string
,
string
|
undefined
>>
({});
export
const
previewPanelKeyAtom
=
atom
<
number
>
(
0
);
src/atoms/proposalAtoms.ts
浏览文件 @
639b3a32
import
{
atom
}
from
"jotai"
;
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
type
{
Code
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
export
const
proposalResultAtom
=
atom
<
ProposalResult
|
null
>
(
null
);
src/components/chat/ChatInput.tsx
浏览文件 @
639b3a32
...
...
@@ -25,17 +25,16 @@ import { Button } from "@/components/ui/button";
import
{
Label
}
from
"@/components/ui/label"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
useProposal
}
from
"@/hooks/useProposal"
;
import
{
Proposal
}
from
"@/lib/schemas"
;
import
{
CodeProposal
,
ActionProposal
,
Proposal
,
SuggestedAction
,
ProposalResult
,
}
from
"@/lib/schemas"
;
import
type
{
Message
}
from
"@/ipc/ipc_types"
;
import
{
isPreviewOpenAtom
}
from
"@/atoms/viewAtoms"
;
interface
ChatInputActionsProps
{
proposal
:
Proposal
;
onApprove
:
()
=>
void
;
onReject
:
()
=>
void
;
isApprovable
:
boolean
;
// Can be used to enable/disable buttons
isApproving
:
boolean
;
// State for approving
isRejecting
:
boolean
;
// State for rejecting
}
import
{
useRunApp
}
from
"@/hooks/useRunApp"
;
export
function
ChatInput
({
chatId
}:
{
chatId
?:
number
})
{
const
[
inputValue
,
setInputValue
]
=
useAtom
(
chatInputValueAtom
);
...
...
@@ -52,12 +51,12 @@ export function ChatInput({ chatId }: { chatId?: number }) {
// Use the hook to fetch the proposal
const
{
proposal
,
messageId
,
proposalResult
,
isLoading
:
isProposalLoading
,
error
:
proposalError
,
refreshProposal
,
}
=
useProposal
(
chatId
);
const
{
proposal
,
chatId
:
proposalChatId
,
messageId
}
=
proposalResult
??
{};
const
adjustHeight
=
()
=>
{
const
textarea
=
textareaRef
.
current
;
...
...
@@ -205,9 +204,9 @@ export function ChatInput({ chatId }: { chatId?: number }) {
</
div
>
)
}
<
div
className=
"p-4"
>
<
div
className=
"flex flex-col
space-y-2
border border-border rounded-lg bg-(--background-lighter) shadow-sm"
>
<
div
className=
"flex flex-col border border-border rounded-lg bg-(--background-lighter) shadow-sm"
>
{
/* Only render ChatInputActions if proposal is loaded */
}
{
proposal
&&
(
{
proposal
&&
proposalResult
?.
chatId
===
chatId
&&
(
<
ChatInputActions
proposal=
{
proposal
}
onApprove=
{
handleApprove
}
...
...
@@ -267,6 +266,47 @@ export function ChatInput({ chatId }: { chatId?: number }) {
);
}
function
mapActionToButton
(
action
:
SuggestedAction
)
{
const
{
restartApp
}
=
useRunApp
();
switch
(
action
.
id
)
{
case
"restart-app"
:
return
(
<
Button
variant=
"outline"
size=
"sm"
key=
{
action
.
id
}
onClick=
{
restartApp
}
>
Restart App
</
Button
>
);
default
:
console
.
error
(
`Unsupported action:
${
action
.
id
}
`
);
return
(
<
Button
variant=
"outline"
size=
"sm"
disabled
key=
{
action
.
id
}
>
Unsupported:
{
action
.
id
}
</
Button
>
);
}
}
function
ActionProposalActions
({
proposal
}:
{
proposal
:
ActionProposal
})
{
return
(
<
div
className=
"p-2 pb-0"
>
{
proposal
.
actions
.
map
((
action
)
=>
mapActionToButton
(
action
))
}
</
div
>
);
}
interface
ChatInputActionsProps
{
proposal
:
Proposal
;
onApprove
:
()
=>
void
;
onReject
:
()
=>
void
;
isApprovable
:
boolean
;
// Can be used to enable/disable buttons
isApproving
:
boolean
;
// State for approving
isRejecting
:
boolean
;
// State for rejecting
}
// Update ChatInputActions to accept props
function
ChatInputActions
({
proposal
,
...
...
@@ -279,6 +319,9 @@ function ChatInputActions({
const
[
autoApprove
,
setAutoApprove
]
=
useState
(
false
);
const
[
isDetailsVisible
,
setIsDetailsVisible
]
=
useState
(
false
);
if
(
proposal
.
type
===
"action-proposal"
)
{
return
<
ActionProposalActions
proposal=
{
proposal
}
></
ActionProposalActions
>;
}
return
(
<
div
className=
"border-b border-border"
>
<
div
className=
"p-2"
>
...
...
src/components/preview_panel/PreviewPanel.tsx
浏览文件 @
639b3a32
import
{
useAtom
,
useAtomValue
}
from
"jotai"
;
import
{
previewModeAtom
,
selectedAppIdAtom
}
from
"../../atoms/appAtoms"
;
import
{
previewModeAtom
,
previewPanelKeyAtom
,
selectedAppIdAtom
,
}
from
"../../atoms/appAtoms"
;
import
{
useLoadApp
}
from
"@/hooks/useLoadApp"
;
import
{
CodeView
}
from
"./CodeView"
;
import
{
PreviewIframe
}
from
"./PreviewIframe"
;
...
...
@@ -99,14 +103,11 @@ export function PreviewPanel() {
const
[
isConsoleOpen
,
setIsConsoleOpen
]
=
useState
(
false
);
const
{
runApp
,
stopApp
,
restartApp
,
error
,
loading
,
app
}
=
useRunApp
();
const
runningAppIdRef
=
useRef
<
number
|
null
>
(
null
);
const
[
key
,
setKey
]
=
useState
(
0
);
const
key
=
useAtomValue
(
previewPanelKeyAtom
);
const
handleRestart
=
useCallback
(()
=>
{
if
(
selectedAppId
!==
null
)
{
restartApp
(
selectedAppId
);
setKey
((
prevKey
)
=>
prevKey
+
1
);
}
},
[
selectedAppId
,
restartApp
]);
restartApp
();
},
[
restartApp
]);
useEffect
(()
=>
{
const
previousAppId
=
runningAppIdRef
.
current
;
...
...
src/hooks/useProposal.ts
浏览文件 @
639b3a32
import
{
useState
,
useEffect
,
useCallback
}
from
"react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
// Import Proposal type
import
type
{
Code
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
// Import Proposal type
import
{
proposalResultAtom
}
from
"@/atoms/proposalAtoms"
;
import
{
useAtom
}
from
"jotai"
;
export
function
useProposal
(
chatId
?:
number
|
undefined
)
{
...
...
@@ -8,40 +8,35 @@ export function useProposal(chatId?: number | undefined) {
const
[
isLoading
,
setIsLoading
]
=
useState
<
boolean
>
(
false
);
const
[
error
,
setError
]
=
useState
<
string
|
null
>
(
null
);
const
fetchProposal
=
useCallback
(
async
(
innerChatId
?:
number
)
=>
{
chatId
=
chatId
??
innerChatId
;
console
.
log
(
"fetching proposal for chatId"
,
chatId
);
if
(
chatId
===
undefined
)
{
setProposalResult
(
null
);
setIsLoading
(
false
);
setError
(
null
);
return
;
}
setIsLoading
(
true
);
const
fetchProposal
=
useCallback
(
async
()
=>
{
if
(
chatId
===
undefined
)
{
setProposalResult
(
null
);
setIsLoading
(
false
);
setError
(
null
);
setProposalResult
(
null
);
// Reset on new fetch
try
{
// Type assertion might be needed depending on how IpcClient is typed
const
result
=
(
await
IpcClient
.
getInstance
().
getProposal
(
chatId
))
as
ProposalResult
|
null
;
return
;
}
setIsLoading
(
true
);
setError
(
null
);
setProposalResult
(
null
);
// Reset on new fetch
try
{
// Type assertion might be needed depending on how IpcClient is typed
const
result
=
(
await
IpcClient
.
getInstance
().
getProposal
(
chatId
))
as
ProposalResult
|
null
;
if
(
result
)
{
setProposalResult
(
result
);
}
else
{
setProposalResult
(
null
);
// Explicitly set to null if IPC returns null
}
}
catch
(
err
:
any
)
{
console
.
error
(
"Error fetching proposal:"
,
err
);
setError
(
err
.
message
||
"Failed to fetch proposal"
);
setProposalResult
(
null
);
// Clear proposal data on error
}
finally
{
setIsLoading
(
false
);
if
(
result
)
{
setProposalResult
(
result
);
}
else
{
setProposalResult
(
null
);
// Explicitly set to null if IPC returns null
}
},
[
chatId
]
);
// Depend on chatId
}
catch
(
err
:
any
)
{
console
.
error
(
"Error fetching proposal:"
,
err
);
setError
(
err
.
message
||
"Failed to fetch proposal"
);
setProposalResult
(
null
);
// Clear proposal data on error
}
finally
{
setIsLoading
(
false
);
}
},
[
chatId
]);
// Depend on chatId
useEffect
(()
=>
{
fetchProposal
();
...
...
@@ -52,18 +47,10 @@ export function useProposal(chatId?: number | undefined) {
// };
},
[
fetchProposal
]);
// Re-run effect if fetchProposal changes (due to chatId change)
const
refreshProposal
=
useCallback
(
(
chatId
?:
number
)
=>
{
fetchProposal
(
chatId
);
},
[
fetchProposal
]
);
return
{
proposal
:
proposalResult
?.
proposal
??
null
,
messageId
:
proposalResult
?.
messageId
,
proposalResult
:
proposalResult
,
isLoading
,
error
,
refreshProposal
,
// Expose the refresh function
refreshProposal
:
fetchProposal
,
// Expose the refresh function
};
}
src/hooks/useRunApp.ts
浏览文件 @
639b3a32
import
{
useState
,
useCallback
}
from
"react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
appOutputAtom
,
appUrlAtom
,
currentAppAtom
}
from
"@/atoms/appAtoms"
;
import
{
useAtom
,
useSetAtom
}
from
"jotai"
;
import
{
appOutputAtom
,
appUrlAtom
,
currentAppAtom
,
previewPanelKeyAtom
,
selectedAppIdAtom
,
}
from
"@/atoms/appAtoms"
;
import
{
useAtom
,
useAtomValue
,
useSetAtom
}
from
"jotai"
;
import
{
App
}
from
"@/ipc/ipc_types"
;
export
function
useRunApp
()
{
...
...
@@ -10,7 +16,8 @@ export function useRunApp() {
const
[
app
,
setApp
]
=
useAtom
(
currentAppAtom
);
const
setAppOutput
=
useSetAtom
(
appOutputAtom
);
const
[
appUrlObj
,
setAppUrlObj
]
=
useAtom
(
appUrlAtom
);
const
setPreviewPanelKey
=
useSetAtom
(
previewPanelKeyAtom
);
const
appId
=
useAtomValue
(
selectedAppIdAtom
);
const
runApp
=
useCallback
(
async
(
appId
:
number
)
=>
{
setLoading
(
true
);
try
{
...
...
@@ -63,7 +70,10 @@ export function useRunApp() {
}
},
[]);
const
restartApp
=
useCallback
(
async
(
appId
:
number
)
=>
{
const
restartApp
=
useCallback
(
async
()
=>
{
if
(
appId
===
null
)
{
return
;
}
setLoading
(
true
);
try
{
const
ipcClient
=
IpcClient
.
getInstance
();
...
...
@@ -91,6 +101,7 @@ export function useRunApp() {
console
.
error
(
`Error restarting app
${
appId
}
:`
,
error
);
setError
(
error
instanceof
Error
?
error
:
new
Error
(
String
(
error
)));
}
finally
{
setPreviewPanelKey
((
prevKey
)
=>
prevKey
+
1
);
setLoading
(
false
);
}
},
[]);
...
...
src/hooks/useStreamChat.ts
浏览文件 @
639b3a32
...
...
@@ -16,6 +16,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
import
{
useLoadVersions
}
from
"./useLoadVersions"
;
import
{
showError
}
from
"@/lib/toast"
;
import
{
useProposal
}
from
"./useProposal"
;
import
{
useSearch
}
from
"@tanstack/react-router"
;
export
function
getRandomString
()
{
return
Math
.
random
().
toString
(
36
).
substring
(
2
,
15
);
...
...
@@ -31,7 +32,8 @@ export function useStreamChat() {
const
{
refreshApp
}
=
useLoadApp
(
selectedAppId
);
const
setStreamCount
=
useSetAtom
(
chatStreamCountAtom
);
const
{
refreshVersions
}
=
useLoadVersions
(
selectedAppId
);
const
{
refreshProposal
}
=
useProposal
();
const
{
id
:
chatId
}
=
useSearch
({
from
:
"/chat"
});
const
{
refreshProposal
}
=
useProposal
(
chatId
);
const
streamMessage
=
useCallback
(
async
({
prompt
,
...
...
@@ -91,7 +93,7 @@ export function useStreamChat() {
if
(
response
.
updatedFiles
)
{
setIsPreviewOpen
(
true
);
}
refreshProposal
(
chatId
);
refreshProposal
();
// Keep the same as below
setIsStreaming
(
false
);
...
...
src/ipc/handlers/proposal_handlers.ts
浏览文件 @
639b3a32
import
{
ipcMain
,
type
IpcMainInvokeEvent
}
from
"electron"
;
import
type
{
Proposal
}
from
"@/lib/schemas"
;
import
type
{
CodeProposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
db
}
from
"../../db"
;
import
{
messages
}
from
"../../db/schema"
;
import
{
desc
,
eq
,
and
,
Update
}
from
"drizzle-orm"
;
...
...
@@ -19,13 +19,6 @@ interface ParsedProposal {
title
:
string
;
files
:
string
[];
}
// Define return type for getProposalHandler
interface
ProposalResult
{
proposal
:
Proposal
;
messageId
:
number
;
}
function
isParsedProposal
(
obj
:
any
):
obj
is
ParsedProposal
{
return
(
obj
&&
...
...
@@ -54,12 +47,23 @@ const getProposalHandler = async (
},
});
if
(
latestAssistantMessage
?.
approvalState
===
"approved"
||
latestAssistantMessage
?.
approvalState
===
"rejected"
)
{
if
(
latestAssistantMessage
?.
approvalState
===
"rejected"
)
{
return
null
;
}
if
(
latestAssistantMessage
?.
approvalState
===
"approved"
)
{
return
{
proposal
:
{
type
:
"action-proposal"
,
actions
:
[
{
id
:
"restart-app"
,
},
],
},
chatId
:
chatId
,
messageId
:
latestAssistantMessage
.
id
,
};
}
if
(
latestAssistantMessage
?.
content
&&
latestAssistantMessage
.
id
)
{
const
messageId
=
latestAssistantMessage
.
id
;
// Get the message ID
...
...
@@ -74,7 +78,8 @@ const getProposalHandler = async (
// Check if we have enough information to create a proposal
if
(
proposalTitle
||
proposalFiles
.
length
>
0
)
{
const
proposal
:
Proposal
=
{
const
proposal
:
CodeProposal
=
{
type
:
"code-proposal"
,
// Use parsed title or a default title if summary tag is missing but write tags exist
title
:
proposalTitle
??
"Proposed File Changes"
,
securityRisks
:
[],
// Keep empty
...
...
@@ -85,7 +90,7 @@ const getProposalHandler = async (
})),
};
console
.
log
(
"Generated proposal on the fly:"
,
proposal
);
return
{
proposal
,
messageId
};
// Return proposal and messageId
return
{
proposal
,
chatId
,
messageId
};
// Return proposal and messageId
}
else
{
console
.
log
(
"No relevant tags found in the latest assistant message content."
...
...
src/ipc/ipc_client.ts
浏览文件 @
639b3a32
...
...
@@ -17,7 +17,7 @@ import type {
Message
,
Version
,
}
from
"./ipc_types"
;
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
type
{
Code
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
export
interface
ChatStreamCallbacks
{
...
...
src/lib/schemas.ts
浏览文件 @
639b3a32
...
...
@@ -111,14 +111,26 @@ export interface FileChange {
summary
:
string
;
}
// New Proposal interface
export
interface
Proposal
{
export
interface
CodeProposal
{
type
:
"code-proposal"
;
title
:
string
;
securityRisks
:
SecurityRisk
[];
filesChanged
:
FileChange
[];
}
export
interface
SuggestedAction
{
id
:
"restart-app"
;
}
export
interface
ActionProposal
{
type
:
"action-proposal"
;
actions
:
SuggestedAction
[];
}
export
type
Proposal
=
CodeProposal
|
ActionProposal
;
export
interface
ProposalResult
{
proposal
:
Proposal
;
chatId
:
number
;
messageId
:
number
;
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论