@steerable/agent-shell-web 0.6.20 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +5 -4
- package/src/auth/gate.ts +26 -0
- package/src/components/ChatHeader.tsx +1 -17
- package/src/components/chat/ApprovalModal.tsx +21 -41
- package/src/components/chat/ApprovalPromptProvider.tsx +98 -0
- package/src/components/chat/AskUserPromptProvider.tsx +99 -0
- package/src/components/chat/AssistantMessage.tsx +25 -2
- package/src/components/chat/ChatInput.tsx +116 -13
- package/src/components/chat/FilePathCode.tsx +65 -0
- package/src/components/chat/LocalChatPanel.tsx +19 -0
- package/src/components/chat/Markdown.tsx +54 -25
- package/src/components/chat/MessageList.tsx +66 -18
- package/src/components/chat/ModelPicker.tsx +5 -3
- package/src/components/chat/SuggestedReplies.tsx +34 -0
- package/src/components/chat/TurnFilesCard.tsx +113 -0
- package/src/components/chat/TurnProcessGroup.tsx +7 -2
- package/src/components/chat/path-mentions.ts +148 -0
- package/src/components/chat/suggested-replies-model.ts +40 -0
- package/src/components/chat/turn-files.ts +50 -0
- package/src/fixtures/browser-dev-data.json +1 -1
- package/src/layouts/AgentLayout.tsx +15 -26
- package/src/lib/browser-dev-electron-mock.ts +15 -0
- package/src/lib/electron-bridge.ts +13 -7
- package/src/lib/http-bridge.ts +8 -2
- package/src/lib/local-api.ts +31 -0
- package/src/main.tsx +41 -19
- package/src/pages/AgentPage.tsx +126 -2
- package/src/pages/ChatPanelPreviewPage.tsx +5 -1
- package/src/components/chat/AskUserModalHost.tsx +0 -61
package/README.md
CHANGED
|
@@ -17,4 +17,10 @@ bundle; there is no prebuilt `dist/` here. See
|
|
|
17
17
|
[`docs/spec/architecture.md`](https://github.com/pathlyapp/steerable-framework/blob/develop/docs/spec/architecture.md)
|
|
18
18
|
for the tier model.
|
|
19
19
|
|
|
20
|
+
Products that require authentication can register an `AppShellGate` from
|
|
21
|
+
`@steerable/agent-shell-web/auth/gate` before calling `bootstrap()`. An
|
|
22
|
+
enabled gate renders before the application router and calls
|
|
23
|
+
`onAuthenticated` to continue normal bootstrap. With no gate, or a disabled
|
|
24
|
+
gate, startup is unchanged.
|
|
25
|
+
|
|
20
26
|
License: Apache-2.0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"url": "https://github.com/pathlyapp/steerable-framework/issues"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
|
+
"./auth/gate": "./src/auth/gate.ts",
|
|
17
18
|
"./vite.base": {
|
|
18
19
|
"types": "./vite.base.d.ts",
|
|
19
20
|
"default": "./vite.base.js"
|
|
@@ -45,8 +46,8 @@
|
|
|
45
46
|
"react-router-dom": "^6.30.0",
|
|
46
47
|
"rehype-raw": "^7.0.0",
|
|
47
48
|
"remark-gfm": "^4.0.1",
|
|
48
|
-
"@steerable/agent-ui": "0.6.
|
|
49
|
-
"@steerable/agent-protocol": "0.6.
|
|
49
|
+
"@steerable/agent-ui": "0.6.22",
|
|
50
|
+
"@steerable/agent-protocol": "0.6.22"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"typescript": "^5.8.3",
|
|
60
61
|
"vite": "^5.4.21",
|
|
61
62
|
"vitest": "^3.2.4",
|
|
62
|
-
"@steerable/pack-sdk": "0.6.
|
|
63
|
+
"@steerable/pack-sdk": "0.6.22"
|
|
63
64
|
},
|
|
64
65
|
"scripts": {
|
|
65
66
|
"gen": "echo agent-shell-web",
|
package/src/auth/gate.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional product-provided gate rendered before the application router.
|
|
5
|
+
*/
|
|
6
|
+
export interface AppShellGate {
|
|
7
|
+
/** Allows one product entry to disable its gate for unsupported runtimes. */
|
|
8
|
+
enabled(): boolean;
|
|
9
|
+
/** Renders authentication UI and resumes bootstrap through the callback. */
|
|
10
|
+
Component: ComponentType<{ onAuthenticated: () => void }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let registeredGate: AppShellGate | null = null;
|
|
14
|
+
|
|
15
|
+
/** Registers the app-shell gate before calling `bootstrap()`. */
|
|
16
|
+
export function registerAppShellGate(gate: AppShellGate): void {
|
|
17
|
+
if (registeredGate) {
|
|
18
|
+
throw new Error('[app-shell-gate] a gate is already registered');
|
|
19
|
+
}
|
|
20
|
+
registeredGate = gate;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns the product-provided gate, if one was registered. */
|
|
24
|
+
export function getAppShellGate(): AppShellGate | null {
|
|
25
|
+
return registeredGate;
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { LuCheck, LuGitBranch, LuListTodo, LuListTree, LuLoaderCircle } from 'react-icons/lu';
|
|
4
|
-
import type { LocalChat,
|
|
4
|
+
import type { LocalChat, LocalTask } from '@/lib/local-api';
|
|
5
5
|
import {
|
|
6
6
|
activateChatBranch,
|
|
7
7
|
getChatBranches,
|
|
@@ -13,7 +13,6 @@ import { actionableTasks, summarizeTasks, type ChatTaskSummary } from './chat/us
|
|
|
13
13
|
|
|
14
14
|
interface ChatHeaderProps {
|
|
15
15
|
chat: LocalChat | null;
|
|
16
|
-
agent: LocalChatAgent | null;
|
|
17
16
|
/**
|
|
18
17
|
* W1.2.1: called after the active branch switches — the page re-hydrates
|
|
19
18
|
* the message list from the re-projected store. When provided (and the
|
|
@@ -29,8 +28,6 @@ interface ChatHeaderProps {
|
|
|
29
28
|
/**
|
|
30
29
|
* ChatHeader — slim title bar above ChatPanel. Shows:
|
|
31
30
|
* - chat title (完整显示,长标题换行不截断)
|
|
32
|
-
* - bound agent name + icon (read-only here; switching is done from the
|
|
33
|
-
* sidebar's expert team list)
|
|
34
31
|
* - shortened chat id (for support / debugging)
|
|
35
32
|
*
|
|
36
33
|
* Terminal / 场景包调试窗 / 本地模型设置 USED to live here, but per the
|
|
@@ -40,7 +37,6 @@ interface ChatHeaderProps {
|
|
|
40
37
|
*/
|
|
41
38
|
export function ChatHeader({
|
|
42
39
|
chat,
|
|
43
|
-
agent,
|
|
44
40
|
onBranchSwitched,
|
|
45
41
|
onInspectTask,
|
|
46
42
|
tasks = [],
|
|
@@ -186,18 +182,6 @@ export function ChatHeader({
|
|
|
186
182
|
>
|
|
187
183
|
{chat?.title ?? '未选择对话'}
|
|
188
184
|
</span>
|
|
189
|
-
<span className="shrink-0 text-xs text-agent-muted-foreground">
|
|
190
|
-
{agent ? (
|
|
191
|
-
<>
|
|
192
|
-
<span className="mr-0.5">{agent.icon || '🤖'}</span>
|
|
193
|
-
{agent.name}
|
|
194
|
-
</>
|
|
195
|
-
) : chat?.agentId ? (
|
|
196
|
-
<span className="font-mono">{chat.agentId}</span>
|
|
197
|
-
) : (
|
|
198
|
-
<span className="italic">no agent</span>
|
|
199
|
-
)}
|
|
200
|
-
</span>
|
|
201
185
|
{chat && (
|
|
202
186
|
<span className="hidden shrink-0 font-mono text-[10px] text-agent-muted-foreground/70 md:inline">
|
|
203
187
|
{shortenId(chat.id)}
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
1
|
import { LuShieldAlert, LuTriangleAlert } from 'react-icons/lu';
|
|
3
2
|
import {
|
|
4
|
-
getElectronBridge,
|
|
5
3
|
type ApprovalDecisionKind,
|
|
6
4
|
type ApprovalPromptRequest,
|
|
7
5
|
} from '@/lib/electron-bridge';
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
8
|
+
* ApprovalPromptMenu — W4-1 审批代数的输入区 UI。
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 决定经 `approval:decide` IPC 回到主进程,再应答 sidecar 的反向调用。
|
|
10
|
+
* 由 ApprovalPromptProvider 提供当前请求,在普通输入框的位置让用户对
|
|
11
|
+
* 工具调用做 7 变体决策(允许/拒绝 × 一次性/会话/持久 + 中止)。
|
|
15
12
|
*
|
|
16
13
|
* 持久化语义(与框架 ApprovalExecutor 对齐):
|
|
17
14
|
* - 一次性:只作用于本次调用,不缓存。
|
|
18
15
|
* - 会话:按 category(默认=工具名)缓存在本 chat 的会话级 cache。
|
|
19
16
|
* - 持久:写入 ~/.steerable/approvals.json,跨会话生效。
|
|
20
17
|
*
|
|
21
|
-
*
|
|
22
|
-
* 每个应答)。组件卸载/无窗口时主进程 fail-closed 为 deny_once。
|
|
18
|
+
* 请求队列及刷新恢复由 Provider 管理。
|
|
23
19
|
*/
|
|
24
20
|
|
|
25
21
|
const MODE_LABEL: Record<string, string> = {
|
|
@@ -88,26 +84,15 @@ function DecisionRow({
|
|
|
88
84
|
);
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
export function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
const current = queue[0] ?? null;
|
|
103
|
-
if (!current) return null;
|
|
104
|
-
|
|
105
|
-
const decide = (kind: ApprovalDecisionKind) => {
|
|
106
|
-
const bridge = getElectronBridge();
|
|
107
|
-
setQueue((prev) => prev.slice(1));
|
|
108
|
-
void bridge?.approval?.decide({ requestId: current.requestId, kind });
|
|
109
|
-
};
|
|
110
|
-
|
|
87
|
+
export function ApprovalPromptMenu({
|
|
88
|
+
request: current,
|
|
89
|
+
pendingCount,
|
|
90
|
+
onDecide,
|
|
91
|
+
}: {
|
|
92
|
+
request: ApprovalPromptRequest;
|
|
93
|
+
pendingCount: number;
|
|
94
|
+
onDecide: (kind: ApprovalDecisionKind) => void;
|
|
95
|
+
}) {
|
|
111
96
|
const destructive = current.mode === 'destructive';
|
|
112
97
|
// 网络出口拓宽(W-egress-ask):sidecar 的 web 工具被 egress 代理 403
|
|
113
98
|
// 后发起。白名单是代理进程级的(会话寿命),所以「始终」变体对网络
|
|
@@ -121,13 +106,9 @@ export function ApprovalModalHost() {
|
|
|
121
106
|
|
|
122
107
|
return (
|
|
123
108
|
<div
|
|
124
|
-
className="
|
|
125
|
-
|
|
126
|
-
aria-modal="true"
|
|
127
|
-
aria-label="工具调用审批"
|
|
128
|
-
data-testid="approval-dialog"
|
|
109
|
+
className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-sm text-agent-foreground shadow-xl"
|
|
110
|
+
data-testid="approval-composer"
|
|
129
111
|
>
|
|
130
|
-
<div className="w-full max-w-lg rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xl">
|
|
131
112
|
<div className="flex items-center gap-2 border-b border-agent-border px-4 py-3">
|
|
132
113
|
{destructive ? (
|
|
133
114
|
<LuTriangleAlert className="h-4 w-4 shrink-0 text-agent-destructive" />
|
|
@@ -154,9 +135,9 @@ export function ApprovalModalHost() {
|
|
|
154
135
|
</>
|
|
155
136
|
)}
|
|
156
137
|
</div>
|
|
157
|
-
{
|
|
138
|
+
{pendingCount > 0 && (
|
|
158
139
|
<div className="mt-0.5 text-[11px] text-agent-muted-foreground">
|
|
159
|
-
还有 {
|
|
140
|
+
还有 {pendingCount} 个待审批
|
|
160
141
|
</div>
|
|
161
142
|
)}
|
|
162
143
|
</div>
|
|
@@ -195,7 +176,7 @@ export function ApprovalModalHost() {
|
|
|
195
176
|
? ALLOW_BUTTONS.filter((b) => b.kind !== 'allow_always')
|
|
196
177
|
: ALLOW_BUTTONS
|
|
197
178
|
}
|
|
198
|
-
onPick={
|
|
179
|
+
onPick={onDecide}
|
|
199
180
|
/>
|
|
200
181
|
<div className="flex items-center justify-between gap-2">
|
|
201
182
|
<DecisionRow
|
|
@@ -204,11 +185,11 @@ export function ApprovalModalHost() {
|
|
|
204
185
|
? DENY_BUTTONS.filter((b) => b.kind !== 'deny_always')
|
|
205
186
|
: DENY_BUTTONS
|
|
206
187
|
}
|
|
207
|
-
onPick={
|
|
188
|
+
onPick={onDecide}
|
|
208
189
|
/>
|
|
209
190
|
<button
|
|
210
191
|
type="button"
|
|
211
|
-
onClick={() =>
|
|
192
|
+
onClick={() => onDecide('abort')}
|
|
212
193
|
className="shrink-0 rounded-agent-md bg-agent-destructive px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
|
213
194
|
data-testid="approval-abort"
|
|
214
195
|
>
|
|
@@ -221,9 +202,8 @@ export function ApprovalModalHost() {
|
|
|
221
202
|
: '「会话」决定在本对话内记住;「始终」决定写入本机 ~/.steerable/approvals.json,跨对话生效。不操作约 2 分钟后按拒绝处理。'}
|
|
222
203
|
</p>
|
|
223
204
|
</div>
|
|
224
|
-
</div>
|
|
225
205
|
</div>
|
|
226
206
|
);
|
|
227
207
|
}
|
|
228
208
|
|
|
229
|
-
export default
|
|
209
|
+
export default ApprovalPromptMenu;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import {
|
|
12
|
+
getElectronBridge,
|
|
13
|
+
type ApprovalDecisionKind,
|
|
14
|
+
type ApprovalPromptRequest,
|
|
15
|
+
} from '@/lib/electron-bridge';
|
|
16
|
+
|
|
17
|
+
interface ApprovalPromptContextValue {
|
|
18
|
+
current: ApprovalPromptRequest | null;
|
|
19
|
+
pendingCount: number;
|
|
20
|
+
decide: (kind: ApprovalDecisionKind) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ApprovalPromptContext = createContext<ApprovalPromptContextValue | null>(null);
|
|
24
|
+
|
|
25
|
+
/** Keeps approval prompts alive while the active composer presents them. */
|
|
26
|
+
export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
|
|
27
|
+
const [queue, setQueue] = useState<ApprovalPromptRequest[]>([]);
|
|
28
|
+
const decidedRequestIds = useRef(new Set<string>());
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const bridge = getElectronBridge();
|
|
32
|
+
if (!bridge?.approval) return;
|
|
33
|
+
let active = true;
|
|
34
|
+
const addRequests = (
|
|
35
|
+
requests: ApprovalPromptRequest[],
|
|
36
|
+
placement: 'append' | 'prepend' = 'append',
|
|
37
|
+
) => {
|
|
38
|
+
setQueue((previous) => {
|
|
39
|
+
const known = new Set(previous.map(({ requestId }) => requestId));
|
|
40
|
+
const additions = requests.filter(
|
|
41
|
+
({ requestId }) =>
|
|
42
|
+
!known.has(requestId) && !decidedRequestIds.current.has(requestId),
|
|
43
|
+
);
|
|
44
|
+
if (additions.length === 0) return previous;
|
|
45
|
+
return placement === 'prepend'
|
|
46
|
+
? [...additions, ...previous]
|
|
47
|
+
: [...previous, ...additions];
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const unsubscribe = bridge.approval.onRequest((request) => {
|
|
51
|
+
addRequests([request]);
|
|
52
|
+
});
|
|
53
|
+
void bridge.approval
|
|
54
|
+
.pending()
|
|
55
|
+
.then((requests) => {
|
|
56
|
+
if (active) addRequests(requests, 'prepend');
|
|
57
|
+
})
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
console.error('恢复待审批请求失败:', error);
|
|
60
|
+
});
|
|
61
|
+
return () => {
|
|
62
|
+
active = false;
|
|
63
|
+
unsubscribe();
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const current = queue[0] ?? null;
|
|
68
|
+
const decide = useCallback(
|
|
69
|
+
(kind: ApprovalDecisionKind) => {
|
|
70
|
+
if (!current) return;
|
|
71
|
+
const bridge = getElectronBridge();
|
|
72
|
+
decidedRequestIds.current.add(current.requestId);
|
|
73
|
+
setQueue((previous) => previous.slice(1));
|
|
74
|
+
void bridge?.approval?.decide({ requestId: current.requestId, kind });
|
|
75
|
+
},
|
|
76
|
+
[current],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const value = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
current,
|
|
82
|
+
pendingCount: Math.max(0, queue.length - 1),
|
|
83
|
+
decide,
|
|
84
|
+
}),
|
|
85
|
+
[current, decide, queue.length],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ApprovalPromptContext.Provider value={value}>
|
|
90
|
+
{children}
|
|
91
|
+
</ApprovalPromptContext.Provider>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Returns the approval prompt currently assigned to the chat composer. */
|
|
96
|
+
export function useApprovalPrompt(): ApprovalPromptContextValue | null {
|
|
97
|
+
return useContext(ApprovalPromptContext);
|
|
98
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { getElectronBridge, type AskUserPromptRequest } from '@/lib/electron-bridge';
|
|
12
|
+
|
|
13
|
+
type AnswerValue = string | string[];
|
|
14
|
+
|
|
15
|
+
interface AskUserPromptContextValue {
|
|
16
|
+
current: AskUserPromptRequest | null;
|
|
17
|
+
pendingCount: number;
|
|
18
|
+
answer: (answers: Record<string, AnswerValue>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AskUserPromptContext = createContext<AskUserPromptContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keeps structured user prompts alive across chat navigation while allowing
|
|
25
|
+
* the active composer to present the current prompt.
|
|
26
|
+
*/
|
|
27
|
+
export function AskUserPromptProvider({ children }: { children: ReactNode }) {
|
|
28
|
+
const [queue, setQueue] = useState<AskUserPromptRequest[]>([]);
|
|
29
|
+
const answeredRequestIds = useRef(new Set<string>());
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const bridge = getElectronBridge();
|
|
33
|
+
if (!bridge?.askUser) return;
|
|
34
|
+
let active = true;
|
|
35
|
+
const addRequests = (
|
|
36
|
+
requests: AskUserPromptRequest[],
|
|
37
|
+
placement: 'append' | 'prepend' = 'append',
|
|
38
|
+
) => {
|
|
39
|
+
setQueue((previous) => {
|
|
40
|
+
const known = new Set(previous.map(({ requestId }) => requestId));
|
|
41
|
+
const additions = requests.filter(
|
|
42
|
+
({ requestId }) =>
|
|
43
|
+
!known.has(requestId) && !answeredRequestIds.current.has(requestId),
|
|
44
|
+
);
|
|
45
|
+
if (additions.length === 0) return previous;
|
|
46
|
+
return placement === 'prepend'
|
|
47
|
+
? [...additions, ...previous]
|
|
48
|
+
: [...previous, ...additions];
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const unsubscribe = bridge.askUser.onRequest((request) => {
|
|
52
|
+
addRequests([request]);
|
|
53
|
+
});
|
|
54
|
+
void bridge.askUser
|
|
55
|
+
.pending()
|
|
56
|
+
.then((requests) => {
|
|
57
|
+
if (active) addRequests(requests, 'prepend');
|
|
58
|
+
})
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
console.error('恢复待回答问题失败:', error);
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
active = false;
|
|
64
|
+
unsubscribe();
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const current = queue[0] ?? null;
|
|
69
|
+
const answer = useCallback(
|
|
70
|
+
(answers: Record<string, AnswerValue>) => {
|
|
71
|
+
if (!current) return;
|
|
72
|
+
const bridge = getElectronBridge();
|
|
73
|
+
answeredRequestIds.current.add(current.requestId);
|
|
74
|
+
setQueue((previous) => previous.slice(1));
|
|
75
|
+
void bridge?.askUser?.answer({ requestId: current.requestId, answers });
|
|
76
|
+
},
|
|
77
|
+
[current],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const value = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
current,
|
|
83
|
+
pendingCount: Math.max(0, queue.length - 1),
|
|
84
|
+
answer,
|
|
85
|
+
}),
|
|
86
|
+
[answer, current, queue.length],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<AskUserPromptContext.Provider value={value}>
|
|
91
|
+
{children}
|
|
92
|
+
</AskUserPromptContext.Provider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Returns the structured prompt currently assigned to the chat composer. */
|
|
97
|
+
export function useAskUserPrompt(): AskUserPromptContextValue | null {
|
|
98
|
+
return useContext(AskUserPromptContext);
|
|
99
|
+
}
|
|
@@ -11,6 +11,8 @@ import { getFriendlyDate } from './timestamp';
|
|
|
11
11
|
import { useCopy } from './useCopy';
|
|
12
12
|
import type { TurnBlock } from './turn-timeline';
|
|
13
13
|
import { TurnProcessGroup } from './TurnProcessGroup';
|
|
14
|
+
import { TurnFilesCard } from './TurnFilesCard';
|
|
15
|
+
import type { TurnFile } from './turn-files';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* AssistantMessage — Tier-1 port of `deeppath`'s assistant-bubble.
|
|
@@ -55,6 +57,11 @@ interface AssistantMessageProps {
|
|
|
55
57
|
*/
|
|
56
58
|
agents: LocalChatAgent[];
|
|
57
59
|
chats?: LocalChat[];
|
|
60
|
+
/**
|
|
61
|
+
* 当前对话。正文行内代码里的路径按它绑定的项目根解析相对路径后,
|
|
62
|
+
* 确认存在的会变成可点击(见 FilePathCode)。
|
|
63
|
+
*/
|
|
64
|
+
chatId?: string | null;
|
|
58
65
|
/** Active chat-level agent — used as a fallback during streaming. */
|
|
59
66
|
currentAgent: LocalChatAgent | null;
|
|
60
67
|
/**
|
|
@@ -91,6 +98,12 @@ interface AssistantMessageProps {
|
|
|
91
98
|
startedAtMs?: number;
|
|
92
99
|
/** Frozen wall-clock of a finished turn. */
|
|
93
100
|
durationMs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* 本回合产生/修改的文件列表(local-backend 回合收尾时收集,经
|
|
103
|
+
* `turn_files` SSE + messageMetadata.turnFiles 持久化)。渲染在回答
|
|
104
|
+
* 气泡之下,点击用系统默认应用打开。空/undefined = 不渲染。
|
|
105
|
+
*/
|
|
106
|
+
turnFiles?: TurnFile[];
|
|
94
107
|
/**
|
|
95
108
|
* 分享当前对话(截图复制到剪贴板)。只传给最近一条助手消息,画在时间戳
|
|
96
109
|
* 行上,跟复制 / 重新生成同一排。
|
|
@@ -198,6 +211,7 @@ export function AssistantMessage({
|
|
|
198
211
|
isStreaming,
|
|
199
212
|
agents,
|
|
200
213
|
chats = [],
|
|
214
|
+
chatId = null,
|
|
201
215
|
currentAgent,
|
|
202
216
|
executedActions,
|
|
203
217
|
timeline,
|
|
@@ -207,6 +221,7 @@ export function AssistantMessage({
|
|
|
207
221
|
onRegenerate,
|
|
208
222
|
startedAtMs,
|
|
209
223
|
durationMs,
|
|
224
|
+
turnFiles,
|
|
210
225
|
onShare,
|
|
211
226
|
}: AssistantMessageProps) {
|
|
212
227
|
const content = message.content || '';
|
|
@@ -263,6 +278,7 @@ export function AssistantMessage({
|
|
|
263
278
|
durationMs={durationMs}
|
|
264
279
|
agents={agents}
|
|
265
280
|
chats={chats}
|
|
281
|
+
chatId={chatId}
|
|
266
282
|
emptyFallback={
|
|
267
283
|
failure ? (
|
|
268
284
|
<TurnErrorBubble reason={failure.reason} />
|
|
@@ -290,7 +306,7 @@ export function AssistantMessage({
|
|
|
290
306
|
renderAnswer={(block, isLast) => (
|
|
291
307
|
<div className={bubbleClass(isPlanMode)}>
|
|
292
308
|
<div className="markdown-content text-sm leading-relaxed text-agent-foreground">
|
|
293
|
-
<Markdown agents={agents} chats={chats}>{block.content}</Markdown>
|
|
309
|
+
<Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
|
|
294
310
|
</div>
|
|
295
311
|
{isStreaming && isLast && <StreamingCursor />}
|
|
296
312
|
</div>
|
|
@@ -312,7 +328,7 @@ export function AssistantMessage({
|
|
|
312
328
|
{displayContent ? (
|
|
313
329
|
<div className={bubbleClass(isPlanMode)}>
|
|
314
330
|
<div className="markdown-content text-sm leading-relaxed text-agent-foreground">
|
|
315
|
-
<Markdown agents={agents} chats={chats}>{displayContent}</Markdown>
|
|
331
|
+
<Markdown agents={agents} chats={chats} chatId={chatId}>{displayContent}</Markdown>
|
|
316
332
|
</div>
|
|
317
333
|
{isStreaming && <StreamingCursor />}
|
|
318
334
|
</div>
|
|
@@ -338,6 +354,13 @@ export function AssistantMessage({
|
|
|
338
354
|
) : null}
|
|
339
355
|
</>
|
|
340
356
|
)}
|
|
357
|
+
{/* 回合产物文件列表:钉在回答之下、时间戳行之上(Codex 式收尾)。
|
|
358
|
+
回合收尾才有数据,流式期间天然为空。 */}
|
|
359
|
+
{!isStreaming && turnFiles && turnFiles.length > 0 && (
|
|
360
|
+
<div className="mt-1.5">
|
|
361
|
+
<TurnFilesCard files={turnFiles} />
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
341
364
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-agent-muted-foreground">
|
|
342
365
|
<span>
|
|
343
366
|
{message.createdAt
|