diff --git a/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts b/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts index f74696f35db..cf3c6a5c0fb 100644 --- a/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +++ b/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts @@ -1,6 +1,6 @@ +import { type ToolTitleKey, translateNow } from '@/i18n' import { normalizeExternalUrl } from '@/lib/external-link' import { extractToolErrorMessage, formatToolResultSummary } from '@/lib/tool-result-summary' -import { translateNow } from '@/i18n' export type ToolTone = 'agent' | 'browser' | 'default' | 'file' | 'image' | 'terminal' | 'web' export type ToolStatus = 'error' | 'running' | 'success' | 'warning' @@ -20,6 +20,12 @@ export interface SearchResultRow { url: string } +export interface ToolTitleAction { + prefix: string + suffix: string + text: string +} + interface CountMetric { count: number noun: string @@ -51,6 +57,7 @@ export interface ToolView { status: ToolStatus subtitle: string title: string + titleAction?: ToolTitleAction tone: ToolTone } @@ -58,6 +65,12 @@ interface ToolMeta { done: string icon?: string pending: string + pendingAction: string + tone: ToolTone +} + +interface ToolMetaSpec { + icon?: string tone: ToolTone } @@ -112,44 +125,78 @@ function fileEditBasename(path: string): string { return normalized.split('/').filter(Boolean).pop() || normalized } -const TOOL_META: Record = { - browser_click: { done: 'Clicked page element', pending: 'Clicking page element', icon: 'globe', tone: 'browser' }, - browser_fill: { done: 'Filled form field', pending: 'Filling form field', icon: 'globe', tone: 'browser' }, - browser_navigate: { done: 'Opened page', pending: 'Opening page', icon: 'globe', tone: 'browser' }, +const TOOL_META: Record = { + browser_click: { + icon: 'globe', + tone: 'browser' + }, + browser_fill: { + icon: 'globe', + tone: 'browser' + }, + browser_navigate: { + icon: 'globe', + tone: 'browser' + }, browser_snapshot: { - done: 'Captured page snapshot', - pending: 'Capturing page snapshot', icon: 'globe', tone: 'browser' }, browser_take_screenshot: { - done: 'Captured screenshot', - pending: 'Capturing screenshot', icon: 'file-media', tone: 'browser' }, - browser_type: { done: 'Typed on page', pending: 'Typing on page', icon: 'globe', tone: 'browser' }, - clarify: { done: 'Asked a question', pending: 'Asking a question', icon: 'question', tone: 'agent' }, - cronjob: { done: 'Cron job', pending: 'Scheduling cron job', icon: 'watch', tone: 'agent' }, - edit_file: { done: 'Edited file', pending: 'Editing file', icon: 'edit', tone: 'file' }, - execute_code: { done: 'Ran code', pending: 'Running code', icon: 'terminal', tone: 'terminal' }, - image_generate: { done: 'Generated image', pending: 'Generating image', icon: 'file-media', tone: 'image' }, - list_files: { done: 'Listed files', pending: 'Listing files', icon: 'files', tone: 'file' }, - patch: { done: 'Patched file', pending: 'Patching file', icon: 'edit', tone: 'file' }, - read_file: { done: 'Read file', pending: 'Reading file', icon: 'file', tone: 'file' }, - search_files: { done: 'Searched files', pending: 'Searching files', icon: 'search', tone: 'file' }, + browser_type: { + icon: 'globe', + tone: 'browser' + }, + clarify: { + icon: 'question', + tone: 'agent' + }, + cronjob: { + icon: 'watch', + tone: 'agent' + }, + edit_file: { icon: 'edit', tone: 'file' }, + execute_code: { + icon: 'terminal', + tone: 'terminal' + }, + image_generate: { + icon: 'file-media', + tone: 'image' + }, + list_files: { + icon: 'files', + tone: 'file' + }, + patch: { icon: 'edit', tone: 'file' }, + read_file: { icon: 'file', tone: 'file' }, + search_files: { + icon: 'search', + tone: 'file' + }, session_search_recall: { - done: 'Searched session history', - pending: 'Searching session history', icon: 'search', tone: 'agent' }, - terminal: { done: 'Ran command', pending: 'Running command', icon: 'terminal', tone: 'terminal' }, - todo: { done: 'Updated todos', pending: 'Updating todos', icon: 'tools', tone: 'agent' }, - vision_analyze: { done: 'Analyzed image', pending: 'Analyzing image', icon: 'eye', tone: 'image' }, - web_extract: { done: 'Read webpage', pending: 'Reading webpage', icon: 'globe', tone: 'web' }, - web_search: { done: 'Searched web', pending: 'Searching web', icon: 'search', tone: 'web' }, - write_file: { done: 'Edited file', pending: 'Editing file', icon: 'edit', tone: 'file' } + terminal: { + icon: 'terminal', + tone: 'terminal' + }, + todo: { icon: 'tools', tone: 'agent' }, + vision_analyze: { + icon: 'eye', + tone: 'image' + }, + web_extract: { icon: 'globe', tone: 'web' }, + web_search: { icon: 'search', tone: 'web' }, + write_file: { icon: 'edit', tone: 'file' } +} + +function isToolTitleKey(name: string): name is ToolTitleKey { + return name in TOOL_META } const INLINE_CODE_SPLIT_RE = /(`[^`\n]+`)/g @@ -171,27 +218,45 @@ function titleForTool(name: string): string { ) } -const PREFIX_META: { icon?: string; prefix: string; tone: ToolTone; verb: string }[] = [ - { prefix: 'browser_', verb: 'Browser', icon: 'globe', tone: 'browser' }, - { prefix: 'web_', verb: 'Web', icon: 'globe', tone: 'web' } +const PREFIX_META: { icon?: string; labelKey: string; prefix: string; tone: ToolTone }[] = [ + { prefix: 'browser_', labelKey: 'browser', icon: 'globe', tone: 'browser' }, + { prefix: 'web_', labelKey: 'web', icon: 'globe', tone: 'web' } ] function toolMeta(name: string): ToolMeta { - if (TOOL_META[name]) { - return TOOL_META[name] + if (isToolTitleKey(name)) { + const meta = TOOL_META[name] + + return { + done: translateNow(`assistant.tool.titles.${name}.done`), + pending: translateNow(`assistant.tool.titles.${name}.pending`), + pendingAction: translateNow(`assistant.tool.titles.${name}.pendingAction`), + icon: meta.icon, + tone: meta.tone + } } const action = titleForTool(name) const prefix = PREFIX_META.find(p => name.startsWith(p.prefix)) - return prefix - ? { - done: `${prefix.verb} ${action}`, - pending: `Running ${prefix.verb.toLowerCase()} ${action.toLowerCase()}`, - icon: prefix.icon, - tone: prefix.tone - } - : { done: action, pending: `Running ${action.toLowerCase()}`, tone: 'default' } + if (prefix) { + const prefixLabel = translateNow(`assistant.tool.prefixes.${prefix.labelKey}`) + + return { + done: translateNow('assistant.tool.titleTemplates.prefixedDone', prefixLabel, action), + pending: translateNow('assistant.tool.titleTemplates.runningPrefixedTool', prefixLabel, action), + pendingAction: translateNow('assistant.tool.actions.running'), + icon: prefix.icon, + tone: prefix.tone + } + } + + return { + done: action, + pending: translateNow('assistant.tool.titleTemplates.runningTool', action), + pendingAction: translateNow('assistant.tool.actions.running'), + tone: 'default' + } } function isRecord(value: unknown): value is Record { @@ -967,8 +1032,13 @@ function fallbackDetailText(args: unknown, result: unknown): string { } function cronScalar(value: unknown): string { - if (typeof value === 'string') return value.trim() - if (typeof value === 'number' && Number.isFinite(value)) return String(value) + if (typeof value === 'string') { + return value.trim() + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } return '' } @@ -976,7 +1046,9 @@ function cronScalar(value: unknown): string { function formatCronTime(iso: string): string { const ts = Date.parse(iso) - if (Number.isNaN(ts)) return iso + if (Number.isNaN(ts)) { + return iso + } return new Date(ts).toLocaleString(undefined, { month: 'short', @@ -986,10 +1058,7 @@ function formatCronTime(iso: string): string { }) } -function cronjobSubtitle( - argsRecord: Record, - resultRecord: Record -): string { +function cronjobSubtitle(argsRecord: Record, resultRecord: Record): string { const jobs = Array.isArray(resultRecord.jobs) ? resultRecord.jobs : null if (jobs) { @@ -998,7 +1067,9 @@ function cronjobSubtitle( const message = firstStringField(resultRecord, ['message']) - if (message) return message + if (message) { + return message + } const action = firstStringField(argsRecord, ['action']) || 'manage' const name = firstStringField(resultRecord, ['name']) || firstStringField(argsRecord, ['name', 'job_id']) @@ -1007,14 +1078,13 @@ function cronjobSubtitle( return name ? `${label} ${name}` : `Cron ${action}` } -function cronjobDetail( - argsRecord: Record, - resultRecord: Record -): string { +function cronjobDetail(argsRecord: Record, resultRecord: Record): string { const jobs = Array.isArray(resultRecord.jobs) ? resultRecord.jobs : null if (jobs) { - if (!jobs.length) return 'No cron jobs scheduled' + if (!jobs.length) { + return 'No cron jobs scheduled' + } return jobs .slice(0, 20) @@ -1029,12 +1099,14 @@ function cronjobDetail( } const nextRun = cronScalar(resultRecord.next_run_at) + const rows: [string, string][] = [ ['Schedule', cronScalar(resultRecord.schedule)], ['Repeat', cronScalar(resultRecord.repeat)], ['Delivery', cronScalar(resultRecord.deliver)], ['Next run', nextRun ? formatCronTime(nextRun) : ''] ] + const lines = rows.filter(([, value]) => value).map(([key, value]) => `${key}: ${value}`) return lines.length ? lines.join('\n') : fallbackDetailText(argsRecord, resultRecord) @@ -1277,6 +1349,7 @@ export function toolCopyPayload(part: ToolPart, view: ToolView): { label: string url: translateNow('assistant.tool.copyUrl'), generic: translateNow('common.copy') } + const args = parseMaybeObject(part.args) const result = parseMaybeObject(part.result) const detail = view.detail.trim() @@ -1359,39 +1432,90 @@ export function toolCopyPayload(part: ToolPart, view: ToolView): { label: string return { label: copy.generic, text: view.title } } +interface ToolTitleParts { + action?: ToolTitleAction + title: string +} + +function titlePartsFromAction(title: string, action?: string): ToolTitleParts { + if (!action) { + return { title } + } + + const actionStart = title.indexOf(action) + + if (actionStart < 0) { + return { title } + } + + return { + action: { + prefix: title.slice(0, actionStart), + suffix: title.slice(actionStart + action.length), + text: action + }, + title + } +} + function dynamicTitle( part: ToolPart, args: Record, result: Record, - fallback: string -): string { + fallback: ToolTitleParts +): ToolTitleParts { const verb = (gerund: string, past: string) => (part.result === undefined ? gerund : past) + const titledAction = (action: string, title: string): ToolTitleParts => + titlePartsFromAction(title, part.result === undefined ? action : undefined) + if (part.toolName === 'web_extract') { const url = findFirstUrl(args, result) + const action = verb(translateNow('assistant.tool.actions.reading'), translateNow('assistant.tool.actions.read')) - return url ? `${verb('Reading', 'Read')} ${hostnameOf(url)}` : fallback + return url + ? titledAction(action, translateNow('assistant.tool.titleTemplates.actionTarget', action, hostnameOf(url))) + : fallback } if (part.toolName === 'browser_navigate') { const url = findFirstUrl(args, result) + const action = verb(translateNow('assistant.tool.actions.opening'), translateNow('assistant.tool.actions.opened')) - return url ? `${verb('Opening', 'Opened')} ${hostnameOf(url)}` : fallback + return url + ? titledAction(action, translateNow('assistant.tool.titleTemplates.actionTarget', action, hostnameOf(url))) + : fallback } if (part.toolName === 'web_search') { const query = firstStringField(args, ['search_term', 'query']) || contextValue(args) - return query ? `${verb('Searching', 'Searched')} “${compactPreview(query, 48)}”` : fallback + const action = verb( + translateNow('assistant.tool.actions.searching'), + translateNow('assistant.tool.actions.searched') + ) + + return query + ? titledAction( + action, + translateNow('assistant.tool.titleTemplates.actionQuoted', action, compactPreview(query, 48)) + ) + : fallback } if (part.toolName === 'terminal' || part.toolName === 'execute_code') { const command = firstStringField(args, ['command', 'code']) || contextValue(args) if (command) { - const verbText = part.toolName === 'execute_code' ? verb('Running code', 'Ran code') : verb('Running', 'Ran') + const action = + part.toolName === 'execute_code' + ? verb(translateNow('assistant.tool.actions.runningCode'), translateNow('assistant.tool.actions.ranCode')) + : verb(translateNow('assistant.tool.actions.running'), translateNow('assistant.tool.actions.ran')) - return `${verbText} · ${compactPreview(command, 160)}` + return titledAction( + action, + translateNow('assistant.tool.titleTemplates.actionCommand', action, compactPreview(command, 160)) + ) } } @@ -1399,7 +1523,7 @@ function dynamicTitle( const path = fileEditPath(args, result) if (path) { - return fileEditBasename(path) + return { title: fileEditBasename(path) } } } @@ -1413,7 +1537,15 @@ export function buildToolView(part: ToolPart, inlineDiff: string): ToolView { const status = toolStatus(part, resultRecord) const error = toolErrorText(part, resultRecord) const baseTitle = part.result === undefined ? meta.pending : meta.done - const title = dynamicTitle(part, argsRecord, resultRecord, baseTitle) + + const titleParts = dynamicTitle( + part, + argsRecord, + resultRecord, + titlePartsFromAction(baseTitle, part.result === undefined ? meta.pendingAction : undefined) + ) + + const title = titleParts.title const titleEnriched = title !== baseTitle const baseSubtitle = error || toolSubtitle(part, argsRecord, resultRecord) @@ -1467,6 +1599,7 @@ export function buildToolView(part: ToolPart, inlineDiff: string): ToolView { status, subtitle, title, + titleAction: titleParts.action, tone: meta.tone } } diff --git a/apps/desktop/src/components/assistant-ui/tool-fallback.tsx b/apps/desktop/src/components/assistant-ui/tool-fallback.tsx index aa2a1b5b0d6..2d2eea54e54 100644 --- a/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +++ b/apps/desktop/src/components/assistant-ui/tool-fallback.tsx @@ -46,7 +46,8 @@ import { toolCopyPayload, type ToolPart, toolPartDisclosureId, - type ToolStatus + type ToolStatus, + type ToolTitleAction } from './tool-fallback-model' // `true` when a ToolEntry is rendered inside an embedding wrapper that owns @@ -203,6 +204,39 @@ function LinkifiedText({ className, text }: { className?: string; text: string } return } +function ToolTitle({ + isPending, + status, + title, + titleAction +}: { + isPending: boolean + status: ToolStatus + title: string + titleAction?: ToolTitleAction +}) { + return ( + + {isPending && titleAction ? ( + <> + {titleAction.prefix} + {titleAction.text} + {titleAction.suffix} + + ) : ( + title + )} + + ) +} + interface ToolEntryProps { part: ToolPart } @@ -414,16 +448,7 @@ function ToolEntry({ part }: ToolEntryProps) { icon={view.icon} status={leadingStatus(isPending, view.status)} /> - - {view.title} - + {!isPending && view.countLabel && {view.countLabel}} {showDiffStats && diffStats && ( diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 195d6426e4f..1f4ae5cfd25 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1845,7 +1845,8 @@ export const en: Translations = { restoreCheckpoint: 'Restore checkpoint', restoreFromHere: 'Restore checkpoint — rerun from this prompt', restoreTitle: 'Restore to this checkpoint?', - restoreBody: 'Everything after this prompt is removed from the conversation, and the prompt runs again from here.', + restoreBody: + 'Everything after this prompt is removed from the conversation, and the prompt runs again from here.', restoreConfirm: 'Restore & rerun', restoreNext: 'Restore next checkpoint', goForward: 'Go forward', @@ -1901,7 +1902,67 @@ export const en: Translations = { statusRunning: 'Running', statusError: 'Error', statusRecovered: 'Recovered', - statusDone: 'Done' + statusDone: 'Done', + actions: { + read: 'Read', + reading: 'Reading', + opened: 'Opened', + opening: 'Opening', + searched: 'Searched', + searching: 'Searching', + ran: 'Ran', + running: 'Running', + ranCode: 'Ran code', + runningCode: 'Scripting' + }, + prefixes: { + browser: 'Browser', + web: 'Web' + }, + titleTemplates: { + actionCommand: (action, command) => `${action} · ${command}`, + actionQuoted: (action, value) => `${action} “${value}”`, + actionTarget: (action, target) => `${action} ${target}`, + prefixedDone: (prefix, action) => `${prefix} ${action}`, + runningPrefixedTool: (prefix, action) => `Running ${prefix.toLowerCase()} ${action.toLowerCase()}`, + runningTool: action => `Running ${action.toLowerCase()}` + }, + titles: { + browser_click: { done: 'Clicked page element', pending: 'Clicking page element', pendingAction: 'Clicking' }, + browser_fill: { done: 'Filled form field', pending: 'Filling form field', pendingAction: 'Filling' }, + browser_navigate: { done: 'Opened page', pending: 'Opening page', pendingAction: 'Opening' }, + browser_snapshot: { + done: 'Captured page snapshot', + pending: 'Capturing page snapshot', + pendingAction: 'Capturing' + }, + browser_take_screenshot: { + done: 'Captured screenshot', + pending: 'Capturing screenshot', + pendingAction: 'Capturing' + }, + browser_type: { done: 'Typed on page', pending: 'Typing on page', pendingAction: 'Typing' }, + clarify: { done: 'Asked a question', pending: 'Asking a question', pendingAction: 'Asking' }, + cronjob: { done: 'Cron job', pending: 'Scheduling cron job', pendingAction: 'Scheduling' }, + edit_file: { done: 'Edited file', pending: 'Editing file', pendingAction: 'Editing' }, + execute_code: { done: 'Ran code', pending: 'Scripting', pendingAction: 'Scripting' }, + image_generate: { done: 'Generated image', pending: 'Generating image', pendingAction: 'Generating' }, + list_files: { done: 'Listed files', pending: 'Listing files', pendingAction: 'Listing' }, + patch: { done: 'Patched file', pending: 'Patching file', pendingAction: 'Patching' }, + read_file: { done: 'Read file', pending: 'Reading file', pendingAction: 'Reading' }, + search_files: { done: 'Searched files', pending: 'Searching files', pendingAction: 'Searching' }, + session_search_recall: { + done: 'Searched session history', + pending: 'Searching session history', + pendingAction: 'Searching' + }, + terminal: { done: 'Ran command', pending: 'Running command', pendingAction: 'Running' }, + todo: { done: 'Updated todos', pending: 'Updating todos', pendingAction: 'Updating' }, + vision_analyze: { done: 'Analyzed image', pending: 'Analyzing image', pendingAction: 'Analyzing' }, + web_extract: { done: 'Read webpage', pending: 'Reading webpage', pendingAction: 'Reading' }, + web_search: { done: 'Searched web', pending: 'Searching web', pendingAction: 'Searching' }, + write_file: { done: 'Edited file', pending: 'Editing file', pendingAction: 'Editing' } + } } }, @@ -1944,7 +2005,8 @@ export const en: Translations = { editFailed: 'Edit failed', resumeFailed: 'Resume failed', resumeStrandedTitle: "Couldn't load this session", - resumeStrandedBody: 'The connection to this session failed and automatic retries gave up. Check that the gateway is running, then try again.', + resumeStrandedBody: + 'The connection to this session failed and automatic retries gave up. Check that the gateway is running, then try again.', resumeRetry: 'Retry', nothingToBranch: 'Nothing to branch', branchNeedsChat: 'Start or resume a chat before branching.', diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts index b04d64948ce..b8e0d5ea2ea 100644 --- a/apps/desktop/src/i18n/index.ts +++ b/apps/desktop/src/i18n/index.ts @@ -17,4 +17,4 @@ export { normalizeLocale } from './languages' export { setRuntimeI18nLocale, translateNow } from './runtime' -export type { Locale, Translations } from './types' +export type { Locale, ToolTitleKey, Translations } from './types' diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 70e93c65c10..c529c109b45 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -201,8 +201,7 @@ export const ja = defineLocale({ }, notifications: { title: '通知', - intro: - 'アプリ内トーストとは別の、ネイティブのデスクトップ通知です。設定は端末ごとに保存されます。', + intro: 'アプリ内トーストとは別の、ネイティブのデスクトップ通知です。設定は端末ごとに保存されます。', enableAll: '通知を有効にする', enableAllDesc: 'マスタースイッチ。オフにすると以下のすべての通知を無効にします。', focusedHint: '完了通知は Hermes がバックグラウンドにあるときのみ表示されます。', @@ -1498,7 +1497,8 @@ export const ja = defineLocale({ queueSend: '送信', queueDelete: '削除', queueStuckTitle: 'キュー内のメッセージを送信できません', - queueStuckBody: 'キューに入れたターンの送信が繰り返し失敗しました。まだキューに残っています。もう一度送信してください。', + queueStuckBody: + 'キューに入れたターンの送信が繰り返し失敗しました。まだキューに残っています。もう一度送信してください。', previewUnavailable: 'プレビューは利用できません', previewLabel: label => `${label} のプレビュー`, couldNotPreview: label => `${label} をプレビューできませんでした`, @@ -1597,7 +1597,8 @@ export const ja = defineLocale({ copy: 'コピー', copied: 'コピーしました', done: '完了', - applyingBody: 'Hermes アップデーターが独自のウィンドウで引き継ぎ、完了後に自動的に Hermes を再度開きます。更新中はご自分で Hermes を開き直さないでください。', + applyingBody: + 'Hermes アップデーターが独自のウィンドウで引き継ぎ、完了後に自動的に Hermes を再度開きます。更新中はご自分で Hermes を開き直さないでください。', applyingBodyBackend: 'リモートバックエンドが更新を適用して再起動します。復帰すると Hermes が自動的に再接続します。', applyingClose: 'このウィンドウは更新中に閉じ、その後 Hermes が自動的に再度開きます。', errorTitle: '更新が完了しませんでした', @@ -2029,7 +2030,83 @@ export const ja = defineLocale({ statusRunning: '実行中', statusError: 'エラー', statusRecovered: '回復しました', - statusDone: '完了' + statusDone: '完了', + actions: { + read: '読み取り完了', + reading: '読み取り中', + opened: 'オープン済み', + opening: 'オープン中', + searched: '検索完了', + searching: '検索中', + ran: '実行完了', + running: '実行中', + ranCode: 'コード実行完了', + runningCode: 'スクリプト作成中' + }, + prefixes: { + browser: 'ブラウザー', + web: 'Web' + }, + titleTemplates: { + actionCommand: (action, command) => `${action} · ${command}`, + actionQuoted: (action, value) => `「${value}」を${action}`, + actionTarget: (action, target) => `${target} を${action}`, + prefixedDone: (prefix, action) => `${prefix} ${action}`, + runningPrefixedTool: (prefix, action) => `${prefix} ${action}を実行中`, + runningTool: action => `${action}を実行中` + }, + titles: { + browser_click: { + done: 'ページ要素をクリックしました', + pending: 'ページ要素をクリック中', + pendingAction: 'クリック中' + }, + browser_fill: { done: 'フォーム欄に入力しました', pending: 'フォーム欄に入力中', pendingAction: '入力中' }, + browser_navigate: { done: 'ページを開きました', pending: 'ページをオープン中', pendingAction: 'オープン中' }, + browser_snapshot: { + done: 'ページスナップショットを取得しました', + pending: 'ページスナップショットを取得中', + pendingAction: '取得中' + }, + browser_take_screenshot: { + done: 'スクリーンショットを取得しました', + pending: 'スクリーンショットを取得中', + pendingAction: '取得中' + }, + browser_type: { done: 'ページに入力しました', pending: 'ページに入力中', pendingAction: '入力中' }, + clarify: { done: '質問しました', pending: '質問中', pendingAction: '質問中' }, + cronjob: { done: 'Cron ジョブ', pending: 'Cron ジョブをスケジュール中', pendingAction: 'スケジュール中' }, + edit_file: { done: 'ファイルを編集しました', pending: 'ファイルを編集中', pendingAction: '編集中' }, + execute_code: { done: 'コードを実行しました', pending: 'スクリプト作成中', pendingAction: 'スクリプト作成中' }, + image_generate: { done: '画像を生成しました', pending: '画像を生成中', pendingAction: '生成中' }, + list_files: { + done: 'ファイルを一覧表示しました', + pending: 'ファイルを一覧表示中', + pendingAction: '一覧表示中' + }, + patch: { + done: 'ファイルにパッチを適用しました', + pending: 'ファイルにパッチ適用中', + pendingAction: 'パッチ適用中' + }, + read_file: { done: 'ファイルを読み取りました', pending: 'ファイルを読み取り中', pendingAction: '読み取り中' }, + search_files: { done: 'ファイルを検索しました', pending: 'ファイルを検索中', pendingAction: '検索中' }, + session_search_recall: { + done: 'セッション履歴を検索しました', + pending: 'セッション履歴を検索中', + pendingAction: '検索中' + }, + terminal: { done: 'コマンドを実行しました', pending: 'コマンドを実行中', pendingAction: '実行中' }, + todo: { done: 'Todo を更新しました', pending: 'Todo を更新中', pendingAction: '更新中' }, + vision_analyze: { done: '画像を分析しました', pending: '画像を分析中', pendingAction: '分析中' }, + web_extract: { + done: 'Web ページを読み取りました', + pending: 'Web ページを読み取り中', + pendingAction: '読み取り中' + }, + web_search: { done: 'Web を検索しました', pending: 'Web を検索中', pendingAction: '検索中' }, + write_file: { done: 'ファイルを編集しました', pending: 'ファイルを編集中', pendingAction: '編集中' } + } } }, @@ -2073,7 +2150,8 @@ export const ja = defineLocale({ editFailed: '編集に失敗しました', resumeFailed: '再開に失敗しました', resumeStrandedTitle: 'このセッションを読み込めませんでした', - resumeStrandedBody: 'このセッションへの接続に失敗し、自動再試行も停止しました。ゲートウェイが実行中か確認してから、もう一度お試しください。', + resumeStrandedBody: + 'このセッションへの接続に失敗し、自動再試行も停止しました。ゲートウェイが実行中か確認してから、もう一度お試しください。', resumeRetry: '再試行', nothingToBranch: 'ブランチするものがありません', branchNeedsChat: 'ブランチする前にチャットを開始または再開してください。', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 1f0a4d4f2d6..f447b18bca3 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -7,6 +7,36 @@ export type Locale = 'en' | 'zh' | 'zh-hant' | 'ja' +export type ToolTitleKey = + | 'browser_click' + | 'browser_fill' + | 'browser_navigate' + | 'browser_snapshot' + | 'browser_take_screenshot' + | 'browser_type' + | 'clarify' + | 'cronjob' + | 'edit_file' + | 'execute_code' + | 'image_generate' + | 'list_files' + | 'patch' + | 'read_file' + | 'search_files' + | 'session_search_recall' + | 'terminal' + | 'todo' + | 'vision_analyze' + | 'web_extract' + | 'web_search' + | 'write_file' + +interface ToolTitleCopy { + done: string + pending: string + pendingAction: string +} + interface ModeOptionCopy { label: string description: string @@ -1533,6 +1563,31 @@ export interface Translations { statusError: string statusRecovered: string statusDone: string + actions: { + read: string + reading: string + opened: string + opening: string + searched: string + searching: string + ran: string + running: string + ranCode: string + runningCode: string + } + prefixes: { + browser: string + web: string + } + titleTemplates: { + actionCommand: (action: string, command: string) => string + actionQuoted: (action: string, value: string) => string + actionTarget: (action: string, target: string) => string + prefixedDone: (prefix: string, action: string) => string + runningPrefixedTool: (prefix: string, action: string) => string + runningTool: (action: string) => string + } + titles: Record } } diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index f125c463662..b32eb94475b 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -280,7 +280,8 @@ export const zhHant = defineLocale({ importedBadge: '已匯入', pet: { title: '寵物', - intro: '領養一隻懸浮在應用上的 petdex 動畫寵物,它會根據 Hermes 的狀態做出反應——工具執行時奔跑、成功時歡呼、出錯時沮喪。', + intro: + '領養一隻懸浮在應用上的 petdex 動畫寵物,它會根據 Hermes 的狀態做出反應——工具執行時奔跑、成功時歡呼、出錯時沮喪。', restartHint: '寵物功能需要重新啟動——目前執行的應用在此功能加入前啟動。請結束並重新開啟 Hermes,然後回到此處。', scaleTitle: '大小', scaleDesc: '調整懸浮寵物的大小,所有介面即時生效。', @@ -1546,7 +1547,8 @@ export const zhHant = defineLocale({ copy: '複製', copied: '已複製', done: '完成', - applyingBody: 'Hermes 更新程式會在自己的視窗中接管,並在完成後自動重新開啟 Hermes。更新期間請勿自行重新開啟 Hermes。', + applyingBody: + 'Hermes 更新程式會在自己的視窗中接管,並在完成後自動重新開啟 Hermes。更新期間請勿自行重新開啟 Hermes。', applyingBodyBackend: '遠端後端正在套用更新並將重新啟動。恢復後 Hermes 會自動重新連線。', applyingClose: '此視窗會在更新期間關閉,隨後 Hermes 會自動重新開啟。', errorTitle: '更新未完成', @@ -1968,7 +1970,59 @@ export const zhHant = defineLocale({ statusRunning: '執行中', statusError: '錯誤', statusRecovered: '已復原', - statusDone: '完成' + statusDone: '完成', + actions: { + read: '已讀取', + reading: '正在讀取', + opened: '已開啟', + opening: '正在開啟', + searched: '已搜尋', + searching: '正在搜尋', + ran: '已執行', + running: '正在執行', + ranCode: '已執行程式碼', + runningCode: '正在撰寫腳本' + }, + prefixes: { + browser: '瀏覽器', + web: '網頁' + }, + titleTemplates: { + actionCommand: (action, command) => `${action} · ${command}`, + actionQuoted: (action, value) => `${action}「${value}」`, + actionTarget: (action, target) => `${action} ${target}`, + prefixedDone: (prefix, action) => `${prefix}${action}`, + runningPrefixedTool: (prefix, action) => `正在執行${prefix}${action}`, + runningTool: action => `正在執行 ${action}` + }, + titles: { + browser_click: { done: '已點擊頁面元素', pending: '正在點擊頁面元素', pendingAction: '正在點擊' }, + browser_fill: { done: '已填寫表單欄位', pending: '正在填寫表單欄位', pendingAction: '正在填寫' }, + browser_navigate: { done: '已開啟頁面', pending: '正在開啟頁面', pendingAction: '正在開啟' }, + browser_snapshot: { done: '已擷取頁面快照', pending: '正在擷取頁面快照', pendingAction: '正在擷取' }, + browser_take_screenshot: { done: '已擷取截圖', pending: '正在擷取截圖', pendingAction: '正在擷取' }, + browser_type: { done: '已在頁面輸入', pending: '正在頁面輸入', pendingAction: '正在輸入' }, + clarify: { done: '已提問', pending: '正在提問', pendingAction: '正在提問' }, + cronjob: { done: 'Cron 工作', pending: '正在安排 Cron 工作', pendingAction: '正在安排' }, + edit_file: { done: '已編輯檔案', pending: '正在編輯檔案', pendingAction: '正在編輯' }, + execute_code: { done: '已執行程式碼', pending: '正在撰寫腳本', pendingAction: '正在撰寫腳本' }, + image_generate: { done: '已生成圖片', pending: '正在生成圖片', pendingAction: '正在生成' }, + list_files: { done: '已列出檔案', pending: '正在列出檔案', pendingAction: '正在列出' }, + patch: { done: '已修補檔案', pending: '正在修補檔案', pendingAction: '正在修補' }, + read_file: { done: '已讀取檔案', pending: '正在讀取檔案', pendingAction: '正在讀取' }, + search_files: { done: '已搜尋檔案', pending: '正在搜尋檔案', pendingAction: '正在搜尋' }, + session_search_recall: { + done: '已搜尋工作階段歷史', + pending: '正在搜尋工作階段歷史', + pendingAction: '正在搜尋' + }, + terminal: { done: '已執行指令', pending: '正在執行指令', pendingAction: '正在執行' }, + todo: { done: '已更新待辦', pending: '正在更新待辦', pendingAction: '正在更新' }, + vision_analyze: { done: '已分析圖片', pending: '正在分析圖片', pendingAction: '正在分析' }, + web_extract: { done: '已讀取網頁', pending: '正在讀取網頁', pendingAction: '正在讀取' }, + web_search: { done: '已搜尋網頁', pending: '正在搜尋網頁', pendingAction: '正在搜尋' }, + write_file: { done: '已編輯檔案', pending: '正在編輯檔案', pendingAction: '正在編輯' } + } } }, diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 91543562b12..2d4a3520ad3 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -369,7 +369,8 @@ export const zh: Translations = { importedBadge: '已导入', pet: { title: '宠物', - intro: '领养一只悬浮在应用上的 petdex 动画宠物,它会根据 Hermes 的状态做出反应——工具执行时奔跑、成功时欢呼、出错时沮丧。', + intro: + '领养一只悬浮在应用上的 petdex 动画宠物,它会根据 Hermes 的状态做出反应——工具执行时奔跑、成功时欢呼、出错时沮丧。', restartHint: '宠物功能需要重启——当前运行的应用在此功能加入前启动。请退出并重新打开 Hermes,然后回到此处。', scaleTitle: '大小', scaleDesc: '调整悬浮宠物的大小,所有界面即时生效。', @@ -1647,11 +1648,13 @@ export const zh: Translations = { manualBody: '你是从命令行安装的 Hermes,因此更新也需要在那里运行。请将此命令粘贴到终端:', manualPickedUp: '下次启动 Hermes 时会使用新版本。', guiSkewTitle: '请更新桌面应用', - guiSkewBody: '后端已更新,但此桌面应用包未更改。请更新或重新安装 Hermes 桌面应用(你的 AppImage / .deb / .rpm)以保持一致。', + guiSkewBody: + '后端已更新,但此桌面应用包未更改。请更新或重新安装 Hermes 桌面应用(你的 AppImage / .deb / .rpm)以保持一致。', copy: '复制', copied: '已复制', done: '完成', - applyingBody: 'Hermes 更新器会在自己的窗口中接管,并在完成后自动重新打开 Hermes。更新期间请不要自行重新打开 Hermes。', + applyingBody: + 'Hermes 更新器会在自己的窗口中接管,并在完成后自动重新打开 Hermes。更新期间请不要自行重新打开 Hermes。', applyingBodyBackend: '远程后端正在应用更新并将重启。恢复后 Hermes 会自动重新连接。', applyingClose: '此窗口会在更新期间关闭,随后 Hermes 会自动重新打开。', errorTitle: '更新未完成', @@ -2075,7 +2078,55 @@ export const zh: Translations = { statusRunning: '运行中', statusError: '错误', statusRecovered: '已恢复', - statusDone: '完成' + statusDone: '完成', + actions: { + read: '已读取', + reading: '正在读取', + opened: '已打开', + opening: '正在打开', + searched: '已搜索', + searching: '正在搜索', + ran: '已运行', + running: '正在运行', + ranCode: '已运行代码', + runningCode: '正在编写脚本' + }, + prefixes: { + browser: '浏览器', + web: '网页' + }, + titleTemplates: { + actionCommand: (action, command) => `${action} · ${command}`, + actionQuoted: (action, value) => `${action}“${value}”`, + actionTarget: (action, target) => `${action} ${target}`, + prefixedDone: (prefix, action) => `${prefix}${action}`, + runningPrefixedTool: (prefix, action) => `正在运行${prefix}${action}`, + runningTool: action => `正在运行 ${action}` + }, + titles: { + browser_click: { done: '已点击页面元素', pending: '正在点击页面元素', pendingAction: '正在点击' }, + browser_fill: { done: '已填写表单字段', pending: '正在填写表单字段', pendingAction: '正在填写' }, + browser_navigate: { done: '已打开页面', pending: '正在打开页面', pendingAction: '正在打开' }, + browser_snapshot: { done: '已捕获页面快照', pending: '正在捕获页面快照', pendingAction: '正在捕获' }, + browser_take_screenshot: { done: '已捕获截图', pending: '正在捕获截图', pendingAction: '正在捕获' }, + browser_type: { done: '已在页面输入', pending: '正在页面输入', pendingAction: '正在输入' }, + clarify: { done: '已提问', pending: '正在提问', pendingAction: '正在提问' }, + cronjob: { done: 'Cron 任务', pending: '正在安排 Cron 任务', pendingAction: '正在安排' }, + edit_file: { done: '已编辑文件', pending: '正在编辑文件', pendingAction: '正在编辑' }, + execute_code: { done: '已运行代码', pending: '正在编写脚本', pendingAction: '正在编写脚本' }, + image_generate: { done: '已生成图片', pending: '正在生成图片', pendingAction: '正在生成' }, + list_files: { done: '已列出文件', pending: '正在列出文件', pendingAction: '正在列出' }, + patch: { done: '已修补文件', pending: '正在修补文件', pendingAction: '正在修补' }, + read_file: { done: '已读取文件', pending: '正在读取文件', pendingAction: '正在读取' }, + search_files: { done: '已搜索文件', pending: '正在搜索文件', pendingAction: '正在搜索' }, + session_search_recall: { done: '已搜索会话历史', pending: '正在搜索会话历史', pendingAction: '正在搜索' }, + terminal: { done: '已运行命令', pending: '正在运行命令', pendingAction: '正在运行' }, + todo: { done: '已更新待办', pending: '正在更新待办', pendingAction: '正在更新' }, + vision_analyze: { done: '已分析图片', pending: '正在分析图片', pendingAction: '正在分析' }, + web_extract: { done: '已读取网页', pending: '正在读取网页', pendingAction: '正在读取' }, + web_search: { done: '已搜索网页', pending: '正在搜索网页', pendingAction: '正在搜索' }, + write_file: { done: '已编辑文件', pending: '正在编辑文件', pendingAction: '正在编辑' } + } } },