fix(desktop): limit pending tool shimmer to action verb

Localize tool titles and split pending rows so only the action segment
shimmers — paths, commands, and URLs stay static.
This commit is contained in:
Brooklyn Nicholson 2026-06-24 21:59:41 -05:00
parent cbe5c5689f
commit f2c45e2c81
8 changed files with 548 additions and 90 deletions

View file

@ -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<string, ToolMeta> = {
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<ToolTitleKey, ToolMetaSpec> = {
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<string, unknown> {
@ -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<string, unknown>,
resultRecord: Record<string, unknown>
): string {
function cronjobSubtitle(argsRecord: Record<string, unknown>, resultRecord: Record<string, unknown>): 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<string, unknown>,
resultRecord: Record<string, unknown>
): string {
function cronjobDetail(argsRecord: Record<string, unknown>, resultRecord: Record<string, unknown>): 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<string, unknown>,
result: Record<string, unknown>,
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
}
}

View file

@ -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 <SharedLinkifiedText className={className} pretty text={cleanVisibleText(text)} />
}
function ToolTitle({
isPending,
status,
title,
titleAction
}: {
isPending: boolean
status: ToolStatus
title: string
titleAction?: ToolTitleAction
}) {
return (
<FadeText
className={cn(
TOOL_HEADER_TITLE_CLASS,
isPending && 'text-(--ui-text-tertiary)',
status === 'error' && 'text-destructive',
status === 'warning' && 'text-amber-700 dark:text-amber-300'
)}
>
{isPending && titleAction ? (
<>
{titleAction.prefix}
<span className="shimmer">{titleAction.text}</span>
{titleAction.suffix}
</>
) : (
title
)}
</FadeText>
)
}
interface ToolEntryProps {
part: ToolPart
}
@ -414,16 +448,7 @@ function ToolEntry({ part }: ToolEntryProps) {
icon={view.icon}
status={leadingStatus(isPending, view.status)}
/>
<FadeText
className={cn(
TOOL_HEADER_TITLE_CLASS,
isPending && 'shimmer text-(--ui-text-tertiary)',
view.status === 'error' && 'text-destructive',
view.status === 'warning' && 'text-amber-700 dark:text-amber-300'
)}
>
{view.title}
</FadeText>
<ToolTitle isPending={isPending} status={view.status} title={view.title} titleAction={view.titleAction} />
{!isPending && view.countLabel && <span className={TOOL_HEADER_DURATION_CLASS}>{view.countLabel}</span>}
{showDiffStats && diffStats && (
<span className="flex shrink-0 items-center gap-1 font-mono text-[0.625rem] tabular-nums">

View file

@ -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.',

View file

@ -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'

View file

@ -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: 'ブランチする前にチャットを開始または再開してください。',

View file

@ -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<ToolTitleKey, ToolTitleCopy>
}
}

View file

@ -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: '正在編輯' }
}
}
},

View file

@ -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: '正在编辑' }
}
}
},