@steerable/agent-shell-web 0.6.52 → 0.6.55
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 +4 -4
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +60 -52
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +29 -19
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +2 -1
- package/src/components/TerminalPanel.tsx +3 -2
- package/src/components/TerminalView.tsx +12 -7
- package/src/components/chat/ApprovalModal.tsx +39 -25
- package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
- package/src/components/chat/AssistantMessage.tsx +18 -13
- package/src/components/chat/ChatInput.tsx +138 -42
- package/src/components/chat/ChatProjectBadge.tsx +24 -19
- 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 +31 -21
- package/src/components/chat/TaskProcessPanel.tsx +14 -13
- 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/settings/AgentCapabilityFields.tsx +24 -21
- package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
- 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 +43 -28
- package/src/components/settings/LlmSettingsPanel.tsx +104 -83
- package/src/components/settings/McpSettingsPanel.tsx +193 -71
- 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 +22 -19
- package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
- package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
- package/src/components/settings/UsagePanel.tsx +19 -20
- package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
- 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/useProjects.ts +1 -1
- 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 +7 -6
- package/src/lib/app-release.ts +15 -10
- package/src/lib/attachments.ts +24 -7
- package/src/lib/browser-dev-electron-mock.ts +34 -34
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +6 -2
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/electron-bridge.ts +19 -6
- package/src/lib/host-file-drop.ts +68 -0
- package/src/lib/host-paste.ts +38 -9
- package/src/lib/http-bridge.ts +4 -1
- package/src/lib/portable.ts +8 -7
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/lib/tauri-bridge.ts +5 -0
- package/src/main.tsx +25 -2
- package/src/pages/AgentPage.tsx +17 -12
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +48 -42
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
|
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
|
|
|
3
3
|
import { LuCheck, LuCopy, LuListChecks, LuLoaderCircle, LuRefreshCw, LuShare2 } from 'react-icons/lu';
|
|
4
4
|
import type { ChatMessage } from '@steerable/agent-protocol';
|
|
5
5
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
6
|
+
import { t } from '@/i18n';
|
|
6
7
|
import { Markdown } from './Markdown';
|
|
7
8
|
import { ExecutedActionsCard, type ExecutedAction } from './ExecutedActionsCard';
|
|
8
9
|
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
@@ -228,7 +229,7 @@ function readPersistedAgentId(message: ChatMessage): string | undefined {
|
|
|
228
229
|
function TurnErrorBubble({ reason }: { reason: string }) {
|
|
229
230
|
return (
|
|
230
231
|
<div className="rounded-agent-lg border border-agent-destructive/40 bg-agent-destructive/5 px-2.5 py-2 text-xs leading-relaxed text-agent-destructive shadow-sm">
|
|
231
|
-
|
|
232
|
+
{t('Request failed: {reason}', { reason })}
|
|
232
233
|
</div>
|
|
233
234
|
);
|
|
234
235
|
}
|
|
@@ -339,7 +340,7 @@ export function AssistantMessage({
|
|
|
339
340
|
{isPlanMode && (
|
|
340
341
|
<span className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800/50 bg-amber-100/70 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-300 select-none">
|
|
341
342
|
<LuListChecks className="h-3.5 w-3.5" />
|
|
342
|
-
<span
|
|
343
|
+
<span>{t('Plan mode (Plan)')}</span>
|
|
343
344
|
</span>
|
|
344
345
|
)}
|
|
345
346
|
</div>
|
|
@@ -369,7 +370,7 @@ export function AssistantMessage({
|
|
|
369
370
|
</div>
|
|
370
371
|
) : (
|
|
371
372
|
<div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
|
|
372
|
-
(
|
|
373
|
+
{t('(empty message)')}
|
|
373
374
|
</div>
|
|
374
375
|
)
|
|
375
376
|
}
|
|
@@ -427,7 +428,7 @@ export function AssistantMessage({
|
|
|
427
428
|
</div>
|
|
428
429
|
) : (
|
|
429
430
|
<div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
|
|
430
|
-
(
|
|
431
|
+
{t('(empty message)')}
|
|
431
432
|
</div>
|
|
432
433
|
)}
|
|
433
434
|
{displayContent && failure ? (
|
|
@@ -458,14 +459,14 @@ export function AssistantMessage({
|
|
|
458
459
|
type="button"
|
|
459
460
|
onClick={() => void copy()}
|
|
460
461
|
className={META_ACTION}
|
|
461
|
-
title={copied ? '
|
|
462
|
-
aria-label={copied ? '
|
|
462
|
+
title={copied ? t('Copied') : t('Copy message')}
|
|
463
|
+
aria-label={copied ? t('Copied') : t('Copy message')}
|
|
463
464
|
>
|
|
464
465
|
{copied ? (
|
|
465
466
|
<>
|
|
466
467
|
<LuCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
|
|
467
468
|
<span className="text-emerald-600 dark:text-emerald-400">
|
|
468
|
-
|
|
469
|
+
{t('Copied')}
|
|
469
470
|
</span>
|
|
470
471
|
</>
|
|
471
472
|
) : (
|
|
@@ -493,8 +494,12 @@ export function AssistantMessage({
|
|
|
493
494
|
.finally(() => setRegenerating(false));
|
|
494
495
|
}}
|
|
495
496
|
className={`${META_ACTION} disabled:cursor-not-allowed`}
|
|
496
|
-
title={
|
|
497
|
-
|
|
497
|
+
title={
|
|
498
|
+
regenerating
|
|
499
|
+
? t('Regenerating...')
|
|
500
|
+
: t('Regenerate (the old reply is kept as a branch)')
|
|
501
|
+
}
|
|
502
|
+
aria-label={regenerating ? t('Regenerating') : t('Regenerate')}
|
|
498
503
|
data-action="regenerate"
|
|
499
504
|
>
|
|
500
505
|
<LuRefreshCw
|
|
@@ -559,12 +564,12 @@ function ShareChatButton({ onShare }: { onShare: () => Promise<boolean> }) {
|
|
|
559
564
|
}`}
|
|
560
565
|
title={
|
|
561
566
|
state === 'done'
|
|
562
|
-
? '
|
|
567
|
+
? t('Screenshot copied to clipboard')
|
|
563
568
|
: state === 'error'
|
|
564
|
-
? '
|
|
565
|
-
: '
|
|
569
|
+
? t('Screenshot failed. Try again.')
|
|
570
|
+
: t('Share chat (copy a screenshot to the clipboard)')
|
|
566
571
|
}
|
|
567
|
-
aria-label=
|
|
572
|
+
aria-label={t('Share chat screenshot')}
|
|
568
573
|
>
|
|
569
574
|
{state === 'busy' ? (
|
|
570
575
|
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
@@ -32,9 +32,12 @@ import {
|
|
|
32
32
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
33
33
|
import type { ExecPolicy } from '@/lib/exec-policy';
|
|
34
34
|
import { getWebChatModes, hostToolCapability, settingsChrome } from '@/lib/host-tools';
|
|
35
|
-
import { hostClipboardAvailable, requestHostPaste } from '@/lib/host-paste';
|
|
36
|
-
import type
|
|
35
|
+
import { hostClipboardAvailable, requestHostPaste, type HostClipboardFile } from '@/lib/host-paste';
|
|
36
|
+
import { attachmentFromPath, type AttachmentFile } from '@/lib/attachments';
|
|
37
|
+
import { dropPointHitsRect, hostFileDropAvailable, listenHostFileDrop } from '@/lib/host-file-drop';
|
|
37
38
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
39
|
+
import { t } from '@/i18n';
|
|
40
|
+
import { agentLabel } from '@/i18n/agent-label';
|
|
38
41
|
import {
|
|
39
42
|
isHiddenSlashSkill,
|
|
40
43
|
resolveSlashTool,
|
|
@@ -277,11 +280,11 @@ function parseMentionSegments(
|
|
|
277
280
|
if (!ranges.some((range) => offset < range.end && end > range.start)) {
|
|
278
281
|
const name = match.slice(1);
|
|
279
282
|
const agent = agents.find((a) => a.name === name || a.slug === name);
|
|
280
|
-
const chat = chats.find((c) => (c.title || '
|
|
283
|
+
const chat = chats.find((c) => (c.title || t('Untitled chat')) === name);
|
|
281
284
|
const ref: MentionReference | undefined = agent
|
|
282
285
|
? { type: 'agent', id: agent.id, label: agent.name }
|
|
283
286
|
: chat
|
|
284
|
-
? { type: 'chat', id: chat.id, label: chat.title || '
|
|
287
|
+
? { type: 'chat', id: chat.id, label: chat.title || t('Untitled chat') }
|
|
285
288
|
: undefined;
|
|
286
289
|
|
|
287
290
|
ranges.push({ start: offset, end, text: match, ref });
|
|
@@ -372,7 +375,7 @@ function writeEditorValue(
|
|
|
372
375
|
const isChat = seg.ref?.type === 'chat';
|
|
373
376
|
const isSkill = seg.ref?.type === 'skill';
|
|
374
377
|
const isMcp = seg.ref?.type === 'mcp';
|
|
375
|
-
const removeLabel =
|
|
378
|
+
const removeLabel = t('Remove {name}', { name: seg.text });
|
|
376
379
|
|
|
377
380
|
const chip = document.createElement('button');
|
|
378
381
|
chip.type = 'button';
|
|
@@ -714,6 +717,23 @@ function clipboardOf(event: ClipboardEvent<HTMLDivElement>): DataTransfer | null
|
|
|
714
717
|
return native.clipboardData ?? event.clipboardData ?? null;
|
|
715
718
|
}
|
|
716
719
|
|
|
720
|
+
function fileFromHostClipboard(file: HostClipboardFile): File | null {
|
|
721
|
+
if (!file.dataBase64) return null;
|
|
722
|
+
let binary: string;
|
|
723
|
+
try {
|
|
724
|
+
binary = atob(file.dataBase64);
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const bytes = new Uint8Array(binary.length);
|
|
729
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
730
|
+
bytes[index] = binary.charCodeAt(index);
|
|
731
|
+
}
|
|
732
|
+
return new File([bytes], file.name || 'image.png', {
|
|
733
|
+
type: file.mime || 'application/octet-stream',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
717
737
|
function namePastedImage(file: File, serial: number, now: Date): File {
|
|
718
738
|
if (!file.type.startsWith('image/')) return file;
|
|
719
739
|
if (file.name.trim() && !GENERIC_CLIPBOARD_IMAGE_NAME.test(file.name.trim())) return file;
|
|
@@ -760,7 +780,7 @@ function AttachmentChip({
|
|
|
760
780
|
type="button"
|
|
761
781
|
onClick={onRemove}
|
|
762
782
|
className="flex h-4 w-4 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
763
|
-
aria-label={
|
|
783
|
+
aria-label={t('Remove file {name}', { name: file.name })}
|
|
764
784
|
>
|
|
765
785
|
<LuX className="h-3 w-3" />
|
|
766
786
|
</button>
|
|
@@ -781,7 +801,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
781
801
|
onRemoveFollowUp,
|
|
782
802
|
isStreaming = false,
|
|
783
803
|
disabled = false,
|
|
784
|
-
placeholder = '
|
|
804
|
+
placeholder = t('Message the Agent...'),
|
|
785
805
|
currentAgent,
|
|
786
806
|
agents = [],
|
|
787
807
|
chats = [],
|
|
@@ -835,7 +855,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
835
855
|
const compositionEndAtRef = useRef<number>(0);
|
|
836
856
|
|
|
837
857
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
858
|
+
const dropBoxRef = useRef<HTMLDivElement>(null);
|
|
838
859
|
const pasteSerialRef = useRef(0);
|
|
860
|
+
const attachDroppedRef = useRef<(paths: string[]) => void>(() => {});
|
|
861
|
+
const dragHoverRef = useRef<(hovering: boolean) => void>(() => {});
|
|
862
|
+
const hostPasteFilesRef = useRef<(files: HostClipboardFile[]) => void>(() => {});
|
|
839
863
|
const adoptPastedTextRef = useRef<(text: string) => void>(() => {});
|
|
840
864
|
// 工具栏"空间不足时优先隐藏快捷键提示"的测量 refs,见下方 useLayoutEffect。
|
|
841
865
|
const toolbarRowRef = useRef<HTMLDivElement>(null);
|
|
@@ -919,8 +943,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
919
943
|
.map((chat) => ({
|
|
920
944
|
type: 'chat',
|
|
921
945
|
id: chat.id,
|
|
922
|
-
label: chat.title || '
|
|
923
|
-
description: chat.updatedAt
|
|
946
|
+
label: chat.title || t('Untitled chat'),
|
|
947
|
+
description: chat.updatedAt
|
|
948
|
+
? t('Past chat · {time}', { time: new Date(chat.updatedAt).toLocaleString() })
|
|
949
|
+
: t('Past chat'),
|
|
924
950
|
}));
|
|
925
951
|
|
|
926
952
|
return [...agentSuggestions, ...chatSuggestions].slice(0, MAX_MENTION_SUGGESTIONS);
|
|
@@ -1020,11 +1046,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1020
1046
|
const mcpGroup = suggestions.filter((s) => s.kind === 'mcp');
|
|
1021
1047
|
let idx = 0;
|
|
1022
1048
|
if (skillGroup.length > 0) {
|
|
1023
|
-
rows.push({ type: 'header', label: '
|
|
1049
|
+
rows.push({ type: 'header', label: t('Run a local skill (Skill)') });
|
|
1024
1050
|
for (const s of skillGroup) rows.push({ type: 'item', item: s, idx: idx++ });
|
|
1025
1051
|
}
|
|
1026
1052
|
if (mcpGroup.length > 0) {
|
|
1027
|
-
rows.push({ type: 'header', label: '
|
|
1053
|
+
rows.push({ type: 'header', label: t('Call an MCP tool (MCP Tool)') });
|
|
1028
1054
|
for (const s of mcpGroup) rows.push({ type: 'item', item: s, idx: idx++ });
|
|
1029
1055
|
}
|
|
1030
1056
|
return rows;
|
|
@@ -1240,6 +1266,33 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1240
1266
|
path: (file as { path?: string }).path || '',
|
|
1241
1267
|
file,
|
|
1242
1268
|
});
|
|
1269
|
+
attachDroppedRef.current = (paths) => {
|
|
1270
|
+
if (disabled || !allowFileAttach) return;
|
|
1271
|
+
addFiles(paths.map(attachmentFromPath));
|
|
1272
|
+
};
|
|
1273
|
+
dragHoverRef.current = (hovering) => {
|
|
1274
|
+
if (disabled || !allowFileAttach) {
|
|
1275
|
+
setIsDragging(false);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
setIsDragging(hovering);
|
|
1279
|
+
};
|
|
1280
|
+
hostPasteFilesRef.current = (incoming) => {
|
|
1281
|
+
if (disabled || !allowFileAttach) return;
|
|
1282
|
+
const now = new Date();
|
|
1283
|
+
const next: AttachmentFile[] = [];
|
|
1284
|
+
for (const item of incoming) {
|
|
1285
|
+
if (item.path) {
|
|
1286
|
+
next.push(attachmentFromPath(item.path));
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const blob = fileFromHostClipboard(item);
|
|
1290
|
+
if (!blob) continue;
|
|
1291
|
+
pasteSerialRef.current += 1;
|
|
1292
|
+
next.push(toAttachment(namePastedImage(blob, pasteSerialRef.current, now)));
|
|
1293
|
+
}
|
|
1294
|
+
if (next.length > 0) addFiles(next);
|
|
1295
|
+
};
|
|
1243
1296
|
|
|
1244
1297
|
// 按钮点击 → 触发隐藏的 <input type="file" multiple>,由浏览器/Electron
|
|
1245
1298
|
// 直接弹出系统文件选择框(不依赖 IPC,避免「点击无反应」)。
|
|
@@ -1449,14 +1502,44 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1449
1502
|
event.preventDefault();
|
|
1450
1503
|
adoptPastedTextRef.current(text);
|
|
1451
1504
|
};
|
|
1505
|
+
const onHostPasteFiles = (event: Event) => {
|
|
1506
|
+
const files = (event as CustomEvent<HostClipboardFile[]>).detail;
|
|
1507
|
+
if (!Array.isArray(files) || files.length === 0) return;
|
|
1508
|
+
event.preventDefault();
|
|
1509
|
+
hostPasteFilesRef.current(files);
|
|
1510
|
+
};
|
|
1452
1511
|
editor.addEventListener('compositionstart', onCompositionStart, true);
|
|
1453
1512
|
editor.addEventListener('hostpaste', onHostPaste);
|
|
1513
|
+
editor.addEventListener('hostpastefiles', onHostPasteFiles);
|
|
1454
1514
|
return () => {
|
|
1455
1515
|
editor.removeEventListener('compositionstart', onCompositionStart, true);
|
|
1456
1516
|
editor.removeEventListener('hostpaste', onHostPaste);
|
|
1517
|
+
editor.removeEventListener('hostpastefiles', onHostPasteFiles);
|
|
1457
1518
|
};
|
|
1458
1519
|
}, []);
|
|
1459
1520
|
|
|
1521
|
+
useEffect(() => {
|
|
1522
|
+
if (!hostFileDropAvailable()) return undefined;
|
|
1523
|
+
return listenHostFileDrop((event) => {
|
|
1524
|
+
const box = dropBoxRef.current;
|
|
1525
|
+
if (event.type === 'leave' || !box || event.x == null || event.y == null) {
|
|
1526
|
+
dragHoverRef.current(false);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const hit = dropPointHitsRect(
|
|
1530
|
+
{ x: event.x, y: event.y },
|
|
1531
|
+
box.getBoundingClientRect(),
|
|
1532
|
+
window.devicePixelRatio,
|
|
1533
|
+
);
|
|
1534
|
+
if (event.type === 'drop') {
|
|
1535
|
+
dragHoverRef.current(false);
|
|
1536
|
+
if (hit) attachDroppedRef.current(event.paths);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
dragHoverRef.current(hit);
|
|
1540
|
+
});
|
|
1541
|
+
}, []);
|
|
1542
|
+
|
|
1460
1543
|
const handlePaste = (event: ClipboardEvent<HTMLDivElement>) => {
|
|
1461
1544
|
// Read before preventDefault. WebKit clears clipboardData once the paste is canceled.
|
|
1462
1545
|
const data = clipboardOf(event);
|
|
@@ -1608,7 +1691,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1608
1691
|
// 进入待发队列 / 作为新回合发出),草稿都可以清;queued 额外
|
|
1609
1692
|
// 提示用户"不是追加进当前回合"。
|
|
1610
1693
|
if (outcome === 'queued') {
|
|
1611
|
-
showSteerNotice(
|
|
1694
|
+
showSteerNotice(
|
|
1695
|
+
t(
|
|
1696
|
+
'This turn cannot take an interjection. The message was queued and will be sent when this turn ends.',
|
|
1697
|
+
),
|
|
1698
|
+
);
|
|
1612
1699
|
}
|
|
1613
1700
|
onChange('');
|
|
1614
1701
|
});
|
|
@@ -1695,7 +1782,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1695
1782
|
onAutoContinue={() => askUserPrompt.answer({})}
|
|
1696
1783
|
bottomHint={
|
|
1697
1784
|
askUserPrompt.pendingCount > 0
|
|
1698
|
-
?
|
|
1785
|
+
? t('{count} more question groups to answer', {
|
|
1786
|
+
count: askUserPrompt.pendingCount,
|
|
1787
|
+
})
|
|
1699
1788
|
: undefined
|
|
1700
1789
|
}
|
|
1701
1790
|
/>
|
|
@@ -1735,7 +1824,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1735
1824
|
{mentionQuery && mentionSuggestions.length > 0 && (
|
|
1736
1825
|
<div className="absolute bottom-full left-3 z-50 mb-1 max-h-72 w-80 max-w-[calc(100%-0.75rem)] overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg">
|
|
1737
1826
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
1738
|
-
@
|
|
1827
|
+
{t('@ Choose an Agent or past chat')}
|
|
1739
1828
|
</div>
|
|
1740
1829
|
{mentionSuggestions.map((item, idx) => (
|
|
1741
1830
|
<button
|
|
@@ -1769,7 +1858,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1769
1858
|
<span className="flex items-center gap-1.5">
|
|
1770
1859
|
<span className="truncate text-xs font-medium">@{item.label}</span>
|
|
1771
1860
|
<span className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] text-agent-muted-foreground">
|
|
1772
|
-
{item.type === 'agent' ? 'Agent' : '
|
|
1861
|
+
{item.type === 'agent' ? 'Agent' : t('Chat')}
|
|
1773
1862
|
</span>
|
|
1774
1863
|
</span>
|
|
1775
1864
|
{item.description && (
|
|
@@ -1822,7 +1911,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1822
1911
|
<div className="text-xs font-medium truncate">
|
|
1823
1912
|
{row.item.skill.displayName ? (
|
|
1824
1913
|
<>
|
|
1825
|
-
{row.item.skill.displayName}
|
|
1914
|
+
{t(row.item.skill.displayName)}
|
|
1826
1915
|
<span className="ml-1.5 text-[10px] font-normal text-agent-muted-foreground/70">
|
|
1827
1916
|
/{row.item.skill.name}
|
|
1828
1917
|
</span>
|
|
@@ -1862,6 +1951,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1862
1951
|
</div>
|
|
1863
1952
|
)}
|
|
1864
1953
|
<div
|
|
1954
|
+
ref={dropBoxRef}
|
|
1865
1955
|
className={`chat-input-box flex flex-col rounded-agent-lg border bg-agent-canvas shadow-sm transition-all duration-200 ${
|
|
1866
1956
|
isDragging
|
|
1867
1957
|
? 'border-blue-500 ring-2 ring-blue-500/20'
|
|
@@ -1876,7 +1966,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1876
1966
|
{pendingFollowUps.length > 0 && (
|
|
1877
1967
|
<div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
|
|
1878
1968
|
<div className="text-[11px] font-medium text-agent-muted-foreground">
|
|
1879
|
-
|
|
1969
|
+
{t(
|
|
1970
|
+
'Queued ({count}) · Sent when this turn ends · Restored to the input box if stopped',
|
|
1971
|
+
{ count: pendingFollowUps.length },
|
|
1972
|
+
)}
|
|
1880
1973
|
</div>
|
|
1881
1974
|
{pendingFollowUps.map((text, i) => (
|
|
1882
1975
|
<div
|
|
@@ -1887,7 +1980,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1887
1980
|
{onRemoveFollowUp && (
|
|
1888
1981
|
<button
|
|
1889
1982
|
type="button"
|
|
1890
|
-
aria-label={
|
|
1983
|
+
aria-label={t('Withdraw queued message {index}', { index: i + 1 })}
|
|
1891
1984
|
onClick={() => onRemoveFollowUp(i)}
|
|
1892
1985
|
className="shrink-0 text-agent-muted-foreground opacity-0 transition-opacity hover:text-agent-destructive group-hover:opacity-100"
|
|
1893
1986
|
>
|
|
@@ -1979,8 +2072,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1979
2072
|
onClick={handlePickFiles}
|
|
1980
2073
|
disabled={disabled || isStreaming}
|
|
1981
2074
|
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"
|
|
1982
|
-
title=
|
|
1983
|
-
aria-label=
|
|
2075
|
+
title={t('Upload files, or paste images into the input box')}
|
|
2076
|
+
aria-label={t('Upload files')}
|
|
1984
2077
|
data-testid="chat-attach"
|
|
1985
2078
|
>
|
|
1986
2079
|
<LuPaperclip className="h-3 w-3" />
|
|
@@ -2013,7 +2106,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2013
2106
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2014
2107
|
Enter
|
|
2015
2108
|
</kbd>
|
|
2016
|
-
<span className="ml-1"
|
|
2109
|
+
<span className="ml-1">{t('Queue')}</span>
|
|
2017
2110
|
</span>
|
|
2018
2111
|
)}
|
|
2019
2112
|
{onSteer && (
|
|
@@ -2025,14 +2118,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2025
2118
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2026
2119
|
Enter
|
|
2027
2120
|
</kbd>
|
|
2028
|
-
<span className="ml-1"
|
|
2121
|
+
<span className="ml-1">{t('Interject')}</span>
|
|
2029
2122
|
</span>
|
|
2030
2123
|
)}
|
|
2031
2124
|
<span className="ml-2">
|
|
2032
2125
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2033
2126
|
Esc
|
|
2034
2127
|
</kbd>
|
|
2035
|
-
<span className="ml-1"
|
|
2128
|
+
<span className="ml-1">{t('Stop')}</span>
|
|
2036
2129
|
</span>
|
|
2037
2130
|
</>
|
|
2038
2131
|
) : (
|
|
@@ -2040,7 +2133,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2040
2133
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2041
2134
|
Enter
|
|
2042
2135
|
</kbd>
|
|
2043
|
-
<span className="ml-1"
|
|
2136
|
+
<span className="ml-1">{t('Send')}</span>
|
|
2044
2137
|
<span className="ml-2">
|
|
2045
2138
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2046
2139
|
Shift
|
|
@@ -2049,7 +2142,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2049
2142
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2050
2143
|
Enter
|
|
2051
2144
|
</kbd>
|
|
2052
|
-
<span className="ml-1"
|
|
2145
|
+
<span className="ml-1">{t('New line')}</span>
|
|
2053
2146
|
</span>
|
|
2054
2147
|
</>
|
|
2055
2148
|
)}
|
|
@@ -2069,11 +2162,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2069
2162
|
title={
|
|
2070
2163
|
isStreaming
|
|
2071
2164
|
? pendingFollowUps.length > 0
|
|
2072
|
-
?
|
|
2073
|
-
|
|
2074
|
-
|
|
2165
|
+
? t(
|
|
2166
|
+
'Stop generating (Esc) · {count} queued messages will be restored to the input box',
|
|
2167
|
+
{ count: pendingFollowUps.length },
|
|
2168
|
+
)
|
|
2169
|
+
: t('Stop generating (Esc)')
|
|
2170
|
+
: t('Send (Enter)')
|
|
2075
2171
|
}
|
|
2076
|
-
aria-label={isStreaming ? '
|
|
2172
|
+
aria-label={isStreaming ? t('Stop generating') : t('Send message')}
|
|
2077
2173
|
data-testid="chat-send"
|
|
2078
2174
|
>
|
|
2079
2175
|
{isStreaming ? (
|
|
@@ -2091,7 +2187,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2091
2187
|
);
|
|
2092
2188
|
|
|
2093
2189
|
function agentInitial(agent: LocalChatAgent): string {
|
|
2094
|
-
return agent.
|
|
2190
|
+
return agentLabel(agent).trim()[0]?.toUpperCase() ?? 'A';
|
|
2095
2191
|
}
|
|
2096
2192
|
|
|
2097
2193
|
/**
|
|
@@ -2113,7 +2209,7 @@ function ModeToggle({
|
|
|
2113
2209
|
<div
|
|
2114
2210
|
className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
|
|
2115
2211
|
role="radiogroup"
|
|
2116
|
-
aria-label=
|
|
2212
|
+
aria-label={t('Chat mode')}
|
|
2117
2213
|
data-testid="mode-toggle"
|
|
2118
2214
|
>
|
|
2119
2215
|
<button
|
|
@@ -2122,7 +2218,7 @@ function ModeToggle({
|
|
|
2122
2218
|
aria-checked={mode === 'agent'}
|
|
2123
2219
|
disabled={disabled}
|
|
2124
2220
|
onClick={() => onChange('agent')}
|
|
2125
|
-
title=
|
|
2221
|
+
title={t('Agent mode: carry out the task directly')}
|
|
2126
2222
|
data-testid="mode-agent"
|
|
2127
2223
|
className={[
|
|
2128
2224
|
'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
@@ -2140,7 +2236,7 @@ function ModeToggle({
|
|
|
2140
2236
|
aria-checked={mode === 'plan'}
|
|
2141
2237
|
disabled={disabled}
|
|
2142
2238
|
onClick={() => onChange('plan')}
|
|
2143
|
-
title=
|
|
2239
|
+
title={t('Plan mode: make a plan first, read only and no execution')}
|
|
2144
2240
|
data-testid="mode-plan"
|
|
2145
2241
|
className={[
|
|
2146
2242
|
'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
@@ -2187,14 +2283,14 @@ function AgentSelect({
|
|
|
2187
2283
|
|
|
2188
2284
|
const buttonTitle = useMemo(() => {
|
|
2189
2285
|
const activeNames = [
|
|
2190
|
-
agent
|
|
2286
|
+
agentLabel(agent),
|
|
2191
2287
|
...mentionReferences
|
|
2192
2288
|
.filter((ref) => ref.type === 'agent' && ref.id !== agent.id)
|
|
2193
2289
|
.map((ref) => ref.label),
|
|
2194
2290
|
].filter(Boolean) as string[];
|
|
2195
2291
|
|
|
2196
|
-
return
|
|
2197
|
-
}, [agent
|
|
2292
|
+
return t('Active experts: {names}', { names: activeNames.join(', ') });
|
|
2293
|
+
}, [agent, mentionReferences]);
|
|
2198
2294
|
|
|
2199
2295
|
return (
|
|
2200
2296
|
<div
|
|
@@ -2213,7 +2309,7 @@ function AgentSelect({
|
|
|
2213
2309
|
>
|
|
2214
2310
|
<AgentDot agent={agent} />
|
|
2215
2311
|
<span className="truncate font-medium">
|
|
2216
|
-
{agent
|
|
2312
|
+
{agentLabel(agent)}
|
|
2217
2313
|
{mentionedAgentCount > 0 ? ` +${mentionedAgentCount}` : ''}
|
|
2218
2314
|
</span>
|
|
2219
2315
|
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
@@ -2225,7 +2321,7 @@ function AgentSelect({
|
|
|
2225
2321
|
className="absolute bottom-full left-0 z-50 mb-1 max-h-64 w-72 overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg"
|
|
2226
2322
|
>
|
|
2227
2323
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
2228
|
-
|
|
2324
|
+
{t('Choose an expert')}
|
|
2229
2325
|
</div>
|
|
2230
2326
|
{agents.map((item) => {
|
|
2231
2327
|
const isPrimary = item.id === agent.id;
|
|
@@ -2252,16 +2348,16 @@ function AgentSelect({
|
|
|
2252
2348
|
<span className="min-w-0 flex-1">
|
|
2253
2349
|
<span className="flex items-center gap-1.5">
|
|
2254
2350
|
<span className="block truncate text-xs font-medium">
|
|
2255
|
-
{item
|
|
2351
|
+
{agentLabel(item)}
|
|
2256
2352
|
</span>
|
|
2257
2353
|
{isPrimary && (
|
|
2258
2354
|
<span className="inline-flex items-center rounded bg-agent-foreground/10 px-1 py-0.5 text-[9px] font-medium text-agent-foreground">
|
|
2259
|
-
|
|
2355
|
+
{t('Current')}
|
|
2260
2356
|
</span>
|
|
2261
2357
|
)}
|
|
2262
2358
|
{!isPrimary && isMentioned && (
|
|
2263
2359
|
<span className="inline-flex items-center rounded bg-agent-muted px-1 py-0.5 text-[9px] font-medium text-agent-muted-foreground border border-agent-border">
|
|
2264
|
-
|
|
2360
|
+
{t('Mentioned')}
|
|
2265
2361
|
</span>
|
|
2266
2362
|
)}
|
|
2267
2363
|
</span>
|
|
@@ -2286,7 +2382,7 @@ function AgentSelect({
|
|
|
2286
2382
|
className="mt-0.5 flex w-full items-center gap-2 rounded border-t border-agent-border/50 px-2 py-2 text-left text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
2287
2383
|
>
|
|
2288
2384
|
<LuBot className="h-3.5 w-3.5 shrink-0" />
|
|
2289
|
-
<span className="text-xs font-medium"
|
|
2385
|
+
<span className="text-xs font-medium">{t('Manage agents')}</span>
|
|
2290
2386
|
</button>
|
|
2291
2387
|
)}
|
|
2292
2388
|
</div>
|