@steerable/agent-shell-web 0.6.33 → 0.6.34
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
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",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"react-router-dom": "^6.30.0",
|
|
44
44
|
"rehype-raw": "^7.0.0",
|
|
45
45
|
"remark-gfm": "^4.0.1",
|
|
46
|
-
"@steerable/agent-
|
|
47
|
-
"@steerable/agent-
|
|
46
|
+
"@steerable/agent-ui": "0.6.34",
|
|
47
|
+
"@steerable/agent-protocol": "0.6.34"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"typescript": "^5.8.3",
|
|
58
58
|
"vite": "^5.4.21",
|
|
59
59
|
"vitest": "^3.2.4",
|
|
60
|
-
"@steerable/pack-sdk": "0.6.
|
|
60
|
+
"@steerable/pack-sdk": "0.6.34"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"gen": "echo agent-shell-web",
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from 'react-icons/lu';
|
|
33
33
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
34
34
|
import type { ExecPolicy } from '@/lib/exec-policy';
|
|
35
|
-
import { getWebChatModes,
|
|
35
|
+
import { getWebChatModes, hostToolCapability, settingsChrome } from '@/lib/host-tools';
|
|
36
36
|
import type { AttachmentFile } from '@/lib/attachments';
|
|
37
37
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
38
38
|
import {
|
|
@@ -1113,7 +1113,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1113
1113
|
const trimmed = value.trim();
|
|
1114
1114
|
const canSend = (trimmed.length > 0 || actualFiles.length > 0) && !disabled && !isStreaming;
|
|
1115
1115
|
|
|
1116
|
-
const allowFileAttach =
|
|
1116
|
+
const allowFileAttach = hostToolCapability('local-fs');
|
|
1117
1117
|
|
|
1118
1118
|
const handleDragOver = (event: React.DragEvent) => {
|
|
1119
1119
|
event.preventDefault();
|
|
@@ -1844,6 +1844,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1844
1844
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1845
1845
|
title="上传文件(可多选)"
|
|
1846
1846
|
aria-label="上传文件"
|
|
1847
|
+
data-testid="chat-attach"
|
|
1847
1848
|
>
|
|
1848
1849
|
<LuPaperclip className="h-3 w-3" />
|
|
1849
1850
|
</button>
|
|
@@ -21,7 +21,7 @@ import type { LlmSpeedSnapshot } from './process-status';
|
|
|
21
21
|
import { SessionTodoList } from './SessionTodoList';
|
|
22
22
|
import { resolveLatestSessionTodos } from './todo-list-model';
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
composeAttachmentUserContent,
|
|
25
25
|
saveChatAttachments,
|
|
26
26
|
type AttachmentFile,
|
|
27
27
|
} from '@/lib/attachments';
|
|
@@ -235,16 +235,8 @@ export function LocalChatPanel({
|
|
|
235
235
|
// 自行持久化,这里 chatId 为空时原样用源路径。
|
|
236
236
|
const resolvedFiles =
|
|
237
237
|
chatId && files.length > 0 ? await saveChatAttachments(chatId, files) : files;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (resolvedFiles.length > 0) {
|
|
241
|
-
const fileRefs = resolvedFiles.map(f => `- \`${f.path}\``).join('\n');
|
|
242
|
-
if (trimmed) {
|
|
243
|
-
trimmed = `${trimmed}\n\n---\n关联文件:\n${fileRefs}`;
|
|
244
|
-
} else {
|
|
245
|
-
trimmed = `关联文件:\n${fileRefs}`;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
238
|
+
const assembled = composeAttachmentUserContent(trimmed, resolvedFiles);
|
|
239
|
+
trimmed = assembled.content;
|
|
248
240
|
|
|
249
241
|
const mentionedAgentIds = mentionReferences
|
|
250
242
|
.filter((ref) => ref.type === 'agent')
|
|
@@ -255,16 +247,13 @@ export function LocalChatPanel({
|
|
|
255
247
|
// W6-3: image attachments ride as metadata so the main process reads the
|
|
256
248
|
// bytes into base64 ImageParts; the text path refs above stay so the
|
|
257
249
|
// persisted record reflects that an image was attached.
|
|
258
|
-
const imageAttachments = resolvedFiles
|
|
259
|
-
.filter((f) => isImageFile(f.path))
|
|
260
|
-
.map((f) => ({ path: f.path, name: f.name }));
|
|
261
250
|
const metadata = {
|
|
262
251
|
...(mode === 'plan' ? { mode: 'plan' as const } : {}),
|
|
263
252
|
...(execPolicy === 'full' ? { execPolicy: 'full' as const } : {}),
|
|
264
253
|
...(selectedAgentId ? { agentId: selectedAgentId } : {}),
|
|
265
254
|
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
266
255
|
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
267
|
-
...(
|
|
256
|
+
...(assembled.images.length > 0 ? { images: assembled.images } : {}),
|
|
268
257
|
};
|
|
269
258
|
|
|
270
259
|
// Snapshot what we're about to clear so a failed submit can restore it —
|
package/src/lib/attachments.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - BS/browser:File 没有路径,这里把字节读成 base64 走 HTTP 端点落盘。
|
|
10
10
|
* 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
|
|
11
11
|
*/
|
|
12
|
-
import { getElectronBridge
|
|
12
|
+
import { getElectronBridge } from './electron-bridge';
|
|
13
13
|
|
|
14
14
|
export interface AttachmentFile {
|
|
15
15
|
name: string;
|
|
@@ -39,17 +39,41 @@ export async function fileToBase64(file: File): Promise<string> {
|
|
|
39
39
|
return btoa(binary);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** 正文引用:优先落盘路径,浏览器未落盘时退回文件名(避免写成空的 ``)。 */
|
|
43
|
+
export function attachmentRefLabel(file: AttachmentFile): string {
|
|
44
|
+
return file.path || file.name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 把附件写进用户消息:正文「关联文件」列表 + 图像 metadata。
|
|
49
|
+
* 只收录有路径或文件名的项。
|
|
50
|
+
*/
|
|
51
|
+
export function composeAttachmentUserContent(
|
|
52
|
+
text: string,
|
|
53
|
+
files: AttachmentFile[],
|
|
54
|
+
): { content: string; images: Array<{ path: string; name: string }> } {
|
|
55
|
+
const usable = files.filter((file) => attachmentRefLabel(file));
|
|
56
|
+
let content = text.trim();
|
|
57
|
+
if (usable.length > 0) {
|
|
58
|
+
const fileRefs = usable.map((file) => `- \`${attachmentRefLabel(file)}\``).join('\n');
|
|
59
|
+
content = content ? `${content}\n\n---\n关联文件:\n${fileRefs}` : `关联文件:\n${fileRefs}`;
|
|
60
|
+
}
|
|
61
|
+
const images = usable
|
|
62
|
+
.filter((file) => isImageFile(file.path || file.name))
|
|
63
|
+
.map((file) => ({ path: attachmentRefLabel(file), name: file.name }));
|
|
64
|
+
return { content, images };
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
/**
|
|
43
68
|
* 把一批附件持久化到会话附件目录,返回「提交时应使用」的文件列表:
|
|
44
69
|
* 成功项用落盘路径,失败项退回原文件(不让用户的选择因单个文件失败而丢失)。
|
|
45
|
-
*
|
|
70
|
+
* 无 attachments 桥 / 空 chatId 时原样返回。
|
|
46
71
|
*/
|
|
47
72
|
export async function saveChatAttachments(
|
|
48
73
|
chatId: string | null | undefined,
|
|
49
74
|
files: AttachmentFile[],
|
|
50
75
|
): Promise<AttachmentFile[]> {
|
|
51
76
|
if (!chatId || files.length === 0) return files;
|
|
52
|
-
if (!isElectron()) return files;
|
|
53
77
|
const bridge = getElectronBridge();
|
|
54
78
|
if (!bridge?.attachments?.save) return files;
|
|
55
79
|
try {
|
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -61,6 +61,10 @@ import {
|
|
|
61
61
|
setPendingFirstMessage,
|
|
62
62
|
takePendingFirstMessage,
|
|
63
63
|
} from "@/lib/pending-first-message";
|
|
64
|
+
import {
|
|
65
|
+
composeAttachmentUserContent,
|
|
66
|
+
saveChatAttachments,
|
|
67
|
+
} from "@/lib/attachments";
|
|
64
68
|
import { BRAND_NAME, pickDefaultAgentId } from "@/brand";
|
|
65
69
|
|
|
66
70
|
/**
|
|
@@ -1299,29 +1303,12 @@ function EmptyChatGate() {
|
|
|
1299
1303
|
length: trimmed.length,
|
|
1300
1304
|
});
|
|
1301
1305
|
|
|
1302
|
-
// 与 LocalChatPanel.handleSubmit 相同的装配规则:文件路径附到正文,
|
|
1303
|
-
// @ 引用进 metadata,plan 模式打 metadata.mode。
|
|
1304
|
-
if (files.length > 0) {
|
|
1305
|
-
const fileRefs = files.map((f) => `- \`${f.path}\``).join("\n");
|
|
1306
|
-
trimmed = trimmed
|
|
1307
|
-
? `${trimmed}\n\n---\n关联文件:\n${fileRefs}`
|
|
1308
|
-
: `关联文件:\n${fileRefs}`;
|
|
1309
|
-
}
|
|
1310
1306
|
const mentionedAgentIds = mentionReferences
|
|
1311
1307
|
.filter((ref) => ref.type === "agent")
|
|
1312
1308
|
.map((ref) => ref.id);
|
|
1313
1309
|
const referencedChatIds = mentionReferences
|
|
1314
1310
|
.filter((ref) => ref.type === "chat")
|
|
1315
1311
|
.map((ref) => ref.id);
|
|
1316
|
-
const metadata = {
|
|
1317
|
-
...(mode === "plan" ? { mode: "plan" as const } : {}),
|
|
1318
|
-
...(execPolicy === "full" ? { execPolicy: "full" as const } : {}),
|
|
1319
|
-
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1320
|
-
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
1321
|
-
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
1322
|
-
...(modelOverride ? { model: modelOverride } : {}),
|
|
1323
|
-
...(effortOverride ? { reasoningEffort: effortOverride } : {}),
|
|
1324
|
-
};
|
|
1325
1312
|
|
|
1326
1313
|
setIsCreating(true);
|
|
1327
1314
|
setCreateError(null);
|
|
@@ -1331,9 +1318,23 @@ function EmptyChatGate() {
|
|
|
1331
1318
|
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1332
1319
|
});
|
|
1333
1320
|
if (!id) throw new Error("创建对话失败,请重试");
|
|
1321
|
+
// 落地页提交时还没有 chatId;先建会话再落盘,正文用落盘路径。
|
|
1322
|
+
const resolvedFiles =
|
|
1323
|
+
files.length > 0 ? await saveChatAttachments(id, files) : files;
|
|
1324
|
+
const assembled = composeAttachmentUserContent(trimmed, resolvedFiles);
|
|
1325
|
+
const metadata = {
|
|
1326
|
+
...(mode === "plan" ? { mode: "plan" as const } : {}),
|
|
1327
|
+
...(execPolicy === "full" ? { execPolicy: "full" as const } : {}),
|
|
1328
|
+
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1329
|
+
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
1330
|
+
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
1331
|
+
...(modelOverride ? { model: modelOverride } : {}),
|
|
1332
|
+
...(effortOverride ? { reasoningEffort: effortOverride } : {}),
|
|
1333
|
+
...(assembled.images.length > 0 ? { images: assembled.images } : {}),
|
|
1334
|
+
};
|
|
1334
1335
|
setPendingFirstMessage({
|
|
1335
1336
|
chatId: id,
|
|
1336
|
-
content:
|
|
1337
|
+
content: assembled.content,
|
|
1337
1338
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1338
1339
|
});
|
|
1339
1340
|
navigate(`/agent/${id}`);
|