@steerable/agent-shell-web 0.6.53 → 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 +56 -40
- 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 +60 -34
- 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 +16 -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 +4 -5
- 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/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" />
|
|
@@ -36,6 +36,8 @@ import { hostClipboardAvailable, requestHostPaste, type HostClipboardFile } from
|
|
|
36
36
|
import { attachmentFromPath, type AttachmentFile } from '@/lib/attachments';
|
|
37
37
|
import { dropPointHitsRect, hostFileDropAvailable, listenHostFileDrop } from '@/lib/host-file-drop';
|
|
38
38
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
39
|
+
import { t } from '@/i18n';
|
|
40
|
+
import { agentLabel } from '@/i18n/agent-label';
|
|
39
41
|
import {
|
|
40
42
|
isHiddenSlashSkill,
|
|
41
43
|
resolveSlashTool,
|
|
@@ -278,11 +280,11 @@ function parseMentionSegments(
|
|
|
278
280
|
if (!ranges.some((range) => offset < range.end && end > range.start)) {
|
|
279
281
|
const name = match.slice(1);
|
|
280
282
|
const agent = agents.find((a) => a.name === name || a.slug === name);
|
|
281
|
-
const chat = chats.find((c) => (c.title || '
|
|
283
|
+
const chat = chats.find((c) => (c.title || t('Untitled chat')) === name);
|
|
282
284
|
const ref: MentionReference | undefined = agent
|
|
283
285
|
? { type: 'agent', id: agent.id, label: agent.name }
|
|
284
286
|
: chat
|
|
285
|
-
? { type: 'chat', id: chat.id, label: chat.title || '
|
|
287
|
+
? { type: 'chat', id: chat.id, label: chat.title || t('Untitled chat') }
|
|
286
288
|
: undefined;
|
|
287
289
|
|
|
288
290
|
ranges.push({ start: offset, end, text: match, ref });
|
|
@@ -373,7 +375,7 @@ function writeEditorValue(
|
|
|
373
375
|
const isChat = seg.ref?.type === 'chat';
|
|
374
376
|
const isSkill = seg.ref?.type === 'skill';
|
|
375
377
|
const isMcp = seg.ref?.type === 'mcp';
|
|
376
|
-
const removeLabel =
|
|
378
|
+
const removeLabel = t('Remove {name}', { name: seg.text });
|
|
377
379
|
|
|
378
380
|
const chip = document.createElement('button');
|
|
379
381
|
chip.type = 'button';
|
|
@@ -778,7 +780,7 @@ function AttachmentChip({
|
|
|
778
780
|
type="button"
|
|
779
781
|
onClick={onRemove}
|
|
780
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"
|
|
781
|
-
aria-label={
|
|
783
|
+
aria-label={t('Remove file {name}', { name: file.name })}
|
|
782
784
|
>
|
|
783
785
|
<LuX className="h-3 w-3" />
|
|
784
786
|
</button>
|
|
@@ -799,7 +801,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
799
801
|
onRemoveFollowUp,
|
|
800
802
|
isStreaming = false,
|
|
801
803
|
disabled = false,
|
|
802
|
-
placeholder = '
|
|
804
|
+
placeholder = t('Message the Agent...'),
|
|
803
805
|
currentAgent,
|
|
804
806
|
agents = [],
|
|
805
807
|
chats = [],
|
|
@@ -941,8 +943,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
941
943
|
.map((chat) => ({
|
|
942
944
|
type: 'chat',
|
|
943
945
|
id: chat.id,
|
|
944
|
-
label: chat.title || '
|
|
945
|
-
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'),
|
|
946
950
|
}));
|
|
947
951
|
|
|
948
952
|
return [...agentSuggestions, ...chatSuggestions].slice(0, MAX_MENTION_SUGGESTIONS);
|
|
@@ -1042,11 +1046,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1042
1046
|
const mcpGroup = suggestions.filter((s) => s.kind === 'mcp');
|
|
1043
1047
|
let idx = 0;
|
|
1044
1048
|
if (skillGroup.length > 0) {
|
|
1045
|
-
rows.push({ type: 'header', label: '
|
|
1049
|
+
rows.push({ type: 'header', label: t('Run a local skill (Skill)') });
|
|
1046
1050
|
for (const s of skillGroup) rows.push({ type: 'item', item: s, idx: idx++ });
|
|
1047
1051
|
}
|
|
1048
1052
|
if (mcpGroup.length > 0) {
|
|
1049
|
-
rows.push({ type: 'header', label: '
|
|
1053
|
+
rows.push({ type: 'header', label: t('Call an MCP tool (MCP Tool)') });
|
|
1050
1054
|
for (const s of mcpGroup) rows.push({ type: 'item', item: s, idx: idx++ });
|
|
1051
1055
|
}
|
|
1052
1056
|
return rows;
|
|
@@ -1687,7 +1691,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1687
1691
|
// 进入待发队列 / 作为新回合发出),草稿都可以清;queued 额外
|
|
1688
1692
|
// 提示用户"不是追加进当前回合"。
|
|
1689
1693
|
if (outcome === 'queued') {
|
|
1690
|
-
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
|
+
);
|
|
1691
1699
|
}
|
|
1692
1700
|
onChange('');
|
|
1693
1701
|
});
|
|
@@ -1774,7 +1782,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1774
1782
|
onAutoContinue={() => askUserPrompt.answer({})}
|
|
1775
1783
|
bottomHint={
|
|
1776
1784
|
askUserPrompt.pendingCount > 0
|
|
1777
|
-
?
|
|
1785
|
+
? t('{count} more question groups to answer', {
|
|
1786
|
+
count: askUserPrompt.pendingCount,
|
|
1787
|
+
})
|
|
1778
1788
|
: undefined
|
|
1779
1789
|
}
|
|
1780
1790
|
/>
|
|
@@ -1814,7 +1824,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1814
1824
|
{mentionQuery && mentionSuggestions.length > 0 && (
|
|
1815
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">
|
|
1816
1826
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
1817
|
-
@
|
|
1827
|
+
{t('@ Choose an Agent or past chat')}
|
|
1818
1828
|
</div>
|
|
1819
1829
|
{mentionSuggestions.map((item, idx) => (
|
|
1820
1830
|
<button
|
|
@@ -1848,7 +1858,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1848
1858
|
<span className="flex items-center gap-1.5">
|
|
1849
1859
|
<span className="truncate text-xs font-medium">@{item.label}</span>
|
|
1850
1860
|
<span className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] text-agent-muted-foreground">
|
|
1851
|
-
{item.type === 'agent' ? 'Agent' : '
|
|
1861
|
+
{item.type === 'agent' ? 'Agent' : t('Chat')}
|
|
1852
1862
|
</span>
|
|
1853
1863
|
</span>
|
|
1854
1864
|
{item.description && (
|
|
@@ -1901,7 +1911,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1901
1911
|
<div className="text-xs font-medium truncate">
|
|
1902
1912
|
{row.item.skill.displayName ? (
|
|
1903
1913
|
<>
|
|
1904
|
-
{row.item.skill.displayName}
|
|
1914
|
+
{t(row.item.skill.displayName)}
|
|
1905
1915
|
<span className="ml-1.5 text-[10px] font-normal text-agent-muted-foreground/70">
|
|
1906
1916
|
/{row.item.skill.name}
|
|
1907
1917
|
</span>
|
|
@@ -1956,7 +1966,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1956
1966
|
{pendingFollowUps.length > 0 && (
|
|
1957
1967
|
<div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
|
|
1958
1968
|
<div className="text-[11px] font-medium text-agent-muted-foreground">
|
|
1959
|
-
|
|
1969
|
+
{t(
|
|
1970
|
+
'Queued ({count}) · Sent when this turn ends · Restored to the input box if stopped',
|
|
1971
|
+
{ count: pendingFollowUps.length },
|
|
1972
|
+
)}
|
|
1960
1973
|
</div>
|
|
1961
1974
|
{pendingFollowUps.map((text, i) => (
|
|
1962
1975
|
<div
|
|
@@ -1967,7 +1980,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1967
1980
|
{onRemoveFollowUp && (
|
|
1968
1981
|
<button
|
|
1969
1982
|
type="button"
|
|
1970
|
-
aria-label={
|
|
1983
|
+
aria-label={t('Withdraw queued message {index}', { index: i + 1 })}
|
|
1971
1984
|
onClick={() => onRemoveFollowUp(i)}
|
|
1972
1985
|
className="shrink-0 text-agent-muted-foreground opacity-0 transition-opacity hover:text-agent-destructive group-hover:opacity-100"
|
|
1973
1986
|
>
|
|
@@ -2059,8 +2072,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2059
2072
|
onClick={handlePickFiles}
|
|
2060
2073
|
disabled={disabled || isStreaming}
|
|
2061
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"
|
|
2062
|
-
title=
|
|
2063
|
-
aria-label=
|
|
2075
|
+
title={t('Upload files, or paste images into the input box')}
|
|
2076
|
+
aria-label={t('Upload files')}
|
|
2064
2077
|
data-testid="chat-attach"
|
|
2065
2078
|
>
|
|
2066
2079
|
<LuPaperclip className="h-3 w-3" />
|
|
@@ -2093,7 +2106,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2093
2106
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2094
2107
|
Enter
|
|
2095
2108
|
</kbd>
|
|
2096
|
-
<span className="ml-1"
|
|
2109
|
+
<span className="ml-1">{t('Queue')}</span>
|
|
2097
2110
|
</span>
|
|
2098
2111
|
)}
|
|
2099
2112
|
{onSteer && (
|
|
@@ -2105,14 +2118,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2105
2118
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2106
2119
|
Enter
|
|
2107
2120
|
</kbd>
|
|
2108
|
-
<span className="ml-1"
|
|
2121
|
+
<span className="ml-1">{t('Interject')}</span>
|
|
2109
2122
|
</span>
|
|
2110
2123
|
)}
|
|
2111
2124
|
<span className="ml-2">
|
|
2112
2125
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2113
2126
|
Esc
|
|
2114
2127
|
</kbd>
|
|
2115
|
-
<span className="ml-1"
|
|
2128
|
+
<span className="ml-1">{t('Stop')}</span>
|
|
2116
2129
|
</span>
|
|
2117
2130
|
</>
|
|
2118
2131
|
) : (
|
|
@@ -2120,7 +2133,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2120
2133
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2121
2134
|
Enter
|
|
2122
2135
|
</kbd>
|
|
2123
|
-
<span className="ml-1"
|
|
2136
|
+
<span className="ml-1">{t('Send')}</span>
|
|
2124
2137
|
<span className="ml-2">
|
|
2125
2138
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2126
2139
|
Shift
|
|
@@ -2129,7 +2142,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2129
2142
|
<kbd className="rounded border border-agent-border bg-agent-muted px-1 font-sans text-[10px]">
|
|
2130
2143
|
Enter
|
|
2131
2144
|
</kbd>
|
|
2132
|
-
<span className="ml-1"
|
|
2145
|
+
<span className="ml-1">{t('New line')}</span>
|
|
2133
2146
|
</span>
|
|
2134
2147
|
</>
|
|
2135
2148
|
)}
|
|
@@ -2149,11 +2162,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2149
2162
|
title={
|
|
2150
2163
|
isStreaming
|
|
2151
2164
|
? pendingFollowUps.length > 0
|
|
2152
|
-
?
|
|
2153
|
-
|
|
2154
|
-
|
|
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)')
|
|
2155
2171
|
}
|
|
2156
|
-
aria-label={isStreaming ? '
|
|
2172
|
+
aria-label={isStreaming ? t('Stop generating') : t('Send message')}
|
|
2157
2173
|
data-testid="chat-send"
|
|
2158
2174
|
>
|
|
2159
2175
|
{isStreaming ? (
|
|
@@ -2171,7 +2187,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
2171
2187
|
);
|
|
2172
2188
|
|
|
2173
2189
|
function agentInitial(agent: LocalChatAgent): string {
|
|
2174
|
-
return agent.
|
|
2190
|
+
return agentLabel(agent).trim()[0]?.toUpperCase() ?? 'A';
|
|
2175
2191
|
}
|
|
2176
2192
|
|
|
2177
2193
|
/**
|
|
@@ -2193,7 +2209,7 @@ function ModeToggle({
|
|
|
2193
2209
|
<div
|
|
2194
2210
|
className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
|
|
2195
2211
|
role="radiogroup"
|
|
2196
|
-
aria-label=
|
|
2212
|
+
aria-label={t('Chat mode')}
|
|
2197
2213
|
data-testid="mode-toggle"
|
|
2198
2214
|
>
|
|
2199
2215
|
<button
|
|
@@ -2202,7 +2218,7 @@ function ModeToggle({
|
|
|
2202
2218
|
aria-checked={mode === 'agent'}
|
|
2203
2219
|
disabled={disabled}
|
|
2204
2220
|
onClick={() => onChange('agent')}
|
|
2205
|
-
title=
|
|
2221
|
+
title={t('Agent mode: carry out the task directly')}
|
|
2206
2222
|
data-testid="mode-agent"
|
|
2207
2223
|
className={[
|
|
2208
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',
|
|
@@ -2220,7 +2236,7 @@ function ModeToggle({
|
|
|
2220
2236
|
aria-checked={mode === 'plan'}
|
|
2221
2237
|
disabled={disabled}
|
|
2222
2238
|
onClick={() => onChange('plan')}
|
|
2223
|
-
title=
|
|
2239
|
+
title={t('Plan mode: make a plan first, read only and no execution')}
|
|
2224
2240
|
data-testid="mode-plan"
|
|
2225
2241
|
className={[
|
|
2226
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',
|
|
@@ -2267,14 +2283,14 @@ function AgentSelect({
|
|
|
2267
2283
|
|
|
2268
2284
|
const buttonTitle = useMemo(() => {
|
|
2269
2285
|
const activeNames = [
|
|
2270
|
-
agent
|
|
2286
|
+
agentLabel(agent),
|
|
2271
2287
|
...mentionReferences
|
|
2272
2288
|
.filter((ref) => ref.type === 'agent' && ref.id !== agent.id)
|
|
2273
2289
|
.map((ref) => ref.label),
|
|
2274
2290
|
].filter(Boolean) as string[];
|
|
2275
2291
|
|
|
2276
|
-
return
|
|
2277
|
-
}, [agent
|
|
2292
|
+
return t('Active experts: {names}', { names: activeNames.join(', ') });
|
|
2293
|
+
}, [agent, mentionReferences]);
|
|
2278
2294
|
|
|
2279
2295
|
return (
|
|
2280
2296
|
<div
|
|
@@ -2293,7 +2309,7 @@ function AgentSelect({
|
|
|
2293
2309
|
>
|
|
2294
2310
|
<AgentDot agent={agent} />
|
|
2295
2311
|
<span className="truncate font-medium">
|
|
2296
|
-
{agent
|
|
2312
|
+
{agentLabel(agent)}
|
|
2297
2313
|
{mentionedAgentCount > 0 ? ` +${mentionedAgentCount}` : ''}
|
|
2298
2314
|
</span>
|
|
2299
2315
|
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
@@ -2305,7 +2321,7 @@ function AgentSelect({
|
|
|
2305
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"
|
|
2306
2322
|
>
|
|
2307
2323
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
2308
|
-
|
|
2324
|
+
{t('Choose an expert')}
|
|
2309
2325
|
</div>
|
|
2310
2326
|
{agents.map((item) => {
|
|
2311
2327
|
const isPrimary = item.id === agent.id;
|
|
@@ -2332,16 +2348,16 @@ function AgentSelect({
|
|
|
2332
2348
|
<span className="min-w-0 flex-1">
|
|
2333
2349
|
<span className="flex items-center gap-1.5">
|
|
2334
2350
|
<span className="block truncate text-xs font-medium">
|
|
2335
|
-
{item
|
|
2351
|
+
{agentLabel(item)}
|
|
2336
2352
|
</span>
|
|
2337
2353
|
{isPrimary && (
|
|
2338
2354
|
<span className="inline-flex items-center rounded bg-agent-foreground/10 px-1 py-0.5 text-[9px] font-medium text-agent-foreground">
|
|
2339
|
-
|
|
2355
|
+
{t('Current')}
|
|
2340
2356
|
</span>
|
|
2341
2357
|
)}
|
|
2342
2358
|
{!isPrimary && isMentioned && (
|
|
2343
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">
|
|
2344
|
-
|
|
2360
|
+
{t('Mentioned')}
|
|
2345
2361
|
</span>
|
|
2346
2362
|
)}
|
|
2347
2363
|
</span>
|
|
@@ -2366,7 +2382,7 @@ function AgentSelect({
|
|
|
2366
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"
|
|
2367
2383
|
>
|
|
2368
2384
|
<LuBot className="h-3.5 w-3.5 shrink-0" />
|
|
2369
|
-
<span className="text-xs font-medium"
|
|
2385
|
+
<span className="text-xs font-medium">{t('Manage agents')}</span>
|
|
2370
2386
|
</button>
|
|
2371
2387
|
)}
|
|
2372
2388
|
</div>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
updateProject,
|
|
18
18
|
type LocalProject,
|
|
19
19
|
} from '@/lib/local-api';
|
|
20
|
+
import { t } from '@/i18n';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 项目徽章 / 选择器(Codex 式 cwd 指示),渲染在输入框上方的 meta 行里,
|
|
@@ -113,8 +114,8 @@ function MenuSectionLabel({
|
|
|
113
114
|
type="button"
|
|
114
115
|
onClick={onAdd}
|
|
115
116
|
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
116
|
-
title=
|
|
117
|
-
aria-label=
|
|
117
|
+
title={t('New project')}
|
|
118
|
+
aria-label={t('New project')}
|
|
118
119
|
>
|
|
119
120
|
<LuPlus className="h-3 w-3" />
|
|
120
121
|
</button>
|
|
@@ -132,7 +133,7 @@ function CreateProjectMenuItem({ onClick }: { onClick: () => void }) {
|
|
|
132
133
|
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
|
|
133
134
|
>
|
|
134
135
|
<LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
135
|
-
|
|
136
|
+
{t('New project')}
|
|
136
137
|
</button>
|
|
137
138
|
);
|
|
138
139
|
}
|
|
@@ -220,7 +221,7 @@ export function ChatProjectBadge({
|
|
|
220
221
|
run(async () => {
|
|
221
222
|
if (!isElectron() || !project) return;
|
|
222
223
|
const result = await getElectronBridge()!.local?.selectDirectory({
|
|
223
|
-
title:
|
|
224
|
+
title: t('Choose a new folder for "{name}"', { name: project.name }),
|
|
224
225
|
});
|
|
225
226
|
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
226
227
|
await updateProject(project.id, { folderPath: result.filePaths[0] });
|
|
@@ -230,7 +231,7 @@ export function ChatProjectBadge({
|
|
|
230
231
|
return (
|
|
231
232
|
<div className="relative">
|
|
232
233
|
<BadgeButton
|
|
233
|
-
label={project ? project.name : '
|
|
234
|
+
label={project ? project.name : t('Choose a project')}
|
|
234
235
|
active={Boolean(project)}
|
|
235
236
|
open={open}
|
|
236
237
|
busy={busy}
|
|
@@ -240,8 +241,10 @@ export function ChatProjectBadge({
|
|
|
240
241
|
}}
|
|
241
242
|
title={
|
|
242
243
|
project
|
|
243
|
-
? `${project.folderPath}\n
|
|
244
|
-
:
|
|
244
|
+
? `${project.folderPath}\n${t('Click to manage the project or change its folder')}`
|
|
245
|
+
: t(
|
|
246
|
+
'Link this chat to a project (file writes are limited to the project home folder, its source folders, and their subfolders)',
|
|
247
|
+
)
|
|
245
248
|
}
|
|
246
249
|
/>
|
|
247
250
|
|
|
@@ -249,7 +252,7 @@ export function ChatProjectBadge({
|
|
|
249
252
|
<MenuShell onClose={() => setOpen(false)}>
|
|
250
253
|
{project ? (
|
|
251
254
|
<>
|
|
252
|
-
<MenuSectionLabel
|
|
255
|
+
<MenuSectionLabel>{t('Current project')}</MenuSectionLabel>
|
|
253
256
|
<div
|
|
254
257
|
className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-agent-foreground"
|
|
255
258
|
title={project.folderPath}
|
|
@@ -268,7 +271,7 @@ export function ChatProjectBadge({
|
|
|
268
271
|
</div>
|
|
269
272
|
</>
|
|
270
273
|
) : (
|
|
271
|
-
<MenuSectionLabel onAdd={openCreate}
|
|
274
|
+
<MenuSectionLabel onAdd={openCreate}>{t('Link to a project')}</MenuSectionLabel>
|
|
272
275
|
)}
|
|
273
276
|
|
|
274
277
|
{otherProjects.length > 0 && (
|
|
@@ -276,7 +279,7 @@ export function ChatProjectBadge({
|
|
|
276
279
|
{project && (
|
|
277
280
|
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
278
281
|
)}
|
|
279
|
-
{project && <MenuSectionLabel
|
|
282
|
+
{project && <MenuSectionLabel>{t('Move to')}</MenuSectionLabel>}
|
|
280
283
|
<div className="max-h-40 overflow-y-auto">
|
|
281
284
|
{otherProjects.map((p) => (
|
|
282
285
|
<ProjectMenuRow
|
|
@@ -291,7 +294,7 @@ export function ChatProjectBadge({
|
|
|
291
294
|
{!project && projects.length === 0 && (
|
|
292
295
|
<>
|
|
293
296
|
<div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
|
|
294
|
-
|
|
297
|
+
{t('No projects yet')}
|
|
295
298
|
</div>
|
|
296
299
|
<CreateProjectMenuItem onClick={openCreate} />
|
|
297
300
|
</>
|
|
@@ -307,7 +310,7 @@ export function ChatProjectBadge({
|
|
|
307
310
|
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
|
|
308
311
|
>
|
|
309
312
|
<LuFolderPen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
310
|
-
|
|
313
|
+
{t('Change project folder...')}
|
|
311
314
|
</button>
|
|
312
315
|
<button
|
|
313
316
|
type="button"
|
|
@@ -316,7 +319,7 @@ export function ChatProjectBadge({
|
|
|
316
319
|
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
|
|
317
320
|
>
|
|
318
321
|
<LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
319
|
-
|
|
322
|
+
{t('Remove from project (becomes a chat without a project)')}
|
|
320
323
|
</button>
|
|
321
324
|
</>
|
|
322
325
|
)}
|
|
@@ -375,21 +378,23 @@ export function ProjectPickerButton({
|
|
|
375
378
|
return (
|
|
376
379
|
<div className="relative">
|
|
377
380
|
<BadgeButton
|
|
378
|
-
label={selected ? selected.name : '
|
|
381
|
+
label={selected ? selected.name : t('Choose a project')}
|
|
379
382
|
active={Boolean(selected)}
|
|
380
383
|
open={open}
|
|
381
384
|
busy={false}
|
|
382
385
|
onClick={() => setOpen((v) => !v)}
|
|
383
386
|
title={
|
|
384
387
|
selected
|
|
385
|
-
? `${selected.folderPath}\n
|
|
386
|
-
:
|
|
388
|
+
? `${selected.folderPath}\n${t('New chats will be linked to this project')}`
|
|
389
|
+
: t(
|
|
390
|
+
'Choose a project for the new chat. Without one, a workspace for this chat is created under Documents/<app name>/conversations/',
|
|
391
|
+
)
|
|
387
392
|
}
|
|
388
393
|
/>
|
|
389
394
|
|
|
390
395
|
{open && (
|
|
391
396
|
<MenuShell onClose={() => setOpen(false)}>
|
|
392
|
-
<MenuSectionLabel onAdd={openCreate}
|
|
397
|
+
<MenuSectionLabel onAdd={openCreate}>{t('Project for new chat')}</MenuSectionLabel>
|
|
393
398
|
<button
|
|
394
399
|
type="button"
|
|
395
400
|
role="menuitem"
|
|
@@ -400,7 +405,7 @@ export function ProjectPickerButton({
|
|
|
400
405
|
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
|
|
401
406
|
>
|
|
402
407
|
<LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
403
|
-
<span className="min-w-0 flex-1 truncate"
|
|
408
|
+
<span className="min-w-0 flex-1 truncate">{t('No project')}</span>
|
|
404
409
|
{!selected && (
|
|
405
410
|
<LuCheck className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
406
411
|
)}
|
|
@@ -421,7 +426,7 @@ export function ProjectPickerButton({
|
|
|
421
426
|
{projects.length === 0 && (
|
|
422
427
|
<>
|
|
423
428
|
<div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
|
|
424
|
-
|
|
429
|
+
{t('No projects yet')}
|
|
425
430
|
</div>
|
|
426
431
|
<CreateProjectMenuItem onClick={openCreate} />
|
|
427
432
|
</>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BRAND_HOME_HINT, BRAND_NAME, getBrandLogoUrl } from '@/brand';
|
|
2
|
+
import { t } from '@/i18n';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 新对话落地页与空会话首屏的品牌区:侧栏同一枚 logo,加一句
|
|
@@ -16,7 +17,7 @@ export function EmptyChatHero() {
|
|
|
16
17
|
draggable={false}
|
|
17
18
|
/>
|
|
18
19
|
</h1>
|
|
19
|
-
{hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{hint}</p> : null}
|
|
20
|
+
{hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{t(hint)}</p> : null}
|
|
20
21
|
</div>
|
|
21
22
|
);
|
|
22
23
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { LuChevronDown, LuFolderLock, LuLockOpen } from 'react-icons/lu';
|
|
3
3
|
import type { ExecPolicy } from '@/lib/exec-policy';
|
|
4
|
+
import { t } from '@/i18n';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* ExecPolicyPicker — 输入框上的命令沙箱切换(类 Codex 底部权限档)。
|
|
@@ -17,14 +18,14 @@ const OPTIONS: Array<{
|
|
|
17
18
|
}> = [
|
|
18
19
|
{
|
|
19
20
|
id: 'workspace',
|
|
20
|
-
label: '
|
|
21
|
-
description:
|
|
22
|
-
'只能写入当前工作区,范围外的路径会被拒绝。',
|
|
21
|
+
label: 'Workspace',
|
|
22
|
+
description: 'Can only write to the current workspace. Paths outside it are blocked.',
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
id: 'full',
|
|
26
|
-
label: '
|
|
27
|
-
description:
|
|
26
|
+
label: 'Full access',
|
|
27
|
+
description:
|
|
28
|
+
'Turns off the command sandbox so any path on this computer can be written. Tool approvals still apply.',
|
|
28
29
|
},
|
|
29
30
|
];
|
|
30
31
|
|
|
@@ -60,9 +61,9 @@ export function ExecPolicyPicker({
|
|
|
60
61
|
disabled={disabled}
|
|
61
62
|
aria-haspopup="listbox"
|
|
62
63
|
aria-expanded={open}
|
|
63
|
-
aria-label=
|
|
64
|
+
aria-label={t('Command sandbox')}
|
|
64
65
|
data-testid="exec-policy-picker"
|
|
65
|
-
title={current.description}
|
|
66
|
+
title={t(current.description)}
|
|
66
67
|
onClick={() => setOpen((next) => !next)}
|
|
67
68
|
className={[
|
|
68
69
|
'inline-flex h-6 max-w-[140px] items-center gap-1 rounded-full border px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
|
|
@@ -76,13 +77,13 @@ export function ExecPolicyPicker({
|
|
|
76
77
|
) : (
|
|
77
78
|
<LuFolderLock className="h-3 w-3 shrink-0" />
|
|
78
79
|
)}
|
|
79
|
-
<span className="truncate">{current.label}</span>
|
|
80
|
+
<span className="truncate">{t(current.label)}</span>
|
|
80
81
|
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
81
82
|
</button>
|
|
82
83
|
{open && (
|
|
83
84
|
<div
|
|
84
85
|
role="listbox"
|
|
85
|
-
aria-label=
|
|
86
|
+
aria-label={t('Command sandbox')}
|
|
86
87
|
className="absolute bottom-full left-0 z-30 mb-1 w-64 overflow-hidden rounded-agent-md border border-agent-border bg-agent-canvas py-1 shadow-sm"
|
|
87
88
|
>
|
|
88
89
|
{OPTIONS.map((option) => {
|
|
@@ -106,10 +107,10 @@ export function ExecPolicyPicker({
|
|
|
106
107
|
].join(' ')}
|
|
107
108
|
>
|
|
108
109
|
<span className="text-[12px] font-medium leading-[1.45] text-agent-foreground">
|
|
109
|
-
{option.label}
|
|
110
|
+
{t(option.label)}
|
|
110
111
|
</span>
|
|
111
112
|
<span className="text-[11px] leading-snug text-agent-muted-foreground">
|
|
112
|
-
{option.description}
|
|
113
|
+
{t(option.description)}
|
|
113
114
|
</span>
|
|
114
115
|
</button>
|
|
115
116
|
);
|