@steerable/agent-shell-web 0.6.53 → 0.6.56
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 +3 -4
- package/package.json +4 -4
- package/src/AppShell.tsx +3 -3
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +64 -56
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +31 -21
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +6 -5
- package/src/components/TerminalPanel.tsx +6 -5
- package/src/components/TerminalView.tsx +15 -10
- package/src/components/chat/ApprovalModal.tsx +40 -26
- package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
- package/src/components/chat/AskUserPromptProvider.tsx +4 -4
- package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
- package/src/components/chat/AssistantMessage.tsx +18 -13
- package/src/components/chat/ChatInput.tsx +56 -40
- package/src/components/chat/ChatProjectBadge.tsx +27 -22
- package/src/components/chat/EmptyChatHero.tsx +2 -1
- package/src/components/chat/ExecPolicyPicker.tsx +12 -11
- package/src/components/chat/ExecutedActionsCard.tsx +45 -33
- package/src/components/chat/FilePathCode.tsx +8 -3
- package/src/components/chat/InterruptedTurnCard.tsx +4 -3
- package/src/components/chat/LocalChatPanel.tsx +2 -1
- package/src/components/chat/Markdown.tsx +2 -1
- package/src/components/chat/MessageList.tsx +3 -2
- package/src/components/chat/ModelPicker.tsx +25 -16
- package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
- package/src/components/chat/PlanTodoList.tsx +2 -1
- package/src/components/chat/ProjectTrustBanner.tsx +19 -5
- package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
- package/src/components/chat/SessionTodoList.tsx +8 -7
- package/src/components/chat/SessionTreeModal.tsx +10 -9
- package/src/components/chat/StreamingStatus.tsx +4 -3
- package/src/components/chat/SuggestedReplies.tsx +3 -2
- package/src/components/chat/TaskOutcomeCards.tsx +8 -5
- package/src/components/chat/TaskPanelModal.tsx +33 -23
- package/src/components/chat/TaskProcessPanel.tsx +16 -15
- package/src/components/chat/TurnFilesCard.tsx +14 -13
- package/src/components/chat/UserMessage.tsx +12 -11
- package/src/components/chat/executed-actions-model.ts +47 -35
- package/src/components/chat/orchestration-flow-model.ts +16 -5
- package/src/components/chat/process-status.ts +8 -7
- package/src/components/chat/todo-list-model.ts +5 -4
- package/src/components/chat/turn-files.ts +8 -8
- package/src/components/chat/useChatTasks.ts +3 -3
- package/src/components/settings/AgentCapabilityFields.tsx +26 -23
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
- package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
- package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
- package/src/components/settings/InsightsSettingsPanel.tsx +58 -43
- package/src/components/settings/LlmSettingsPanel.tsx +107 -86
- package/src/components/settings/McpSettingsPanel.tsx +72 -46
- package/src/components/settings/ModelIdCombobox.tsx +2 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
- package/src/components/settings/PortableSettingsPanel.tsx +83 -55
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +26 -23
- package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
- package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
- package/src/components/settings/UsagePanel.tsx +22 -23
- package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
- package/src/components/settings/llm-vendors.ts +12 -7
- package/src/components/settings/model-capabilities.ts +16 -15
- package/src/components/settings/preset-choice-model.ts +6 -3
- package/src/fixtures/browser-dev-data.json +24 -24
- package/src/hooks/useChatsAndAgents.ts +9 -9
- package/src/hooks/useProjects.ts +3 -3
- package/src/i18n/agent-label.ts +15 -0
- package/src/i18n/index.tsx +68 -0
- package/src/i18n/runtime.ts +117 -0
- package/src/i18n/translate.ts +65 -0
- package/src/layouts/AgentLayout.tsx +13 -12
- package/src/lib/app-release.ts +16 -11
- package/src/lib/attachments.ts +18 -9
- package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +16 -12
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/host-bridge.ts +371 -4
- package/src/lib/host-paste.ts +1 -1
- package/src/lib/http-bridge.ts +12 -9
- package/src/lib/insights.ts +2 -2
- package/src/lib/local-api.ts +5 -5
- package/src/lib/portable.ts +11 -10
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/lib/slash-sources.ts +3 -3
- package/src/lib/tauri-bridge.ts +1 -1
- package/src/main.tsx +27 -4
- package/src/pages/AgentPage.tsx +39 -34
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +51 -45
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
- package/src/lib/electron-bridge.ts +0 -390
package/src/main.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { StrictMode, lazy, Suspense, type ReactNode } from 'react';
|
|
|
2
2
|
import { createRoot, type Root } from 'react-dom/client';
|
|
3
3
|
import { createHashRouter, RouterProvider } from 'react-router-dom';
|
|
4
4
|
import { AppShell } from './AppShell';
|
|
5
|
+
import { I18nProvider, configureI18n, getLocale } from './i18n';
|
|
5
6
|
import { AgentLayout } from './layouts/AgentLayout';
|
|
6
7
|
import { AgentPage } from './pages/AgentPage';
|
|
7
8
|
import { SettingsPage } from './pages/SettingsPage';
|
|
@@ -82,7 +83,9 @@ function renderApplication(root: Root): void {
|
|
|
82
83
|
|
|
83
84
|
root.render(
|
|
84
85
|
<StrictMode>
|
|
85
|
-
<
|
|
86
|
+
<I18nProvider>
|
|
87
|
+
<RouterProvider router={router} />
|
|
88
|
+
</I18nProvider>
|
|
86
89
|
</StrictMode>,
|
|
87
90
|
);
|
|
88
91
|
}
|
|
@@ -91,12 +94,18 @@ export async function bootstrap(): Promise<void> {
|
|
|
91
94
|
if (import.meta.env.DEV) {
|
|
92
95
|
// 动态 import:prod 构建里 Rollup 把 DEV 分支连同 mock(含其 fixtures)
|
|
93
96
|
// 整体树摇掉,浏览器 dev mock 数据不进产物。
|
|
94
|
-
const {
|
|
95
|
-
|
|
97
|
+
const { installBrowserDevHostMock } = await import('./lib/browser-dev-host-mock');
|
|
98
|
+
installBrowserDevHostMock();
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
// 标题与 favicon 在 bootstrap 时设置——此刻产品入口已完成包注册
|
|
99
102
|
// (品牌 logo 由包经 setBrandLogoUrl 注入,见 brand.ts)。
|
|
103
|
+
configureI18n({
|
|
104
|
+
locales: parseLocales(import.meta.env.VITE_LOCALES),
|
|
105
|
+
defaultLocale: import.meta.env.VITE_DEFAULT_LOCALE || 'en',
|
|
106
|
+
});
|
|
107
|
+
document.documentElement.lang = getLocale() === 'zh' ? 'zh-CN' : getLocale();
|
|
108
|
+
window.steerableHost?.setLocale?.(getLocale());
|
|
100
109
|
document.title = BRAND_NAME;
|
|
101
110
|
document.querySelector<HTMLLinkElement>('link[rel="icon"]')?.setAttribute('href', getBrandLogoUrl());
|
|
102
111
|
installHostPaste();
|
|
@@ -117,7 +126,9 @@ export async function bootstrap(): Promise<void> {
|
|
|
117
126
|
const Gate = gate.Component;
|
|
118
127
|
root.render(
|
|
119
128
|
<StrictMode>
|
|
120
|
-
<
|
|
129
|
+
<I18nProvider>
|
|
130
|
+
<Gate onAuthenticated={onAuthenticated} />
|
|
131
|
+
</I18nProvider>
|
|
121
132
|
</StrictMode>,
|
|
122
133
|
);
|
|
123
134
|
return;
|
|
@@ -125,3 +136,15 @@ export async function bootstrap(): Promise<void> {
|
|
|
125
136
|
|
|
126
137
|
renderApplication(root);
|
|
127
138
|
}
|
|
139
|
+
|
|
140
|
+
function parseLocales(raw: string | undefined): string[] {
|
|
141
|
+
if (!raw) return ['en'];
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
144
|
+
if (!Array.isArray(parsed)) return ['en'];
|
|
145
|
+
const locales = parsed.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
146
|
+
return locales.length > 0 ? locales : ['en'];
|
|
147
|
+
} catch {
|
|
148
|
+
return ['en'];
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -2,10 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { useNavigate, useOutletContext, useParams, useSearchParams } from "react-router-dom";
|
|
3
3
|
import { useChatStream, type SteerOutcome } from "@steerable/agent-ui";
|
|
4
4
|
import type { ChatMessage, SSEEvent } from "@steerable/agent-protocol";
|
|
5
|
-
import {
|
|
5
|
+
import { getHostBridge, hasHostBridge } from "@/lib/host-bridge";
|
|
6
6
|
import { trackBehavior } from "@/lib/insights";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
createHostChatTransport,
|
|
9
9
|
regenerateChatMessage,
|
|
10
10
|
} from "@/lib/chat-transport";
|
|
11
11
|
import { ChatHeader } from "@/components/ChatHeader";
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from "@/lib/pending-first-message";
|
|
64
64
|
import { isOrchestrationSettingEnabled } from "@/lib/orchestration-settings";
|
|
65
65
|
import { pickDefaultAgentId } from "@/brand";
|
|
66
|
+
import { t } from "@/i18n";
|
|
66
67
|
import {
|
|
67
68
|
appendAttachmentRefs,
|
|
68
69
|
collectImageAttachments,
|
|
@@ -263,12 +264,12 @@ function AgentChatLoader({
|
|
|
263
264
|
useEffect(() => {
|
|
264
265
|
let cancelled = false;
|
|
265
266
|
void (async () => {
|
|
266
|
-
if (!
|
|
267
|
+
if (!hasHostBridge()) {
|
|
267
268
|
if (!cancelled) setInitialMessages([]);
|
|
268
269
|
return;
|
|
269
270
|
}
|
|
270
271
|
try {
|
|
271
|
-
const bridge =
|
|
272
|
+
const bridge = getHostBridge()!;
|
|
272
273
|
const [response, live] = await Promise.all([
|
|
273
274
|
bridge.localBackend.request<{
|
|
274
275
|
messages?: ChatMessageWithMetadata[];
|
|
@@ -318,7 +319,7 @@ function AgentChatLoader({
|
|
|
318
319
|
if (initialMessages === null) {
|
|
319
320
|
return (
|
|
320
321
|
<div className="flex h-full w-full items-center justify-center text-xs text-agent-muted-foreground">
|
|
321
|
-
|
|
322
|
+
{t("Loading chat history…")}
|
|
322
323
|
</div>
|
|
323
324
|
);
|
|
324
325
|
}
|
|
@@ -386,7 +387,7 @@ function AgentChatView({
|
|
|
386
387
|
? (projects.find((p) => p.id === chat.projectId) ?? null)
|
|
387
388
|
: null;
|
|
388
389
|
const transport = useMemo(
|
|
389
|
-
() =>
|
|
390
|
+
() => createHostChatTransport(chatId),
|
|
390
391
|
[chatId],
|
|
391
392
|
);
|
|
392
393
|
|
|
@@ -889,7 +890,7 @@ function AgentChatView({
|
|
|
889
890
|
updateRemoteFollowUps(() => []);
|
|
890
891
|
void (async () => {
|
|
891
892
|
try {
|
|
892
|
-
const response = await
|
|
893
|
+
const response = await getHostBridge()?.localBackend.request<{
|
|
893
894
|
messages?: ChatMessageWithMetadata[];
|
|
894
895
|
}>({
|
|
895
896
|
method: "GET",
|
|
@@ -922,7 +923,7 @@ function AgentChatView({
|
|
|
922
923
|
]);
|
|
923
924
|
|
|
924
925
|
useEffect(() => {
|
|
925
|
-
const bridge =
|
|
926
|
+
const bridge = getHostBridge();
|
|
926
927
|
if (!bridge?.onSuggestedReplies) return;
|
|
927
928
|
return bridge.onSuggestedReplies((payload) => {
|
|
928
929
|
if (payload.chatId !== chatId) return;
|
|
@@ -1041,14 +1042,14 @@ function AgentChatView({
|
|
|
1041
1042
|
const handleExecutePlan = useCallback(() => {
|
|
1042
1043
|
handleModeChange("agent");
|
|
1043
1044
|
setPlanReady(false);
|
|
1044
|
-
void handleSubmit({ content: "
|
|
1045
|
+
void handleSubmit({ content: t("Start executing the plan above.") });
|
|
1045
1046
|
}, [handleModeChange, handleSubmit]);
|
|
1046
1047
|
|
|
1047
1048
|
// 分享当前对话:截取整个聊天面板区域(含 header + 消息 + 输入框),
|
|
1048
1049
|
// 由主进程 capturePage 后写入系统剪贴板,用户可直接粘贴到任何地方。
|
|
1049
1050
|
const handleShare = useCallback(async (): Promise<boolean> => {
|
|
1050
|
-
if (!
|
|
1051
|
-
const bridge =
|
|
1051
|
+
if (!hasHostBridge()) return false;
|
|
1052
|
+
const bridge = getHostBridge()!;
|
|
1052
1053
|
const panel = document.querySelector('.chat-panel-container');
|
|
1053
1054
|
const rect = panel?.getBoundingClientRect();
|
|
1054
1055
|
const result = await bridge.local?.captureScreenshot(
|
|
@@ -1154,7 +1155,7 @@ function AgentChatView({
|
|
|
1154
1155
|
<div className="flex h-full w-full flex-col">
|
|
1155
1156
|
{hydrationError && (
|
|
1156
1157
|
<div className="border-b border-agent-border bg-agent-muted/60 px-2.5 py-1 text-xs text-agent-destructive">
|
|
1157
|
-
|
|
1158
|
+
{t("Failed to load chat history: {error}", { error: hydrationError })}
|
|
1158
1159
|
</div>
|
|
1159
1160
|
)}
|
|
1160
1161
|
<LocalChatPanel
|
|
@@ -1172,7 +1173,7 @@ function AgentChatView({
|
|
|
1172
1173
|
header={
|
|
1173
1174
|
<ChatHeader
|
|
1174
1175
|
chat={chat}
|
|
1175
|
-
onBranchSwitched={
|
|
1176
|
+
onBranchSwitched={hasHostBridge() ? onBranchTick : undefined}
|
|
1176
1177
|
onInspectTask={ctx.inspectTask}
|
|
1177
1178
|
tasks={tasks}
|
|
1178
1179
|
chatSlots={ctx.chatSlots}
|
|
@@ -1182,7 +1183,7 @@ function AgentChatView({
|
|
|
1182
1183
|
/>
|
|
1183
1184
|
}
|
|
1184
1185
|
onRegenerate={
|
|
1185
|
-
|
|
1186
|
+
hasHostBridge()
|
|
1186
1187
|
? async (messageId) => {
|
|
1187
1188
|
// 后端 regenerate 流跑完后桌面 store 已是新分支投影;
|
|
1188
1189
|
// bump tick 重挂消息列表。
|
|
@@ -1191,7 +1192,7 @@ function AgentChatView({
|
|
|
1191
1192
|
}
|
|
1192
1193
|
: undefined
|
|
1193
1194
|
}
|
|
1194
|
-
inputPlaceholder="
|
|
1195
|
+
inputPlaceholder={t("Message the local Agent…")}
|
|
1195
1196
|
agents={ctx.agents}
|
|
1196
1197
|
chats={ctx.chats}
|
|
1197
1198
|
currentAgent={agent}
|
|
@@ -1216,11 +1217,11 @@ function AgentChatView({
|
|
|
1216
1217
|
finishedTasks={finishedTasks}
|
|
1217
1218
|
onInspectTask={handleInspectTask}
|
|
1218
1219
|
onDismissFinishedTask={dismissFinished}
|
|
1219
|
-
onShare={
|
|
1220
|
+
onShare={hasHostBridge() ? handleShare : undefined}
|
|
1220
1221
|
suggestedReplies={visibleSuggestedReplies}
|
|
1221
1222
|
onSelectSuggestion={handleSelectSuggestion}
|
|
1222
1223
|
onOpenSettings={
|
|
1223
|
-
|
|
1224
|
+
hasHostBridge() && settingsChrome("llm")
|
|
1224
1225
|
? () => setLlmSettingsOpen(true)
|
|
1225
1226
|
: undefined
|
|
1226
1227
|
}
|
|
@@ -1231,7 +1232,7 @@ function AgentChatView({
|
|
|
1231
1232
|
onSelectModel={setModelOverride}
|
|
1232
1233
|
onSelectEffort={setEffortOverride}
|
|
1233
1234
|
onOpenSettings={
|
|
1234
|
-
|
|
1235
|
+
hasHostBridge() && settingsChrome("llm")
|
|
1235
1236
|
? () => setLlmSettingsOpen(true)
|
|
1236
1237
|
: undefined
|
|
1237
1238
|
}
|
|
@@ -1269,7 +1270,9 @@ function AgentChatView({
|
|
|
1269
1270
|
<div className="mx-2.5 mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-amber-400/50 bg-amber-400/10 px-2.5 py-1.5 text-xs">
|
|
1270
1271
|
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
|
|
1271
1272
|
<LuListChecks className="h-4 w-4 shrink-0" />
|
|
1272
|
-
<span
|
|
1273
|
+
<span>
|
|
1274
|
+
{t("The plan is ready. Once it looks right, switch to Agent mode to execute it.")}
|
|
1275
|
+
</span>
|
|
1273
1276
|
</div>
|
|
1274
1277
|
<div className="flex items-center gap-2">
|
|
1275
1278
|
<button
|
|
@@ -1278,7 +1281,7 @@ function AgentChatView({
|
|
|
1278
1281
|
className="rounded-full px-2 py-1 text-agent-muted-foreground transition-colors hover:text-agent-foreground"
|
|
1279
1282
|
data-testid="plan-dismiss"
|
|
1280
1283
|
>
|
|
1281
|
-
|
|
1284
|
+
{t("Dismiss")}
|
|
1282
1285
|
</button>
|
|
1283
1286
|
<button
|
|
1284
1287
|
type="button"
|
|
@@ -1286,7 +1289,7 @@ function AgentChatView({
|
|
|
1286
1289
|
className="inline-flex items-center gap-1 rounded-full bg-agent-foreground px-3 py-1 font-medium text-agent-canvas transition hover:opacity-90"
|
|
1287
1290
|
data-testid="plan-execute"
|
|
1288
1291
|
>
|
|
1289
|
-
|
|
1292
|
+
{t("Execute plan")}
|
|
1290
1293
|
<LuArrowRight className="h-3.5 w-3.5" />
|
|
1291
1294
|
</button>
|
|
1292
1295
|
</div>
|
|
@@ -1295,7 +1298,7 @@ function AgentChatView({
|
|
|
1295
1298
|
</>
|
|
1296
1299
|
}
|
|
1297
1300
|
/>
|
|
1298
|
-
{
|
|
1301
|
+
{hasHostBridge() && settingsChrome("llm") && (
|
|
1299
1302
|
<LocalLlmSettingsModal
|
|
1300
1303
|
open={llmSettingsOpen}
|
|
1301
1304
|
onClose={() => setLlmSettingsOpen(false)}
|
|
@@ -1416,7 +1419,7 @@ function EmptyChatGate() {
|
|
|
1416
1419
|
...(selectedProjectId && showProjectsChrome ? { projectId: selectedProjectId } : {}),
|
|
1417
1420
|
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1418
1421
|
});
|
|
1419
|
-
if (!id) throw new Error("
|
|
1422
|
+
if (!id) throw new Error(t("Failed to create the chat. Please try again."));
|
|
1420
1423
|
// 会话已建,把附件落进会话空间(与 LocalChatPanel 同一套语义):
|
|
1421
1424
|
// 所有文件都写落盘路径引用(agent 用 local_read_file 读回);仅图片
|
|
1422
1425
|
// 额外进 metadata.images 走多模态,非图片文件不会被图片逻辑吞掉。
|
|
@@ -1463,11 +1466,11 @@ function EmptyChatGate() {
|
|
|
1463
1466
|
value={inputValue}
|
|
1464
1467
|
onChange={setInputValue}
|
|
1465
1468
|
onSubmit={handleSubmit}
|
|
1466
|
-
disabled={!
|
|
1469
|
+
disabled={!hasHostBridge() || isCreating}
|
|
1467
1470
|
placeholder={
|
|
1468
1471
|
mode === "plan"
|
|
1469
|
-
? "
|
|
1470
|
-
: "
|
|
1472
|
+
? t("Describe your goal. The Agent will make a plan first…")
|
|
1473
|
+
: t("Message the local Agent…")
|
|
1471
1474
|
}
|
|
1472
1475
|
currentAgent={selectedAgent}
|
|
1473
1476
|
agents={ctx.agents}
|
|
@@ -1475,7 +1478,7 @@ function EmptyChatGate() {
|
|
|
1475
1478
|
selectedAgentId={ctx.selectedAgentId}
|
|
1476
1479
|
onSelectAgent={handleSelectAgent}
|
|
1477
1480
|
onOpenSettings={
|
|
1478
|
-
|
|
1481
|
+
hasHostBridge() && settingsChrome("llm")
|
|
1479
1482
|
? () => setLlmSettingsOpen(true)
|
|
1480
1483
|
: undefined
|
|
1481
1484
|
}
|
|
@@ -1486,15 +1489,15 @@ function EmptyChatGate() {
|
|
|
1486
1489
|
onSelectModel={setModelOverride}
|
|
1487
1490
|
onSelectEffort={setEffortOverride}
|
|
1488
1491
|
onOpenSettings={
|
|
1489
|
-
|
|
1492
|
+
hasHostBridge() && settingsChrome("llm")
|
|
1490
1493
|
? () => setLlmSettingsOpen(true)
|
|
1491
1494
|
: undefined
|
|
1492
1495
|
}
|
|
1493
|
-
disabled={!
|
|
1496
|
+
disabled={!hasHostBridge() || isCreating}
|
|
1494
1497
|
/>
|
|
1495
1498
|
}
|
|
1496
1499
|
leadingChrome={
|
|
1497
|
-
|
|
1500
|
+
hasHostBridge() && showProjectsChrome ? (
|
|
1498
1501
|
<ProjectPickerButton
|
|
1499
1502
|
projects={projects}
|
|
1500
1503
|
value={selectedProjectId}
|
|
@@ -1516,7 +1519,7 @@ function EmptyChatGate() {
|
|
|
1516
1519
|
</div>
|
|
1517
1520
|
{isCreating && (
|
|
1518
1521
|
<p className="text-xs text-agent-muted-foreground">
|
|
1519
|
-
|
|
1522
|
+
{t("Creating chat…")}
|
|
1520
1523
|
</p>
|
|
1521
1524
|
)}
|
|
1522
1525
|
{createError && (
|
|
@@ -1524,13 +1527,15 @@ function EmptyChatGate() {
|
|
|
1524
1527
|
{createError}
|
|
1525
1528
|
</p>
|
|
1526
1529
|
)}
|
|
1527
|
-
{!
|
|
1530
|
+
{!hasHostBridge() && (
|
|
1528
1531
|
<p className="text-xs text-agent-destructive">
|
|
1529
|
-
|
|
1532
|
+
{t(
|
|
1533
|
+
"Browser preview mode: no host bridge, so the chat list and streaming responses are unavailable.",
|
|
1534
|
+
)}
|
|
1530
1535
|
</p>
|
|
1531
1536
|
)}
|
|
1532
1537
|
</div>
|
|
1533
|
-
{
|
|
1538
|
+
{hasHostBridge() && settingsChrome("llm") && (
|
|
1534
1539
|
<LocalLlmSettingsModal
|
|
1535
1540
|
open={llmSettingsOpen}
|
|
1536
1541
|
onClose={() => setLlmSettingsOpen(false)}
|
|
@@ -5,6 +5,7 @@ import { ChatHeader } from '@/components/ChatHeader';
|
|
|
5
5
|
import type { ExecutedAction } from '@/components/chat/ExecutedActionsCard';
|
|
6
6
|
import type { TurnBlock } from '@/components/chat/turn-timeline';
|
|
7
7
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
8
|
+
import { t } from '@/i18n';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* ChatPanelPreviewPage — dev-only visual harness for `LocalChatPanel`.
|
|
@@ -22,10 +23,10 @@ import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
|
22
23
|
const MOCK_AGENT: LocalChatAgent = {
|
|
23
24
|
id: 'agent_local_default',
|
|
24
25
|
slug: 'local-coder',
|
|
25
|
-
name: '
|
|
26
|
+
name: 'Local coder',
|
|
26
27
|
icon: '🤖',
|
|
27
28
|
color: '#0ea5e9',
|
|
28
|
-
description: '
|
|
29
|
+
description: 'Local coding assistant — shell, tool demos, and simple file edits',
|
|
29
30
|
rolePrompt: null,
|
|
30
31
|
isBuiltin: true,
|
|
31
32
|
};
|
|
@@ -34,7 +35,7 @@ const MOCK_CHAT: LocalChat = {
|
|
|
34
35
|
id: 'preview-chat',
|
|
35
36
|
projectId: null,
|
|
36
37
|
userId: 'preview-user',
|
|
37
|
-
title: '
|
|
38
|
+
title: 'Preview: check the service, then run the test suite',
|
|
38
39
|
agentId: MOCK_AGENT.id,
|
|
39
40
|
createdAt: new Date().toISOString(),
|
|
40
41
|
updatedAt: new Date().toISOString(),
|
|
@@ -47,21 +48,21 @@ const MOCK_MESSAGES: ChatMessage[] = [
|
|
|
47
48
|
{
|
|
48
49
|
id: 'm1',
|
|
49
50
|
role: 'user',
|
|
50
|
-
content: '
|
|
51
|
+
content: 'Check whether the local service is up, then run the test suite in this directory.',
|
|
51
52
|
createdAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
id: 'm2',
|
|
55
56
|
role: 'assistant',
|
|
56
57
|
agentId: MOCK_AGENT.id,
|
|
57
|
-
content:
|
|
58
|
+
content: `I'll check the service, then run the tests.
|
|
58
59
|
|
|
59
60
|
\`\`\`bash
|
|
60
61
|
$ check_connection
|
|
61
62
|
✓ Mock service @ 127.0.0.1:8765 reachable (latency 4ms)
|
|
62
63
|
\`\`\`
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
The connection is fine. Next, the test suite:
|
|
65
66
|
|
|
66
67
|
\`\`\`bash
|
|
67
68
|
$ npm test
|
|
@@ -74,18 +75,17 @@ FAIL src/components/chat/AssistantMessage.test.tsx
|
|
|
74
75
|
Tests: 1 failed, 2 passed, 3 total
|
|
75
76
|
\`\`\`
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
\`agentInitial\` 之前先 \`a.color ?? '#7c3aed'\`,让 fallback 优先于硬编码。
|
|
78
|
+
1 failure: \`AssistantMessage\` did not take the agent color from props. Use \`a.color ?? '#7c3aed'\` before \`agentInitial\` so the fallback wins over the hardcoded color.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Want me to edit that file?`,
|
|
81
81
|
createdAt: new Date(Date.now() - 1000 * 60 * 3).toISOString(),
|
|
82
82
|
},
|
|
83
83
|
];
|
|
84
84
|
|
|
85
85
|
const MOCK_TODOS = [
|
|
86
|
-
{ id: 'survey', content: '
|
|
87
|
-
{ id: 'test', content: '
|
|
88
|
-
{ id: 'fix', content: '
|
|
86
|
+
{ id: 'survey', content: 'Check service connectivity', status: 'completed' as const },
|
|
87
|
+
{ id: 'test', content: 'Run the test suite in this directory', status: 'in_progress' as const },
|
|
88
|
+
{ id: 'fix', content: 'Fix the failing assertion color', status: 'pending' as const },
|
|
89
89
|
];
|
|
90
90
|
|
|
91
91
|
const MOCK_TODO_ACTION: ExecutedAction = {
|
|
@@ -127,12 +127,12 @@ const MOCK_ACTIONS: ExecutedAction[] = [
|
|
|
127
127
|
];
|
|
128
128
|
|
|
129
129
|
const MOCK_TIMELINE: TurnBlock[] = [
|
|
130
|
-
{ type: 'reasoning', content: '
|
|
130
|
+
{ type: 'reasoning', content: 'Check connectivity first, then run the test suite.' },
|
|
131
131
|
{ type: 'tools', actions: [MOCK_TODO_ACTION] },
|
|
132
132
|
{ type: 'tools', actions: [MOCK_ACTIONS[1]] },
|
|
133
|
-
{ type: 'text', content: '
|
|
133
|
+
{ type: 'text', content: 'The connection is fine. Next, the test suite.' },
|
|
134
134
|
{ type: 'tools', actions: [MOCK_ACTIONS[2]] },
|
|
135
|
-
{ type: 'reasoning', content: '
|
|
135
|
+
{ type: 'reasoning', content: 'The tests failed. Read the failing file, then suggest a fix.' },
|
|
136
136
|
{ type: 'tools', actions: [MOCK_ACTIONS[3]] },
|
|
137
137
|
{ type: 'text', content: MOCK_MESSAGES[1].content ?? '' },
|
|
138
138
|
];
|
|
@@ -142,33 +142,35 @@ const MOCK_TIMELINE: TurnBlock[] = [
|
|
|
142
142
|
* • `static` — finished conversation; verifies copy buttons, action
|
|
143
143
|
* cards, markdown rendering on history.
|
|
144
144
|
* • `thinking` — in-flight assistant bubble before any content; verifies
|
|
145
|
-
* StreamingStatus's "
|
|
145
|
+
* StreamingStatus's "Now thinking..." baseline.
|
|
146
146
|
* • `tools-run` — in-flight bubble, round 2, 2 tools just ran; verifies
|
|
147
|
-
* StreamingStatus's "
|
|
147
|
+
* StreamingStatus's "Called N tools" line and the Round N badge.
|
|
148
148
|
* • `reasoning-stream` — reasoning tokens arrive over time; verifies the
|
|
149
149
|
* 5-line peek stays visible and the list scrollbar sticks to the bottom.
|
|
150
150
|
*/
|
|
151
151
|
type PreviewScene = 'static' | 'thinking' | 'tools-run' | 'reasoning-stream';
|
|
152
152
|
|
|
153
153
|
const STREAMING_REASONING = [
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'
|
|
162
|
-
'
|
|
163
|
-
'
|
|
154
|
+
'Check whether the service is still reachable.',
|
|
155
|
+
'If the port is open, run the test suite in this directory.',
|
|
156
|
+
'If it fails, read only the failing file. Do not scan the whole repo.',
|
|
157
|
+
"When an assertion color is wrong, look at AssistantMessage's default color first.",
|
|
158
|
+
'After the edit, run the tests again and check for new failures.',
|
|
159
|
+
'Keep tool calls few: connectivity, tests, and a file read only if needed.',
|
|
160
|
+
'Finish with one or two sentences on the result and the next step.',
|
|
161
|
+
'If thinking is still streaming, later sentences are cut by the 5-line window.',
|
|
162
|
+
'Comparing a < b must not be eaten as an HTML tag by Markdown.',
|
|
163
|
+
'The scrollbar should stay pinned to the bottom and not jump with each token.',
|
|
164
164
|
].join('\n');
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
function previewScenes(): { id: PreviewScene; label: string }[] {
|
|
167
|
+
return [
|
|
168
|
+
{ id: 'static', label: t('Static history') },
|
|
169
|
+
{ id: 'thinking', label: t('Streaming · empty content') },
|
|
170
|
+
{ id: 'tools-run', label: t('Streaming · tools ran · round 2') },
|
|
171
|
+
{ id: 'reasoning-stream', label: t('Streaming · reasoning typing') },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
172
174
|
|
|
173
175
|
export function ChatPanelPreviewPage() {
|
|
174
176
|
const [scene, setScene] = useState<PreviewScene>('static');
|
|
@@ -249,14 +251,14 @@ export function ChatPanelPreviewPage() {
|
|
|
249
251
|
{
|
|
250
252
|
type: 'reasoning',
|
|
251
253
|
content: [
|
|
252
|
-
'
|
|
253
|
-
'
|
|
254
|
-
'
|
|
255
|
-
|
|
256
|
-
'
|
|
257
|
-
'
|
|
258
|
-
'
|
|
259
|
-
'
|
|
254
|
+
'Check whether the service is still reachable.',
|
|
255
|
+
'If the port is open, run the test suite in this directory.',
|
|
256
|
+
'If it fails, read only the failing file. Do not scan the whole repo.',
|
|
257
|
+
"When an assertion color is wrong, look at AssistantMessage's default color first.",
|
|
258
|
+
'After the edit, run the tests again and check for new failures.',
|
|
259
|
+
'Keep tool calls few: connectivity, tests, and a file read only if needed.',
|
|
260
|
+
'Finish with one or two sentences on the result and the next step.',
|
|
261
|
+
'If thinking is still streaming, later sentences are cut by the 5-line window.',
|
|
260
262
|
].join('\n'),
|
|
261
263
|
},
|
|
262
264
|
{
|
|
@@ -299,7 +301,7 @@ export function ChatPanelPreviewPage() {
|
|
|
299
301
|
timelineByMsgId: { m2: MOCK_TIMELINE },
|
|
300
302
|
durationByMessageId: { m2: 83_000 },
|
|
301
303
|
currentRound: 1,
|
|
302
|
-
suggestedReplies: ['
|
|
304
|
+
suggestedReplies: ['Fix the failing assertion color', 'Run the tests again', 'Explain this failure'],
|
|
303
305
|
};
|
|
304
306
|
}
|
|
305
307
|
}, [scene, streamedReasoning]);
|
|
@@ -311,7 +313,7 @@ export function ChatPanelPreviewPage() {
|
|
|
311
313
|
/preview/chat (dev harness)
|
|
312
314
|
</span>
|
|
313
315
|
<div className="flex gap-1">
|
|
314
|
-
{
|
|
316
|
+
{previewScenes().map((s) => (
|
|
315
317
|
<button
|
|
316
318
|
key={s.id}
|
|
317
319
|
type="button"
|
|
@@ -339,7 +341,7 @@ export function ChatPanelPreviewPage() {
|
|
|
339
341
|
header={
|
|
340
342
|
<ChatHeader chat={MOCK_CHAT} />
|
|
341
343
|
}
|
|
342
|
-
inputPlaceholder=
|
|
344
|
+
inputPlaceholder={t('Preview mode: input is not actually sent…')}
|
|
343
345
|
agents={[MOCK_AGENT]}
|
|
344
346
|
currentAgent={MOCK_AGENT}
|
|
345
347
|
executedActionsByMessageId={config.actionsByMsgId}
|