fix(desktop): keep live tool activity inspectable and name skill loads
Fixes NousResearch/hermes-agent#107268
This commit is contained in:
@@ -8,6 +8,8 @@ import { isCardTool, isFileEditTool, isSilentTool } from '@/lib/tool-render-clas
|
||||
import { toolResultRecord } from '@/lib/tool-result-metadata'
|
||||
import { extractToolErrorMessage, formatToolResultSummary } from '@/lib/tool-result-summary'
|
||||
|
||||
import { skillActivityTitle } from '../skill-activity'
|
||||
|
||||
import {
|
||||
browserExecStepLabel,
|
||||
compactPreview,
|
||||
@@ -1310,6 +1312,12 @@ function dynamicTitle(
|
||||
result: Record<string, unknown>,
|
||||
fallback: ToolTitleParts
|
||||
): ToolTitleParts {
|
||||
const skillTitle = skillActivityTitle(part)
|
||||
|
||||
if (skillTitle) {
|
||||
return { title: skillTitle }
|
||||
}
|
||||
|
||||
const verb = (gerund: string, past: string) => (part.result === undefined ? gerund : past)
|
||||
|
||||
const titledAction = (action: string, title: string): ToolTitleParts =>
|
||||
|
||||
@@ -82,6 +82,7 @@ import { ToolRunTicker } from './run-ticker'
|
||||
// false, so every row currently owns its own chrome; kept as a seam for any
|
||||
// future embedding surface.
|
||||
const ToolEmbedContext = createContext(false)
|
||||
const ToolRunDisclosureContext = createContext<string | null>(null)
|
||||
|
||||
// A search hit's title is result *content* inside an expanded row, not one of
|
||||
// the scaffolding lines, so it keeps the brighter secondary grey.
|
||||
@@ -355,6 +356,7 @@ function ToolEntry({ part }: ToolEntryProps) {
|
||||
const messageId = useAuiState(s => s.message.id)
|
||||
const messageRunning = useAuiState(selectMessageRunning)
|
||||
const embedded = useContext(ToolEmbedContext)
|
||||
const runDisclosureId = useContext(ToolRunDisclosureContext)
|
||||
const toolViewMode = useStore($toolViewMode)
|
||||
|
||||
// `ToolFallback` rebuilds the `part` wrapper each render, defeating the memos
|
||||
@@ -565,7 +567,18 @@ function ToolEntry({ part }: ToolEntryProps) {
|
||||
<div className={cn(open && 'border-b border-(--ui-stroke-tertiary) px-2 py-1.5')}>
|
||||
<DisclosureRow
|
||||
action={dismissAction}
|
||||
onToggle={hasExpandableContent ? () => setToolDisclosureOpen(disclosureId, !open) : undefined}
|
||||
onToggle={
|
||||
hasExpandableContent
|
||||
? () => {
|
||||
// Opening a row is newer intent than an earlier group collapse.
|
||||
if (!open && runDisclosureId) {
|
||||
setToolDisclosureOpen(runDisclosureId, true)
|
||||
}
|
||||
|
||||
setToolDisclosureOpen(disclosureId, !open)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
open={open}
|
||||
trailing={trailing}
|
||||
>
|
||||
@@ -806,7 +819,7 @@ export function splitRunItems(toolNames: readonly string[]): RunItem[] {
|
||||
*/
|
||||
// The one grey line that stands in for a run of tool calls — "Explored 3
|
||||
// files, ran 5 commands". Live, it narrates in the present tense above the
|
||||
// ticker and offers no toggle, since there is nothing settled to unfold yet.
|
||||
// ticker by default; its toggle can reveal the activity before it settles.
|
||||
function ToolRunHeader({
|
||||
completedAt,
|
||||
live,
|
||||
@@ -855,7 +868,8 @@ interface ToolRunState {
|
||||
// re-render the group on every text delta in the turn. The run only changes
|
||||
// when a call arrives or one finishes; cache on exactly that.
|
||||
function useToolRun(startIndex: number, endIndex: number): ToolRunState {
|
||||
const cache = useRef<null | { signature: string; value: ToolRunState }>(null)
|
||||
const { locale } = useI18n()
|
||||
const cache = useRef<null | { signature: string; tools: readonly ToolPart[]; value: ToolRunState }>(null)
|
||||
|
||||
return useAuiState(state => {
|
||||
const parts = state.message.parts
|
||||
@@ -877,12 +891,28 @@ function useToolRun(startIndex: number, endIndex: number): ToolRunState {
|
||||
tool =>
|
||||
`${tool.toolCallId}:${tool.result === undefined ? 0 : 1}:${tool.timestamp ?? ''}:${tool.completedAt ?? ''}`
|
||||
)
|
||||
.concat(String(live))
|
||||
.concat(String(live), state.message.id, locale)
|
||||
.join('|')
|
||||
|
||||
if (cache.current?.signature !== signature) {
|
||||
// The arguments may arrive after tool.start. Compare references rather
|
||||
// than stringify potentially huge args/results on every streaming tick.
|
||||
const sameInputs =
|
||||
cache.current?.tools.length === timelineTools.length &&
|
||||
timelineTools.every((tool, index) => {
|
||||
const previous = cache.current!.tools[index]
|
||||
|
||||
return (
|
||||
tool.args === previous.args &&
|
||||
tool.result === previous.result &&
|
||||
tool.toolName === previous.toolName &&
|
||||
tool.isError === previous.isError
|
||||
)
|
||||
})
|
||||
|
||||
if (cache.current?.signature !== signature || !sameInputs) {
|
||||
cache.current = {
|
||||
signature,
|
||||
tools: timelineTools,
|
||||
value: {
|
||||
completedAt: timelineTools.reduce<number | undefined>(
|
||||
(latest, tool) =>
|
||||
@@ -895,7 +925,7 @@ function useToolRun(startIndex: number, endIndex: number): ToolRunState {
|
||||
),
|
||||
count: tools.length,
|
||||
entryIds: tools.map(tool => toolEntryDisclosureId(state.message.id, tool)),
|
||||
key: tools[0]?.toolCallId ?? '',
|
||||
key: `${state.message.id}:${tools[0]?.toolCallId ?? ''}`,
|
||||
live,
|
||||
startedAt: timelineTools.reduce<number | undefined>(
|
||||
(earliest, tool) =>
|
||||
@@ -954,7 +984,7 @@ const ToolRun: FC<PropsWithChildren<{ endIndex: number; startIndex: number }>> =
|
||||
// A lone call is already its own one-line summary; heading it with a second
|
||||
// line would say the same thing twice.
|
||||
if (count < 2) {
|
||||
return <>{children}</>
|
||||
return <ToolRunDisclosureContext.Provider value={disclosureId}>{children}</ToolRunDisclosureContext.Provider>
|
||||
}
|
||||
|
||||
// Two things a one-line window can't hold. An approval is a question the
|
||||
@@ -963,27 +993,28 @@ const ToolRun: FC<PropsWithChildren<{ endIndex: number; startIndex: number }>> =
|
||||
// keeps going. Either one hands the run back its full height until the run
|
||||
// settles and the row can be reached through the summary instead.
|
||||
const blocked = Boolean(approval) && pendingApprovalTool
|
||||
const unfurled = blocked || rowOpen
|
||||
const expanded = live ? unfurled : (persistedOpen ?? false)
|
||||
const expanded = blocked || (persistedOpen ?? rowOpen)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid min-w-0 max-w-full gap-(--tool-row-gap) overflow-hidden"
|
||||
data-slot="tool-block"
|
||||
data-tool-group=""
|
||||
ref={enterRef}
|
||||
>
|
||||
<ToolRunHeader
|
||||
completedAt={completedAt}
|
||||
live={live}
|
||||
onToggle={live ? undefined : () => setToolDisclosureOpen(disclosureId, !expanded)}
|
||||
open={expanded}
|
||||
startedAt={startedAt}
|
||||
summary={summary}
|
||||
/>
|
||||
{live && !unfurled && <ToolRunTicker>{children}</ToolRunTicker>}
|
||||
{expanded && <div className="grid min-w-0 max-w-full gap-(--tool-row-gap)">{children}</div>}
|
||||
</div>
|
||||
<ToolRunDisclosureContext.Provider value={disclosureId}>
|
||||
<div
|
||||
className="grid min-w-0 max-w-full gap-(--tool-row-gap) overflow-hidden"
|
||||
data-slot="tool-block"
|
||||
data-tool-group=""
|
||||
ref={enterRef}
|
||||
>
|
||||
<ToolRunHeader
|
||||
completedAt={completedAt}
|
||||
live={live}
|
||||
onToggle={blocked ? undefined : () => setToolDisclosureOpen(disclosureId, !expanded)}
|
||||
open={expanded}
|
||||
startedAt={startedAt}
|
||||
summary={summary}
|
||||
/>
|
||||
{live && !expanded && <ToolRunTicker>{children}</ToolRunTicker>}
|
||||
{expanded && <div className="grid min-w-0 max-w-full gap-(--tool-row-gap)">{children}</div>}
|
||||
</div>
|
||||
</ToolRunDisclosureContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { translateNow } from '@/i18n'
|
||||
import { summarizeShellCommand } from '@/lib/summarize-command'
|
||||
import { firstStringField } from '@/lib/text'
|
||||
import { extractToolErrorMessage } from '@/lib/tool-result-summary'
|
||||
|
||||
import { fileEditBasename, isFileEditTool, parseMaybeObject } from './fallback-model'
|
||||
import { skillActivityTitle } from './skill-activity'
|
||||
|
||||
/**
|
||||
* The little a summary needs from a tool call, stated structurally so both
|
||||
@@ -10,6 +13,8 @@ import { fileEditBasename, isFileEditTool, parseMaybeObject } from './fallback-m
|
||||
*/
|
||||
export interface ToolCallLike {
|
||||
args?: unknown
|
||||
completedAt?: number
|
||||
isError?: boolean
|
||||
result?: unknown
|
||||
toolCallId?: string
|
||||
toolName: string
|
||||
@@ -64,7 +69,7 @@ function toolCategory(toolName: string): RunCategory {
|
||||
}
|
||||
|
||||
function isPending(tool: ToolCallLike): boolean {
|
||||
return tool.result === undefined
|
||||
return tool.result === undefined && tool.completedAt === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +78,10 @@ function isPending(tool: ToolCallLike): boolean {
|
||||
* described in the same words from the moment the model drafts it.
|
||||
*/
|
||||
export function toolPresentVerb(toolName: string): string {
|
||||
if (toolName === 'skill_view') {
|
||||
return translateNow('assistant.tool.skillActivity.loading')
|
||||
}
|
||||
|
||||
return CATEGORY_COPY[toolCategory(toolName)].present
|
||||
}
|
||||
|
||||
@@ -135,8 +144,17 @@ export function summarizeToolRun(tools: readonly ToolCallLike[], live: boolean):
|
||||
const liveCategory = narrating ? toolCategory(narrating.toolName) : null
|
||||
|
||||
const byCategory = new Map<RunCategory, ToolCallLike[]>()
|
||||
const skillClauses: string[] = []
|
||||
|
||||
for (const tool of tools) {
|
||||
const skill = skillActivityTitle(tool, live)
|
||||
|
||||
if (skill) {
|
||||
skillClauses.push(skill)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const category = toolCategory(tool.toolName)
|
||||
const group = byCategory.get(category)
|
||||
|
||||
@@ -153,5 +171,20 @@ export function summarizeToolRun(tools: readonly ToolCallLike[], live: boolean):
|
||||
return group ? [clause(category, group, category === liveCategory)] : []
|
||||
})
|
||||
|
||||
return clauses.map((text, index) => (index === 0 ? text : lowerFirst(text))).join(', ')
|
||||
const failed = tools.filter(tool => {
|
||||
const result = parseMaybeObject(tool.result)
|
||||
|
||||
// Explicit success beats stale envelope errors, as in individual rows.
|
||||
return (
|
||||
result.success !== true &&
|
||||
result.ok !== true &&
|
||||
Boolean(tool.isError || result.success === false || result.ok === false || extractToolErrorMessage(tool.result))
|
||||
)
|
||||
}).length
|
||||
|
||||
if (failed) {
|
||||
clauses.push(translateNow('assistant.tool.failedCalls', failed))
|
||||
}
|
||||
|
||||
return [...skillClauses, ...clauses].map((text, index) => (index === 0 ? text : lowerFirst(text))).join(', ')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildToolView } from './fallback-model'
|
||||
import { summarizeToolRun } from './run-summary'
|
||||
|
||||
const skill = { type: 'tool-call' as const, toolName: 'skill_view', args: { name: 'research-notes' } }
|
||||
|
||||
describe('skill activity identity', () => {
|
||||
it('names instruction loads, resource reads, failures and unobserved outcomes without claiming task completion', () => {
|
||||
expect(buildToolView(skill, '').title).toBe('Loading skill: research-notes')
|
||||
expect(buildToolView({ ...skill, result: { success: true } }, '').title).toBe('Loaded skill: research-notes')
|
||||
const resource = { ...skill, args: { ...skill.args, file_path: 'references/example.md' } }
|
||||
expect(buildToolView(resource, '').title).toBe('Reading skill resource: research-notes → references/example.md')
|
||||
expect(buildToolView({ ...resource, result: 'resource body' }, '').title).toBe(
|
||||
'Read skill resource: research-notes → references/example.md'
|
||||
)
|
||||
expect(buildToolView({ ...skill, isError: true, result: 'missing' }, '').title).toBe(
|
||||
'Failed to load skill: research-notes'
|
||||
)
|
||||
expect(summarizeToolRun([skill], false)).toBe('Skill result unavailable: research-notes')
|
||||
})
|
||||
|
||||
it('keeps each skill identity and failed outcome in mixed collapsed summaries', () => {
|
||||
const summary = summarizeToolRun(
|
||||
[
|
||||
{ ...skill, result: 'instructions' },
|
||||
{ ...skill, args: { name: 'second-skill' }, result: { error: 'not found' } },
|
||||
{ toolName: 'terminal', args: { command: 'echo ok' }, result: { stdout: 'ok' } }
|
||||
],
|
||||
false
|
||||
)
|
||||
|
||||
expect(summary).toContain('Loaded skill: research-notes')
|
||||
expect(summary).toContain('failed to load skill: second-skill')
|
||||
expect(summary).toContain('ran 1 command')
|
||||
expect(summary).toContain('1 tool call failed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { translateNow } from '@/i18n'
|
||||
import { firstStringField } from '@/lib/text'
|
||||
import { extractToolErrorMessage } from '@/lib/tool-result-summary'
|
||||
|
||||
import { parseMaybeObject } from './fallback-model/format'
|
||||
|
||||
interface SkillCall {
|
||||
args?: unknown
|
||||
completedAt?: number
|
||||
isError?: boolean
|
||||
result?: unknown
|
||||
toolName: string
|
||||
}
|
||||
|
||||
/** Loading instructions is not the same action as reading a skill resource,
|
||||
* and neither proves the task described by the skill has been completed. */
|
||||
export function skillActivityTitle(part: SkillCall, live = true): string | undefined {
|
||||
if (part.toolName !== 'skill_view' && part.toolName !== 'skills_list') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const args = parseMaybeObject(part.args)
|
||||
const result = parseMaybeObject(part.result)
|
||||
|
||||
const failed = Boolean(
|
||||
part.isError || extractToolErrorMessage(part.result) || result.success === false || result.ok === false
|
||||
)
|
||||
|
||||
const pending = live && part.result === undefined && part.completedAt === undefined
|
||||
const missing = !pending && part.result === undefined
|
||||
const file = firstStringField(args, ['file_path'])
|
||||
const name = firstStringField(args, ['name'])
|
||||
const target = [name, file].filter(Boolean).join(' → ')
|
||||
|
||||
const keys =
|
||||
part.toolName === 'skills_list'
|
||||
? { pending: 'listing', done: 'listed', failed: 'listFailed' }
|
||||
: file
|
||||
? { pending: 'readingResource', done: 'readResource', failed: 'resourceFailed' }
|
||||
: { pending: 'loading', done: 'loaded', failed: 'loadFailed' }
|
||||
|
||||
const key = failed ? keys.failed : missing ? 'unavailable' : pending ? keys.pending : keys.done
|
||||
const label = translateNow(`assistant.tool.skillActivity.${key}`)
|
||||
|
||||
return target ? `${label}: ${target}` : label
|
||||
}
|
||||
@@ -478,14 +478,56 @@ describe('live tool run', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot be collapsed while a tool is still running', async () => {
|
||||
const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
|
||||
it('honors explicit disclosure across live updates and completion', async () => {
|
||||
const message = groupedPendingMessage()
|
||||
const { container, rerender } = render(<GroupHarness message={message} />)
|
||||
const toggle = () => container.querySelector('[data-tool-summary] button[aria-expanded]') as HTMLButtonElement
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-tool-summary]')).not.toBeNull()
|
||||
})
|
||||
await waitFor(() => expect(toggle()).not.toBeNull())
|
||||
fireEvent.click(toggle())
|
||||
expect(toggle().getAttribute('aria-expanded')).toBe('true')
|
||||
expect(container.querySelector('[data-tool-ticker]')).toBeNull()
|
||||
fireEvent.click(toggle())
|
||||
const row = container.querySelector('[data-tool-ticker] [data-tool-row] button[aria-expanded="false"]')
|
||||
expect(row).not.toBeNull()
|
||||
fireEvent.click(row as Element)
|
||||
await waitFor(() => expect(container.querySelector('[data-tool-ticker]')).toBeNull())
|
||||
|
||||
expect(container.querySelector('[data-tool-summary] button[aria-expanded]')).toBeNull()
|
||||
const next = {
|
||||
...message,
|
||||
content: [
|
||||
...message.content,
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolCallId: 'read-next',
|
||||
toolName: 'read_file',
|
||||
args: { path: '/tmp/next' },
|
||||
argsText: '{}'
|
||||
}
|
||||
]
|
||||
} as ThreadMessage
|
||||
|
||||
rerender(<GroupHarness message={next} />)
|
||||
await waitFor(() => expect(container.querySelectorAll('[data-tool-row]')).toHaveLength(3))
|
||||
rerender(<GroupHarness message={{ ...next, status: { type: 'complete', reason: 'stop' } }} />)
|
||||
await waitFor(() => expect(toggle().getAttribute('aria-expanded')).toBe('true'))
|
||||
fireEvent.click(toggle())
|
||||
expect(container.querySelectorAll('[data-tool-row]')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('updates named skill summaries when identifying arguments arrive late', async () => {
|
||||
const message = groupedPendingMessage()
|
||||
const first = { ...message.content[0], toolName: 'skill_view', args: {}, result: { success: true } }
|
||||
|
||||
const { container, rerender } = render(
|
||||
<GroupHarness message={{ ...message, content: [first, message.content[1]] } as ThreadMessage} />
|
||||
)
|
||||
|
||||
const summary = () => container.querySelector('[data-tool-summary]')?.textContent
|
||||
await waitFor(() => expect(summary()).toContain('Loaded skill'))
|
||||
const named = { ...first, args: { name: 'research-notes' } }
|
||||
rerender(<GroupHarness message={{ ...message, content: [named, message.content[1]] } as ThreadMessage} />)
|
||||
await waitFor(() => expect(summary()).toContain('research-notes'))
|
||||
})
|
||||
|
||||
// Liveness used to also require an unresolved call, which is false for the
|
||||
@@ -497,7 +539,7 @@ describe('live tool run', () => {
|
||||
|
||||
expect(await screen.findByText('Running 2 commands')).toBeTruthy()
|
||||
expect(container.querySelector('[data-tool-ticker]')).not.toBeNull()
|
||||
expect(container.querySelector('[data-tool-summary] button[aria-expanded]')).toBeNull()
|
||||
expect(container.querySelector('[data-tool-summary] button[aria-expanded]')).not.toBeNull()
|
||||
})
|
||||
|
||||
// The ticker is a one-line window, so a row opened inside it had its output
|
||||
|
||||
@@ -2901,6 +2901,19 @@ export const ar = defineLocale({
|
||||
copyQuery: 'نسخ الاستعلام',
|
||||
copyFile: 'نسخ الملف',
|
||||
copyPath: 'نسخ المسار',
|
||||
failedCalls: (count: number) => `عدد استدعاءات الأدوات الفاشلة: ${count}`,
|
||||
skillActivity: {
|
||||
loading: 'جارٍ تحميل المهارة',
|
||||
loaded: 'تم تحميل المهارة',
|
||||
loadFailed: 'تعذر تحميل المهارة',
|
||||
readingResource: 'جارٍ قراءة مورد المهارة',
|
||||
readResource: 'تمت قراءة مورد المهارة',
|
||||
resourceFailed: 'تعذرت قراءة مورد المهارة',
|
||||
listing: 'جارٍ عرض المهارات',
|
||||
listed: 'تم عرض المهارات',
|
||||
listFailed: 'تعذر عرض المهارات',
|
||||
unavailable: 'نتيجة المهارة غير متاحة'
|
||||
},
|
||||
outputAlt: 'إخراج الأداة',
|
||||
rawResponse: 'الرد الخام',
|
||||
copyActivity: 'نسخ النشاط',
|
||||
|
||||
@@ -3964,6 +3964,19 @@ export const en: Translations = {
|
||||
copyQuery: 'Copy query',
|
||||
copyFile: 'Copy file',
|
||||
copyPath: 'Copy path',
|
||||
failedCalls: (count: number) => `${count} tool call${count === 1 ? '' : 's'} failed`,
|
||||
skillActivity: {
|
||||
loading: 'Loading skill',
|
||||
loaded: 'Loaded skill',
|
||||
loadFailed: 'Failed to load skill',
|
||||
readingResource: 'Reading skill resource',
|
||||
readResource: 'Read skill resource',
|
||||
resourceFailed: 'Failed to read skill resource',
|
||||
listing: 'Listing skills',
|
||||
listed: 'Listed skills',
|
||||
listFailed: 'Failed to list skills',
|
||||
unavailable: 'Skill result unavailable'
|
||||
},
|
||||
outputAlt: 'Tool output',
|
||||
rawResponse: 'Raw response',
|
||||
copyActivity: 'Copy activity',
|
||||
|
||||
@@ -3359,6 +3359,19 @@ export const ja = defineLocale({
|
||||
copyQuery: 'クエリをコピー',
|
||||
copyFile: 'ファイルをコピー',
|
||||
copyPath: 'パスをコピー',
|
||||
failedCalls: (count: number) => `失敗したツール呼び出し: ${count}`,
|
||||
skillActivity: {
|
||||
loading: 'スキルを読み込み中',
|
||||
loaded: 'スキルを読み込みました',
|
||||
loadFailed: 'スキルの読み込みに失敗しました',
|
||||
readingResource: 'スキルのリソースを読み込み中',
|
||||
readResource: 'スキルのリソースを読み込みました',
|
||||
resourceFailed: 'スキルのリソースの読み込みに失敗しました',
|
||||
listing: 'スキル一覧を取得中',
|
||||
listed: 'スキル一覧を取得しました',
|
||||
listFailed: 'スキル一覧の取得に失敗しました',
|
||||
unavailable: 'スキルの結果を取得できません'
|
||||
},
|
||||
outputAlt: 'ツール出力',
|
||||
rawResponse: '生の応答',
|
||||
copyActivity: 'アクティビティをコピー',
|
||||
|
||||
@@ -3661,6 +3661,19 @@ export const ru = defineLocale({
|
||||
copyQuery: 'Копировать запрос',
|
||||
copyFile: 'Копировать файл',
|
||||
copyPath: 'Копировать путь',
|
||||
failedCalls: (count: number) => `Вызовов с ошибкой: ${count}`,
|
||||
skillActivity: {
|
||||
loading: 'Загружается скилл',
|
||||
loaded: 'Загружен скилл',
|
||||
loadFailed: 'Не удалось загрузить скилл',
|
||||
readingResource: 'Читается ресурс скилла',
|
||||
readResource: 'Прочитан ресурс скилла',
|
||||
resourceFailed: 'Не удалось прочитать ресурс скилла',
|
||||
listing: 'Загружается список скиллов',
|
||||
listed: 'Получен список скиллов',
|
||||
listFailed: 'Не удалось получить список скиллов',
|
||||
unavailable: 'Результат работы со скиллом недоступен'
|
||||
},
|
||||
outputAlt: 'Вывод инструмента',
|
||||
rawResponse: 'Сырой ответ',
|
||||
copyActivity: 'Копировать активность',
|
||||
|
||||
@@ -3490,6 +3490,19 @@ export interface Translations {
|
||||
copyQuery: string
|
||||
copyFile: string
|
||||
copyPath: string
|
||||
failedCalls: (count: number) => string
|
||||
skillActivity: {
|
||||
loading: string
|
||||
loaded: string
|
||||
loadFailed: string
|
||||
readingResource: string
|
||||
readResource: string
|
||||
resourceFailed: string
|
||||
listing: string
|
||||
listed: string
|
||||
listFailed: string
|
||||
unavailable: string
|
||||
}
|
||||
outputAlt: string
|
||||
rawResponse: string
|
||||
copyActivity: string
|
||||
|
||||
@@ -3312,6 +3312,19 @@ export const zhHant = defineLocale({
|
||||
copyQuery: '複製查詢',
|
||||
copyFile: '複製檔案',
|
||||
copyPath: '複製路徑',
|
||||
failedCalls: (count: number) => `${count} 次工具呼叫失敗`,
|
||||
skillActivity: {
|
||||
loading: '正在載入技能',
|
||||
loaded: '已載入技能',
|
||||
loadFailed: '技能載入失敗',
|
||||
readingResource: '正在讀取技能資源',
|
||||
readResource: '已讀取技能資源',
|
||||
resourceFailed: '技能資源讀取失敗',
|
||||
listing: '正在列出技能',
|
||||
listed: '已列出技能',
|
||||
listFailed: '技能清單取得失敗',
|
||||
unavailable: '技能結果無法使用'
|
||||
},
|
||||
outputAlt: '工具輸出',
|
||||
rawResponse: '原始回應',
|
||||
copyActivity: '複製活動',
|
||||
|
||||
@@ -4118,6 +4118,19 @@ export const zh = defineLocale({
|
||||
copyQuery: '复制查询',
|
||||
copyFile: '复制文件',
|
||||
copyPath: '复制路径',
|
||||
failedCalls: (count: number) => `${count} 次工具调用失败`,
|
||||
skillActivity: {
|
||||
loading: '正在加载技能',
|
||||
loaded: '已加载技能',
|
||||
loadFailed: '技能加载失败',
|
||||
readingResource: '正在读取技能资源',
|
||||
readResource: '已读取技能资源',
|
||||
resourceFailed: '技能资源读取失败',
|
||||
listing: '正在列出技能',
|
||||
listed: '已列出技能',
|
||||
listFailed: '技能列表获取失败',
|
||||
unavailable: '技能结果不可用'
|
||||
},
|
||||
outputAlt: '工具输出',
|
||||
rawResponse: '原始响应',
|
||||
copyActivity: '复制活动',
|
||||
|
||||
Reference in New Issue
Block a user