diff --git a/Cargo.lock b/Cargo.lock index 7b85ba5fb5f..f362e2b5d7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6472,6 +6472,7 @@ dependencies = [ "regex", "reqwest", "rusqlite", + "rustix", "serde", "serde_json", "sha2 0.11.0", @@ -6531,6 +6532,7 @@ version = "2.1.2" dependencies = [ "async-trait", "chrono", + "serde_json", "thiserror 2.0.20", "tinyagents-harness", "tinyagents-session", diff --git a/app/src/AppRoutes.guards.test.tsx b/app/src/AppRoutes.guards.test.tsx index 77957883888..468c5b122d9 100644 --- a/app/src/AppRoutes.guards.test.tsx +++ b/app/src/AppRoutes.guards.test.tsx @@ -65,6 +65,9 @@ vi.mock('./pages/Accounts', () => ({ default: () =>
})); vi.mock('./pages/Brain', () => ({ default: () =>
})); vi.mock('./pages/dev/AgentInsightsPreview', () => ({ default: () =>
})); vi.mock('./pages/dev/UiGallery', () => ({ default: () =>
})); +vi.mock('./pages/dev/ToolCallGallery', () => ({ + default: () =>
, +})); vi.mock('./pages/Invites', () => ({ default: () =>
})); vi.mock('./pages/Notifications', () => ({ default: () =>
, @@ -102,6 +105,7 @@ const OWNED: Array<{ path: string; page: string; guard: Guard }> = [ { path: '/notifications', page: 'page-notifications', guard: 'protected' }, { path: '/ptt-overlay', page: 'page-ptt-overlay', guard: 'none' }, { path: '/dev/ui', page: 'page-ui-gallery', guard: 'none' }, + { path: '/dev/tools', page: 'page-tool-call-gallery', guard: 'none' }, ]; describe('AppRoutes — each route renders its page behind the right guard', () => { @@ -200,6 +204,7 @@ describe('AppRoutes — the whole route table stays classified', () => { '/ptt-overlay': 'none', '/dev/agent-insights': 'none', '/dev/ui': 'none', + '/dev/tools': 'none', '/dev/assistant-ui': 'none', '*': 'none', }; diff --git a/app/src/AppRoutes.tsx b/app/src/AppRoutes.tsx index 23de9170b7b..866083606fd 100644 --- a/app/src/AppRoutes.tsx +++ b/app/src/AppRoutes.tsx @@ -12,6 +12,7 @@ import Activity from './pages/Activity'; import Brain from './pages/Brain'; import AgentInsightsPreview from './pages/dev/AgentInsightsPreview'; import AssistantUiDemoPage from './pages/dev/assistant-ui-demo'; +import ToolCallGallery from './pages/dev/ToolCallGallery'; import UiGallery from './pages/dev/UiGallery'; import FlowCanvasPage, { FlowCanvasDraftPage } from './pages/FlowCanvasPage'; import FlowsPage from './pages/FlowsPage'; @@ -260,6 +261,9 @@ const AppRoutes = ({ location }: AppRoutesProps = {}) => { {/* Gallery of every shared UI primitive, in the active theme. */} } /> + {/* Tool-call presentation: every state and the whole core catalog. */} + } /> + {/* The upstream assistant-ui `base` demo on a mock runtime. */} } /> diff --git a/app/src/components/assistant-ui/elements/code-diff.tsx b/app/src/components/assistant-ui/elements/code-diff.tsx new file mode 100644 index 00000000000..1657d4a194f --- /dev/null +++ b/app/src/components/assistant-ui/elements/code-diff.tsx @@ -0,0 +1,78 @@ +'use client'; + +/** + * assistant-ui's code-diff element: a file's added and removed lines. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/code-diff.tsx` + * (commit 1abca347). Only the `cn` import path differs from upstream. + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import type { ComponentProps } from 'react'; + +import { codeScroll, codeSurface, mono, paper } from './surfaces'; + +export type DiffKind = 'context' | 'added' | 'removed'; + +export interface DiffLine { + kind: DiffKind; + text: string; +} + +const GUTTER: Record = { context: '', added: '+', removed: '−' }; + +export function CodeDiff({ + filename, + additions, + deletions, + lines, + cycle, + className, + ...props +}: Omit< + ComponentProps<'div'>, + 'children' | 'filename' | 'additions' | 'deletions' | 'lines' | 'cycle' +> & { + filename: string; + additions: number; + deletions: number; + lines: readonly DiffLine[]; + cycle: number; +}) { + return ( +
+
+ {filename} + + +{additions}{' '} + −{deletions} + +
+
+
+ {lines.map((line, i) => ( +
+ {GUTTER[line.kind]} + {line.text} +
+ ))} +
+
+
+ ); +} diff --git a/app/src/components/assistant-ui/elements/surfaces.tsx b/app/src/components/assistant-ui/elements/surfaces.tsx index 9a8f8319e96..ed80c5538e7 100644 --- a/app/src/components/assistant-ui/elements/surfaces.tsx +++ b/app/src/components/assistant-ui/elements/surfaces.tsx @@ -3,8 +3,10 @@ /** * Shared surface primitives for the assistant-ui elements, vendored verbatim * from the `elements-surfaces` registry item - * (https://r.assistant-ui.com/elements-surfaces.json). The one local change is - * `collapsePanel`, adapted from Base UI to Radix (see below). + * (https://r.assistant-ui.com/elements-surfaces.json). Local changes, all for + * this app's Radix collapsible and jsdom: `collapsePanel` adapted from Base UI + * to Radix, an added `openRotate` chevron selector, and a `ResizeObserver` + * guard in `SwapLabel`. */ import { cn } from '@/components/assistant-ui/lib/utils'; import { type ComponentProps, useLayoutEffect, useRef, useState } from 'react'; @@ -47,6 +49,11 @@ export const labelSwapOut = 'pointer-events-none select-none opacity-0 blur-[2px export const collapsePanel = 'overflow-hidden ease-[cubic-bezier(0.32,0.72,0,1)] data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none'; +// Local addition: the Radix trigger reports `data-state=open`, not Base UI's +// `data-open` / `data-panel-open`, so the chevron rotation needs this selector. +export const openRotate = + 'transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[state=open]/trigger:rotate-90 motion-reduce:transition-none'; + export const live = 'text-blue-500 dark:text-blue-400'; export const mono = 'font-mono text-[11px] tracking-tight'; @@ -92,6 +99,8 @@ export function SwapLabel({ if (!target) return undefined; const measure = () => setWidth(Math.ceil(target.getBoundingClientRect().width)); measure(); + // Local guard: jsdom (unit tests) has no ResizeObserver. + if (typeof ResizeObserver === 'undefined') return undefined; const observer = new ResizeObserver(measure); observer.observe(target); return () => observer.disconnect(); diff --git a/app/src/components/assistant-ui/elements/terminal-block.tsx b/app/src/components/assistant-ui/elements/terminal-block.tsx new file mode 100644 index 00000000000..a3655c15255 --- /dev/null +++ b/app/src/components/assistant-ui/elements/terminal-block.tsx @@ -0,0 +1,115 @@ +'use client'; + +/** + * assistant-ui's terminal-block element: a command and its output lines, + * with a spinner while it runs and an exit marker once it is done. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/terminal-block.tsx` + * (commit 1abca347). Changes from upstream: + * - `exitLabel` / `failed`: upstream always reads "exit 0" with a check. + * - The output scrolls past `max-h-72`, and the `min-h` floor applies only + * while output is still streaming (a settled one-line result should not + * reserve a tall box). + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import { CheckIcon, CircleXIcon, Loader2Icon } from 'lucide-react'; +import type { ComponentProps } from 'react'; + +import { take } from '../utils/range'; +import { mono, paper } from './surfaces'; + +export function TerminalBlock({ + command, + lines, + visibleCount, + done, + failed = false, + exitLabel = 'exit 0', + variant = 'paper', + className, + ...props +}: Omit< + ComponentProps<'div'>, + 'children' | 'command' | 'lines' | 'visibleCount' | 'done' | 'variant' +> & { + command: string; + lines: readonly string[]; + visibleCount: number; + done: boolean; + failed?: boolean; + exitLabel?: string; + variant?: 'paper' | 'ink'; +}) { + const ink = variant === 'ink'; + + return ( +
+
+ + {command} + + {done ? ( +
+ {failed ? ( + + ) : ( + + )} + + {exitLabel} + +
+ ) : ( + + )} +
+
+ {take(lines, visibleCount).map((line, i) => { + const isLast = i === lines.length - 1; + return ( +
+ {line} +
+ ); + })} + {!done && ( + + )} +
+
+ ); +} diff --git a/app/src/components/assistant-ui/elements/tool-call.tsx b/app/src/components/assistant-ui/elements/tool-call.tsx new file mode 100644 index 00000000000..79543941856 --- /dev/null +++ b/app/src/components/assistant-ui/elements/tool-call.tsx @@ -0,0 +1,148 @@ +'use client'; + +/** + * assistant-ui's tool-call element: a disclosure row with a label that + * shimmers while the call runs and swaps to its settled form, the call's + * primary argument as a chip, and a Request / Result panel. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/tool-call.tsx` + * (commit 1abca347). Changes from upstream: + * - Radix collapsible (the app's), so the open-state selectors differ. + * - Open state may be uncontrolled (`defaultOpen`). + * - `icon`, `outcome` and `meta` slots: an OpenHuman call can fail, be + * cancelled or wait on the user, and carries a duration; upstream only + * knows running / done. + * - `request` / `result` take nodes, and `children` replaces the panel body, + * so a call can expand into a richer element (terminal, diff, preview). + * - `aside` renders between the row and the panel, always visible: a + * decision the turn is blocked on must not sit behind a disclosure. + * - The panel headings are props, for translation. + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/assistant-ui/ui/collapsible'; +import { CheckIcon, ChevronRightIcon, CircleXIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; + +import { collapsePanel, field, mono, openRotate, ShimmerLabel, SwapLabel } from './surfaces'; + +export type ToolCallOutcome = 'success' | 'error' | 'cancelled' | 'awaiting'; + +export interface ToolCallProps { + label: string; + activeLabel: string; + query?: string; + request?: ReactNode; + result?: ReactNode; + running: boolean; + outcome?: ToolCallOutcome; + icon?: ReactNode; + meta?: ReactNode; + aside?: ReactNode; + children?: ReactNode; + requestLabel?: string; + resultLabel?: string; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + className?: string; + 'data-testid'?: string; +} + +export function ToolCall({ + label, + activeLabel, + query, + request, + result, + running, + outcome = 'success', + icon, + meta, + aside, + children, + requestLabel = 'Request', + resultLabel = 'Result', + open, + defaultOpen, + onOpenChange, + className, + ...props +}: ToolCallProps) { + const hasPanel = children != null || request != null || result != null; + return ( + + + + {icon} + + + {activeLabel} + + <>{label} + + {query ? ( + + {query} + + ) : null} + + {meta} + {!running && outcome === 'success' ? ( + + ) : null} + {!running && (outcome === 'error' || outcome === 'cancelled') ? ( + + ) : null} + + + {aside} + {hasPanel ? ( + + {children ?? ( +
+ {request != null ? ( +
+

{requestLabel}

+
+ {request} +
+
+ ) : null} + {request != null && result != null ? ( +
+ ) : null} + {result != null ? ( +
+

{resultLabel}

+
{result}
+
+ ) : null} +
+ )} + + ) : null} + + ); +} diff --git a/app/src/components/assistant-ui/elements/tool-timeline.tsx b/app/src/components/assistant-ui/elements/tool-timeline.tsx new file mode 100644 index 00000000000..0eecbd42bef --- /dev/null +++ b/app/src/components/assistant-ui/elements/tool-timeline.tsx @@ -0,0 +1,128 @@ +'use client'; + +/** + * assistant-ui's tool-timeline element: a run of tool calls under one + * disclosure whose label shimmers ("Working") while the run streams and + * swaps to a summary once it settles. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/tool-timeline.tsx` + * (commit 1abca347). Changes from upstream: + * - Radix collapsible (the app's), so the open-state selectors differ. + * - Open state may be uncontrolled (`defaultOpen`). + * - `children` may replace `steps`: a live chat step is a full tool-call + * element (it expands, carries approvals), not only a verb and a chip. + * - Step keys are positional; two steps may share a chip. + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/assistant-ui/ui/collapsible'; +import { ChevronRightIcon, type LucideIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; + +import { take } from '../utils/range'; +import { collapsePanel, openRotate, ShimmerLabel, SwapLabel } from './surfaces'; + +export interface TimelineStep { + verb: string; + chip: string; + icon: LucideIcon; +} + +export interface TimelineStat { + file: string; + added?: number; + removed?: number; +} + +export interface ToolTimelineProps { + steps?: readonly TimelineStep[]; + visibleSteps?: number; + children?: ReactNode; + streaming: boolean; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + restingLabel: string; + activeLabel: string; + stats?: TimelineStat[]; + className?: string; + 'data-testid'?: string; +} + +export function ToolTimeline({ + steps = [], + visibleSteps = steps.length, + children, + streaming, + open, + defaultOpen, + onOpenChange, + restingLabel, + activeLabel, + stats = [], + className, + ...props +}: ToolTimelineProps) { + return ( + + + + + + {activeLabel} + + <>{restingLabel} + + + +
+ {children ?? + take(steps, visibleSteps).map((step, index, shown) => { + const Icon = step.icon; + const active = streaming && index === shown.length - 1; + + return ( +
+ + + {step.verb} + + + {step.chip} + +
+ ); + })} + {stats.length > 0 && ( +
+ {stats.map(stat => ( + + {stat.file} + {stat.added !== undefined && ( + +{stat.added} + )} + {stat.removed !== undefined && ( + −{stat.removed} + )} + + ))} +
+ )} +
+
+
+ ); +} diff --git a/app/src/components/assistant-ui/elements/web-preview.tsx b/app/src/components/assistant-ui/elements/web-preview.tsx new file mode 100644 index 00000000000..a4de868e588 --- /dev/null +++ b/app/src/components/assistant-ui/elements/web-preview.tsx @@ -0,0 +1,103 @@ +'use client'; + +/** + * assistant-ui's web-preview element: a URL bar with reload and open-in-new + * around preview content. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/web-preview.tsx` + * (commit 1abca347). Changes from upstream: the reload button renders only + * when `onReload` is given (a fetched page has nothing to reload), and the + * button and loading labels are props, for translation. + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import { ExternalLinkIcon, RotateCwIcon } from 'lucide-react'; +import type { ComponentProps } from 'react'; + +import { field, ghostButton, mono, paper, ShimmerLabel } from './surfaces'; + +/** + * Chrome around a preview: a URL bar, reload, and open-in-new. It renders + * `children` as given and enforces no isolation of its own, so the caller is + * responsible for passing an already-sandboxed frame. + */ +export function WebPreview({ + origin, + loading, + children, + onReload, + onOpenExternal, + reloadLabel = 'Reload the preview', + openExternalLabel = 'Open the preview in a new tab', + loadingLabel = 'Loading preview', + className, + ...props +}: Omit< + ComponentProps<'div'>, + 'origin' | 'loading' | 'children' | 'onReload' | 'onOpenExternal' +> & { + origin: string; + loading: boolean; + children: React.ReactNode; + onReload?: () => void; + onOpenExternal?: () => void; + reloadLabel?: string; + openExternalLabel?: string; + loadingLabel?: string; +}) { + return ( +
+
+ {onReload ? ( + + ) : null} + + + {origin} + + + {onOpenExternal ? ( + + ) : null} +
+ +
+
+ {children} +
+ {loading && ( +
+ + {loadingLabel} + +
+ )} +
+
+ ); +} diff --git a/app/src/components/assistant-ui/elements/web-search.tsx b/app/src/components/assistant-ui/elements/web-search.tsx new file mode 100644 index 00000000000..ac97630fc5e --- /dev/null +++ b/app/src/components/assistant-ui/elements/web-search.tsx @@ -0,0 +1,107 @@ +'use client'; + +/** + * assistant-ui's web-search element: the query as a pill, a status line that + * shimmers while searching, and the hits with a domain-initial avatar. + * + * Vendored from assistant-ui `packages/ui/src/components/react/assistant-ui/elements/web-search.tsx` + * (commit 1abca347). Changes from upstream: + * - The status line is props (`searchingLabel`, `statusLabel`); upstream + * hardcodes "Searching" / "Read 3 sources". + * - A result may carry a `url`; the row then renders through `renderLink` + * so the host decides how an external link opens. A result's `url` is + * provider-supplied, so the host must only pass vetted http(s) URLs. + * - Result keys include the URL: two hits often share a domain. + * - The empty-results floor (`min-h`) applies only while hits are expected. + */ +import { cn } from '@/components/assistant-ui/lib/utils'; +import { SearchIcon } from 'lucide-react'; +import type { ComponentProps, ReactNode } from 'react'; + +import { take } from '../utils/range'; +import { field, mono, ShimmerLabel } from './surfaces'; + +export interface WebSearchResult { + title: string; + domain: string; + url?: string; +} + +const rowClass = + 'fade-in slide-in-from-bottom-1 animate-in fill-mode-both hover:bg-foreground/[0.03] -mx-2.5 flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 transition-colors duration-300'; + +export function WebSearch({ + query, + results, + visibleResults, + searching, + cycle, + searchingLabel = 'Searching', + statusLabel, + renderLink, + className, + ...props +}: Omit< + ComponentProps<'div'>, + 'children' | 'query' | 'results' | 'visibleResults' | 'searching' | 'cycle' +> & { + query: string; + results: readonly WebSearchResult[]; + visibleResults: number; + searching: boolean; + cycle: number; + searchingLabel?: string; + statusLabel: string; + renderLink?: (props: { href: string; className: string; children: ReactNode }) => ReactNode; +}) { + return ( +
+ + + {query} + +
+ {searching ? ( + + {searchingLabel} + + ) : ( + {statusLabel} + )} +
+
+ {take(results, visibleResults).map(result => { + const content = ( + <> + + {result.domain.charAt(0).toUpperCase()} + + + {result.title} + + {result.domain} + + ); + const key = `${cycle}-${result.url ?? result.domain}-${result.title}`; + return result.url && renderLink ? ( + + {renderLink({ href: result.url, className: rowClass, children: content })} + + ) : ( +
+ {content} +
+ ); + })} +
+
+ ); +} diff --git a/app/src/components/composio/toolkitMeta.tsx b/app/src/components/composio/toolkitMeta.tsx index 2733a0bbe1a..0640d49ab3e 100644 --- a/app/src/components/composio/toolkitMeta.tsx +++ b/app/src/components/composio/toolkitMeta.tsx @@ -398,6 +398,42 @@ export const KNOWN_COMPOSIO_TOOLKITS = Object.freeze( MANAGED_COMPOSIO_TOOLKITS.map(entry => entry.slug) ); +/** + * Resolve a Composio action slug (`GMAIL_SEND_EMAIL`, + * `GOOGLECALENDAR_CREATE_EVENT`) to the toolkit it belongs to and a readable + * action ("Send email"). + * + * The core registers these actions under their raw upper-snake slug, and the + * chat timeline used to humanize that to "GMAIL SEND EMAIL". The longest known + * toolkit prefix wins (`GOOGLE_CALENDAR_…` before `GOOGLE_…`). A slug on a + * toolkit this catalog has not heard of still gets a readable name from its + * first segment, so a new toolkit never renders shouting. + * + * Returns `undefined` for anything that is not an upper-snake name with at + * least two segments. + */ +export function matchComposioActionSlug( + toolName: string +): { slug: string; name: string; action: string; known: boolean } | undefined { + if (!/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(toolName)) return undefined; + const parts = toolName.toLowerCase().split('_'); + const action = (rest: string[]) => { + const text = rest.join(' '); + return text.charAt(0).toUpperCase() + text.slice(1); + }; + for (let i = Math.min(parts.length - 1, 3); i >= 1; i -= 1) { + const candidate = canonicalizeComposioToolkitSlug(parts.slice(0, i).join('_')); + const name = MANAGED_TOOLKIT_NAME_BY_SLUG.get(candidate); + if (name) return { slug: candidate, name, action: action(parts.slice(i)), known: true }; + } + return { + slug: parts[0], + name: prettifyUnknownSlug(parts[0]), + action: action(parts.slice(1)), + known: false, + }; +} + function descriptionForToolkit(key: string, name: string, category: SkillCategory): string { if (key === 'instagram') { return ( diff --git a/app/src/features/conversations/components/AgentProcessSourcePanel.tsx b/app/src/features/conversations/components/AgentProcessSourcePanel.tsx index 23f44e7b9c5..b8318d01acd 100644 --- a/app/src/features/conversations/components/AgentProcessSourcePanel.tsx +++ b/app/src/features/conversations/components/AgentProcessSourcePanel.tsx @@ -63,7 +63,7 @@ export function AgentProcessSourcePanel({ // For a scoped *non*-sub-agent step, the detail (args / output) to show. const scopedDetail = scopedEntry ? (normalizeScopedBody(scopedEntry.result) ?? - normalizeScopedBody(formatTimelineEntry(scopedEntry).detail) ?? + normalizeScopedBody(formatTimelineEntry(scopedEntry, t).detail) ?? normalizeScopedBody(scopedEntry.argsBuffer)) : undefined; @@ -102,7 +102,7 @@ export function AgentProcessSourcePanel({ {scopedEntry - ? formatTimelineEntry(scopedEntry).title + ? formatTimelineEntry(scopedEntry, t).title : t('conversations.agentTaskInsights.processSourceTitle')} @@ -171,7 +171,7 @@ export function AgentProcessSourcePanel({ {subagentEntries.map(entry => (

- {formatTimelineEntry(entry).title} + {formatTimelineEntry(entry, t).title}

diff --git a/app/src/features/conversations/components/AssistantUiSubagentCall.tsx b/app/src/features/conversations/components/AssistantUiSubagentCall.tsx index e978071fad2..930ac0c546a 100644 --- a/app/src/features/conversations/components/AssistantUiSubagentCall.tsx +++ b/app/src/features/conversations/components/AssistantUiSubagentCall.tsx @@ -4,7 +4,6 @@ import { CircleXIcon, Loader2Icon, MessageCircleQuestionIcon, - WorkflowIcon, } from 'lucide-react'; import { useState } from 'react'; @@ -26,6 +25,8 @@ import { } from '../../../store/chatRuntimeSlice'; import { basename } from '../../../utils/pathUtils'; import { stripToolCallEnvelopes } from '../../../utils/toolTimelineFormatting'; +import { ToolIcon } from '../tools/ToolIcon'; +import { describeToolCall } from '../tools/toolPresentation'; import { BubbleMarkdown } from './AgentMessageBubble'; import { AssistantUiToolCallCard } from './AssistantUiToolCall'; @@ -331,6 +332,8 @@ export function AssistantUiSubagentCall({ // long as the delegation is actually blocked on the user, and the user's own // open/closed choice is remembered underneath and restored on resume. const disclosureOpen = open || awaiting; + const presentation = describeToolCall({ name: `subagent:${activity.agentId ?? 'subagent'}` }); + const [before, after] = t('conversations.tools.delegatedTo').split('{agent}'); return ( - + - Delegated to {name} + {before} + {name} + {after} {awaiting ? ( // Not a spinner: the child is not working, it is blocked on the user. @@ -358,7 +363,8 @@ export function AssistantUiSubagentCall({ ) : active ? ( - running + {' '} + {t('conversations.tools.status.running')} ) : ( diff --git a/app/src/features/conversations/components/AssistantUiToolCall.test.tsx b/app/src/features/conversations/components/AssistantUiToolCall.test.tsx new file mode 100644 index 00000000000..f9c0716749a --- /dev/null +++ b/app/src/features/conversations/components/AssistantUiToolCall.test.tsx @@ -0,0 +1,158 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; + +import { AssistantUiToolCallCard, formatElapsed } from './AssistantUiToolCall'; +import { ChatToolGroup } from './ChatToolParts'; + +const SEARCH_TEXT = [ + 'Search results for: rust async traits (via Exa)', + '1. Async fn in traits', + ' https://blog.rust-lang.org/async-fn', + ' Stable in 1.75.', + '2. Bad link', + ' javascript:alert(1)', +].join('\n'); + +describe('AssistantUiToolCallCard', () => { + it('renders a web search through the web-search element, visible without expanding', () => { + render( + + ); + const results = screen.getByTestId('web-search-results'); + expect(results).toHaveTextContent('rust async traits'); + expect(results).toHaveTextContent('Found 1 result · via Exa'); + const hits = within(results).getAllByTestId('web-search-hit'); + expect(hits).toHaveLength(1); + expect(hits[0]).toHaveAttribute('href', 'https://blog.rust-lang.org/async-fn'); + // The javascript: hit never becomes a link or a row. + expect(results).not.toHaveTextContent('Bad link'); + expect(screen.getByTestId('tool-call-elapsed')).toHaveTextContent('1.8s'); + }); + + it('prefers the structured search payload the core attaches', () => { + render( + + ); + expect(screen.getByTestId('web-search-results')).toHaveTextContent('From payload'); + expect(screen.getByTestId('web-search-results')).toHaveTextContent('via Parallel'); + }); + + it('swaps the label tense as the call settles', () => { + const { rerender } = render( + + ); + const card = screen.getByTestId('assistant-ui-tool-call'); + expect(card).toHaveAttribute('data-outcome', 'running'); + expect(within(card).getByRole('button', { name: /Reading file/ })).toBeInTheDocument(); + rerender( + + ); + expect(card).toHaveAttribute('data-outcome', 'success'); + expect(within(card).getByRole('button', { name: /Read file/ })).toBeInTheDocument(); + }); + + it('spells out states that need attention and keeps the rest for screen readers', () => { + render( + <> + + + + ); + const [failed, done] = screen.getAllByTestId('tool-call-status'); + expect(failed).toHaveTextContent('failed'); + expect(failed).not.toHaveClass('sr-only'); + expect(done).toHaveTextContent('done'); + expect(done).toHaveClass('sr-only'); + }); + + it('expands an edit into the code-diff element and a read into a code block', async () => { + render( + <> + + + + ); + const [edit, read] = screen.getAllByTestId('assistant-ui-tool-call'); + await userEvent.click(within(edit).getByRole('button', { name: /Edited file/ })); + const diff = screen.getByTestId('tool-body-file-diff'); + expect(diff).toHaveTextContent('+1'); + expect(diff).toHaveTextContent('old'); + await userEvent.click(within(read).getByRole('button', { name: /Read file/ })); + expect(screen.getByTestId('tool-body-file')).toHaveTextContent('fn main() {}'); + }); + + it('names a connected-app action by its app, never the slug', () => { + render(); + const card = screen.getByTestId('assistant-ui-tool-call'); + expect(card).toHaveTextContent('Used Gmail'); + expect(card).toHaveTextContent('Send email'); + expect(card).not.toHaveTextContent('GMAIL_SEND_EMAIL'); + }); +}); + +describe('ChatToolGroup', () => { + it('renders a lone call without a timeline header', () => { + render( + + only step + + ); + expect(screen.getByText('only step')).toBeVisible(); + expect(screen.queryByTestId('tool-timeline')).toBeNull(); + }); + + it('wraps several calls in the assistant-ui tool timeline', () => { + render( + + step one + step two + + ); + const timeline = screen.getByTestId('tool-timeline'); + expect(timeline).toHaveAttribute('data-slot', 'tool-timeline'); + expect(screen.getByText('step two')).toBeVisible(); + }); +}); + +describe('formatElapsed', () => { + it('formats milliseconds, seconds and minutes', () => { + expect(formatElapsed(850)).toBe('850ms'); + expect(formatElapsed(1840)).toBe('1.8s'); + expect(formatElapsed(75_000)).toBe('1m 15s'); + expect(formatElapsed(59_999)).toBe('1m 0s'); + expect(formatElapsed(-1)).toBe('0ms'); + }); +}); diff --git a/app/src/features/conversations/components/AssistantUiToolCall.tsx b/app/src/features/conversations/components/AssistantUiToolCall.tsx index e3ef08d87b6..8c5791cc993 100644 --- a/app/src/features/conversations/components/AssistantUiToolCall.tsx +++ b/app/src/features/conversations/components/AssistantUiToolCall.tsx @@ -1,129 +1,35 @@ import type { ToolCallMessagePart, ToolCallMessagePartProps } from '@assistant-ui/react'; -import { CheckIcon, ChevronDownIcon, CircleXIcon, Loader2Icon, WrenchIcon } from 'lucide-react'; import type { FC, ReactNode } from 'react'; -import { cn } from '../../../components/assistant-ui/lib/utils'; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '../../../components/assistant-ui/ui/collapsible'; + ToolCall, + type ToolCallOutcome, +} from '../../../components/assistant-ui/elements/tool-call'; +import { useT } from '../../../lib/i18n/I18nContext'; +import { readOpenHumanToolArtifact } from '../../../providers/assistantUiMessages'; import type { ToolFailureExplanation, ToolTimelineEntryStatus, } from '../../../store/chatRuntimeSlice'; -import { formatToolName, inferIntegrationActionName } from '../../../utils/toolTimelineFormatting'; -import { BubbleMarkdown } from './AgentMessageBubble'; +import { openUrl } from '../../../utils/openUrl'; +import { FetchBody, FileBody, ShellBody, WebSearchBody } from '../tools/ToolBodies'; +import { hasDisplayValue, parsedValue, ToolDataView } from '../tools/ToolDataView'; +import { ToolIcon } from '../tools/ToolIcon'; +import { describeToolCall, parseToolArgs, toolLabel } from '../tools/toolPresentation'; import { ToolFailureLines } from './ToolFailureLines'; -function friendlyLabel(key: string): string { - return key - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .replace(/[_-]+/g, ' ') - .replace(/^./, char => char.toUpperCase()); -} - -function parsedValue(value: unknown): unknown { - if (typeof value !== 'string') return value; - const trimmed = value.trim(); - if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return value; - try { - return JSON.parse(trimmed); - } catch { - return value; - } -} - -function hasDisplayValue(value: unknown): boolean { - if (value === undefined || value === null || value === '') return false; - if (Array.isArray(value)) return value.length > 0; - if (typeof value === 'object') return Object.keys(value as object).length > 0; - return true; -} - -function ToolDataView({ value }: { value: unknown }) { - const parsed = parsedValue(value); - if (Array.isArray(parsed)) { - return ( -
    - {parsed.map((item, index) => ( -
  • - -
  • - ))} -
- ); - } - if (parsed && typeof parsed === 'object') { - const entries = Object.entries(parsed); - for (const key of ['content', 'output', 'result', 'message', 'query', 'q']) { - const semantic = entries.find(([candidate]) => candidate === key)?.[1]; - if (hasDisplayValue(semantic)) return ; - } - return ( -
- {entries.map(([key, item]) => ( -
-
{friendlyLabel(key)}
-
- -
-
- ))} -
- ); - } - if (typeof parsed === 'boolean') return {parsed ? 'Yes' : 'No'}; - if (typeof parsed === 'string') return ; - return {String(parsed ?? '')}; -} - -function inferredToolLabel(toolName: string, running: boolean, args: unknown, result: unknown) { - const lowerName = toolName.toLowerCase(); - const parsedArgs = parsedValue(args); - const argKeys = - parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs) - ? Object.keys(parsedArgs as object).map(key => key.toLowerCase()) - : []; - const renderedResult = typeof result === 'string' ? result : JSON.stringify(result ?? ''); - // The harness's tool-discovery bridge, resolved BEFORE the heuristics below. - // `tool_search` matches `looksLikeSearch` twice — its name contains "search" - // AND its argument is `query` — so it rendered as "Searched the web" even - // though it never touches the network: it ranks the deferred tool catalogue - // (`vendor/tinyagents/crates/tinyagents-harness/src/tool/discover/bridge.rs`). - // Observed cost: a turn that fetched the user's own Google Calendar through - // Composio showed "Searched the web" as its first row, and the call that did - // the work showed as a bare "Tool Call". - // - // Both intrinsics are `ToolSchema` values, not `Tool` impls, so they carry no - // server `display_label` and there is nothing upstream to override. - if (lowerName === 'tool_search') { - return running ? 'Finding the right tool' : 'Found the right tool'; - } - if (lowerName === 'tool_call') { - // `tool_call_schema()` declares `{name, arguments}` with both required, so - // `name` is always the wrapped tool. Label the row by what was actually - // invoked rather than by the wrapper. - const wrapped = - parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs) - ? (parsedArgs as { name?: unknown }).name - : undefined; - if (typeof wrapped === 'string' && wrapped.trim()) { - const action = inferIntegrationActionName(wrapped); - return action ? `${action.provider}: ${action.action}` : formatToolName(wrapped); - } - } - const looksLikeSearch = - lowerName.includes('search') || - argKeys.some(key => ['query', 'q', 'search_query'].includes(key)) || - /(?:^|\n)#?\s*search results\b/i.test(renderedResult); - const looksLikeFetch = - lowerName.includes('fetch') || - argKeys.some(key => ['url', 'uri'].includes(key)) || - /\bstatus=\d{3}\s+url=/i.test(renderedResult); - if (looksLikeSearch) return running ? 'Searching the web' : 'Searched the web'; - if (looksLikeFetch) return running ? 'Fetching from the web' : 'Fetched from the web'; - return formatToolName(toolName); +/** `1234` → "1.2s", `850` → "850ms", `75000` → "1m 15s". */ +export function formatElapsed(ms: number): string { + const roundedMs = Math.max(0, Math.round(ms)); + if (roundedMs < 1000) return `${roundedMs}ms`; + // A one-decimal seconds label would round 59.95s up to "60.0s". Switch + // to the minutes form before that point, then derive both fields from one + // rounded total so the seconds component is always below 60. + if (roundedMs < 59_950) return `${(roundedMs / 1000).toFixed(1)}s`; + const totalSeconds = Math.round(roundedMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m ${seconds}s`; } /** @@ -143,9 +49,12 @@ export interface AssistantUiToolCallCardProps { argsText?: string; result?: unknown; status?: ToolTimelineEntryStatus; + /** Server label; used only for tools the presentation registry cannot describe. */ displayName?: string; detail?: string; elapsedMs?: number; + /** Machine-readable result from the core (e.g. structured web-search hits). */ + structured?: unknown; failure?: ToolFailureExplanation; /** * The call is parked on the user — an ApprovalGate request, or a sub-agent @@ -157,7 +66,15 @@ export interface AssistantUiToolCallCardProps { footer?: ReactNode; } -/** The single assistant-ui tool-call presentation used at every nesting level. */ +/** + * One tool call, rendered with assistant-ui's tool-call element. + * + * The icon, the label (in both tenses, which the element swaps between as + * the call settles) and the target chip all come from the presentation + * registry, so every surface names a call the same way. The panel expands + * into the tool's own assistant-ui element (search results, terminal, diff, + * page preview) or the generic Request / Result view. + */ export function AssistantUiToolCallCard({ toolName, args, @@ -167,106 +84,137 @@ export function AssistantUiToolCallCard({ displayName, detail, elapsedMs, + structured, failure, awaitingUser = false, footer, }: AssistantUiToolCallCardProps) { + const { t } = useT(); const running = awaitingUser || (status ? status === 'running' || status === 'awaiting_user' : result === undefined); + const effectiveStatus: ToolTimelineEntryStatus = + status ?? (awaitingUser ? 'awaiting_user' : running ? 'running' : 'success'); const input = hasDisplayValue(args) ? args : parsedValue(argsText ?? ''); - const output = result === '' && status && !running ? 'No output' : parsedValue(result); - const suppliedLabel = displayName?.trim(); - const label = - suppliedLabel && suppliedLabel.toLowerCase() !== 'tool' - ? suppliedLabel - : inferredToolLabel(toolName, running, args, result); - // `awaiting input` was previously reachable only via `status`, which the - // adapter forwards for `error` / `cancelled` alone — so the label could never - // render for the case it was written for. A parked call now says so. - const statusLabel = - status === 'error' - ? 'failed' - : status === 'cancelled' - ? 'cancelled' - : awaitingUser || status === 'awaiting_user' - ? 'awaiting input' - : running - ? 'running' - : 'done'; + const parsedArgs = parseToolArgs(input); + const output = result === '' && status && !running ? t('conversations.tools.noOutput') : result; + const presentation = describeToolCall({ + name: toolName, + args: parsedArgs, + status: effectiveStatus, + serverLabel: displayName, + serverDetail: detail, + }); + const activeLabel = toolLabel({ ...presentation, tense: 'active' }, t); + const doneLabel = toolLabel({ ...presentation, tense: 'done' }, t); const failed = status === 'error'; - // `failed` gates the failure-explanation block, which only an `error` carries. - // The icon is a wider question: a cancelled call did not succeed either, and - // before the adapter forwarded a status this branch was unreachable, so the - // check icon sat next to the word "cancelled". - const terminalNonSuccess = failed || status === 'cancelled'; + const awaiting = awaitingUser || status === 'awaiting_user'; + const outcome: ToolCallOutcome = failed + ? 'error' + : status === 'cancelled' + ? 'cancelled' + : awaiting + ? 'awaiting' + : 'success'; + const statusText = + outcome === 'error' + ? t('conversations.tools.status.failed') + : outcome === 'cancelled' + ? t('conversations.tools.status.cancelled') + : outcome === 'awaiting' + ? t('conversations.tools.status.awaiting') + : running + ? t('conversations.tools.status.running') + : t('conversations.tools.status.done'); + // Running and done are carried by the spinner / check; they stay readable + // to a screen reader. The states that need attention are spelled out. + const statusVisible = outcome !== 'success'; + + const searchBody = + presentation.body === 'webSearch' + ? WebSearchBody({ args: parsedArgs, result: output, structured, searching: running, t }) + : null; + const richBody = running + ? null + : presentation.body === 'shell' + ? ShellBody({ args: parsedArgs, result: output, failed, t }) + : presentation.body === 'webFetch' + ? FetchBody({ args: parsedArgs, result: output, t, onOpenExternal: openExternal }) + : presentation.body === 'file' + ? FileBody({ args: parsedArgs, result: output }) + : null; + const showOutput = !searchBody && hasDisplayValue(parsedValue(output)); return ( - - - - {label} - {detail ? ( - - {detail} + className="max-w-none" + label={doneLabel} + activeLabel={activeLabel} + // The web-search element shows the query as its own pill. + query={searchBody ? undefined : presentation.chip} + running={running} + outcome={outcome} + defaultOpen={awaitingUser} + icon={} + requestLabel={t('conversations.subagent.input')} + resultLabel={t('conversations.subagent.output')} + request={ + !richBody && hasDisplayValue(input) ? ( +
+ +
+ ) : undefined + } + result={ + !richBody && showOutput ? ( +
+ +
+ ) : undefined + } + meta={ + <> + + {statusText} - ) : null} - - {running ? ( - - ) : terminalNonSuccess ? ( - - ) : ( - - )} - {statusLabel} {elapsedMs != null && !running ? ( - - {elapsedMs >= 1000 ? `${(elapsedMs / 1000).toFixed(1)}s` : `${elapsedMs}ms`} + + {formatElapsed(elapsedMs)} ) : null} - - -
- {failed && failure ? ( -
- -
- ) : null} - {/* Outside `CollapsibleContent` on purpose: a decision the turn is - blocked on must not be hidden behind a disclosure the user has to - find and open. */} - {footer} - - {hasDisplayValue(input) ? ( -
-

Input

-
- -
-
- ) : null} - {hasDisplayValue(output) ? ( -
-

Output

-
- + + } + aside={ + <> + {failed && failure ? ( +
+
-
- ) : null} - - + ) : null} + {footer ?
{footer}
: null} + {/* Search results are the call's whole point: visible without + opening the disclosure, as in assistant-ui's own web-search. */} + {searchBody ?
{searchBody}
: null} + + }> + {richBody ?
{richBody}
: undefined} + ); } +function openExternal(url: string): void { + void openUrl(url).catch(() => undefined); +} + /** * Terminal status carried inside a settled tool part's `result`. * @@ -299,6 +247,10 @@ function toolStatusEnvelope( * to destructure four fields and drop the rest, which is why a parked call * rendered as an ordinary running one with no way to answer it. * + * The part's `artifact` carries what the core said about the call (its label + * for a dynamic tool, the duration, a structured result). The adapter used to + * drop all of it, so the card guessed a label from the tool name. + * * The decision surface itself is passed in rather than built here. It is * `ApprovalRequestCard`, which needs the thread id and the store's * `PendingApproval` — neither of which belongs in this file, and both of which @@ -311,6 +263,7 @@ export const OpenHumanToolCall: FC< } > = props => { const envelope = toolStatusEnvelope(props.result); + const artifact = readOpenHumanToolArtifact(props.artifact); return ( diff --git a/app/src/features/conversations/components/ChatToolParts.test.tsx b/app/src/features/conversations/components/ChatToolParts.test.tsx index f2a0180304d..03f917bf679 100644 --- a/app/src/features/conversations/components/ChatToolParts.test.tsx +++ b/app/src/features/conversations/components/ChatToolParts.test.tsx @@ -158,7 +158,7 @@ describe('ChatToolParts', () => { expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent('Searched the web'); await userEvent.click(screen.getByRole('button', { name: /Searched the web/ })); - expect(screen.getByText(/Lean open conjectures/)).toBeInTheDocument(); + expect(screen.getAllByText(/Lean open conjectures/).length).toBeGreaterThan(0); expect(screen.queryByText('Query', { exact: true })).not.toBeInTheDocument(); expect(screen.getByText('Found 12 candidate problems')).toBeInTheDocument(); }); @@ -179,12 +179,16 @@ describe('ChatToolParts', () => { /> ); - await userEvent.click(screen.getByRole('button', { name: /Fetched from the web/ })); + await userEvent.click(screen.getByRole('button', { name: /Read webpage/ })); expect(screen.getByRole('strong')).toHaveTextContent('Example Domain'); expect(screen.queryByText('Content', { exact: true })).not.toBeInTheDocument(); }); - it('infers web search labels when a persisted tool name degraded to tool', () => { + // The old card called any call with a `query` argument "Searched the web", + // which mislabelled memory, tool and email searches. A call whose name + // degraded to `tool` is now labelled as what is known about it: an + // unnamed tool, with its query as the chip. + it('does not guess a web search from a query argument alone', () => { render( { /> ); - expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent('Searched the web'); - expect(screen.getByTestId('assistant-ui-tool-call')).not.toHaveTextContent(/^Tool done$/); + const card = screen.getByTestId('assistant-ui-tool-call'); + expect(card).not.toHaveTextContent('Searched the web'); + expect(card).toHaveTextContent('Used tool'); + expect(card).toHaveTextContent('latest world news'); }); it('names the tool-discovery bridge for what it is, not a web search', () => { @@ -225,7 +231,7 @@ describe('ChatToolParts', () => { /> ); - expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent('Found the right tool'); + expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent('Found tools'); // The regression this pins: BOTH heuristic legs still match this row, so // dropping the explicit branch renders the web label again. expect(screen.getByTestId('assistant-ui-tool-call')).not.toHaveTextContent('Searched the web'); @@ -251,11 +257,12 @@ describe('ChatToolParts', () => { ); // Composio slugs carry no separator inside the toolkit name, so this also - // pins the `googlecalendar` spelling in `KNOWN_TOOLKIT_RE`: without it the - // row degrades to the raw "GOOGLECALENDAR EVENTS LIST". - expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent( - 'Google Calendar: Events list' - ); + // pins the `googlecalendar` catalog lookup: without it the row degrades to + // the raw "GOOGLECALENDAR EVENTS LIST". + const card = screen.getByTestId('assistant-ui-tool-call'); + expect(card).toHaveTextContent('Used Google Calendar'); + expect(card).toHaveTextContent('Events list'); + expect(card).not.toHaveTextContent('GOOGLECALENDAR'); expect(screen.getByTestId('assistant-ui-tool-call')).not.toHaveTextContent('Tool Call'); }); }); diff --git a/app/src/features/conversations/components/ChatToolParts.tsx b/app/src/features/conversations/components/ChatToolParts.tsx index 478b2ad40d1..8c09b5b02c3 100644 --- a/app/src/features/conversations/components/ChatToolParts.tsx +++ b/app/src/features/conversations/components/ChatToolParts.tsx @@ -1,15 +1,23 @@ import { + type AssistantState, type ToolCallMessagePart, type ToolCallMessagePartComponent, useAui, + useAuiState, } from '@assistant-ui/react'; -import { useCallback } from 'react'; +import { type FC, type PropsWithChildren, useCallback, useMemo } from 'react'; +import { ToolTimeline } from '../../../components/assistant-ui/elements/tool-timeline'; +import type { ThreadGroupPart } from '../../../components/assistant-ui/thread'; import ApprovalRequestCard from '../../../components/chat/ApprovalRequestCard'; import IntegrationConnectCard from '../../../components/chat/IntegrationConnectCard'; +import { useT } from '../../../lib/i18n/I18nContext'; +import { readOpenHumanToolArtifact } from '../../../providers/assistantUiMessages'; import { useAuiThreadId } from '../../../providers/AssistantUiRuntimeProvider'; import type { PendingApproval, SubagentActivity } from '../../../store/chatRuntimeSlice'; import { useAppSelector } from '../../../store/hooks'; +import { summarizeToolCalls } from '../../../utils/toolTimelineFormatting'; +import { describeToolCall, toolLabel } from '../tools/toolPresentation'; import { AssistantUiSubagentCall, isActiveSubagentStatus } from './AssistantUiSubagentCall'; import { isApprovalPending, OpenHumanToolCall } from './AssistantUiToolCall'; import { useSubagentDrawerHost } from './aui/subagentDrawerHost'; @@ -204,3 +212,74 @@ export const ChatToolFallback: ToolCallMessagePartComponent = props => { if (props.toolName === COMPOSIO_CONNECT_TOOL) return ; return ; }; +const NO_PARTS: readonly never[] = []; +// `optional`: a group rendered outside a message (tests, previews) has no +// message scope, and reading `state.message` there throws. +const selectMessageParts = (state: AssistantState) => state.optional.message?.parts ?? NO_PARTS; + +/** + * The chat's tool timeline: a run of adjacent tool calls under assistant-ui's + * tool-timeline element. + * + * Its label shimmers with what is happening now ("Searching the web") while + * the run is in flight, and swaps to a summary once it settles ("5 steps · + * Read file ×3, Searched the web ×2"). Each step is a full tool-call element. + * A lone call needs no header over itself, so it renders bare. + */ +export const ChatToolGroup: FC> = ({ + group, + children, +}) => { + const { t } = useT(); + const parts = useAuiState(selectMessageParts); + const running = group.status.type === 'running'; + const presentations = useMemo( + () => + group.indices + .map(index => parts[index]) + .filter(part => part?.type === 'tool-call') + .map(part => toolPartPresentation(part as unknown as ToolCallMessagePart)), + [group.indices, parts] + ); + if (group.indices.length <= 1) { + return
{children}
; + } + const active = [...presentations].reverse().find(p => p.tense === 'active'); + return ( + + {children} + + ); +}; + +/** Resolve a raw assistant-ui tool part (status packed into `result`). */ +function toolPartPresentation(part: ToolCallMessagePart) { + const result = part.result as { status?: unknown } | undefined; + const envelopeStatus = + result && typeof result === 'object' && !Array.isArray(result) ? result.status : undefined; + const status = + envelopeStatus === 'error' || envelopeStatus === 'cancelled' + ? envelopeStatus + : part.result === undefined + ? 'running' + : 'success'; + if (part.toolName === 'task') { + const args = part.args as { subagent_type?: unknown } | undefined; + const agent = typeof args?.subagent_type === 'string' ? args.subagent_type : 'subagent'; + return describeToolCall({ name: `subagent:${agent}`, status }); + } + const artifact = readOpenHumanToolArtifact(part.artifact); + return describeToolCall({ + name: part.toolName, + args: part.args, + status, + serverLabel: artifact?.displayName, + serverDetail: artifact?.detail, + }); +} diff --git a/app/src/features/conversations/components/PastTurnInsights.tsx b/app/src/features/conversations/components/PastTurnInsights.tsx index 6389ed70e56..f8f5bc9bd62 100644 --- a/app/src/features/conversations/components/PastTurnInsights.tsx +++ b/app/src/features/conversations/components/PastTurnInsights.tsx @@ -1,3 +1,4 @@ +import { useT } from '../../../lib/i18n/I18nContext'; import type { ProcessingTranscriptItem, ToolTimelineEntry } from '../../../store/chatRuntimeSlice'; import { formatTimelineEntry } from '../../../utils/toolTimelineFormatting'; import { AssistantUiSubagentCall } from './AssistantUiSubagentCall'; @@ -27,6 +28,7 @@ export function PastTurnInsights({ entries: ToolTimelineEntry[]; transcript: ProcessingTranscriptItem[]; }) { + const { t } = useT(); // No reasoning/narration trail persisted (legacy snapshot): render the // tool-only timeline, which already nests each sub-agent's activity inline. if (transcript.length === 0) { @@ -49,7 +51,7 @@ export function PastTurnInsights({ {subagentEntries.map(entry => (

- {formatTimelineEntry(entry).title} + {formatTimelineEntry(entry, t).title}

diff --git a/app/src/features/conversations/components/ProcessingTranscriptView.tsx b/app/src/features/conversations/components/ProcessingTranscriptView.tsx index 465a4ed4381..a3c885cfcc5 100644 --- a/app/src/features/conversations/components/ProcessingTranscriptView.tsx +++ b/app/src/features/conversations/components/ProcessingTranscriptView.tsx @@ -9,11 +9,11 @@ import type { } from '../../../store/chatRuntimeSlice'; import { buildProcessingBlocks, - categorizeTool, formatTimelineEntry, + presentTimelineEntry, stripToolCallEnvelopes, - type ToolCategory, } from '../../../utils/toolTimelineFormatting'; +import { ToolIcon } from '../tools/ToolIcon'; import { ToolFailureLines } from './ToolFailureLines'; /** @@ -58,7 +58,8 @@ export function ProcessingTranscriptView({ */ renderSubagent?: (subagent: NonNullable) => React.ReactNode; }) { - const blocks = buildProcessingBlocks(transcript, entries); + const { t } = useT(); + const blocks = buildProcessingBlocks(transcript, entries, t); if (blocks.length === 0) return null; return ( @@ -185,12 +186,13 @@ function ToolRow({ entry: ToolTimelineEntry; renderSubagent?: (subagent: NonNullable) => React.ReactNode; }) { - const { title, detail } = formatTimelineEntry(entry); + const { t } = useT(); + const { title, detail } = formatTimelineEntry(entry, t); return (
  • - + {title} @@ -225,47 +227,3 @@ function StatusGlyph({ status }: { status: ToolTimelineEntryStatus }) { } return ✓; } - -/** Minimal monochrome glyph per tool category (inherits `currentColor`). */ -function CategoryIcon({ category }: { category: ToolCategory }) { - const common = { width: 12, height: 12, viewBox: '0 0 12 12', 'aria-hidden': true } as const; - switch (category) { - case 'search': - return ( - - - - - ); - case 'run': - return ( - - - - - ); - case 'fetch': - case 'browse': - return ( - - - - - ); - case 'write': - return ( - - - - - ); - case 'read': - default: - return ( - - - - - ); - } -} diff --git a/app/src/features/conversations/components/ToolTimelineBlock.tsx b/app/src/features/conversations/components/ToolTimelineBlock.tsx index b768ddd0975..e4ebe5523a0 100644 --- a/app/src/features/conversations/components/ToolTimelineBlock.tsx +++ b/app/src/features/conversations/components/ToolTimelineBlock.tsx @@ -433,7 +433,7 @@ export function ToolTimelineBlock({ ) : (
    {rows.map(({ entry, count }, index) => { - const formatted = formatTimelineEntry(entry); + const formatted = formatTimelineEntry(entry, t); const detailContent = normalizeToolBody(formatted.detail) ?? normalizeToolBody(entry.argsBuffer); const workerRef = parseWorkerThreadRef(formatted.detail ?? entry.detail); diff --git a/app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx b/app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx index fd0f5fd301e..677628dde3f 100644 --- a/app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx +++ b/app/src/features/conversations/components/__tests__/AgentProcessSourcePanel.test.tsx @@ -160,7 +160,7 @@ describe('AgentProcessSourcePanel', () => { expect(screen.getByText('Let me check both docs first.')).toBeInTheDocument(); expect(screen.getByText('Now I can see what is missing.')).toBeInTheDocument(); // The two consecutive reads collapse into one human-summarized group. - expect(screen.getByText('Read 2 files')).toBeInTheDocument(); + expect(screen.getByText('2 steps · Read file ×2')).toBeInTheDocument(); expect(screen.getByTestId('processing-transcript')).toBeInTheDocument(); }); @@ -226,7 +226,7 @@ describe('AgentProcessSourcePanel', () => { /> ); // Header shows the step's label, not the generic title. - expect(screen.getByText('Researching')).toBeInTheDocument(); + expect(screen.getByText('Researched')).toBeInTheDocument(); // Only the scoped step's activity renders… openFirstSubagent(); expect(screen.getByTestId('subagent-activity').textContent).toContain('scoped thought'); @@ -248,7 +248,7 @@ describe('AgentProcessSourcePanel', () => { renderPanel( {}} /> ); - expect(screen.getByText('Run Code')).toBeInTheDocument(); + expect(screen.getByText('Ran code')).toBeInTheDocument(); expect(screen.getByText(/All checks passed/)).toBeInTheDocument(); expect(screen.queryByText(/pnpm test/)).toBeNull(); }); diff --git a/app/src/features/conversations/components/__tests__/SubagentDrawer.test.tsx b/app/src/features/conversations/components/__tests__/SubagentDrawer.test.tsx index 0a9ce2dd014..db5327e9614 100644 --- a/app/src/features/conversations/components/__tests__/SubagentDrawer.test.tsx +++ b/app/src/features/conversations/components/__tests__/SubagentDrawer.test.tsx @@ -348,7 +348,7 @@ describe('SubagentDrawer', () => { expect(screen.queryByTestId('assistant-ui-tool-output')).toBeNull(); }); - it('derives a search label from the arguments when the server label degraded to "tool"', () => { + it('keeps a degraded "tool" row readable from its arguments, without guessing a web search', () => { // A provider that hands back a generic `tool` name leaves the row with // nothing better than "Tool" unless the arguments are there to read. Those // arguments only survive a reload because the snapshot now carries them @@ -367,6 +367,9 @@ describe('SubagentDrawer', () => { render( {}} /> ); - expect(screen.getByTestId('assistant-ui-tool-call').textContent).toContain('Searched the web'); + const row = screen.getByTestId('assistant-ui-tool-call'); + expect(row.textContent).toContain('Used tool'); + expect(row.textContent).toContain('openhuman turn state'); + expect(row.textContent).not.toContain('Searched the web'); }); }); diff --git a/app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx b/app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx index 4c68fa44cd1..2c9229442af 100644 --- a/app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx +++ b/app/src/features/conversations/components/__tests__/ToolTimelineBlock.test.tsx @@ -124,10 +124,10 @@ describe('SubagentActivityBlock', () => { expect(calls[0].textContent).toContain('Searched the web'); expect(calls[0].textContent?.toLowerCase()).toContain('done'); expect(calls[0].textContent).toContain('312ms'); - expect(calls[1].textContent).toContain('Composio Execute'); + expect(calls[1].textContent).toContain('Running app action'); expect(calls[1].textContent?.toLowerCase()).toContain('running'); expect(calls[1].textContent).not.toContain('·t2'); - expect(calls[2].textContent).toContain('Reading file'); + expect(calls[2].textContent).toContain('Read file'); expect(calls[2].textContent?.toLowerCase()).toContain('failed'); expect(calls[2].textContent).toContain('50ms'); }); @@ -160,7 +160,9 @@ describe('SubagentActivityBlock', () => { expect(screen.queryByText(/"content"/)).not.toBeInTheDocument(); }); - it('infers a descriptive search label for a degraded subagent tool name', () => { + // A query argument alone no longer makes a call "Searched the web"; that + // heuristic mislabelled memory, tool and email searches. + it('labels a degraded subagent tool name honestly, keeping its query visible', () => { renderInStore( { /> ); - expect(screen.getByTestId('assistant-ui-tool-call')).toHaveTextContent('Searched the web'); + const call = screen.getByTestId('assistant-ui-tool-call'); + expect(call).toHaveTextContent('Used tool'); + expect(call).toHaveTextContent('world news'); + expect(call).not.toHaveTextContent('Searched the web'); }); it('labels cancelled / awaiting-user calls distinctly (not the green "Done" pill)', () => { @@ -204,7 +209,7 @@ describe('SubagentActivityBlock', () => { expect(calls[1].textContent?.toLowerCase()).not.toContain('done'); }); - it('prefers the server-supplied label + contextual detail for a child tool call', () => { + it('names a connected-app action by its app, with the server detail beside the action', () => { renderInStore( { /> ); const row = screen.getByTestId('assistant-ui-tool-call'); - expect(row.textContent).toContain('Reading messages'); - expect(row.textContent).toContain('steven@gmail.com'); + expect(row.textContent).toContain('Used Gmail'); + expect(row.textContent).toContain('Read messages · steven@gmail.com'); // Never the raw snake_case slug. expect(row.textContent).not.toContain('GMAIL_READ_MESSAGES'); }); @@ -420,8 +425,8 @@ describe('ToolTimelineBlock — agentic task insights surface', () => { // Two rows on the timeline rail. expect(screen.getAllByTestId('agent-timeline-row')).toHaveLength(2); // Running row name pulses; done row name is solid. - const running = screen.getByText('Searching: f1'); - const done = screen.getByText('Reading file'); + const running = screen.getByText('Searching the web'); + const done = screen.getByText('Read file'); expect(running.className).toContain('animate-pulse'); expect(done.className).not.toContain('animate-pulse'); }); @@ -440,9 +445,9 @@ describe('ToolTimelineBlock — agentic task insights surface', () => { renderInStore(); const rows = screen.getAllByTestId('agent-timeline-row'); expect(rows).toHaveLength(3); - expect(rows[0].textContent).toContain('Searching the web'); - expect(rows[1].textContent).toContain('Reading file'); - expect(rows[2].textContent).toContain('Run Code'); + expect(rows[0].textContent).toContain('Searched the web'); + expect(rows[1].textContent).toContain('Read file'); + expect(rows[2].textContent).toContain('Ran code'); }); it('renders nothing for an empty timeline', () => { @@ -1454,7 +1459,7 @@ describe('ToolTimelineBlock — sub-agent activity survives the transcript path' expect(calls[0].textContent).toContain('Searched the web'); expect(calls[0].textContent?.toLowerCase()).toContain('done'); // Human label, not the raw `web_fetch` slug. - expect(calls[1].textContent).toContain('Fetching'); + expect(calls[1].textContent).toContain('Reading webpage'); expect(calls[1].textContent?.toLowerCase()).toContain('running'); }); diff --git a/app/src/features/conversations/components/aui/InferenceStatusLine.tsx b/app/src/features/conversations/components/aui/InferenceStatusLine.tsx index 0f1849c1e19..401dc954aba 100644 --- a/app/src/features/conversations/components/aui/InferenceStatusLine.tsx +++ b/app/src/features/conversations/components/aui/InferenceStatusLine.tsx @@ -76,7 +76,8 @@ export function InferenceStatusLine({ round: status.iteration, seq: 0, status: 'running', - } + }, + t ).title }...`} {status.phase === 'subagent' && @@ -88,7 +89,8 @@ export function InferenceStatusLine({ round: status.iteration, seq: 0, status: 'running', - } + }, + t ).title }...`} diff --git a/app/src/features/conversations/derived/mapDisplayItems.test.ts b/app/src/features/conversations/derived/mapDisplayItems.test.ts index 28c4a3eded0..2efeb4f5068 100644 --- a/app/src/features/conversations/derived/mapDisplayItems.test.ts +++ b/app/src/features/conversations/derived/mapDisplayItems.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { DerivedDisplayItem } from '../../../types/derivedTranscript'; +import { formatTimelineEntry } from '../../../utils/toolTimelineFormatting'; import { mapDisplayItems } from './mapDisplayItems'; /** @@ -188,7 +189,7 @@ describe('mapDisplayItems', () => { expect(timelines['req-1'][0].failure?.causePlain).toBe('raw error text'); }); - it('derives displayName/detail for a tool row (parity with turn_state rows)', () => { + it('derives the detail for a tool row but leaves displayName to the server', () => { const chronological: DerivedDisplayItem[] = [ { kind: 'turnBoundary', requestId: 'req-1' }, { @@ -204,8 +205,11 @@ describe('mapDisplayItems', () => { const { timelines } = mapDisplayItems(newestFirst(chronological)); const row = timelines['req-1'][0]; - expect(typeof row.displayName).toBe('string'); - expect(row.displayName?.length ?? 0).toBeGreaterThan(0); + // A baked client title froze its tense ("Running command" on a finished + // row); surfaces resolve the title at render time instead. + expect(row.displayName).toBeUndefined(); + expect(row.detail).toBe('ls -la'); + expect(formatTimelineEntry(row).title).toBe('Ran command'); }); it('anchors a subagent to its own requestId, not the current turn cursor', () => { diff --git a/app/src/features/conversations/derived/mapDisplayItems.ts b/app/src/features/conversations/derived/mapDisplayItems.ts index 5290eed84e9..d415c7e5cad 100644 --- a/app/src/features/conversations/derived/mapDisplayItems.ts +++ b/app/src/features/conversations/derived/mapDisplayItems.ts @@ -459,12 +459,12 @@ function pushToolCall(turn: TurnAccumulator, item: DerivedToolCall): void { // A failed tool renders its "why / next" explanation via `ToolFailureLines`. const failure = toFailureExplanation(item.failure, item.result); if (failure) entry.failure = failure; - // Derive the human label + detail from tool name + args (the same TS - // formatter the live path runs), so settled rows carry `displayName`/`detail` - // at parity with `turn_state` rows instead of being unlabelled. + // Derive the detail from tool name + args (the same registry the live path + // runs). The title is *not* baked into `displayName`: that field carries + // only a server label, and a baked title froze its tense at "Reading file" + // on a finished row. Surfaces resolve the title at render time. const formatted = formatTimelineEntry(entry); - entry.displayName = formatted.title; - if (formatted.detail !== undefined) entry.detail = formatted.detail; + if (entry.detail === undefined && formatted.detail !== undefined) entry.detail = formatted.detail; turn.entries.push(entry); turn.transcript.push({ kind: 'toolCall', round: turn.round, seq, callId }); } diff --git a/app/src/features/conversations/tools/ToolBodies.tsx b/app/src/features/conversations/tools/ToolBodies.tsx new file mode 100644 index 00000000000..7efa2a5274e --- /dev/null +++ b/app/src/features/conversations/tools/ToolBodies.tsx @@ -0,0 +1,267 @@ +/** + * Rich bodies for a tool call, built from assistant-ui's elements + * (`components/assistant-ui/elements/`): the web-search element for + * searches, the terminal block for commands, the web preview for fetched + * pages and the code diff for file edits. + * + * This file only adapts OpenHuman's tool data onto those elements; it adds no + * styling of its own beyond width. Each adapter returns `null` when the call + * left nothing to show, so the caller can fall back to the generic + * Request / Result panel. + */ +import type { ReactNode } from 'react'; + +import { Source } from '../../../components/ai-elements'; +import { CodeDiff, type DiffLine } from '../../../components/assistant-ui/elements/code-diff'; +import { TerminalBlock } from '../../../components/assistant-ui/elements/terminal-block'; +import { WebPreview } from '../../../components/assistant-ui/elements/web-preview'; +import { WebSearch } from '../../../components/assistant-ui/elements/web-search'; +import { BubbleMarkdown } from '../components/AgentMessageBubble'; +import { parseWebSearchResult } from './parseWebSearchResult'; +import { displayUrl, shortenPath, type ToolArgs } from './toolChips'; +import { fillPlaceholders } from './toolPhrases'; +import type { Translate } from './toolPresentation'; + +const FULL_WIDTH = 'max-w-none'; +const MAX_LINES = 400; + +function renderSearchLink({ + href, + className, + children, +}: { + href: string; + className: string; + children: ReactNode; +}) { + // `Source` is the app's one external-link anchor; the global link guard + // routes it to the OS browser. `href` is an http(s) URL vetted by + // `parseWebSearchResult`. + return ( + + {children} + + ); +} + +/** A web search through assistant-ui's web-search element. */ +export function WebSearchBody({ + args, + result, + structured, + searching, + t, +}: { + args: ToolArgs; + result: unknown; + structured?: unknown; + searching: boolean; + t: Translate; +}): ReactNode { + const parsed = searching ? undefined : parseWebSearchResult(result, structured); + if (!searching && !parsed) return null; + const argQuery = + typeof args.query === 'string' + ? args.query + : typeof args.objective === 'string' + ? args.objective + : ''; + const hits = parsed?.results ?? []; + const count = + hits.length === 0 + ? t('conversations.tools.search.none', 'No results') + : fillPlaceholders( + hits.length === 1 + ? t('conversations.tools.search.found.one', 'Found {count} result') + : t('conversations.tools.search.found.other', 'Found {count} results'), + { count: String(hits.length) } + ); + const statusLabel = parsed?.provider + ? `${count} · ${fillPlaceholders(t('conversations.tools.search.via', 'via {provider}'), { + provider: parsed.provider, + })}` + : count; + return ( + ({ title: hit.title, domain: hit.domain, url: hit.url }))} + visibleResults={hits.length} + searching={searching} + cycle={0} + searchingLabel={t('conversations.tools.search.searching', 'Searching')} + statusLabel={statusLabel} + renderLink={renderSearchLink} + /> + ); +} + +function stringOf(value: unknown): string | undefined { + if (typeof value === 'string') return value; + if (value === undefined || value === null) return undefined; + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function linesOf(text: string): string[] { + const lines = text.replace(/\s+$/, '').split('\n'); + return lines.length > MAX_LINES ? [...lines.slice(0, MAX_LINES), '…'] : lines; +} + +/** A command and its output through assistant-ui's terminal block. */ +export function ShellBody({ + args, + result, + failed, + t, +}: { + args: ToolArgs; + result: unknown; + failed: boolean; + t: Translate; +}): ReactNode { + const command = + (typeof args.command === 'string' && args.command) || + (typeof args.subcommand === 'string' && `npm ${args.subcommand}`) || + (typeof args.script_path === 'string' && args.script_path) || + (typeof args.inline_code === 'string' && args.inline_code) || + ''; + const output = stringOf(result) ?? ''; + if (!command && !output.trim()) return null; + const lines = output.trim() ? linesOf(output) : []; + return ( + + ); +} + +/** `status=200 url=https://… content=markdown` header, then the page. */ +function splitFetchOutput(text: string): { status?: string; url?: string; body: string } { + const newline = text.indexOf('\n'); + const head = newline === -1 ? text : text.slice(0, newline); + if (!/^status=\d{3}\b/.test(head)) return { body: text }; + return { + status: head.match(/^status=(\d{3})/)?.[1], + url: head.match(/\burl=(\S+)/)?.[1], + body: newline === -1 ? '' : text.slice(newline + 1), + }; +} + +function isHttpUrl(value: string): boolean { + try { + const { protocol } = new URL(value); + return protocol === 'http:' || protocol === 'https:'; + } catch { + return false; + } +} + +/** A fetched page through assistant-ui's web preview. */ +export function FetchBody({ + args, + result, + t, + onOpenExternal, +}: { + args: ToolArgs; + result: unknown; + t: Translate; + onOpenExternal?: (url: string) => void; +}): ReactNode { + const text = typeof result === 'string' ? result : undefined; + if (!text) return null; + const { status, url, body } = splitFetchOutput(text); + const source = url ?? (typeof args.url === 'string' ? args.url : undefined); + const externalSource = source && isHttpUrl(source) ? source : undefined; + const origin = [status, source ? displayUrl(source) : undefined].filter(Boolean).join(' · '); + return ( + onOpenExternal(externalSource) : undefined} + openExternalLabel={t('conversations.tools.openInBrowser')}> +
    + +
    +
    + ); +} + +/** A file edit, write or read through assistant-ui's code diff. */ +export function FileBody({ args, result }: { args: ToolArgs; result: unknown }): ReactNode { + const path = + typeof args.path === 'string' + ? args.path + : typeof args.file_path === 'string' + ? args.file_path + : ''; + const filename = shortenPath(path); + const oldText = typeof args.old_string === 'string' ? args.old_string : undefined; + const newText = typeof args.new_string === 'string' ? args.new_string : undefined; + if (oldText !== undefined || newText !== undefined) { + const removed = oldText ? linesOf(oldText) : []; + const added = newText ? linesOf(newText) : []; + const lines: DiffLine[] = [ + ...removed.map(text => ({ kind: 'removed' as const, text })), + ...added.map(text => ({ kind: 'added' as const, text })), + ]; + return ( + + ); + } + const written = typeof args.content === 'string' ? args.content : undefined; + if (written !== undefined) { + if (!written.trim()) return null; + const lines = linesOf(written); + return ( + ({ kind: 'added' as const, text }))} + cycle={0} + /> + ); + } + // A read changed nothing, so a diff header ("+0 −0") would mislead: show the + // content as a fenced code block through the chat's markdown renderer. + const content = typeof result === 'string' ? result : ''; + if (!content.trim()) return null; + const language = /\.([a-z0-9]+)$/i.exec(path)?.[1]?.toLowerCase() ?? ''; + const fence = content.includes('```') ? '````' : '```'; + return ( +
    + +
    + ); +} diff --git a/app/src/features/conversations/tools/ToolDataView.tsx b/app/src/features/conversations/tools/ToolDataView.tsx new file mode 100644 index 00000000000..eb5832d3843 --- /dev/null +++ b/app/src/features/conversations/tools/ToolDataView.tsx @@ -0,0 +1,69 @@ +import { BubbleMarkdown } from '../components/AgentMessageBubble'; + +/** + * Generic, readable rendering of a tool's input or output: JSON objects as a + * definition list, arrays as a list, strings as markdown. The fallback body + * for any tool without a dedicated renderer. + */ + +function friendlyLabel(key: string): string { + return key + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + .replace(/^./, char => char.toUpperCase()); +} + +export function parsedValue(value: unknown): unknown { + if (typeof value !== 'string') return value; + const trimmed = value.trim(); + if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return value; + try { + return JSON.parse(trimmed); + } catch { + return value; + } +} + +export function hasDisplayValue(value: unknown): boolean { + if (value === undefined || value === null || value === '') return false; + if (Array.isArray(value)) return value.length > 0; + if (typeof value === 'object') return Object.keys(value as object).length > 0; + return true; +} + +export function ToolDataView({ value }: { value: unknown }) { + const parsed = parsedValue(value); + if (Array.isArray(parsed)) { + return ( +
      + {parsed.map((item, index) => ( +
    • + +
    • + ))} +
    + ); + } + if (parsed && typeof parsed === 'object') { + const entries = Object.entries(parsed); + for (const key of ['content', 'output', 'result', 'message', 'query', 'q']) { + const semantic = entries.find(([candidate]) => candidate === key)?.[1]; + if (hasDisplayValue(semantic)) return ; + } + return ( +
    + {entries.map(([key, item]) => ( +
    +
    {friendlyLabel(key)}
    +
    + +
    +
    + ))} +
    + ); + } + if (typeof parsed === 'boolean') return {parsed ? 'Yes' : 'No'}; + if (typeof parsed === 'string') return ; + return {String(parsed ?? '')}; +} diff --git a/app/src/features/conversations/tools/ToolIcon.tsx b/app/src/features/conversations/tools/ToolIcon.tsx new file mode 100644 index 00000000000..6107ae95ade --- /dev/null +++ b/app/src/features/conversations/tools/ToolIcon.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react'; + +import { cn } from '../../../components/assistant-ui/lib/utils'; +import { composioLogoUrl } from '../../../components/composio/toolkitMeta'; +import type { ToolCallPresentation } from './toolPresentation'; + +/** + * The glyph for a tool call: the connected app's logo for an integration + * action (the same Composio-hosted logo the Skills page already shows), or the + * registry's lucide icon. A logo that fails to load falls back to the icon, so + * an unknown toolkit never renders a broken image. + */ +export function ToolIcon({ + presentation, + className, +}: { + presentation: Pick; + className?: string; +}) { + const [logoFailed, setLogoFailed] = useState(false); + const Icon = presentation.icon; + const integration = presentation.integration; + if (integration?.known && !logoFailed) { + return ( + setLogoFailed(true)} + /> + ); + } + return ; +} diff --git a/app/src/features/conversations/tools/__fixtures__/coreToolNames.json b/app/src/features/conversations/tools/__fixtures__/coreToolNames.json new file mode 100644 index 00000000000..ab8126eec57 --- /dev/null +++ b/app/src/features/conversations/tools/__fixtures__/coreToolNames.json @@ -0,0 +1,193 @@ +[ + "agent_prepare_context", + "apply_patch", + "artifact_delete", + "artifact_get", + "artifact_list", + "ask_user_clarification", + "await_workflow", + "browser", + "browser_open", + "cancel_flow_run", + "close_subagent", + "config_get_autonomy", + "config_get_client_config", + "config_get_data_paths", + "config_get_runtime_flags", + "config_get_search", + "config_resolve_api_url", + "config_snapshot", + "continue_subagent", + "cost_get_daily_history", + "cost_get_dashboard", + "cost_get_summary", + "create_skill", + "create_workflow", + "credential_list", + "cron", + "cron_add", + "cron_list", + "cron_remove", + "cron_run", + "cron_runs", + "cron_update", + "csv_export", + "curl", + "current_time", + "daemon_host_prefs_get", + "daemon_host_prefs_set", + "dashboard_model_health", + "delegate_graph", + "describe_workflow", + "detect_tools", + "doctor_health", + "doctor_models", + "dry_run_workflow", + "duplicate_flow", + "edit", + "edit_workflow", + "file_read", + "file_write", + "flow_memory_recall", + "flow_memory_remember", + "get_flow", + "get_flow_history", + "get_flow_run", + "get_node_kind_contract", + "get_tool_contract", + "get_tool_output_sample", + "git_operations", + "gitbooks_get_page", + "gitbooks_search", + "glob", + "gmail_unsubscribe", + "goal_complete", + "goal_get", + "goal_set", + "goals", + "grep", + "health_snapshot", + "health_system_info", + "http_request", + "image_info", + "install_tool", + "install_workflow_from_url", + "learning_cache_stats", + "learning_enrich_profile", + "learning_forget_facet", + "learning_get_facet", + "learning_list_facets", + "learning_pin_facet", + "learning_rebuild_cache", + "learning_reset_cache", + "learning_save_profile", + "learning_unpin_facet", + "learning_update_facet", + "list", + "list_agent_definitions", + "list_connectable_toolkits", + "list_flow_connections", + "list_flow_runs", + "list_flows", + "list_node_kinds", + "list_subagents", + "list_workflow_runs", + "list_workflows", + "mcp_call_tool", + "mcp_list_servers", + "mcp_list_tools", + "mcp_registry_connect", + "mcp_registry_disconnect", + "mcp_registry_get", + "mcp_registry_installed_list", + "mcp_registry_list_tools", + "mcp_registry_search", + "mcp_registry_status", + "mcp_registry_tool_call", + "mcp_registry_uninstall", + "memory", + "memory_chunk_context", + "memory_doctor", + "memory_flavour", + "memory_forget", + "memory_hybrid_search", + "memory_recall", + "memory_store", + "memory_store_kinds", + "memory_store_raw_chunks", + "memory_store_raw_search", + "memory_tree", + "memory_vector_search", + "oauth_connect_url", + "oauth_list", + "plan_exit", + "propose_workflow", + "proxy_config", + "pushover", + "python_exec", + "read_workflow_resource", + "read_workflow_run_log", + "read_workspace_state", + "remember_preference", + "request_plan_review", + "resolve_time", + "resume_flow_run", + "retrieve_tool_output", + "revise_workflow", + "run_flow", + "run_workflow", + "save_preference", + "save_workflow", + "schedule", + "search_tool_catalog", + "security_policy_info", + "service_install", + "service_restart", + "service_shutdown", + "service_start", + "service_status", + "service_stop", + "service_uninstall", + "session_state", + "shell", + "skill_registry_browse", + "skill_registry_install", + "skill_registry_search", + "skill_registry_sources", + "skill_registry_uninstall", + "skill_runtime_resolve_runtimes", + "skill_search", + "spawn_async_subagent", + "spawn_parallel_agents", + "spawn_subagent", + "steer_subagent", + "suggest_workflows", + "task_source_add", + "task_source_fetch", + "task_source_get", + "task_source_list", + "task_source_list_tasks", + "task_source_preview_filter", + "task_source_remove", + "task_source_status", + "task_source_update", + "tinyjuice_retrieve", + "todo", + "tool_call", + "tool_search", + "uninstall_workflow", + "update_apply", + "update_check", + "update_memory_md", + "use_skill", + "validate_workflow", + "wait", + "wait_loop", + "wait_subagent", + "web_fetch", + "web_search_tool", + "workspace_init", + "workspace_read_persona", + "workspace_reset_persona", + "workspace_update_persona" +] diff --git a/app/src/features/conversations/tools/parseWebSearchResult.test.ts b/app/src/features/conversations/tools/parseWebSearchResult.test.ts new file mode 100644 index 00000000000..f7ce0944ea2 --- /dev/null +++ b/app/src/features/conversations/tools/parseWebSearchResult.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; + +import { extractAgentSources } from '../../../utils/toolTimelineFormatting'; +import { extractSearchProvider, parseWebSearchResult } from './parseWebSearchResult'; + +const TEXT = [ + 'Search results for: rust async traits (via Exa)', + '1. Async fn in traits are now stable', + ' https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html', + ' Published: 2023-12-21', + ' Rust 1.75 stabilizes async fn in traits.', + 'This line wraps from the excerpt.', + '2. javascript link', + ' javascript:alert(1)', + '3. Tokio tutorial', + ' https://tokio.rs/tokio/tutorial', + ' Learn async Rust.', +].join('\n'); + +describe('parseWebSearchResult', () => { + it('parses the plain-text rendering every engine returns', () => { + const parsed = parseWebSearchResult(TEXT); + expect(parsed?.query).toBe('rust async traits'); + expect(parsed?.provider).toBe('Exa'); + expect(parsed?.results.map(r => r.domain)).toEqual(['blog.rust-lang.org', 'tokio.rs']); + expect(parsed?.results[0]).toMatchObject({ + title: 'Async fn in traits are now stable', + published: '2023-12-21', + excerpt: 'Rust 1.75 stabilizes async fn in traits. This line wraps from the excerpt.', + }); + }); + + it('drops non-http(s) urls instead of rendering them as links', () => { + const urls = parseWebSearchResult(TEXT)?.results.map(r => r.url) ?? []; + expect(urls.every(url => url.startsWith('https://'))).toBe(true); + }); + + it('reports an empty search as empty, not unparseable', () => { + expect(parseWebSearchResult('No results found for: zzqx (via Brave)')).toEqual({ + query: 'zzqx', + provider: 'Brave', + results: [], + empty: true, + }); + }); + + it('keeps a "(via …)" inside the query out of the provider', () => { + const parsed = parseWebSearchResult('Search results for: login (via OAuth) (via Exa)'); + expect(parsed?.provider).toBe('Exa'); + expect(parsed?.query).toBe('login (via OAuth)'); + expect(extractSearchProvider('Search results for: login (via OAuth) (via Exa)')).toBe('Exa'); + }); + + it('parses the markdown rendering', () => { + const md = [ + '# Search results — `vite plugins` (via Tavily)', + '', + '## [Vite plugin API](https://vite.dev/guide/api-plugin)', + '_Published: 2025-01-01_', + '', + '> Plugins extend Vite.', + ].join('\n'); + const parsed = parseWebSearchResult(md); + expect(parsed?.provider).toBe('Tavily'); + expect(parsed?.results).toEqual([ + { + title: 'Vite plugin API', + url: 'https://vite.dev/guide/api-plugin', + domain: 'vite.dev', + published: '2025-01-01', + excerpt: 'Plugins extend Vite.', + }, + ]); + }); + + it('prefers the structured payload over the text', () => { + const parsed = parseWebSearchResult('Search results for: ignored (via Exa)', { + kind: 'web_search', + query: 'structured', + provider: 'Parallel', + results: [{ title: 'A', url: 'https://www.a.dev/x', excerpt: 'e' }, { url: 'file:///etc' }], + }); + expect(parsed).toEqual({ + query: 'structured', + provider: 'Parallel', + results: [{ title: 'A', url: 'https://www.a.dev/x', domain: 'a.dev', excerpt: 'e' }], + empty: false, + }); + }); + + it('returns undefined for output it does not recognise', () => { + expect(parseWebSearchResult('some other text')).toBeUndefined(); + expect(parseWebSearchResult(undefined)).toBeUndefined(); + }); +}); + +describe('extractAgentSources', () => { + it('lists the hits of a completed web search as sources', () => { + const sources = extractAgentSources([ + { + id: 's1', + name: 'web_search_tool', + round: 1, + seq: 0, + status: 'success', + argsBuffer: '{"query":"rust async traits"}', + result: TEXT, + }, + { + id: 'f1', + name: 'web_fetch', + round: 1, + seq: 1, + status: 'success', + argsBuffer: '{"url":"https://tokio.rs/tokio/tutorial"}', + }, + ]); + expect(sources.map(s => s.url)).toEqual([ + 'https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html', + 'https://tokio.rs/tokio/tutorial', + ]); + expect(sources[0].title).toBe('Async fn in traits are now stable'); + }); +}); diff --git a/app/src/features/conversations/tools/parseWebSearchResult.ts b/app/src/features/conversations/tools/parseWebSearchResult.ts new file mode 100644 index 00000000000..2644c1e2dcc --- /dev/null +++ b/app/src/features/conversations/tools/parseWebSearchResult.ts @@ -0,0 +1,220 @@ +/** + * Turn a web-search tool result into rows the search element can render. + * + * Three inputs, most trustworthy first: + * + * 1. The structured payload a current core attaches to `tool_result` + * (`{ kind: "web_search", query, provider, results: [...] }`). + * 2. The plain-text rendering every engine returns to the model: + * + * Search results for: (via ) + * 1. + * <url> + * Published: <date> + * <excerpt> + * + * 3. The markdown rendering (`## [title](url)` / `> excerpt`) used when the + * core prefers markdown. + * + * Every URL is model- or provider-supplied, so only well-formed `http(s)` + * URLs are admitted; anything else is dropped rather than rendered as a + * link. + */ +/** Upper bound on a provider label, so a malformed marker can't blow up a row. */ +const MAX_SEARCH_PROVIDER_LENGTH = 32; + +/** + * Extract the resolved search provider from a completed web-search result. + * Every search engine tags its output with a `(via <Provider>)` marker on the + * heading line (managed resolves to "Exa" by default, or to whatever the + * backend reports; BYOK engines tag "Brave"/"Querit"/"Seltz"/"Tavily"). Reading it back + * keeps the attribution dynamic: it is driven by what actually ran, never by + * a hardcoded provider name (#5136). + * + * Only the first line is inspected, and only its *trailing* marker, so neither + * a `(via …)` string inside a result excerpt nor one inside the echoed query + * (`Search results for: login (via OAuth) (via Exa)`) can be mistaken for the + * provider. Returns `undefined` while the call is still running (no result + * yet) or if no marker is present. + */ +export function extractSearchProvider(result: string | undefined): string | undefined { + if (!result) return undefined; + const headingLine = result.split('\n', 1)[0]; + const provider = headingLine?.match(/\(via ([^)]+)\)\s*_?$/i)?.[1]?.trim(); + if (!provider || provider.length > MAX_SEARCH_PROVIDER_LENGTH) return undefined; + return provider; +} + +export interface WebSearchHit { + title: string; + url: string; + domain: string; + published?: string; + excerpt?: string; +} + +export interface ParsedWebSearch { + query?: string; + provider?: string; + results: WebSearchHit[]; + /** The call completed and found nothing (distinct from "not parseable"). */ + empty: boolean; +} + +const MAX_EXCERPT = 280; + +function safeHttpUrl(value: string): URL | undefined { + try { + const url = new URL(value.trim()); + return url.protocol === 'http:' || url.protocol === 'https:' ? url : undefined; + } catch { + return undefined; + } +} + +function clip(text: string | undefined, max = MAX_EXCERPT): string | undefined { + const cleaned = text?.replace(/\s+/g, ' ').trim(); + if (!cleaned) return undefined; + return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned; +} + +function hit( + title: string | undefined, + rawUrl: string | undefined, + published?: string, + excerpt?: string +): WebSearchHit | undefined { + const url = rawUrl ? safeHttpUrl(rawUrl) : undefined; + if (!url) return undefined; + const domain = url.hostname.replace(/^www\./, ''); + return { + title: clip(title, 160) ?? domain, + url: url.toString(), + domain, + ...(clip(published, 40) ? { published: clip(published, 40) } : {}), + ...(clip(excerpt) ? { excerpt: clip(excerpt) } : {}), + }; +} + +function fromStructured(value: unknown): ParsedWebSearch | undefined { + if (!value || typeof value !== 'object') return undefined; + const payload = value as Record<string, unknown>; + if (payload.kind !== 'web_search' || !Array.isArray(payload.results)) return undefined; + const results = payload.results + .map(item => { + if (!item || typeof item !== 'object') return undefined; + const row = item as Record<string, unknown>; + const str = (key: string) => + typeof row[key] === 'string' ? (row[key] as string) : undefined; + return hit(str('title'), str('url'), str('published'), str('excerpt')); + }) + .filter((row): row is WebSearchHit => row !== undefined); + return { + query: typeof payload.query === 'string' ? payload.query : undefined, + provider: typeof payload.provider === 'string' ? payload.provider : undefined, + results, + empty: results.length === 0, + }; +} + +/** Strip the trailing `(via X)` marker from a heading's query part. */ +function headingQuery(heading: string): string | undefined { + const query = heading.replace(/\s*\(via [^)]+\)\s*$/i, '').trim(); + return query.replace(/^`|`$/g, '').trim() || undefined; +} + +function fromText(text: string): ParsedWebSearch | undefined { + const lines = text.split('\n'); + const heading = lines[0]?.trim() ?? ''; + const provider = extractSearchProvider(heading); + + const emptyMatch = heading.match(/^_?No (?:\w+ )?results (?:found )?for:?\s*(.+?)_?$/i); + if (emptyMatch) { + return { + query: headingQuery(emptyMatch[1].replace(/_$/, '').replace(/[._]+$/, '')), + provider, + results: [], + empty: true, + }; + } + + // Markdown rendering. + const mdHeading = heading.match(/^#\s+\w+ results\s*(?:--|:|—)\s*(.+)$/i); + if (mdHeading || lines.some(line => /^##\s+\[.+\]\(.+\)\s*$/.test(line))) { + const results: WebSearchHit[] = []; + let current: { title: string; url: string; published?: string; excerpt: string[] } | null = + null; + const flush = () => { + if (!current) return; + const row = hit(current.title, current.url, current.published, current.excerpt.join(' ')); + if (row) results.push(row); + current = null; + }; + for (const line of lines.slice(1)) { + const link = line.match(/^##\s+\[(.+)\]\((\S+)\)\s*$/); + if (link) { + flush(); + current = { title: link[1], url: link[2], excerpt: [] }; + continue; + } + if (!current) continue; + const published = line.match(/^_Published:\s*(.+?)_\s*$/); + if (published) current.published = published[1]; + else if (line.startsWith('>')) current.excerpt.push(line.replace(/^>\s?/, '')); + } + flush(); + return { + query: mdHeading ? headingQuery(mdHeading[1]) : undefined, + provider, + results, + empty: results.length === 0, + }; + } + + // Plain-text rendering. + const textHeading = heading.match(/^(?:Search|\w+) results for:\s*(.+)$/i); + if (!textHeading) return undefined; + // An item is a numbered title line followed by its indented URL line. An + // excerpt's continuation lines are not indented, so that shape is what + // separates the next item from a wrapped excerpt. + const isItemStart = (index: number) => + /^\s*\d+\.\s+\S/.test(lines[index] ?? '') && /^\s{2,}\S/.test(lines[index + 1] ?? ''); + const results: WebSearchHit[] = []; + let i = 1; + while (i < lines.length) { + if (!isItemStart(i)) { + i += 1; + continue; + } + const title = lines[i].replace(/^\s*\d+\.\s+/, ''); + const urlLine = lines[i + 1].trim(); + let published: string | undefined; + const excerpt: string[] = []; + let j = i + 2; + for (; j < lines.length && !isItemStart(j); j += 1) { + const trimmed = lines[j].trim(); + const date = trimmed.match(/^Published:\s*(.+)$/); + if (date) published = date[1]; + else if (!/^Author:/.test(trimmed) && trimmed) excerpt.push(trimmed); + } + const row = hit(title, urlLine, published, excerpt.join(' ')); + if (row) results.push(row); + i = j; + } + return { query: headingQuery(textHeading[1]), provider, results, empty: results.length === 0 }; +} + +/** + * Parse a web-search result. `structured` wins when present; otherwise the + * text `output` is parsed. Returns `undefined` when neither is recognisable, + * so the caller can fall back to the generic output view. + */ +export function parseWebSearchResult( + output: unknown, + structured?: unknown +): ParsedWebSearch | undefined { + const fromPayload = fromStructured(structured) ?? fromStructured(output); + if (fromPayload) return fromPayload; + if (typeof output !== 'string' || !output.trim()) return undefined; + return fromText(output.trim()); +} diff --git a/app/src/features/conversations/tools/toolChips.ts b/app/src/features/conversations/tools/toolChips.ts new file mode 100644 index 00000000000..3c75d659538 --- /dev/null +++ b/app/src/features/conversations/tools/toolChips.ts @@ -0,0 +1,137 @@ +/** + * Chip extractors: the short target shown beside a tool's label ("Read file + * `…/src/main.ts`", "Searched the web `rust async traits`"). + * + * Every value here comes from a model-emitted argument, so it is treated as + * untrusted display text: trimmed, whitespace-collapsed and length-capped. + * Nothing in this file produces a link. + */ + +export type ToolArgs = Record<string, unknown>; +export type ChipRule = (args: ToolArgs) => string | undefined; + +const MAX_CHIP_LENGTH = 80; + +export function truncateChip(value: string, max = MAX_CHIP_LENGTH): string { + const cleaned = value.trim().replace(/\s+/g, ' '); + if (cleaned.length <= max) return cleaned; + return `${cleaned.slice(0, max - 1)}…`; +} + +function stringArg(args: ToolArgs, key: string): string | undefined { + const value = args[key]; + if (typeof value === 'string' && value.trim()) return value; + if (typeof value === 'number' && Number.isFinite(value)) return String(value); + return undefined; +} + +/** First non-empty string among `keys`. */ +export function firstArg(args: ToolArgs, ...keys: string[]): string | undefined { + for (const key of keys) { + const value = stringArg(args, key); + if (value) return value; + } + return undefined; +} + +/** `/a/b/c/d.ts` → `…/c/d.ts`; short paths pass through. */ +export function shortenPath(filePath: string): string { + const parts = filePath.split('/'); + if (parts.length <= 3) return filePath; + return `…/${parts.slice(-2).join('/')}`; +} + +/** `https://docs.rs/tokio/latest/x` → `docs.rs/tokio/latest/x`, capped. */ +export function displayUrl(url: string): string { + try { + const parsed = new URL(url); + const path = parsed.pathname === '/' ? '' : parsed.pathname; + return truncateChip(`${parsed.hostname}${path}`); + } catch { + return truncateChip(url); + } +} + +export function hostnameOf(url: string): string | undefined { + try { + return new URL(url).hostname || undefined; + } catch { + return undefined; + } +} + +/** Rule factories, so the spec tables stay declarative. */ +export const chip = { + text: + (...keys: string[]): ChipRule => + args => { + const value = firstArg(args, ...keys); + return value ? truncateChip(value) : undefined; + }, + path: + (...keys: string[]): ChipRule => + args => { + const value = firstArg(args, ...(keys.length ? keys : ['path', 'file_path'])); + return value ? truncateChip(shortenPath(value.trim())) : undefined; + }, + url: + (...keys: string[]): ChipRule => + args => { + const value = firstArg(args, ...(keys.length ? keys : ['url', 'uri'])); + if (value) return displayUrl(value.trim()); + const list = args.urls; + if (Array.isArray(list) && typeof list[0] === 'string') { + const first = displayUrl(list[0]); + return list.length > 1 ? `${first} +${list.length - 1}` : first; + } + return undefined; + }, + query: (): ChipRule => args => { + const value = firstArg(args, 'query', 'q', 'search_query', 'objective'); + if (value) return truncateChip(value); + const queries = args.search_queries; + if (Array.isArray(queries) && typeof queries[0] === 'string') return truncateChip(queries[0]); + return undefined; + }, + command: + (...keys: string[]): ChipRule => + args => { + const value = firstArg(args, ...(keys.length ? keys : ['command'])); + return value ? truncateChip(value, 120) : undefined; + }, + /** First path among a multi-edit payload (`apply_patch { edits: [{ path }] }`). */ + editsPath: (): ChipRule => args => { + const edits = args.edits; + if (!Array.isArray(edits) || edits.length === 0) return firstArg(args, 'path'); + const first = edits[0] as ToolArgs | undefined; + const path = first && typeof first.path === 'string' ? shortenPath(first.path) : undefined; + if (!path) return undefined; + return edits.length > 1 ? `${path} +${edits.length - 1}` : path; + }, +}; + +/** + * Generic chip for a tool the tables do not describe: the same key order the + * core's `context_detail_from_args` walks, so an unknown tool still shows its + * obvious target. + */ +export const genericChip: ChipRule = args => { + const value = firstArg( + args, + 'to', + 'recipient', + 'email', + 'query', + 'q', + 'url', + 'file_path', + 'path', + 'command', + 'subject', + 'title', + 'channel', + 'repo', + 'name' + ); + return value ? truncateChip(value) : undefined; +}; diff --git a/app/src/features/conversations/tools/toolPhrases.test.ts b/app/src/features/conversations/tools/toolPhrases.test.ts new file mode 100644 index 00000000000..f3e9f00d220 --- /dev/null +++ b/app/src/features/conversations/tools/toolPhrases.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; + +import en from '../../../lib/i18n/en'; +import { phraseKey, TOOL_PHRASES, type ToolPhraseId } from './toolPhrases'; + +const enMap = en as Record<string, string>; +const placeholders = (value: string) => [...value.matchAll(/\{(\w+)\}/g)].map(m => m[1]).sort(); + +describe('tool phrases', () => { + it.each(Object.keys(TOOL_PHRASES) as ToolPhraseId[])( + '%s is served by en.ts with the same English', + id => { + for (const tense of ['active', 'done'] as const) { + expect(enMap[phraseKey(id, tense)]).toBe(TOOL_PHRASES[id][tense]); + } + } + ); + + it.each(Object.keys(TOOL_PHRASES) as ToolPhraseId[])( + '%s reads differently once done, with the same placeholders', + id => { + const { active, done } = TOOL_PHRASES[id]; + expect(active).not.toBe(done); + expect(placeholders(active)).toEqual(placeholders(done)); + } + ); +}); diff --git a/app/src/features/conversations/tools/toolPhrases.ts b/app/src/features/conversations/tools/toolPhrases.ts new file mode 100644 index 00000000000..5a6f2a62a80 --- /dev/null +++ b/app/src/features/conversations/tools/toolPhrases.ts @@ -0,0 +1,235 @@ +/** + * The vocabulary a tool call is described with. + * + * Every phrase has two tenses: `active` while the call runs ("Reading file") + * and `done` once it settled ("Read file"). A finished row used to keep the + * progressive form next to a check mark, so it read as still running. + * + * Phrases are shared between tools on purpose: the managed search and every + * bring-your-own-key engine all read "Searching the web", which keeps the + * translation surface to one entry per *meaning* instead of one per tool. + * + * The English here is the source; each phrase is served through the i18n + * keys `conversations.tools.<id>.active` / `.done` (see {@link phraseKey}), + * so this table and `lib/i18n/en.ts` must agree. `toolPhrases.test.ts` + * enforces that. + * + * Placeholders (`{app}`, `{tool}`) are filled by the caller and must survive + * translation unchanged. + */ +export const TOOL_PHRASES = { + // ── Files and code ────────────────────────────────────────────────────── + readFile: { active: 'Reading file', done: 'Read file' }, + writeFile: { active: 'Writing file', done: 'Wrote file' }, + editFile: { active: 'Editing file', done: 'Edited file' }, + applyEdits: { active: 'Applying edits', done: 'Applied edits' }, + searchCode: { active: 'Searching code', done: 'Searched code' }, + findFiles: { active: 'Finding files', done: 'Found files' }, + listFolder: { active: 'Listing folder', done: 'Listed folder' }, + exportCsv: { active: 'Exporting CSV', done: 'Exported CSV' }, + updateMemoryNotes: { active: 'Updating memory notes', done: 'Updated memory notes' }, + runGit: { active: 'Running git', done: 'Ran git' }, + readChanges: { active: 'Reading changes', done: 'Read changes' }, + runLinter: { active: 'Running linter', done: 'Ran linter' }, + runTests: { active: 'Running tests', done: 'Ran tests' }, + analyzeCode: { active: 'Analyzing code', done: 'Analyzed code' }, + insertRecord: { active: 'Inserting record', done: 'Inserted record' }, + + // ── Shell and system ──────────────────────────────────────────────────── + runCommand: { active: 'Running command', done: 'Ran command' }, + runCode: { active: 'Running code', done: 'Ran code' }, + runPackageManager: { active: 'Running npm', done: 'Ran npm' }, + checkInstalledTools: { active: 'Checking installed tools', done: 'Checked installed tools' }, + installTool: { active: 'Installing tool', done: 'Installed tool' }, + checkTime: { active: 'Checking the time', done: 'Checked the time' }, + resolveDate: { active: 'Working out the date', done: 'Worked out the date' }, + retrieveOutput: { active: 'Retrieving full output', done: 'Retrieved full output' }, + reviewWorkspace: { active: 'Reviewing workspace', done: 'Reviewed workspace' }, + configureProxy: { active: 'Configuring proxy', done: 'Configured proxy' }, + checkUpdates: { active: 'Checking for updates', done: 'Checked for updates' }, + installUpdate: { active: 'Installing update', done: 'Installed update' }, + sendNotification: { active: 'Sending notification', done: 'Sent notification' }, + reviewToolUsage: { active: 'Reviewing tool usage', done: 'Reviewed tool usage' }, + typeKeys: { active: 'Typing', done: 'Typed' }, + click: { active: 'Clicking', done: 'Clicked' }, + + // ── Web ───────────────────────────────────────────────────────────────── + searchWeb: { active: 'Searching the web', done: 'Searched the web' }, + searchNews: { active: 'Searching news', done: 'Searched news' }, + searchImages: { active: 'Searching images', done: 'Searched images' }, + searchVideos: { active: 'Searching videos', done: 'Searched videos' }, + findSimilarPages: { active: 'Finding similar pages', done: 'Found similar pages' }, + readPages: { active: 'Reading pages', done: 'Read pages' }, + readWebpage: { active: 'Reading webpage', done: 'Read webpage' }, + research: { active: 'Researching', done: 'Researched' }, + enrichData: { active: 'Enriching data', done: 'Enriched data' }, + buildDataset: { active: 'Building dataset', done: 'Built dataset' }, + askTheWeb: { active: 'Asking the web', done: 'Asked the web' }, + browseForYou: { active: 'Browsing for you', done: 'Browsed for you' }, + callApi: { active: 'Calling API', done: 'Called API' }, + downloadFile: { active: 'Downloading file', done: 'Downloaded file' }, + makePaidRequest: { active: 'Making paid request', done: 'Made paid request' }, + searchDocs: { active: 'Searching docs', done: 'Searched docs' }, + readDocs: { active: 'Reading docs', done: 'Read docs' }, + + // ── Browser ───────────────────────────────────────────────────────────── + useBrowser: { active: 'Using browser', done: 'Used browser' }, + openPage: { active: 'Opening page', done: 'Opened page' }, + navigate: { active: 'Navigating', done: 'Navigated' }, + takeScreenshot: { active: 'Taking screenshot', done: 'Took screenshot' }, + scrollPage: { active: 'Scrolling', done: 'Scrolled' }, + readPage: { active: 'Reading page', done: 'Read page' }, + + // ── Media and documents ──────────────────────────────────────────────── + analyzeImage: { active: 'Analyzing image', done: 'Analyzed image' }, + generateImage: { active: 'Generating image', done: 'Generated image' }, + generateVideo: { active: 'Generating video', done: 'Generated video' }, + checkMediaModels: { active: 'Checking media models', done: 'Checked media models' }, + createDocument: { active: 'Creating document', done: 'Created document' }, + createPresentation: { active: 'Creating presentation', done: 'Created presentation' }, + generatePodcast: { active: 'Generating podcast', done: 'Generated podcast' }, + emailPodcast: { active: 'Emailing podcast', done: 'Emailed podcast' }, + createAndEmailPodcast: { + active: 'Creating and emailing podcast', + done: 'Created and emailed podcast', + }, + + // ── Memory ────────────────────────────────────────────────────────────── + recallMemories: { active: 'Recalling memories', done: 'Recalled memories' }, + saveToMemory: { active: 'Saving to memory', done: 'Saved to memory' }, + forgetMemory: { active: 'Forgetting memory', done: 'Forgot memory' }, + searchMemory: { active: 'Searching memory', done: 'Searched memory' }, + inspectMemory: { active: 'Inspecting memory', done: 'Inspected memory' }, + exploreMemory: { active: 'Exploring memory', done: 'Explored memory' }, + saveDocumentToMemory: { active: 'Saving document to memory', done: 'Saved document to memory' }, + updateGoals: { active: 'Updating goals', done: 'Updated goals' }, + reviewGoals: { active: 'Reviewing goals', done: 'Reviewed goals' }, + savePreference: { active: 'Saving preference', done: 'Saved preference' }, + reviewLearnings: { active: 'Reviewing what I learned', done: 'Reviewed what I learned' }, + updateLearnings: { active: 'Updating what I learned', done: 'Updated what I learned' }, + + // ── Agents and delegation ────────────────────────────────────────────── + delegateTask: { active: 'Delegating task', done: 'Delegated task' }, + runAgentsInParallel: { active: 'Running agents in parallel', done: 'Ran agents in parallel' }, + messageAgent: { active: 'Messaging agent', done: 'Messaged agent' }, + waitForAgent: { active: 'Waiting for agent', done: 'Waited for agent' }, + wait: { active: 'Waiting', done: 'Waited' }, + closeAgent: { active: 'Closing agent', done: 'Closed agent' }, + checkAgents: { active: 'Checking agents', done: 'Checked agents' }, + askQuestion: { active: 'Asking you a question', done: 'Asked you a question' }, + prepareContext: { active: 'Preparing context', done: 'Prepared context' }, + extractDetails: { active: 'Extracting details', done: 'Extracted details' }, + planNextSteps: { active: 'Planning next steps', done: 'Planned next steps' }, + reviewWork: { active: 'Reviewing the work', done: 'Reviewed the work' }, + scoutContext: { active: 'Scouting context', done: 'Scouted context' }, + useTools: { active: 'Using tools', done: 'Used tools' }, + checkConnectedApp: { active: 'Checking your connected app', done: 'Checked your connected app' }, + + // ── Planning ──────────────────────────────────────────────────────────── + updateTodos: { active: 'Updating to-do list', done: 'Updated to-do list' }, + requestPlanReview: { active: 'Requesting plan review', done: 'Requested plan review' }, + finishPlan: { active: 'Finishing plan', done: 'Finished plan' }, + setGoal: { active: 'Setting goal', done: 'Set goal' }, + checkGoal: { active: 'Checking goal', done: 'Checked goal' }, + completeGoal: { active: 'Completing goal', done: 'Completed goal' }, + + // ── Scheduling ────────────────────────────────────────────────────────── + scheduleTask: { active: 'Scheduling task', done: 'Scheduled task' }, + checkSchedules: { active: 'Checking schedules', done: 'Checked schedules' }, + updateSchedule: { active: 'Updating scheduled task', done: 'Updated scheduled task' }, + removeSchedule: { active: 'Removing scheduled task', done: 'Removed scheduled task' }, + runScheduledTask: { active: 'Running scheduled task', done: 'Ran scheduled task' }, + checkRunHistory: { active: 'Checking run history', done: 'Checked run history' }, + + // ── Connected apps ───────────────────────────────────────────────────── + useApp: { active: 'Using {app}', done: 'Used {app}' }, + checkAvailableApps: { active: 'Checking available apps', done: 'Checked available apps' }, + checkConnections: { active: 'Checking your connections', done: 'Checked your connections' }, + connectApp: { active: 'Connecting app', done: 'Connected app' }, + authorizeApp: { active: 'Authorizing app', done: 'Authorized app' }, + findAppActions: { active: 'Finding app actions', done: 'Found app actions' }, + runAppAction: { active: 'Running app action', done: 'Ran app action' }, + findTools: { active: 'Finding tools', done: 'Found tools' }, + useTool: { active: 'Using {tool}', done: 'Used {tool}' }, + unsubscribe: { active: 'Unsubscribing', done: 'Unsubscribed' }, + searchPlaces: { active: 'Searching places', done: 'Searched places' }, + lookUpPlace: { active: 'Looking up place', done: 'Looked up place' }, + checkMarkets: { active: 'Checking markets', done: 'Checked markets' }, + placeCall: { active: 'Placing call', done: 'Placed call' }, + checkTaskSources: { active: 'Checking task sources', done: 'Checked task sources' }, + updateTaskSources: { active: 'Updating task sources', done: 'Updated task sources' }, + fetchTasks: { active: 'Fetching tasks', done: 'Fetched tasks' }, + + // ── MCP ───────────────────────────────────────────────────────────────── + checkMcpServers: { active: 'Checking MCP servers', done: 'Checked MCP servers' }, + checkMcpTools: { active: 'Checking MCP tools', done: 'Checked MCP tools' }, + callMcpTool: { active: 'Calling {tool}', done: 'Called {tool}' }, + searchMcpServers: { active: 'Searching MCP servers', done: 'Searched MCP servers' }, + connectMcpServer: { active: 'Connecting MCP server', done: 'Connected MCP server' }, + disconnectMcpServer: { active: 'Disconnecting MCP server', done: 'Disconnected MCP server' }, + removeMcpServer: { active: 'Removing MCP server', done: 'Removed MCP server' }, + + // ── Storage and hosting ──────────────────────────────────────────────── + uploadFile: { active: 'Uploading file', done: 'Uploaded file' }, + listStoredFiles: { active: 'Listing stored files', done: 'Listed stored files' }, + createShareLink: { active: 'Creating share link', done: 'Created share link' }, + deleteFile: { active: 'Deleting file', done: 'Deleted file' }, + updateFileAccess: { active: 'Updating file access', done: 'Updated file access' }, + deploySite: { active: 'Deploying site', done: 'Deployed site' }, + checkHosting: { active: 'Checking hosting', done: 'Checked hosting' }, + updateHosting: { active: 'Updating hosting', done: 'Updated hosting' }, + rollBackDeployment: { active: 'Rolling back deployment', done: 'Rolled back deployment' }, + + // ── Wallet ────────────────────────────────────────────────────────────── + checkWallet: { active: 'Checking wallet', done: 'Checked wallet' }, + prepareTransfer: { active: 'Preparing transfer', done: 'Prepared transfer' }, + checkTransaction: { active: 'Checking transaction', done: 'Checked transaction' }, + getSwapQuote: { active: 'Getting swap quote', done: 'Got swap quote' }, + swapTokens: { active: 'Swapping tokens', done: 'Swapped tokens' }, + getBridgeQuote: { active: 'Getting bridge quote', done: 'Got bridge quote' }, + bridgeTokens: { active: 'Bridging tokens', done: 'Bridged tokens' }, + callDapp: { active: 'Calling app contract', done: 'Called app contract' }, + + // ── Skills and workflows ─────────────────────────────────────────────── + useSkill: { active: 'Using skill', done: 'Used skill' }, + searchSkills: { active: 'Searching skills', done: 'Searched skills' }, + checkSkills: { active: 'Checking skills', done: 'Checked skills' }, + installSkill: { active: 'Installing skill', done: 'Installed skill' }, + removeSkill: { active: 'Removing skill', done: 'Removed skill' }, + createSkill: { active: 'Creating skill', done: 'Created skill' }, + runWorkflow: { active: 'Running workflow', done: 'Ran workflow' }, + waitForWorkflow: { active: 'Waiting for workflow', done: 'Waited for workflow' }, + designWorkflow: { active: 'Designing workflow', done: 'Designed workflow' }, + saveWorkflow: { active: 'Saving workflow', done: 'Saved workflow' }, + validateWorkflow: { active: 'Validating workflow', done: 'Validated workflow' }, + testWorkflow: { active: 'Testing workflow', done: 'Tested workflow' }, + checkWorkflows: { active: 'Checking workflows', done: 'Checked workflows' }, + cancelWorkflow: { active: 'Cancelling workflow run', done: 'Cancelled workflow run' }, + suggestWorkflows: { active: 'Suggesting workflows', done: 'Suggested workflows' }, + + // ── Settings and platform ────────────────────────────────────────────── + checkSettings: { active: 'Checking settings', done: 'Checked settings' }, + checkSecurity: { active: 'Checking security', done: 'Checked security' }, + runDiagnostics: { active: 'Running diagnostics', done: 'Ran diagnostics' }, + checkUsageCosts: { active: 'Checking usage costs', done: 'Checked usage costs' }, + manageService: { active: 'Managing background service', done: 'Managed background service' }, + readPersona: { active: 'Reading persona', done: 'Read persona' }, + updatePersona: { active: 'Updating persona', done: 'Updated persona' }, + setUpWorkspace: { active: 'Setting up workspace', done: 'Set up workspace' }, + checkArtifacts: { active: 'Checking artifacts', done: 'Checked artifacts' }, + deleteArtifact: { active: 'Deleting artifact', done: 'Deleted artifact' }, +} as const satisfies Record<string, { active: string; done: string }>; + +export type ToolPhraseId = keyof typeof TOOL_PHRASES; +export type ToolPhraseTense = 'active' | 'done'; + +/** The i18n key a phrase is served under. */ +export function phraseKey(id: ToolPhraseId, tense: ToolPhraseTense): string { + return `conversations.tools.${id}.${tense}`; +} + +/** Substitute `{name}` placeholders. Unknown placeholders are left in place. */ +export function fillPlaceholders(template: string, params?: Record<string, string>): string { + if (!params) return template; + return template.replace(/\{(\w+)\}/g, (match, name: string) => params[name] ?? match); +} diff --git a/app/src/features/conversations/tools/toolPresentation.catalog.test.ts b/app/src/features/conversations/tools/toolPresentation.catalog.test.ts new file mode 100644 index 00000000000..743e2e5405d --- /dev/null +++ b/app/src/features/conversations/tools/toolPresentation.catalog.test.ts @@ -0,0 +1,48 @@ +/** + * The "never again" guard for tool labels. + * + * `__fixtures__/coreToolNames.json` is every tool name the core registers. It + * is written and checked by the Rust test next to the core's tool registry + * (`UPDATE_TOOL_CATALOG=1` regenerates it), so a tool added to the core + * without updating the fixture fails there, and a fixture name the registry + * here cannot describe fails here. Between them a new core tool cannot reach + * the chat as a raw identifier. + */ +import { describe, expect, it } from 'vitest'; + +import coreToolNames from './__fixtures__/coreToolNames.json'; +import { describeToolCall, type ToolCallStatus, toolLabel } from './toolPresentation'; + +const NAMES = [...(coreToolNames as string[])].sort(); +const STATUSES: ToolCallStatus[] = ['running', 'success', 'error']; +/** Brand and protocol words allowed to stay upper-case inside a label. */ +const ALLOWED_CAPS = new Set(['MCP', 'CSV', 'API']); + +describe('core tool catalog', () => { + it('is not empty', () => { + expect(NAMES.length).toBeGreaterThan(100); + }); + + it.each(NAMES)('%s is described by the registry, not the generic fallback', name => { + const presentation = describeToolCall({ name }); + expect(presentation.source).not.toBe('fallback'); + expect(presentation.source).not.toBe('server'); + expect(presentation.icon).toBeTruthy(); + }); + + it.each(NAMES)('%s reads as a human label in every state', name => { + const labels = STATUSES.map(status => toolLabel(describeToolCall({ name, status }))); + for (const label of labels) { + expect(label.trim().length).toBeGreaterThan(0); + expect(label).not.toBe(name); + expect(label).not.toContain('_'); + expect(label).not.toMatch(/^Using [A-Z]\w*ing\b/); + const shouting = label + .split(/\s+/) + .filter(word => /^[A-Z]{2,}$/.test(word) && !ALLOWED_CAPS.has(word)); + expect(shouting).toEqual([]); + } + // The tense changes as the call settles. + expect(labels[0]).not.toBe(labels[1]); + }); +}); diff --git a/app/src/features/conversations/tools/toolPresentation.test.ts b/app/src/features/conversations/tools/toolPresentation.test.ts new file mode 100644 index 00000000000..e04e88a5d77 --- /dev/null +++ b/app/src/features/conversations/tools/toolPresentation.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, it } from 'vitest'; + +import { + describeToolCall, + type DescribeToolCallInput, + sentenceCase, + toolLabel, +} from './toolPresentation'; + +const label = (input: DescribeToolCallInput) => toolLabel(describeToolCall(input)); +const done = (name: string, args?: unknown, extra: Partial<DescribeToolCallInput> = {}) => + label({ name, args, status: 'success', ...extra }); +const active = (name: string, args?: unknown, extra: Partial<DescribeToolCallInput> = {}) => + label({ name, args, status: 'running', ...extra }); + +/** + * One test per mislabelling that shipped. Each names the bug it pins so a + * regression reads as that bug coming back, not as a changed string. + */ +describe('tool labels: regressions', () => { + it('does not call non-web searches "Searched the web" (tool_search, memory, skills)', () => { + for (const name of [ + 'tool_search', + 'memory_hybrid_search', + 'memory_vector_search', + 'skill_registry_search', + 'mcp_registry_search', + 'search_tool_catalog', + 'gitbooks_search', + ]) { + expect(done(name, { query: 'x' }), name).not.toMatch(/web/i); + } + }); + + it('does not call a Composio fetch with a query argument a web search', () => { + expect(done('GMAIL_FETCH_EMAILS', { query: 'from:boss' })).toBe('Used Gmail'); + expect(describeToolCall({ name: 'GMAIL_FETCH_EMAILS' }).chip).toBe('Fetch emails'); + }); + + it('does not call any tool with a url argument "Fetched from the web"', () => { + expect(done('storage_get_link', { url: 'https://x.dev' })).toBe('Created share link'); + expect(done('gitbooks_get_page', { url: 'https://docs.x' })).toBe('Read docs'); + expect(done('install_workflow_from_url', { url: 'https://x' })).toBe('Installed skill'); + }); + + it('labels the docs search as a docs search', () => { + expect(active('gitbooks_search', { query: 'install' })).toBe('Searching docs'); + }); + + it('never shouts a Composio action slug', () => { + expect(done('GMAIL_SEND_EMAIL')).toBe('Used Gmail'); + expect(describeToolCall({ name: 'GMAIL_SEND_EMAIL' }).chip).toBe('Send email'); + expect(done('OUTLOOK_SEND_EMAIL')).toBe('Used Outlook'); + expect(done('GOOGLECALENDAR_CREATE_EVENT')).toBe('Used Google Calendar'); + expect(describeToolCall({ name: 'GOOGLECALENDAR_CREATE_EVENT' }).chip).toBe('Create event'); + // An unknown toolkit still reads as words, not a slug. + expect(done('ACMECORP_SYNC_ALL_RECORDS')).toBe('Used Acmecorp'); + expect(describeToolCall({ name: 'ACMECORP_SYNC_ALL_RECORDS' }).chip).toBe('Sync all records'); + }); + + it('names the MCP tool and server instead of "Calling MCP tool"', () => { + const p = describeToolCall({ + name: 'mcp_call_tool', + args: { server: 'linear', tool: 'create_issue' }, + status: 'success', + }); + expect(toolLabel(p)).toBe('Called create_issue'); + expect(p.chip).toBe('linear'); + expect(active('mcp_registry_tool_call', { server_id: 'gh', tool_name: 'list_prs' })).toBe( + 'Calling list_prs' + ); + }); + + it('uses the past tense once a call settles', () => { + expect(active('file_read', { path: 'a.ts' })).toBe('Reading file'); + expect(done('file_read', { path: 'a.ts' })).toBe('Read file'); + expect(label({ name: 'file_read', status: 'error' })).toBe('Read file'); + expect(active('web_search_tool')).toBe('Searching the web'); + expect(done('web_search_tool')).toBe('Searched the web'); + }); + + it('labels the search tool the core actually streams, not only its settings id', () => { + expect(done('web_search_tool', { query: 'rust' })).toBe('Searched the web'); + expect(describeToolCall({ name: 'web_search_tool', args: { query: 'rust' } }).chip).toBe( + 'rust' + ); + }); + + it('covers every bring-your-own-key search engine', () => { + for (const name of [ + 'exa_search', + 'tavily_search', + 'querit_search', + 'parallel_search', + 'tinyfish_search', + ]) { + expect(done(name), name).toBe('Searched the web'); + expect(describeToolCall({ name }).body, name).toBe('webSearch'); + } + expect(done('brave_news_search')).toBe('Searched news'); + expect(done('brave_image_search')).toBe('Searched images'); + }); + + it('describes the deferred-tool bridge as the tool it calls', () => { + expect(done('tool_call', { name: 'SLACK_SEND_MESSAGE', arguments: {} })).toBe('Used Slack'); + expect(done('tool_call', { name: 'file_read', arguments: { path: '/a/b/c/d.ts' } })).toBe( + 'Read file' + ); + }); + + it('switches collapsed tools on their action argument', () => { + expect(done('memory', { action: 'recall', query: 'x' })).toBe('Recalled memories'); + expect(done('memory', { action: 'store', key: 'k' })).toBe('Saved to memory'); + expect(done('cron', { action: 'add', name: 'daily' })).toBe('Scheduled task'); + expect(done('browser', { action: 'click', selector: '#go' })).toBe('Clicked'); + }); + + it('labels named agents and delegations by what they do', () => { + expect(done('subagent:researcher')).toBe('Researched'); + expect(done('spawn_subagent', { agent_id: 'critic' })).toBe('Reviewed the work'); + expect(done('delegate_gmail')).toBe('Used Gmail'); + expect(active('run_code')).toBe('Running code'); + expect( + done('spawn_subagent', { agent_id: 'integrations_agent', toolkit: 'notion', prompt: 'p' }) + ).toBe('Used Notion'); + }); + + it('prefers the server label only for tools it cannot describe', () => { + // A known tool ignores the core's humanized label. + expect(done('file_read', {}, { serverLabel: 'File Read' })).toBe('Read file'); + // An unknown tool takes a readable server label. + expect(done('frobnicate', {}, { serverLabel: 'Frobnicated the widget' })).toBe( + 'Frobnicated the widget' + ); + // A server label that is itself a leaked identifier is not trusted. + expect(done('frobnicate', {}, { serverLabel: 'FROB NICATE' })).toBe('Used frobnicate'); + expect(done('frobnicate', {}, { serverLabel: 'frob_nicate' })).toBe('Used frobnicate'); + }); + + it('falls back to a sentence-cased, tense-aware label', () => { + expect(active('some_new_tool')).toBe('Using some new tool'); + expect(done('some_new_tool')).toBe('Used some new tool'); + expect(sentenceCase('GMAIL_SEND_EMAIL')).toBe('Gmail send email'); + }); + + it('renders a degraded placeholder name without shouting', () => { + expect(done('tool')).toBe('Used tool'); + expect(done('')).toBe('Used tool'); + }); +}); + +describe('tool chips', () => { + it('shortens paths and urls', () => { + expect(describeToolCall({ name: 'file_read', args: { path: '/a/b/c/d.ts' } }).chip).toBe( + '…/c/d.ts' + ); + expect( + describeToolCall({ name: 'web_fetch', args: { url: 'https://docs.rs/tokio/latest' } }).chip + ).toBe('docs.rs/tokio/latest'); + }); + + it('reads args from a JSON buffer', () => { + expect(describeToolCall({ name: 'shell', args: '{"command":"ls -la"}' }).chip).toBe('ls -la'); + }); + + it('caps a chip so model text cannot blow up a row', () => { + const chip = describeToolCall({ name: 'grep', args: { pattern: 'x'.repeat(500) } }).chip; + expect(chip?.length).toBeLessThanOrEqual(80); + }); +}); + +describe('tool labels: translation', () => { + it('serves the label through the phrase key and fills placeholders', () => { + const t = (key: string, fallback?: string) => + key === 'conversations.tools.useApp.done' ? '{app} benutzt' : (fallback ?? key); + expect(toolLabel(describeToolCall({ name: 'GMAIL_SEND_EMAIL', status: 'success' }), t)).toBe( + 'Gmail benutzt' + ); + }); +}); diff --git a/app/src/features/conversations/tools/toolPresentation.ts b/app/src/features/conversations/tools/toolPresentation.ts new file mode 100644 index 00000000000..a44d3d1f78b --- /dev/null +++ b/app/src/features/conversations/tools/toolPresentation.ts @@ -0,0 +1,393 @@ +/** + * The single answer to "how do we show this tool call?". + * + * Before this module four systems labelled tool calls and disagreed: a name + * table, an args-sniffing heuristic that called anything with a `query` + * argument "Searched the web", a category icon table, and the core's + * humanized name. Every surface (chat card, timeline, processing panel, + * status line, mascot) now resolves through {@link describeToolCall}. + * + * Resolution order, first hit wins: + * + * 1. `tool_call { name, arguments }`: the harness's deferred-tool bridge is + * described as the tool it invokes. + * 2. Named agents: `subagent:<id>`, `spawn_subagent { agent_id }`, + * `delegate_<id>` and custom delegate names. + * 3. Collapsed tools switching on an argument (`memory { action }`). + * 4. An exact entry in `toolSpecs.ts`. + * 5. A prefix family rule. + * 6. A Composio action slug (`GMAIL_SEND_EMAIL` → "Used Gmail · Send email"). + * 7. The server's display label, for dynamic tools the client cannot know. + * 8. A sentence-cased fallback ("Used Frobnicate widget"). Never raw + * snake_case, never ALL CAPS. + * + * Pure and synchronous: safe in reducers, selectors and tests. Translation + * happens at the edge through {@link toolLabel} with the caller's `t`. + */ +import type { LucideIcon } from 'lucide-react'; + +import { matchComposioActionSlug } from '../../../components/composio/toolkitMeta'; +import { chip as chipRules, genericChip, type ToolArgs, truncateChip } from './toolChips'; +import { + fillPlaceholders, + phraseKey, + TOOL_PHRASES, + type ToolPhraseId, + type ToolPhraseTense, +} from './toolPhrases'; +import { + ACTION_TOOL_SPECS, + AGENT_SPECS, + EXACT_TOOL_SPECS, + FALLBACK_ICON, + FAMILY_TOOL_SPECS, + INTEGRATION_ICON, + INTEGRATIONS_AGENT_ID, + type ToolBodyKind, + type ToolCategory, + type ToolSpec, +} from './toolSpecs'; + +export type { ToolBodyKind, ToolCategory } from './toolSpecs'; + +/** Mirrors `ToolTimelineEntryStatus`; kept local so this module has no store import. */ +export type ToolCallStatus = 'running' | 'success' | 'error' | 'awaiting_user' | 'cancelled'; + +/** How a presentation was resolved. `fallback` is what the catalog test forbids. */ +export type ToolPresentationSource = + | 'exact' + | 'action' + | 'family' + | 'agent' + | 'integration' + | 'server' + | 'fallback'; + +export interface DescribeToolCallInput { + /** Tool name as streamed; may carry a `subagent:` prefix. */ + name: string; + /** Parsed args object, or the raw JSON args buffer. */ + args?: unknown; + status?: ToolCallStatus; + /** `tool_display_label` from the core, for tools the client cannot know. */ + serverLabel?: string; + /** `tool_display_detail` from the core. */ + serverDetail?: string; + /** + * Connected-app slug known from context rather than args: a spawned + * `integrations_agent` row carries the `delegate_<toolkit>` tool that + * spawned it. + */ + toolkitHint?: string; +} + +export interface ToolCallPresentation { + /** Name with any `subagent:` prefix removed. */ + baseName: string; + icon: LucideIcon; + category: ToolCategory; + body: ToolBodyKind; + tense: ToolPhraseTense; + /** Translatable phrase. Absent only when {@link literal} carries the label. */ + phrase?: ToolPhraseId; + params?: Record<string, string>; + /** Untranslatable label (a server-supplied one). */ + literal?: string; + /** Short target beside the label: a path, query, host or app action. */ + chip?: string; + /** Connected app, for Composio actions and the integrations agent. */ + integration?: { slug: string; name: string; known: boolean }; + source: ToolPresentationSource; +} + +export type Translate = (key: string, fallback?: string) => string; + +const ACTIVE_STATUSES = new Set<ToolCallStatus>(['running', 'awaiting_user']); + +export function tenseForStatus(status: ToolCallStatus | undefined): ToolPhraseTense { + return !status || ACTIVE_STATUSES.has(status) ? 'active' : 'done'; +} + +/** Parse args from an object or a JSON buffer; anything else is `{}`. */ +export function parseToolArgs(args: unknown): ToolArgs { + if (args && typeof args === 'object' && !Array.isArray(args)) return args as ToolArgs; + if (typeof args !== 'string' || !args.trim()) return {}; + try { + const parsed: unknown = JSON.parse(args); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? (parsed as ToolArgs) + : {}; + } catch { + return {}; + } +} + +/** + * `web_search_tool` → "Web search tool", `GMAIL_SEND_EMAIL` → "Gmail send + * email", `fooBar` → "Foo bar". Lower-cases everything after the first + * letter so a slug never renders shouting. + */ +export function sentenceCase(value: string): string { + const words = value + .replace(/^subagent:/, '') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[_\-.:/]+/g, ' ') + .trim() + .toLowerCase(); + return words.charAt(0).toUpperCase() + words.slice(1); +} + +/** Is a server label readable as-is, or is it a leaked identifier? */ +function isReadableLabel(label: string, rawName: string): boolean { + const trimmed = label.trim(); + if (!trimmed || trimmed.toLowerCase() === 'tool') return false; + if (trimmed === rawName) return false; + if (/[_]/.test(trimmed)) return false; + // Two or more consecutive ALL-CAPS words ("GMAIL SEND EMAIL"). + if (/\b[A-Z]{2,}\b\s+\b[A-Z]{2,}\b/.test(trimmed)) return false; + return true; +} + +function fromSpec( + spec: ToolSpec, + source: ToolPresentationSource, + baseName: string, + args: ToolArgs, + tense: ToolPhraseTense, + serverDetail: string | undefined +): ToolCallPresentation { + return { + baseName, + icon: spec.icon, + category: spec.category, + body: spec.body ?? 'generic', + tense, + phrase: spec.phrase, + chip: spec.chip?.(args) ?? cleanDetail(serverDetail), + source, + }; +} + +function cleanDetail(detail: string | undefined): string | undefined { + return detail?.trim() ? truncateChip(detail) : undefined; +} + +function agentPresentation( + agentId: string, + baseName: string, + args: ToolArgs, + tense: ToolPhraseTense, + serverDetail: string | undefined, + toolkitHint?: string +): ToolCallPresentation | undefined { + if (agentId === INTEGRATIONS_AGENT_ID) { + const toolkit = typeof args.toolkit === 'string' ? args.toolkit : toolkitHint; + const app = toolkit ? integrationFromToolkit(toolkit) : undefined; + const prompt = typeof args.prompt === 'string' ? args.prompt : serverDetail; + return { + baseName, + icon: INTEGRATION_ICON, + category: 'app', + body: 'generic', + tense, + ...(app + ? { phrase: 'useApp' as const, params: { app: app.name }, integration: app } + : { phrase: 'checkConnectedApp' as const }), + chip: cleanDetail(prompt), + source: 'agent', + }; + } + const spec = AGENT_SPECS[agentId]; + if (!spec) return undefined; + const prompt = typeof args.prompt === 'string' ? args.prompt : undefined; + return { + ...fromSpec(spec, 'agent', baseName, args, tense, serverDetail), + chip: cleanDetail(serverDetail) ?? cleanDetail(prompt), + }; +} + +function integrationFromToolkit( + toolkit: string +): { slug: string; name: string; known: boolean } | undefined { + const slug = toolkit.trim().toLowerCase(); + if (!slug) return undefined; + // Reuse the action matcher's catalog lookup with a synthetic action. + const match = matchComposioActionSlug(`${slug.toUpperCase()}_X`); + return match ? { slug: match.slug, name: match.name, known: match.known } : undefined; +} + +export function describeToolCall(input: DescribeToolCallInput): ToolCallPresentation { + const rawName = input.name?.trim() || 'tool'; + const baseName = rawName.replace(/^subagent:/, ''); + const args = parseToolArgs(input.args); + const tense = tenseForStatus(input.status); + const { serverDetail } = input; + + // 1. Deferred-tool bridge: describe the tool it actually calls. + if (baseName === 'tool_call' && typeof args.name === 'string' && args.name.trim()) { + return describeToolCall({ + ...input, + name: args.name, + args: args.arguments, + serverLabel: undefined, + }); + } + + // 2. Named agents. + if (rawName.startsWith('subagent:') || baseName === INTEGRATIONS_AGENT_ID) { + const hint = input.toolkitHint?.replace(/^delegate_/, ''); + const agent = agentPresentation(baseName, baseName, args, tense, serverDetail, hint); + if (agent) return agent; + } + if ( + (baseName === 'spawn_subagent' || baseName === 'spawn_async_subagent') && + typeof args.agent_id === 'string' + ) { + const agent = agentPresentation(args.agent_id, baseName, args, tense, serverDetail); + if (agent) return agent; + } + if (baseName.startsWith('delegate_') && !EXACT_TOOL_SPECS[baseName]) { + const id = baseName.slice('delegate_'.length); + // An app named in the args (`delegate_tools_agent { toolkit: "github" }`) + // says more than the generic agent does, so it wins over the agent spec. + const argApp = + typeof args.toolkit === 'string' ? integrationFromToolkit(args.toolkit) : undefined; + const app = argApp?.known ? argApp : integrationFromToolkit(id); + const agent = argApp?.known + ? undefined + : agentPresentation(id, baseName, args, tense, serverDetail); + if (agent) return agent; + if (app?.known) { + return { + baseName, + icon: INTEGRATION_ICON, + category: 'app', + body: 'generic', + tense, + phrase: 'useApp', + params: { app: app.name }, + integration: app, + chip: cleanDetail(typeof args.prompt === 'string' ? args.prompt : serverDetail), + source: 'agent', + }; + } + return { + baseName, + icon: EXACT_TOOL_SPECS.delegate.icon, + category: 'agent', + body: 'generic', + tense, + phrase: 'delegateTask', + chip: sentenceCase(id), + source: 'agent', + }; + } + if (AGENT_SPECS[baseName] && !EXACT_TOOL_SPECS[baseName]) { + const agent = agentPresentation(baseName, baseName, args, tense, serverDetail); + if (agent) return agent; + } + + // 3. Collapsed tools that switch on an argument. + const action = ACTION_TOOL_SPECS[baseName]; + if (action) { + const value = args[action.arg]; + const actionSpec = typeof value === 'string' ? action.specs[value] : undefined; + if (actionSpec) return fromSpec(actionSpec, 'action', baseName, args, tense, serverDetail); + } + + // 4. Exact entry. + const exact = EXACT_TOOL_SPECS[baseName]; + if (exact) { + const presentation = fromSpec(exact, 'exact', baseName, args, tense, serverDetail); + if (baseName === 'mcp_call_tool' || baseName === 'mcp_registry_tool_call') { + const tool = + typeof args.tool === 'string' + ? args.tool + : typeof args.tool_name === 'string' + ? args.tool_name + : undefined; + if (tool?.trim()) presentation.params = { tool: truncateChip(tool, 48) }; + else presentation.phrase = 'checkMcpTools'; + } + if (baseName === 'composio_execute' && typeof args.tool === 'string') { + const match = matchComposioActionSlug(args.tool); + if (match) { + return { + ...presentation, + phrase: 'useApp', + params: { app: match.name }, + integration: { slug: match.slug, name: match.name, known: match.known }, + chip: match.action, + }; + } + } + return presentation; + } + + // 5. Prefix family. + const family = FAMILY_TOOL_SPECS.find(rule => rule.test.test(baseName)); + if (family) return fromSpec(family.spec, 'family', baseName, args, tense, serverDetail); + + // 6. Composio action slug. + const composio = matchComposioActionSlug(baseName); + if (composio) { + // The action names what was done; the server detail (or the obvious + // argument, e.g. a recipient) names what it was done to. + const target = cleanDetail(serverDetail) ?? genericChip(args); + return { + baseName, + icon: INTEGRATION_ICON, + category: 'app', + body: 'generic', + tense, + phrase: 'useApp', + params: { app: composio.name }, + integration: { slug: composio.slug, name: composio.name, known: composio.known }, + chip: target ? truncateChip(`${composio.action} · ${target}`) : composio.action, + source: 'integration', + }; + } + + // 7. Server label for a dynamic tool. + const serverLabel = input.serverLabel?.trim(); + if (serverLabel && isReadableLabel(serverLabel, rawName)) { + return { + baseName, + icon: FALLBACK_ICON, + category: 'other', + body: 'generic', + tense, + literal: serverLabel, + chip: cleanDetail(serverDetail) ?? genericChip(args), + source: 'server', + }; + } + + // 8. Fallback. + return { + baseName, + icon: FALLBACK_ICON, + category: 'other', + body: 'generic', + tense, + phrase: 'useTool', + params: { tool: sentenceCase(baseName).toLowerCase() }, + chip: cleanDetail(serverDetail) ?? genericChip(args), + source: 'fallback', + }; +} + +/** English label, for logs and non-React callers. */ +export function toolLabel(presentation: ToolCallPresentation, t?: Translate): string { + if (presentation.literal) return presentation.literal; + const id = presentation.phrase ?? 'useTool'; + const english = TOOL_PHRASES[id][presentation.tense]; + const template = t ? t(phraseKey(id, presentation.tense), english) : english; + const label = fillPlaceholders(template, presentation.params); + // `useTool` with a lower-cased tool name reads "Using frobnicate"; lift the + // first letter of the whole label only. + return label.charAt(0).toUpperCase() + label.slice(1); +} + +/** Every chip rule, exported for the gallery and tests. */ +export const TOOL_CHIP_RULES = chipRules; diff --git a/app/src/features/conversations/tools/toolSpecs.ts b/app/src/features/conversations/tools/toolSpecs.ts new file mode 100644 index 00000000000..f9b896b0afe --- /dev/null +++ b/app/src/features/conversations/tools/toolSpecs.ts @@ -0,0 +1,622 @@ +/** + * How each core tool is presented: icon, phrase, category, target chip and + * the rich body its detail panel renders. + * + * Resolution order lives in `toolPresentation.ts`; this file is only data. + * Three layers: + * + * - {@link EXACT_TOOL_SPECS}: one entry per registered tool name. + * - {@link ACTION_TOOL_SPECS}: collapsed tools that switch on an argument + * (`memory { action: "recall" }`, `browser { action: "click" }`). + * - {@link FAMILY_TOOL_SPECS}: prefix rules for tool families whose members + * share a meaning (`hosting_*`, `wallet_*`), so a new member of a known + * family is labelled without an edit here. + * + * `toolPresentation.catalog.test.ts` walks every name the core registers + * (`__fixtures__/coreToolNames.json`) and fails if any falls through to the + * generic fallback, so a new core tool cannot ship unlabelled. + */ +import { + AppWindowIcon, + ArchiveRestoreIcon, + ArrowLeftRightIcon, + BellIcon, + BlocksIcon, + BookOpenIcon, + BotIcon, + BrainCircuitIcon, + BrainIcon, + CalendarClockIcon, + CameraIcon, + ChartBarIcon, + ClapperboardIcon, + ClipboardCheckIcon, + ClockIcon, + CodeIcon, + CoinsIcon, + DatabaseIcon, + DownloadIcon, + EraserIcon, + FilePenIcon, + FilePlusIcon, + FileSpreadsheetIcon, + FileTextIcon, + FlagIcon, + FolderOpenIcon, + FolderSearchIcon, + GitBranchIcon, + GitCompareIcon, + GlobeIcon, + GraduationCapIcon, + HardDriveIcon, + HeartIcon, + HourglassIcon, + ImageIcon, + ImagePlusIcon, + KeyboardIcon, + LayersIcon, + Link2Icon, + LinkIcon, + ListChecksIcon, + ListTodoIcon, + type LucideIcon, + MailXIcon, + MapPinIcon, + MessageCircleQuestionIcon, + MessageSquareReplyIcon, + MousePointerClickIcon, + NetworkIcon, + NewspaperIcon, + PackageIcon, + PackagePlusIcon, + PackageSearchIcon, + PhoneIcon, + PlugIcon, + PodcastIcon, + PowerIcon, + PresentationIcon, + ReceiptIcon, + RocketIcon, + SaveIcon, + ScanEyeIcon, + ScanSearchIcon, + ScrollTextIcon, + ServerIcon, + SettingsIcon, + ShieldIcon, + SparklesIcon, + SquareTerminalIcon, + StethoscopeIcon, + TargetIcon, + TelescopeIcon, + TextSearchIcon, + TrendingUpIcon, + UserRoundIcon, + UsersIcon, + VideoIcon, + WalletIcon, + WorkflowIcon, + WrenchIcon, +} from 'lucide-react'; + +import { chip, type ChipRule } from './toolChips'; +import type { ToolPhraseId } from './toolPhrases'; + +/** Broad activity category, used for grouping summaries and the timeline. */ +export type ToolCategory = + | 'file' + | 'code' + | 'shell' + | 'web' + | 'browser' + | 'media' + | 'memory' + | 'agent' + | 'plan' + | 'schedule' + | 'app' + | 'mcp' + | 'storage' + | 'wallet' + | 'skill' + | 'system' + | 'other'; + +/** Which rich body the expanded row renders. `generic` is the Input/Output view. */ +export type ToolBodyKind = 'webSearch' | 'webFetch' | 'shell' | 'file' | 'mcp' | 'generic'; + +export interface ToolSpec { + phrase: ToolPhraseId; + icon: LucideIcon; + category: ToolCategory; + chip?: ChipRule; + body?: ToolBodyKind; +} + +const spec = ( + phrase: ToolPhraseId, + icon: LucideIcon, + category: ToolCategory, + extra: Partial<Pick<ToolSpec, 'chip' | 'body'>> = {} +): ToolSpec => ({ phrase, icon, category, ...extra }); + +const webSearch = (phrase: ToolPhraseId, icon: LucideIcon = GlobeIcon) => + spec(phrase, icon, 'web', { chip: chip.query(), body: 'webSearch' }); +const readPages = spec('readPages', LinkIcon, 'web', { chip: chip.url(), body: 'webFetch' }); + +export const FALLBACK_ICON = WrenchIcon; +export const INTEGRATION_ICON = PlugIcon; + +export const EXACT_TOOL_SPECS: Record<string, ToolSpec> = { + // ── Files and code ────────────────────────────────────────────────────── + file_read: spec('readFile', FileTextIcon, 'file', { chip: chip.path(), body: 'file' }), + file_write: spec('writeFile', FilePlusIcon, 'file', { chip: chip.path(), body: 'file' }), + edit: spec('editFile', FilePenIcon, 'file', { chip: chip.path(), body: 'file' }), + apply_patch: spec('applyEdits', FilePenIcon, 'file', { chip: chip.editsPath(), body: 'file' }), + vault_write_markdown: spec('writeFile', FilePlusIcon, 'file', { chip: chip.path() }), + // Older and foreign spellings of the file tools, seen in persisted + // transcripts and other harnesses' tool names. + read_file: spec('readFile', FileTextIcon, 'file', { chip: chip.path(), body: 'file' }), + write_file: spec('writeFile', FilePlusIcon, 'file', { chip: chip.path(), body: 'file' }), + grep: spec('searchCode', TextSearchIcon, 'code', { chip: chip.text('pattern') }), + glob: spec('findFiles', FolderSearchIcon, 'file', { chip: chip.text('pattern') }), + list: spec('listFolder', FolderOpenIcon, 'file', { chip: chip.path() }), + csv_export: spec('exportCsv', FileSpreadsheetIcon, 'file', { chip: chip.text('filename') }), + update_memory_md: spec('updateMemoryNotes', ScrollTextIcon, 'memory', { + chip: chip.text('file'), + }), + git_operations: spec('runGit', GitBranchIcon, 'code', { + chip: chip.text('operation', 'command'), + }), + read_diff: spec('readChanges', GitCompareIcon, 'code', { chip: chip.path() }), + run_linter: spec('runLinter', ListChecksIcon, 'code'), + run_tests: spec('runTests', ListChecksIcon, 'code'), + lsp: spec('analyzeCode', CodeIcon, 'code', { chip: chip.path() }), + insert_sql_record: spec('insertRecord', DatabaseIcon, 'system', { chip: chip.text('table') }), + + // ── Shell and system ──────────────────────────────────────────────────── + shell: spec('runCommand', SquareTerminalIcon, 'shell', { chip: chip.command(), body: 'shell' }), + node_exec: spec('runCode', SquareTerminalIcon, 'shell', { + chip: chip.command('script_path', 'inline_code'), + body: 'shell', + }), + python_exec: spec('runCode', SquareTerminalIcon, 'shell', { + chip: chip.command('script_path', 'inline_code'), + body: 'shell', + }), + npm_exec: spec('runPackageManager', SquareTerminalIcon, 'shell', { + chip: chip.command('subcommand'), + body: 'shell', + }), + detect_tools: spec('checkInstalledTools', ScanSearchIcon, 'system'), + install_tool: spec('installTool', PackagePlusIcon, 'system', { + chip: chip.text('package', 'tool_name'), + }), + current_time: spec('checkTime', ClockIcon, 'system'), + resolve_time: spec('resolveDate', ClockIcon, 'system', { chip: chip.text('expr') }), + retrieve_tool_output: spec('retrieveOutput', ArchiveRestoreIcon, 'system'), + tinyjuice_retrieve: spec('retrieveOutput', ArchiveRestoreIcon, 'system'), + read_workspace_state: spec('reviewWorkspace', FolderOpenIcon, 'system'), + proxy_config: spec('configureProxy', SettingsIcon, 'system', { chip: chip.text('action') }), + update_check: spec('checkUpdates', DownloadIcon, 'system'), + update_apply: spec('installUpdate', DownloadIcon, 'system'), + pushover: spec('sendNotification', BellIcon, 'system', { chip: chip.text('title', 'message') }), + tool_stats: spec('reviewToolUsage', ChartBarIcon, 'system'), + keyboard: spec('typeKeys', KeyboardIcon, 'browser', { chip: chip.text('text', 'key') }), + mouse: spec('click', MousePointerClickIcon, 'browser'), + + // ── Web ───────────────────────────────────────────────────────────────── + web_search: webSearch('searchWeb'), + web_search_tool: webSearch('searchWeb'), + exa_search: webSearch('searchWeb'), + tavily_search: webSearch('searchWeb'), + querit_search: webSearch('searchWeb'), + parallel_search: webSearch('searchWeb'), + tinyfish_search: webSearch('searchWeb'), + searxng_search: webSearch('searchWeb'), + seltz_search: webSearch('searchWeb'), + brave_news_search: webSearch('searchNews', NewspaperIcon), + brave_image_search: webSearch('searchImages', ImageIcon), + brave_video_search: webSearch('searchVideos', VideoIcon), + exa_find_similar: spec('findSimilarPages', GlobeIcon, 'web', { + chip: chip.url(), + body: 'webSearch', + }), + exa_get_contents: readPages, + tavily_extract: readPages, + parallel_extract: readPages, + tinyfish_fetch: readPages, + parallel_research: spec('research', TelescopeIcon, 'web', { chip: chip.query() }), + parallel_chat: spec('askTheWeb', GlobeIcon, 'web', { chip: chip.query() }), + parallel_enrich: spec('enrichData', SparklesIcon, 'web', { chip: chip.query() }), + parallel_dataset: spec('buildDataset', DatabaseIcon, 'web', { chip: chip.query() }), + tinyfish_agent_run: spec('browseForYou', MousePointerClickIcon, 'web', { + chip: chip.text('goal', 'url'), + }), + web_fetch: spec('readWebpage', LinkIcon, 'web', { chip: chip.url(), body: 'webFetch' }), + http_request: spec('callApi', ArrowLeftRightIcon, 'web', { chip: chip.url(), body: 'webFetch' }), + curl: spec('downloadFile', DownloadIcon, 'web', { chip: chip.url() }), + x402_request: spec('makePaidRequest', CoinsIcon, 'web', { chip: chip.url() }), + gitbooks_search: spec('searchDocs', BookOpenIcon, 'web', { chip: chip.query() }), + gitbooks_get_page: spec('readDocs', BookOpenIcon, 'web', { chip: chip.url() }), + + // ── Browser ───────────────────────────────────────────────────────────── + browser: spec('useBrowser', AppWindowIcon, 'browser', { chip: chip.url() }), + browser_open: spec('openPage', AppWindowIcon, 'browser', { chip: chip.url() }), + + // ── Media and documents ──────────────────────────────────────────────── + image_info: spec('analyzeImage', ScanEyeIcon, 'media', { chip: chip.path() }), + media_generate_image: spec('generateImage', ImagePlusIcon, 'media', { + chip: chip.text('prompt'), + }), + media_generate_video: spec('generateVideo', ClapperboardIcon, 'media', { + chip: chip.text('prompt'), + }), + media_list_models: spec('checkMediaModels', ImageIcon, 'media'), + generate_document: spec('createDocument', FileTextIcon, 'media', { chip: chip.text('title') }), + generate_presentation: spec('createPresentation', PresentationIcon, 'media', { + chip: chip.text('title'), + }), + audio_generate_podcast: spec('generatePodcast', PodcastIcon, 'media', { + chip: chip.text('title', 'topic'), + }), + audio_email_podcast: spec('emailPodcast', PodcastIcon, 'media', { chip: chip.text('to') }), + audio_generate_and_email_podcast: spec('createAndEmailPodcast', PodcastIcon, 'media', { + chip: chip.text('title', 'topic'), + }), + + // ── Memory ────────────────────────────────────────────────────────────── + memory: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + memory_store: spec('saveToMemory', SaveIcon, 'memory', { chip: chip.text('key', 'content') }), + memory_recall: spec('recallMemories', BrainCircuitIcon, 'memory', { chip: chip.query() }), + memory_forget: spec('forgetMemory', EraserIcon, 'memory', { chip: chip.text('key') }), + memory_hybrid_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + memory_vector_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + memory_chunk_context: spec('inspectMemory', BrainIcon, 'memory'), + memory_store_raw_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + memory_store_raw_chunks: spec('inspectMemory', BrainIcon, 'memory'), + memory_store_kinds: spec('inspectMemory', BrainIcon, 'memory'), + memory_doctor: spec('inspectMemory', StethoscopeIcon, 'memory'), + memory_flavour: spec('inspectMemory', BrainIcon, 'memory'), + memory_tree: spec('exploreMemory', NetworkIcon, 'memory', { chip: chip.query() }), + goals: spec('reviewGoals', TargetIcon, 'memory'), + remember_preference: spec('savePreference', HeartIcon, 'memory', { + chip: chip.text('preference', 'key'), + }), + save_preference: spec('savePreference', HeartIcon, 'memory', { + chip: chip.text('preference', 'key'), + }), + flow_memory_recall: spec('recallMemories', BrainCircuitIcon, 'memory', { chip: chip.query() }), + flow_memory_remember: spec('saveToMemory', SaveIcon, 'memory', { chip: chip.text('key') }), + memory_tools_list: spec('inspectMemory', BrainIcon, 'memory'), + memory_tools_put: spec('saveToMemory', SaveIcon, 'memory'), + call_memory_agent: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + + // ── Agents and delegation ────────────────────────────────────────────── + spawn_subagent: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent_id') }), + spawn_async_subagent: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent_id') }), + spawn_worker_thread: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent_id') }), + delegate_graph: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent_id') }), + delegate: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent') }), + delegate_to: spec('delegateTask', BotIcon, 'agent', { chip: chip.text('agent', 'agent_id') }), + spawn_parallel_agents: spec('runAgentsInParallel', UsersIcon, 'agent'), + continue_subagent: spec('messageAgent', MessageSquareReplyIcon, 'agent', { + chip: chip.text('agent_id'), + }), + steer_subagent: spec('messageAgent', MessageSquareReplyIcon, 'agent', { + chip: chip.text('agent_id'), + }), + wait_subagent: spec('waitForAgent', HourglassIcon, 'agent'), + close_subagent: spec('closeAgent', BotIcon, 'agent'), + list_subagents: spec('checkAgents', BotIcon, 'agent'), + wait: spec('wait', HourglassIcon, 'agent'), + wait_loop: spec('wait', HourglassIcon, 'agent'), + ask_user_clarification: spec('askQuestion', MessageCircleQuestionIcon, 'agent', { + chip: chip.text('question'), + }), + agent_prepare_context: spec('prepareContext', LayersIcon, 'agent', { + chip: chip.text('question'), + }), + extract_from_result: spec('extractDetails', LayersIcon, 'agent'), + + // ── Planning ──────────────────────────────────────────────────────────── + todo: spec('updateTodos', ListTodoIcon, 'plan'), + request_plan_review: spec('requestPlanReview', ClipboardCheckIcon, 'plan'), + plan_exit: spec('finishPlan', ClipboardCheckIcon, 'plan'), + goal_set: spec('setGoal', FlagIcon, 'plan', { chip: chip.text('objective') }), + goal_get: spec('checkGoal', FlagIcon, 'plan'), + goal_complete: spec('completeGoal', FlagIcon, 'plan'), + + // ── Scheduling ────────────────────────────────────────────────────────── + cron: spec('checkSchedules', CalendarClockIcon, 'schedule'), + cron_add: spec('scheduleTask', CalendarClockIcon, 'schedule', { chip: chip.text('name') }), + cron_list: spec('checkSchedules', CalendarClockIcon, 'schedule'), + cron_update: spec('updateSchedule', CalendarClockIcon, 'schedule', { chip: chip.text('name') }), + cron_remove: spec('removeSchedule', CalendarClockIcon, 'schedule'), + cron_run: spec('runScheduledTask', CalendarClockIcon, 'schedule'), + cron_runs: spec('checkRunHistory', CalendarClockIcon, 'schedule'), + schedule: spec('scheduleTask', CalendarClockIcon, 'schedule'), + + // ── Connected apps ───────────────────────────────────────────────────── + composio_list_toolkits: spec('checkAvailableApps', PlugIcon, 'app'), + composio_list_connections: spec('checkConnections', PlugIcon, 'app'), + composio_connect: spec('connectApp', Link2Icon, 'app', { chip: chip.text('toolkit') }), + composio_authorize: spec('authorizeApp', Link2Icon, 'app', { chip: chip.text('toolkit') }), + composio_list_tools: spec('findAppActions', PlugIcon, 'app', { + chip: chip.text('toolkits', 'toolkit'), + }), + composio_execute: spec('runAppAction', PlugIcon, 'app', { chip: chip.text('tool') }), + tool_search: spec('findTools', PackageSearchIcon, 'system', { chip: chip.query() }), + // The deferred-tool bridge is described as the tool it calls + // (`toolPresentation.ts`); this entry covers it before its args arrive. + tool_call: spec('useTools', WrenchIcon, 'system', { chip: chip.text('name') }), + search_tool_catalog: spec('findTools', PackageSearchIcon, 'system', { chip: chip.query() }), + gmail_unsubscribe: spec('unsubscribe', MailXIcon, 'app', { chip: chip.text('sender', 'email') }), + google_places_search: spec('searchPlaces', MapPinIcon, 'app', { chip: chip.query() }), + google_places_details: spec('lookUpPlace', MapPinIcon, 'app', { chip: chip.text('place_id') }), + twilio_call: spec('placeCall', PhoneIcon, 'app', { chip: chip.text('to') }), + + // ── MCP ───────────────────────────────────────────────────────────────── + mcp_list_servers: spec('checkMcpServers', ServerIcon, 'mcp'), + mcp_list_tools: spec('checkMcpTools', BlocksIcon, 'mcp', { chip: chip.text('server') }), + mcp_call_tool: spec('callMcpTool', BlocksIcon, 'mcp', { chip: chip.text('server'), body: 'mcp' }), + mcp_registry_tool_call: spec('callMcpTool', BlocksIcon, 'mcp', { + chip: chip.text('server_id'), + body: 'mcp', + }), + mcp_registry_search: spec('searchMcpServers', ServerIcon, 'mcp', { chip: chip.query() }), + mcp_registry_get: spec('checkMcpServers', ServerIcon, 'mcp', { + chip: chip.text('qualified_name'), + }), + mcp_registry_installed_list: spec('checkMcpServers', ServerIcon, 'mcp'), + mcp_registry_status: spec('checkMcpServers', ServerIcon, 'mcp'), + mcp_registry_list_tools: spec('checkMcpTools', BlocksIcon, 'mcp', { + chip: chip.text('server_id'), + }), + mcp_registry_connect: spec('connectMcpServer', ServerIcon, 'mcp', { + chip: chip.text('qualified_name', 'server_id'), + }), + mcp_registry_disconnect: spec('disconnectMcpServer', ServerIcon, 'mcp', { + chip: chip.text('server_id'), + }), + mcp_registry_uninstall: spec('removeMcpServer', ServerIcon, 'mcp', { + chip: chip.text('server_id'), + }), + + // ── Storage and hosting ──────────────────────────────────────────────── + storage_upload_file: spec('uploadFile', HardDriveIcon, 'storage', { chip: chip.path() }), + storage_download_file: spec('downloadFile', HardDriveIcon, 'storage', { + chip: chip.text('key', 'name'), + }), + storage_list_files: spec('listStoredFiles', HardDriveIcon, 'storage'), + storage_get_link: spec('createShareLink', LinkIcon, 'storage', { + chip: chip.text('key', 'name'), + }), + storage_delete_file: spec('deleteFile', HardDriveIcon, 'storage', { + chip: chip.text('key', 'name'), + }), + storage_set_visibility: spec('updateFileAccess', HardDriveIcon, 'storage', { + chip: chip.text('key', 'name'), + }), + hosting_launch_site: spec('deploySite', RocketIcon, 'storage', { chip: chip.text('name') }), + hosting_rollback: spec('rollBackDeployment', RocketIcon, 'storage'), + + // ── Wallet ────────────────────────────────────────────────────────────── + wallet_prepare_transfer: spec('prepareTransfer', WalletIcon, 'wallet', { chip: chip.text('to') }), + web3_swap_quote: spec('getSwapQuote', ArrowLeftRightIcon, 'wallet'), + web3_swap_routes: spec('getSwapQuote', ArrowLeftRightIcon, 'wallet'), + web3_swap_execute: spec('swapTokens', ArrowLeftRightIcon, 'wallet'), + web3_bridge_quote: spec('getBridgeQuote', ArrowLeftRightIcon, 'wallet'), + web3_bridge_execute: spec('bridgeTokens', ArrowLeftRightIcon, 'wallet'), + web3_dapp_call: spec('callDapp', CoinsIcon, 'wallet'), + web3_dapp_execute: spec('callDapp', CoinsIcon, 'wallet'), + + // ── Skills and workflows ─────────────────────────────────────────────── + use_skill: spec('useSkill', SparklesIcon, 'skill', { chip: chip.text('skill') }), + skill_search: spec('searchSkills', SparklesIcon, 'skill', { chip: chip.query() }), + create_skill: spec('createSkill', SparklesIcon, 'skill', { chip: chip.text('name') }), + install_workflow_from_url: spec('installSkill', SparklesIcon, 'skill', { chip: chip.url() }), + uninstall_workflow: spec('removeSkill', SparklesIcon, 'skill', { chip: chip.text('name', 'id') }), + run_workflow: spec('runWorkflow', WorkflowIcon, 'skill', { chip: chip.text('workflow_id') }), + await_workflow: spec('waitForWorkflow', HourglassIcon, 'skill'), + run_flow: spec('runWorkflow', WorkflowIcon, 'skill', { chip: chip.text('name', 'flow_id') }), + propose_workflow: spec('designWorkflow', WorkflowIcon, 'skill', { chip: chip.text('name') }), + revise_workflow: spec('designWorkflow', WorkflowIcon, 'skill'), + edit_workflow: spec('designWorkflow', WorkflowIcon, 'skill'), + create_workflow: spec('designWorkflow', WorkflowIcon, 'skill', { chip: chip.text('name') }), + duplicate_flow: spec('saveWorkflow', WorkflowIcon, 'skill'), + save_workflow: spec('saveWorkflow', WorkflowIcon, 'skill', { chip: chip.text('name') }), + validate_workflow: spec('validateWorkflow', WorkflowIcon, 'skill'), + dry_run_workflow: spec('testWorkflow', WorkflowIcon, 'skill'), + cancel_flow_run: spec('cancelWorkflow', WorkflowIcon, 'skill'), + resume_flow_run: spec('runWorkflow', WorkflowIcon, 'skill'), + suggest_workflows: spec('suggestWorkflows', WorkflowIcon, 'skill'), + + // ── Settings and platform ────────────────────────────────────────────── + security_policy_info: spec('checkSecurity', ShieldIcon, 'system'), + credential_list: spec('checkSecurity', ShieldIcon, 'system'), + session_state: spec('checkSecurity', ShieldIcon, 'system'), + oauth_connect_url: spec('connectApp', Link2Icon, 'app', { chip: chip.text('provider') }), + oauth_list: spec('checkConnections', PlugIcon, 'app'), + dashboard_model_health: spec('runDiagnostics', StethoscopeIcon, 'system'), + workspace_read_persona: spec('readPersona', UserRoundIcon, 'system'), + workspace_update_persona: spec('updatePersona', UserRoundIcon, 'system'), + workspace_reset_persona: spec('updatePersona', UserRoundIcon, 'system'), + workspace_init: spec('setUpWorkspace', FolderOpenIcon, 'system'), + artifact_delete: spec('deleteArtifact', PackageIcon, 'system'), +}; + +/** + * Collapsed tools that do different things per argument. Keyed by tool name, + * then by the argument named in `arg`. A value the table does not list falls + * back to the tool's {@link EXACT_TOOL_SPECS} entry. + */ +export const ACTION_TOOL_SPECS: Record<string, { arg: string; specs: Record<string, ToolSpec> }> = { + memory: { + arg: 'action', + specs: { + recall: spec('recallMemories', BrainCircuitIcon, 'memory', { chip: chip.query() }), + store: spec('saveToMemory', SaveIcon, 'memory', { chip: chip.text('key', 'content') }), + forget: spec('forgetMemory', EraserIcon, 'memory', { chip: chip.text('key') }), + hybrid_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + vector_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + raw_search: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }), + chunk_context: spec('inspectMemory', BrainIcon, 'memory'), + raw_chunks: spec('inspectMemory', BrainIcon, 'memory'), + kinds: spec('inspectMemory', BrainIcon, 'memory'), + flavour: spec('inspectMemory', BrainIcon, 'memory'), + doctor: spec('inspectMemory', StethoscopeIcon, 'memory'), + }, + }, + memory_tree: { + arg: 'mode', + specs: { + ingest_document: spec('saveDocumentToMemory', SaveIcon, 'memory', { + chip: chip.text('title', 'path'), + }), + }, + }, + goals: { + arg: 'op', + specs: { + list: spec('reviewGoals', TargetIcon, 'memory'), + add: spec('updateGoals', TargetIcon, 'memory', { chip: chip.text('text', 'goal') }), + edit: spec('updateGoals', TargetIcon, 'memory', { chip: chip.text('text', 'goal') }), + delete: spec('updateGoals', TargetIcon, 'memory'), + }, + }, + cron: { + arg: 'action', + specs: { + list: spec('checkSchedules', CalendarClockIcon, 'schedule'), + add: spec('scheduleTask', CalendarClockIcon, 'schedule', { chip: chip.text('name') }), + update: spec('updateSchedule', CalendarClockIcon, 'schedule', { chip: chip.text('name') }), + remove: spec('removeSchedule', CalendarClockIcon, 'schedule'), + run: spec('runScheduledTask', CalendarClockIcon, 'schedule'), + runs: spec('checkRunHistory', CalendarClockIcon, 'schedule'), + }, + }, + schedule: { + arg: 'action', + specs: { + list: spec('checkSchedules', CalendarClockIcon, 'schedule'), + get: spec('checkSchedules', CalendarClockIcon, 'schedule'), + cancel: spec('removeSchedule', CalendarClockIcon, 'schedule'), + remove: spec('removeSchedule', CalendarClockIcon, 'schedule'), + pause: spec('updateSchedule', CalendarClockIcon, 'schedule'), + resume: spec('updateSchedule', CalendarClockIcon, 'schedule'), + }, + }, + browser: { + arg: 'action', + specs: { + open: spec('openPage', AppWindowIcon, 'browser', { chip: chip.url() }), + snapshot: spec('takeScreenshot', CameraIcon, 'browser'), + click: spec('click', MousePointerClickIcon, 'browser', { chip: chip.text('selector') }), + mouse_click: spec('click', MousePointerClickIcon, 'browser'), + hover: spec('click', MousePointerClickIcon, 'browser', { chip: chip.text('selector') }), + fill: spec('typeKeys', KeyboardIcon, 'browser', { chip: chip.text('selector') }), + type: spec('typeKeys', KeyboardIcon, 'browser', { chip: chip.text('selector') }), + key_type: spec('typeKeys', KeyboardIcon, 'browser'), + key_press: spec('typeKeys', KeyboardIcon, 'browser', { chip: chip.text('key') }), + press: spec('typeKeys', KeyboardIcon, 'browser', { chip: chip.text('key') }), + scroll: spec('scrollPage', AppWindowIcon, 'browser'), + get_text: spec('readPage', AppWindowIcon, 'browser'), + get_title: spec('readPage', AppWindowIcon, 'browser'), + get_url: spec('readPage', AppWindowIcon, 'browser'), + find: spec('readPage', AppWindowIcon, 'browser', { chip: chip.text('value', 'selector') }), + is_visible: spec('readPage', AppWindowIcon, 'browser'), + wait: spec('wait', HourglassIcon, 'browser'), + }, + }, +}; + +/** + * Prefix families. Ordered: the first matching rule wins, so a narrower rule + * must precede a broader one that shares its prefix. + */ +export const FAMILY_TOOL_SPECS: ReadonlyArray<{ test: RegExp; spec: ToolSpec }> = [ + { test: /^memory_/, spec: spec('searchMemory', BrainIcon, 'memory', { chip: chip.query() }) }, + { + test: /^learning_(update|pin|unpin|forget|rebuild|reset|save|enrich)/, + spec: spec('updateLearnings', GraduationCapIcon, 'memory'), + }, + { test: /^learning_/, spec: spec('reviewLearnings', GraduationCapIcon, 'memory') }, + { + test: /^skill_registry_(install)/, + spec: spec('installSkill', SparklesIcon, 'skill', { chip: chip.text('name', 'id') }), + }, + { test: /^skill_registry_uninstall/, spec: spec('removeSkill', SparklesIcon, 'skill') }, + { + test: /^skill_registry_search/, + spec: spec('searchSkills', SparklesIcon, 'skill', { chip: chip.query() }), + }, + { test: /^(skill_|skill_runtime_)/, spec: spec('checkSkills', SparklesIcon, 'skill') }, + { + test: /^(list_workflows|describe_workflow|read_workflow_|list_workflow_runs|list_flows|get_flow|list_flow_|get_tool_|list_agent_definitions|list_connectable_toolkits|list_node_kinds|get_node_kind_contract)/, + spec: spec('checkWorkflows', WorkflowIcon, 'skill'), + }, + { + test: /^task_source_(add|update|remove)/, + spec: spec('updateTaskSources', ListChecksIcon, 'app'), + }, + { test: /^task_source_(fetch|list_tasks)/, spec: spec('fetchTasks', ListChecksIcon, 'app') }, + { test: /^task_source_/, spec: spec('checkTaskSources', ListChecksIcon, 'app') }, + { test: /^hosting_(set_env|add_domain)/, spec: spec('updateHosting', RocketIcon, 'storage') }, + { test: /^hosting_/, spec: spec('checkHosting', RocketIcon, 'storage') }, + { test: /^storage_/, spec: spec('listStoredFiles', HardDriveIcon, 'storage') }, + { + test: /^stock_/, + spec: spec('checkMarkets', TrendingUpIcon, 'app', { chip: chip.text('symbol') }), + }, + { + test: /^wallet_(tx_|lookup_tx)/, + spec: spec('checkTransaction', WalletIcon, 'wallet', { chip: chip.text('tx_hash', 'hash') }), + }, + { test: /^(wallet_|web3_)/, spec: spec('checkWallet', WalletIcon, 'wallet') }, + { test: /^composio_/, spec: spec('runAppAction', PlugIcon, 'app') }, + { test: /^mcp_/, spec: spec('checkMcpServers', ServerIcon, 'mcp') }, + { test: /^config_/, spec: spec('checkSettings', SettingsIcon, 'system') }, + { test: /^(daemon_host_prefs_|service_)/, spec: spec('manageService', PowerIcon, 'system') }, + { test: /^(doctor_|health_)/, spec: spec('runDiagnostics', StethoscopeIcon, 'system') }, + { test: /^cost_/, spec: spec('checkUsageCosts', ReceiptIcon, 'system') }, + { test: /^artifact_/, spec: spec('checkArtifacts', PackageIcon, 'system') }, + { test: /^cron_/, spec: spec('checkSchedules', CalendarClockIcon, 'schedule') }, + { test: /^goal_/, spec: spec('checkGoal', FlagIcon, 'plan') }, +]; + +/** + * Named agents, reached as `subagent:<id>`, as `spawn_subagent { agent_id }`, + * as `delegate_<id>`, or as the custom delegate tool names agent TOMLs + * declare (`delegate_name`). + */ +export const AGENT_SPECS: Record<string, ToolSpec> = { + researcher: spec('research', TelescopeIcon, 'agent'), + research: spec('research', TelescopeIcon, 'agent'), + context_scout: spec('scoutContext', LayersIcon, 'agent'), + orchestrator: spec('planNextSteps', BotIcon, 'agent'), + plan: spec('planNextSteps', BotIcon, 'agent'), + planner: spec('planNextSteps', BotIcon, 'agent'), + critic: spec('reviewWork', ClipboardCheckIcon, 'agent'), + review_code: spec('reviewWork', ClipboardCheckIcon, 'agent'), + tools_agent: spec('useTools', WrenchIcon, 'agent'), + code_executor: spec('runCode', SquareTerminalIcon, 'agent'), + run_code: spec('runCode', SquareTerminalIcon, 'agent'), + ask_docs: spec('searchDocs', BookOpenIcon, 'agent'), + create_image: spec('generateImage', ImagePlusIcon, 'agent'), + create_video: spec('generateVideo', ClapperboardIcon, 'agent'), + analyze_image: spec('analyzeImage', ScanEyeIcon, 'agent'), + make_presentation: spec('createPresentation', PresentationIcon, 'agent'), + do_crypto: spec('checkWallet', WalletIcon, 'agent'), + schedule_task: spec('scheduleTask', CalendarClockIcon, 'agent'), + manage_tasks: spec('checkTaskSources', ListChecksIcon, 'agent'), + manage_settings: spec('checkSettings', SettingsIcon, 'agent'), + use_mcp_server: spec('checkMcpTools', BlocksIcon, 'agent'), + curate_goals: spec('updateGoals', TargetIcon, 'agent'), + manage_profile_memory: spec('updateLearnings', GraduationCapIcon, 'agent'), + archive_session: spec('saveToMemory', SaveIcon, 'agent'), + retrieve_flow_context: spec('prepareContext', LayersIcon, 'agent'), +}; + +/** The integrations agent: labelled by the app it works in, when known. */ +export const INTEGRATIONS_AGENT_ID = 'integrations_agent'; diff --git a/app/src/features/human/SubMascotLayer.test.tsx b/app/src/features/human/SubMascotLayer.test.tsx index c2a0285cd0b..7203056dd9a 100644 --- a/app/src/features/human/SubMascotLayer.test.tsx +++ b/app/src/features/human/SubMascotLayer.test.tsx @@ -76,7 +76,7 @@ describe('subMascotModelsFromTimeline', () => { }), ]); - expect(running?.activity).toBe('Using Read File'); + expect(running?.activity).toBe('Reading file'); expect(running?.face).toBe('thinking'); // success and error are filtered out — only 1 model returned. expect( @@ -133,7 +133,9 @@ describe('subMascotModelsFromTimeline', () => { }), ]); - expect(models[0].activity).toBe('Using Fetching'); + // The label is already an activity; the old "Using " prefix made this + // "Using Fetching". + expect(models[0].activity).toBe('Reading webpage'); }); }); diff --git a/app/src/features/human/SubMascotLayer.tsx b/app/src/features/human/SubMascotLayer.tsx index 6cf9803159e..4a18335f8bc 100644 --- a/app/src/features/human/SubMascotLayer.tsx +++ b/app/src/features/human/SubMascotLayer.tsx @@ -85,7 +85,9 @@ function activityForEntry(entry: ToolTimelineEntry): string { const lastRunningTool = [...subagent.toolCalls].reverse().find(call => call.status === 'running'); if (lastRunningTool) { - return `Using ${formatToolName(lastRunningTool.toolName)}`; + // The label is already a present-tense activity ("Searching the web"); + // prefixing "Using" produced "Using Searching the web". + return formatToolName(lastRunningTool.toolName); } if (subagent.childIteration) { diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index a07ba02958b..06f60457a33 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -3239,6 +3239,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'لا يوجد ناتج بعد', 'conversations.subagent.input': 'المدخلات', 'conversations.subagent.output': 'المخرجات', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} خطوة', + 'conversations.tools.steps.other': '{count} خطوات', + 'conversations.tools.working': 'جارٍ العمل', + 'conversations.tools.noOutput': 'لا توجد مخرجات', + 'conversations.tools.delegatedTo': 'تم التفويض إلى {agent}', + 'conversations.tools.openInBrowser': 'فتح في المتصفح', + 'conversations.tools.status.running': 'قيد التشغيل', + 'conversations.tools.status.done': 'مكتمل', + 'conversations.tools.status.failed': 'فشل', + 'conversations.tools.status.cancelled': 'ملغى', + 'conversations.tools.status.awaiting': 'بانتظار الإدخال', + 'conversations.tools.search.searching': 'جارٍ البحث', + 'conversations.tools.search.none': 'لا توجد نتائج', + 'conversations.tools.search.found.one': 'تم العثور على {count} نتيجة', + 'conversations.tools.search.found.other': 'تم العثور على {count} نتائج', + 'conversations.tools.search.via': 'عبر {provider}', + 'conversations.tools.readFile.active': 'جارٍ قراءة الملف', + 'conversations.tools.readFile.done': 'تمت قراءة الملف', + 'conversations.tools.writeFile.active': 'جارٍ كتابة الملف', + 'conversations.tools.writeFile.done': 'تمت كتابة الملف', + 'conversations.tools.editFile.active': 'جارٍ تعديل الملف', + 'conversations.tools.editFile.done': 'تم تعديل الملف', + 'conversations.tools.applyEdits.active': 'جارٍ تطبيق التعديلات', + 'conversations.tools.applyEdits.done': 'تم تطبيق التعديلات', + 'conversations.tools.searchCode.active': 'جارٍ البحث في الكود', + 'conversations.tools.searchCode.done': 'تم البحث في الكود', + 'conversations.tools.findFiles.active': 'جارٍ البحث عن الملفات', + 'conversations.tools.findFiles.done': 'تم العثور على الملفات', + 'conversations.tools.listFolder.active': 'جارٍ عرض محتويات المجلد', + 'conversations.tools.listFolder.done': 'تم عرض محتويات المجلد', + 'conversations.tools.exportCsv.active': 'جارٍ تصدير CSV', + 'conversations.tools.exportCsv.done': 'تم تصدير CSV', + 'conversations.tools.updateMemoryNotes.active': 'جارٍ تحديث ملاحظات الذاكرة', + 'conversations.tools.updateMemoryNotes.done': 'تم تحديث ملاحظات الذاكرة', + 'conversations.tools.runGit.active': 'جارٍ تشغيل git', + 'conversations.tools.runGit.done': 'تم تشغيل git', + 'conversations.tools.readChanges.active': 'جارٍ قراءة التغييرات', + 'conversations.tools.readChanges.done': 'تمت قراءة التغييرات', + 'conversations.tools.runLinter.active': 'جارٍ تشغيل أداة فحص الكود', + 'conversations.tools.runLinter.done': 'تم تشغيل أداة فحص الكود', + 'conversations.tools.runTests.active': 'جارٍ تشغيل الاختبارات', + 'conversations.tools.runTests.done': 'تم تشغيل الاختبارات', + 'conversations.tools.analyzeCode.active': 'جارٍ تحليل الكود', + 'conversations.tools.analyzeCode.done': 'تم تحليل الكود', + 'conversations.tools.insertRecord.active': 'جارٍ إدراج سجل', + 'conversations.tools.insertRecord.done': 'تم إدراج السجل', + 'conversations.tools.runCommand.active': 'جارٍ تشغيل الأمر', + 'conversations.tools.runCommand.done': 'تم تشغيل الأمر', + 'conversations.tools.runCode.active': 'جارٍ تشغيل الكود', + 'conversations.tools.runCode.done': 'تم تشغيل الكود', + 'conversations.tools.runPackageManager.active': 'جارٍ تشغيل npm', + 'conversations.tools.runPackageManager.done': 'تم تشغيل npm', + 'conversations.tools.checkInstalledTools.active': 'جارٍ فحص الأدوات المثبتة', + 'conversations.tools.checkInstalledTools.done': 'تم فحص الأدوات المثبتة', + 'conversations.tools.installTool.active': 'جارٍ تثبيت الأداة', + 'conversations.tools.installTool.done': 'تم تثبيت الأداة', + 'conversations.tools.checkTime.active': 'جارٍ معرفة الوقت', + 'conversations.tools.checkTime.done': 'تمت معرفة الوقت', + 'conversations.tools.resolveDate.active': 'جارٍ تحديد التاريخ', + 'conversations.tools.resolveDate.done': 'تم تحديد التاريخ', + 'conversations.tools.retrieveOutput.active': 'جارٍ جلب المخرجات الكاملة', + 'conversations.tools.retrieveOutput.done': 'تم جلب المخرجات الكاملة', + 'conversations.tools.reviewWorkspace.active': 'جارٍ مراجعة مساحة العمل', + 'conversations.tools.reviewWorkspace.done': 'تمت مراجعة مساحة العمل', + 'conversations.tools.configureProxy.active': 'جارٍ إعداد الوكيل', + 'conversations.tools.configureProxy.done': 'تم إعداد الوكيل', + 'conversations.tools.checkUpdates.active': 'جارٍ البحث عن تحديثات', + 'conversations.tools.checkUpdates.done': 'تم البحث عن تحديثات', + 'conversations.tools.installUpdate.active': 'جارٍ تثبيت التحديث', + 'conversations.tools.installUpdate.done': 'تم تثبيت التحديث', + 'conversations.tools.sendNotification.active': 'جارٍ إرسال إشعار', + 'conversations.tools.sendNotification.done': 'تم إرسال الإشعار', + 'conversations.tools.reviewToolUsage.active': 'جارٍ مراجعة استخدام الأدوات', + 'conversations.tools.reviewToolUsage.done': 'تمت مراجعة استخدام الأدوات', + 'conversations.tools.typeKeys.active': 'جارٍ الكتابة', + 'conversations.tools.typeKeys.done': 'تمت الكتابة', + 'conversations.tools.click.active': 'جارٍ النقر', + 'conversations.tools.click.done': 'تم النقر', + 'conversations.tools.searchWeb.active': 'جارٍ البحث في الويب', + 'conversations.tools.searchWeb.done': 'تم البحث في الويب', + 'conversations.tools.searchNews.active': 'جارٍ البحث في الأخبار', + 'conversations.tools.searchNews.done': 'تم البحث في الأخبار', + 'conversations.tools.searchImages.active': 'جارٍ البحث عن الصور', + 'conversations.tools.searchImages.done': 'تم البحث عن الصور', + 'conversations.tools.searchVideos.active': 'جارٍ البحث عن الفيديوهات', + 'conversations.tools.searchVideos.done': 'تم البحث عن الفيديوهات', + 'conversations.tools.findSimilarPages.active': 'جارٍ البحث عن صفحات مشابهة', + 'conversations.tools.findSimilarPages.done': 'تم العثور على صفحات مشابهة', + 'conversations.tools.readPages.active': 'جارٍ قراءة الصفحات', + 'conversations.tools.readPages.done': 'تمت قراءة الصفحات', + 'conversations.tools.readWebpage.active': 'جارٍ قراءة صفحة الويب', + 'conversations.tools.readWebpage.done': 'تمت قراءة صفحة الويب', + 'conversations.tools.research.active': 'جارٍ البحث والتقصي', + 'conversations.tools.research.done': 'تم البحث والتقصي', + 'conversations.tools.enrichData.active': 'جارٍ إثراء البيانات', + 'conversations.tools.enrichData.done': 'تم إثراء البيانات', + 'conversations.tools.buildDataset.active': 'جارٍ بناء مجموعة البيانات', + 'conversations.tools.buildDataset.done': 'تم بناء مجموعة البيانات', + 'conversations.tools.askTheWeb.active': 'جارٍ سؤال الويب', + 'conversations.tools.askTheWeb.done': 'تم سؤال الويب', + 'conversations.tools.browseForYou.active': 'جارٍ التصفح نيابةً عنك', + 'conversations.tools.browseForYou.done': 'تم التصفح نيابةً عنك', + 'conversations.tools.callApi.active': 'جارٍ استدعاء API', + 'conversations.tools.callApi.done': 'تم استدعاء API', + 'conversations.tools.downloadFile.active': 'جارٍ تنزيل الملف', + 'conversations.tools.downloadFile.done': 'تم تنزيل الملف', + 'conversations.tools.makePaidRequest.active': 'جارٍ إرسال طلب مدفوع', + 'conversations.tools.makePaidRequest.done': 'تم إرسال طلب مدفوع', + 'conversations.tools.searchDocs.active': 'جارٍ البحث في الوثائق', + 'conversations.tools.searchDocs.done': 'تم البحث في الوثائق', + 'conversations.tools.readDocs.active': 'جارٍ قراءة الوثائق', + 'conversations.tools.readDocs.done': 'تمت قراءة الوثائق', + 'conversations.tools.useBrowser.active': 'جارٍ استخدام المتصفح', + 'conversations.tools.useBrowser.done': 'تم استخدام المتصفح', + 'conversations.tools.openPage.active': 'جارٍ فتح الصفحة', + 'conversations.tools.openPage.done': 'تم فتح الصفحة', + 'conversations.tools.navigate.active': 'جارٍ التنقل', + 'conversations.tools.navigate.done': 'تم التنقل', + 'conversations.tools.takeScreenshot.active': 'جارٍ التقاط لقطة شاشة', + 'conversations.tools.takeScreenshot.done': 'تم التقاط لقطة شاشة', + 'conversations.tools.scrollPage.active': 'جارٍ التمرير', + 'conversations.tools.scrollPage.done': 'تم التمرير', + 'conversations.tools.readPage.active': 'جارٍ قراءة الصفحة', + 'conversations.tools.readPage.done': 'تمت قراءة الصفحة', + 'conversations.tools.analyzeImage.active': 'جارٍ تحليل الصورة', + 'conversations.tools.analyzeImage.done': 'تم تحليل الصورة', + 'conversations.tools.generateImage.active': 'جارٍ إنشاء صورة', + 'conversations.tools.generateImage.done': 'تم إنشاء الصورة', + 'conversations.tools.generateVideo.active': 'جارٍ إنشاء فيديو', + 'conversations.tools.generateVideo.done': 'تم إنشاء الفيديو', + 'conversations.tools.checkMediaModels.active': 'جارٍ فحص نماذج الوسائط', + 'conversations.tools.checkMediaModels.done': 'تم فحص نماذج الوسائط', + 'conversations.tools.createDocument.active': 'جارٍ إنشاء مستند', + 'conversations.tools.createDocument.done': 'تم إنشاء المستند', + 'conversations.tools.createPresentation.active': 'جارٍ إنشاء عرض تقديمي', + 'conversations.tools.createPresentation.done': 'تم إنشاء العرض التقديمي', + 'conversations.tools.generatePodcast.active': 'جارٍ إنشاء بودكاست', + 'conversations.tools.generatePodcast.done': 'تم إنشاء البودكاست', + 'conversations.tools.emailPodcast.active': 'جارٍ إرسال البودكاست بالبريد', + 'conversations.tools.emailPodcast.done': 'تم إرسال البودكاست بالبريد', + 'conversations.tools.createAndEmailPodcast.active': 'جارٍ إنشاء البودكاست وإرساله بالبريد', + 'conversations.tools.createAndEmailPodcast.done': 'تم إنشاء البودكاست وإرساله بالبريد', + 'conversations.tools.recallMemories.active': 'جارٍ استرجاع الذكريات', + 'conversations.tools.recallMemories.done': 'تم استرجاع الذكريات', + 'conversations.tools.saveToMemory.active': 'جارٍ الحفظ في الذاكرة', + 'conversations.tools.saveToMemory.done': 'تم الحفظ في الذاكرة', + 'conversations.tools.forgetMemory.active': 'جارٍ حذف ذكرى', + 'conversations.tools.forgetMemory.done': 'تم حذف الذكرى', + 'conversations.tools.searchMemory.active': 'جارٍ البحث في الذاكرة', + 'conversations.tools.searchMemory.done': 'تم البحث في الذاكرة', + 'conversations.tools.inspectMemory.active': 'جارٍ فحص الذاكرة', + 'conversations.tools.inspectMemory.done': 'تم فحص الذاكرة', + 'conversations.tools.exploreMemory.active': 'جارٍ استكشاف الذاكرة', + 'conversations.tools.exploreMemory.done': 'تم استكشاف الذاكرة', + 'conversations.tools.saveDocumentToMemory.active': 'جارٍ حفظ المستند في الذاكرة', + 'conversations.tools.saveDocumentToMemory.done': 'تم حفظ المستند في الذاكرة', + 'conversations.tools.updateGoals.active': 'جارٍ تحديث الأهداف', + 'conversations.tools.updateGoals.done': 'تم تحديث الأهداف', + 'conversations.tools.reviewGoals.active': 'جارٍ مراجعة الأهداف', + 'conversations.tools.reviewGoals.done': 'تمت مراجعة الأهداف', + 'conversations.tools.savePreference.active': 'جارٍ حفظ التفضيل', + 'conversations.tools.savePreference.done': 'تم حفظ التفضيل', + 'conversations.tools.reviewLearnings.active': 'جارٍ مراجعة ما تعلمته', + 'conversations.tools.reviewLearnings.done': 'تمت مراجعة ما تعلمته', + 'conversations.tools.updateLearnings.active': 'جارٍ تحديث ما تعلمته', + 'conversations.tools.updateLearnings.done': 'تم تحديث ما تعلمته', + 'conversations.tools.delegateTask.active': 'جارٍ تفويض المهمة', + 'conversations.tools.delegateTask.done': 'تم تفويض المهمة', + 'conversations.tools.runAgentsInParallel.active': 'جارٍ تشغيل الوكلاء بالتوازي', + 'conversations.tools.runAgentsInParallel.done': 'تم تشغيل الوكلاء بالتوازي', + 'conversations.tools.messageAgent.active': 'جارٍ مراسلة الوكيل', + 'conversations.tools.messageAgent.done': 'تمت مراسلة الوكيل', + 'conversations.tools.waitForAgent.active': 'جارٍ انتظار الوكيل', + 'conversations.tools.waitForAgent.done': 'تم انتظار الوكيل', + 'conversations.tools.wait.active': 'جارٍ الانتظار', + 'conversations.tools.wait.done': 'تم الانتظار', + 'conversations.tools.closeAgent.active': 'جارٍ إغلاق الوكيل', + 'conversations.tools.closeAgent.done': 'تم إغلاق الوكيل', + 'conversations.tools.checkAgents.active': 'جارٍ فحص الوكلاء', + 'conversations.tools.checkAgents.done': 'تم فحص الوكلاء', + 'conversations.tools.askQuestion.active': 'جارٍ طرح سؤال عليك', + 'conversations.tools.askQuestion.done': 'تم طرح سؤال عليك', + 'conversations.tools.prepareContext.active': 'جارٍ تجهيز السياق', + 'conversations.tools.prepareContext.done': 'تم تجهيز السياق', + 'conversations.tools.extractDetails.active': 'جارٍ استخراج التفاصيل', + 'conversations.tools.extractDetails.done': 'تم استخراج التفاصيل', + 'conversations.tools.planNextSteps.active': 'جارٍ تخطيط الخطوات التالية', + 'conversations.tools.planNextSteps.done': 'تم تخطيط الخطوات التالية', + 'conversations.tools.reviewWork.active': 'جارٍ مراجعة العمل', + 'conversations.tools.reviewWork.done': 'تمت مراجعة العمل', + 'conversations.tools.scoutContext.active': 'جارٍ استطلاع السياق', + 'conversations.tools.scoutContext.done': 'تم استطلاع السياق', + 'conversations.tools.useTools.active': 'جارٍ استخدام الأدوات', + 'conversations.tools.useTools.done': 'تم استخدام الأدوات', + 'conversations.tools.checkConnectedApp.active': 'جارٍ فحص تطبيقك المتصل', + 'conversations.tools.checkConnectedApp.done': 'تم فحص تطبيقك المتصل', + 'conversations.tools.updateTodos.active': 'جارٍ تحديث قائمة المهام', + 'conversations.tools.updateTodos.done': 'تم تحديث قائمة المهام', + 'conversations.tools.requestPlanReview.active': 'جارٍ طلب مراجعة الخطة', + 'conversations.tools.requestPlanReview.done': 'تم طلب مراجعة الخطة', + 'conversations.tools.finishPlan.active': 'جارٍ إنهاء الخطة', + 'conversations.tools.finishPlan.done': 'تم إنهاء الخطة', + 'conversations.tools.setGoal.active': 'جارٍ تحديد الهدف', + 'conversations.tools.setGoal.done': 'تم تحديد الهدف', + 'conversations.tools.checkGoal.active': 'جارٍ فحص الهدف', + 'conversations.tools.checkGoal.done': 'تم فحص الهدف', + 'conversations.tools.completeGoal.active': 'جارٍ إكمال الهدف', + 'conversations.tools.completeGoal.done': 'تم إكمال الهدف', + 'conversations.tools.scheduleTask.active': 'جارٍ جدولة المهمة', + 'conversations.tools.scheduleTask.done': 'تمت جدولة المهمة', + 'conversations.tools.checkSchedules.active': 'جارٍ فحص الجداول', + 'conversations.tools.checkSchedules.done': 'تم فحص الجداول', + 'conversations.tools.updateSchedule.active': 'جارٍ تحديث المهمة المجدولة', + 'conversations.tools.updateSchedule.done': 'تم تحديث المهمة المجدولة', + 'conversations.tools.removeSchedule.active': 'جارٍ إزالة المهمة المجدولة', + 'conversations.tools.removeSchedule.done': 'تمت إزالة المهمة المجدولة', + 'conversations.tools.runScheduledTask.active': 'جارٍ تشغيل المهمة المجدولة', + 'conversations.tools.runScheduledTask.done': 'تم تشغيل المهمة المجدولة', + 'conversations.tools.checkRunHistory.active': 'جارٍ فحص سجل التشغيل', + 'conversations.tools.checkRunHistory.done': 'تم فحص سجل التشغيل', + 'conversations.tools.useApp.active': 'جارٍ استخدام {app}', + 'conversations.tools.useApp.done': 'تم استخدام {app}', + 'conversations.tools.checkAvailableApps.active': 'جارٍ فحص التطبيقات المتاحة', + 'conversations.tools.checkAvailableApps.done': 'تم فحص التطبيقات المتاحة', + 'conversations.tools.checkConnections.active': 'جارٍ فحص اتصالاتك', + 'conversations.tools.checkConnections.done': 'تم فحص اتصالاتك', + 'conversations.tools.connectApp.active': 'جارٍ ربط التطبيق', + 'conversations.tools.connectApp.done': 'تم ربط التطبيق', + 'conversations.tools.authorizeApp.active': 'جارٍ تفويض التطبيق', + 'conversations.tools.authorizeApp.done': 'تم تفويض التطبيق', + 'conversations.tools.findAppActions.active': 'جارٍ البحث عن إجراءات التطبيق', + 'conversations.tools.findAppActions.done': 'تم العثور على إجراءات التطبيق', + 'conversations.tools.runAppAction.active': 'جارٍ تنفيذ إجراء التطبيق', + 'conversations.tools.runAppAction.done': 'تم تنفيذ إجراء التطبيق', + 'conversations.tools.findTools.active': 'جارٍ البحث عن الأدوات', + 'conversations.tools.findTools.done': 'تم العثور على الأدوات', + 'conversations.tools.useTool.active': 'جارٍ استخدام {tool}', + 'conversations.tools.useTool.done': 'تم استخدام {tool}', + 'conversations.tools.unsubscribe.active': 'جارٍ إلغاء الاشتراك', + 'conversations.tools.unsubscribe.done': 'تم إلغاء الاشتراك', + 'conversations.tools.searchPlaces.active': 'جارٍ البحث عن أماكن', + 'conversations.tools.searchPlaces.done': 'تم البحث عن أماكن', + 'conversations.tools.lookUpPlace.active': 'جارٍ الاستعلام عن المكان', + 'conversations.tools.lookUpPlace.done': 'تم الاستعلام عن المكان', + 'conversations.tools.checkMarkets.active': 'جارٍ متابعة الأسواق', + 'conversations.tools.checkMarkets.done': 'تمت متابعة الأسواق', + 'conversations.tools.placeCall.active': 'جارٍ إجراء مكالمة', + 'conversations.tools.placeCall.done': 'تم إجراء المكالمة', + 'conversations.tools.checkTaskSources.active': 'جارٍ فحص مصادر المهام', + 'conversations.tools.checkTaskSources.done': 'تم فحص مصادر المهام', + 'conversations.tools.updateTaskSources.active': 'جارٍ تحديث مصادر المهام', + 'conversations.tools.updateTaskSources.done': 'تم تحديث مصادر المهام', + 'conversations.tools.fetchTasks.active': 'جارٍ جلب المهام', + 'conversations.tools.fetchTasks.done': 'تم جلب المهام', + 'conversations.tools.checkMcpServers.active': 'جارٍ فحص خوادم MCP', + 'conversations.tools.checkMcpServers.done': 'تم فحص خوادم MCP', + 'conversations.tools.checkMcpTools.active': 'جارٍ فحص أدوات MCP', + 'conversations.tools.checkMcpTools.done': 'تم فحص أدوات MCP', + 'conversations.tools.callMcpTool.active': 'جارٍ استدعاء {tool}', + 'conversations.tools.callMcpTool.done': 'تم استدعاء {tool}', + 'conversations.tools.searchMcpServers.active': 'جارٍ البحث عن خوادم MCP', + 'conversations.tools.searchMcpServers.done': 'تم البحث عن خوادم MCP', + 'conversations.tools.connectMcpServer.active': 'جارٍ الاتصال بخادم MCP', + 'conversations.tools.connectMcpServer.done': 'تم الاتصال بخادم MCP', + 'conversations.tools.disconnectMcpServer.active': 'جارٍ قطع الاتصال بخادم MCP', + 'conversations.tools.disconnectMcpServer.done': 'تم قطع الاتصال بخادم MCP', + 'conversations.tools.removeMcpServer.active': 'جارٍ إزالة خادم MCP', + 'conversations.tools.removeMcpServer.done': 'تمت إزالة خادم MCP', + 'conversations.tools.uploadFile.active': 'جارٍ رفع الملف', + 'conversations.tools.uploadFile.done': 'تم رفع الملف', + 'conversations.tools.listStoredFiles.active': 'جارٍ عرض الملفات المخزنة', + 'conversations.tools.listStoredFiles.done': 'تم عرض الملفات المخزنة', + 'conversations.tools.createShareLink.active': 'جارٍ إنشاء رابط مشاركة', + 'conversations.tools.createShareLink.done': 'تم إنشاء رابط المشاركة', + 'conversations.tools.deleteFile.active': 'جارٍ حذف الملف', + 'conversations.tools.deleteFile.done': 'تم حذف الملف', + 'conversations.tools.updateFileAccess.active': 'جارٍ تحديث صلاحيات الوصول للملف', + 'conversations.tools.updateFileAccess.done': 'تم تحديث صلاحيات الوصول للملف', + 'conversations.tools.deploySite.active': 'جارٍ نشر الموقع', + 'conversations.tools.deploySite.done': 'تم نشر الموقع', + 'conversations.tools.checkHosting.active': 'جارٍ فحص الاستضافة', + 'conversations.tools.checkHosting.done': 'تم فحص الاستضافة', + 'conversations.tools.updateHosting.active': 'جارٍ تحديث الاستضافة', + 'conversations.tools.updateHosting.done': 'تم تحديث الاستضافة', + 'conversations.tools.rollBackDeployment.active': 'جارٍ التراجع عن النشر', + 'conversations.tools.rollBackDeployment.done': 'تم التراجع عن النشر', + 'conversations.tools.checkWallet.active': 'جارٍ فحص المحفظة', + 'conversations.tools.checkWallet.done': 'تم فحص المحفظة', + 'conversations.tools.prepareTransfer.active': 'جارٍ تجهيز التحويل', + 'conversations.tools.prepareTransfer.done': 'تم تجهيز التحويل', + 'conversations.tools.checkTransaction.active': 'جارٍ فحص المعاملة', + 'conversations.tools.checkTransaction.done': 'تم فحص المعاملة', + 'conversations.tools.getSwapQuote.active': 'جارٍ جلب عرض سعر المبادلة', + 'conversations.tools.getSwapQuote.done': 'تم جلب عرض سعر المبادلة', + 'conversations.tools.swapTokens.active': 'جارٍ مبادلة الرموز', + 'conversations.tools.swapTokens.done': 'تمت مبادلة الرموز', + 'conversations.tools.getBridgeQuote.active': 'جارٍ جلب عرض سعر الجسر', + 'conversations.tools.getBridgeQuote.done': 'تم جلب عرض سعر الجسر', + 'conversations.tools.bridgeTokens.active': 'جارٍ نقل الرموز عبر الجسر', + 'conversations.tools.bridgeTokens.done': 'تم نقل الرموز عبر الجسر', + 'conversations.tools.callDapp.active': 'جارٍ استدعاء عقد التطبيق', + 'conversations.tools.callDapp.done': 'تم استدعاء عقد التطبيق', + 'conversations.tools.useSkill.active': 'جارٍ استخدام المهارة', + 'conversations.tools.useSkill.done': 'تم استخدام المهارة', + 'conversations.tools.searchSkills.active': 'جارٍ البحث عن المهارات', + 'conversations.tools.searchSkills.done': 'تم البحث عن المهارات', + 'conversations.tools.checkSkills.active': 'جارٍ فحص المهارات', + 'conversations.tools.checkSkills.done': 'تم فحص المهارات', + 'conversations.tools.installSkill.active': 'جارٍ تثبيت المهارة', + 'conversations.tools.installSkill.done': 'تم تثبيت المهارة', + 'conversations.tools.removeSkill.active': 'جارٍ إزالة المهارة', + 'conversations.tools.removeSkill.done': 'تمت إزالة المهارة', + 'conversations.tools.createSkill.active': 'جارٍ إنشاء المهارة', + 'conversations.tools.createSkill.done': 'تم إنشاء المهارة', + 'conversations.tools.runWorkflow.active': 'جارٍ تشغيل سير العمل', + 'conversations.tools.runWorkflow.done': 'تم تشغيل سير العمل', + 'conversations.tools.waitForWorkflow.active': 'جارٍ انتظار سير العمل', + 'conversations.tools.waitForWorkflow.done': 'تم انتظار سير العمل', + 'conversations.tools.designWorkflow.active': 'جارٍ تصميم سير العمل', + 'conversations.tools.designWorkflow.done': 'تم تصميم سير العمل', + 'conversations.tools.saveWorkflow.active': 'جارٍ حفظ سير العمل', + 'conversations.tools.saveWorkflow.done': 'تم حفظ سير العمل', + 'conversations.tools.validateWorkflow.active': 'جارٍ التحقق من سير العمل', + 'conversations.tools.validateWorkflow.done': 'تم التحقق من سير العمل', + 'conversations.tools.testWorkflow.active': 'جارٍ اختبار سير العمل', + 'conversations.tools.testWorkflow.done': 'تم اختبار سير العمل', + 'conversations.tools.checkWorkflows.active': 'جارٍ فحص مسارات العمل', + 'conversations.tools.checkWorkflows.done': 'تم فحص مسارات العمل', + 'conversations.tools.cancelWorkflow.active': 'جارٍ إلغاء تشغيل سير العمل', + 'conversations.tools.cancelWorkflow.done': 'تم إلغاء تشغيل سير العمل', + 'conversations.tools.suggestWorkflows.active': 'جارٍ اقتراح مسارات عمل', + 'conversations.tools.suggestWorkflows.done': 'تم اقتراح مسارات عمل', + 'conversations.tools.checkSettings.active': 'جارٍ فحص الإعدادات', + 'conversations.tools.checkSettings.done': 'تم فحص الإعدادات', + 'conversations.tools.checkSecurity.active': 'جارٍ فحص الأمان', + 'conversations.tools.checkSecurity.done': 'تم فحص الأمان', + 'conversations.tools.runDiagnostics.active': 'جارٍ تشغيل التشخيص', + 'conversations.tools.runDiagnostics.done': 'تم تشغيل التشخيص', + 'conversations.tools.checkUsageCosts.active': 'جارٍ فحص تكاليف الاستخدام', + 'conversations.tools.checkUsageCosts.done': 'تم فحص تكاليف الاستخدام', + 'conversations.tools.manageService.active': 'جارٍ إدارة خدمة الخلفية', + 'conversations.tools.manageService.done': 'تمت إدارة خدمة الخلفية', + 'conversations.tools.readPersona.active': 'جارٍ قراءة الشخصية', + 'conversations.tools.readPersona.done': 'تمت قراءة الشخصية', + 'conversations.tools.updatePersona.active': 'جارٍ تحديث الشخصية', + 'conversations.tools.updatePersona.done': 'تم تحديث الشخصية', + 'conversations.tools.setUpWorkspace.active': 'جارٍ إعداد مساحة العمل', + 'conversations.tools.setUpWorkspace.done': 'تم إعداد مساحة العمل', + 'conversations.tools.checkArtifacts.active': 'جارٍ فحص الملفات المُنشأة', + 'conversations.tools.checkArtifacts.done': 'تم فحص الملفات المُنشأة', + 'conversations.tools.deleteArtifact.active': 'جارٍ حذف الملف المُنشأ', + 'conversations.tools.deleteArtifact.done': 'تم حذف الملف المُنشأ', 'conversations.subagent.noOutput': 'لم يتم إرجاع أي مخرجات', 'conversations.subagent.close': 'إغلاق', 'conversations.subagent.cancel': 'إلغاء المهمة', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 8898d5d5c67..e2559fee698 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -3316,6 +3316,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'এখনও কোনো আউটপুট নেই', 'conversations.subagent.input': 'ইনপুট', 'conversations.subagent.output': 'আউটপুট', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count}টি ধাপ', + 'conversations.tools.steps.other': '{count}টি ধাপ', + 'conversations.tools.working': 'কাজ চলছে', + 'conversations.tools.noOutput': 'কোনো আউটপুট নেই', + 'conversations.tools.delegatedTo': '{agent}-কে দায়িত্ব দেওয়া হয়েছে', + 'conversations.tools.openInBrowser': 'ব্রাউজারে খুলুন', + 'conversations.tools.status.running': 'চলছে', + 'conversations.tools.status.done': 'সম্পন্ন', + 'conversations.tools.status.failed': 'ব্যর্থ', + 'conversations.tools.status.cancelled': 'বাতিল', + 'conversations.tools.status.awaiting': 'ইনপুটের অপেক্ষায়', + 'conversations.tools.search.searching': 'খোঁজা হচ্ছে', + 'conversations.tools.search.none': 'কোনো ফলাফল নেই', + 'conversations.tools.search.found.one': '{count}টি ফলাফল পাওয়া গেছে', + 'conversations.tools.search.found.other': '{count}টি ফলাফল পাওয়া গেছে', + 'conversations.tools.search.via': '{provider}-এর মাধ্যমে', + 'conversations.tools.readFile.active': 'ফাইল পড়া হচ্ছে', + 'conversations.tools.readFile.done': 'ফাইল পড়া হয়েছে', + 'conversations.tools.writeFile.active': 'ফাইল লেখা হচ্ছে', + 'conversations.tools.writeFile.done': 'ফাইল লেখা হয়েছে', + 'conversations.tools.editFile.active': 'ফাইল সম্পাদনা করা হচ্ছে', + 'conversations.tools.editFile.done': 'ফাইল সম্পাদনা করা হয়েছে', + 'conversations.tools.applyEdits.active': 'সম্পাদনা প্রয়োগ করা হচ্ছে', + 'conversations.tools.applyEdits.done': 'সম্পাদনা প্রয়োগ করা হয়েছে', + 'conversations.tools.searchCode.active': 'কোড খোঁজা হচ্ছে', + 'conversations.tools.searchCode.done': 'কোড খোঁজা হয়েছে', + 'conversations.tools.findFiles.active': 'ফাইল খোঁজা হচ্ছে', + 'conversations.tools.findFiles.done': 'ফাইল পাওয়া গেছে', + 'conversations.tools.listFolder.active': 'ফোল্ডারের তালিকা তৈরি হচ্ছে', + 'conversations.tools.listFolder.done': 'ফোল্ডারের তালিকা তৈরি হয়েছে', + 'conversations.tools.exportCsv.active': 'CSV রপ্তানি করা হচ্ছে', + 'conversations.tools.exportCsv.done': 'CSV রপ্তানি করা হয়েছে', + 'conversations.tools.updateMemoryNotes.active': 'মেমরি নোট হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateMemoryNotes.done': 'মেমরি নোট হালনাগাদ করা হয়েছে', + 'conversations.tools.runGit.active': 'git চালানো হচ্ছে', + 'conversations.tools.runGit.done': 'git চালানো হয়েছে', + 'conversations.tools.readChanges.active': 'পরিবর্তন পড়া হচ্ছে', + 'conversations.tools.readChanges.done': 'পরিবর্তন পড়া হয়েছে', + 'conversations.tools.runLinter.active': 'লিন্টার চালানো হচ্ছে', + 'conversations.tools.runLinter.done': 'লিন্টার চালানো হয়েছে', + 'conversations.tools.runTests.active': 'টেস্ট চালানো হচ্ছে', + 'conversations.tools.runTests.done': 'টেস্ট চালানো হয়েছে', + 'conversations.tools.analyzeCode.active': 'কোড বিশ্লেষণ করা হচ্ছে', + 'conversations.tools.analyzeCode.done': 'কোড বিশ্লেষণ করা হয়েছে', + 'conversations.tools.insertRecord.active': 'রেকর্ড যোগ করা হচ্ছে', + 'conversations.tools.insertRecord.done': 'রেকর্ড যোগ করা হয়েছে', + 'conversations.tools.runCommand.active': 'কমান্ড চালানো হচ্ছে', + 'conversations.tools.runCommand.done': 'কমান্ড চালানো হয়েছে', + 'conversations.tools.runCode.active': 'কোড চালানো হচ্ছে', + 'conversations.tools.runCode.done': 'কোড চালানো হয়েছে', + 'conversations.tools.runPackageManager.active': 'npm চালানো হচ্ছে', + 'conversations.tools.runPackageManager.done': 'npm চালানো হয়েছে', + 'conversations.tools.checkInstalledTools.active': 'ইনস্টল করা টুল যাচাই করা হচ্ছে', + 'conversations.tools.checkInstalledTools.done': 'ইনস্টল করা টুল যাচাই করা হয়েছে', + 'conversations.tools.installTool.active': 'টুল ইনস্টল করা হচ্ছে', + 'conversations.tools.installTool.done': 'টুল ইনস্টল করা হয়েছে', + 'conversations.tools.checkTime.active': 'সময় দেখা হচ্ছে', + 'conversations.tools.checkTime.done': 'সময় দেখা হয়েছে', + 'conversations.tools.resolveDate.active': 'তারিখ নির্ণয় করা হচ্ছে', + 'conversations.tools.resolveDate.done': 'তারিখ নির্ণয় করা হয়েছে', + 'conversations.tools.retrieveOutput.active': 'সম্পূর্ণ আউটপুট আনা হচ্ছে', + 'conversations.tools.retrieveOutput.done': 'সম্পূর্ণ আউটপুট আনা হয়েছে', + 'conversations.tools.reviewWorkspace.active': 'ওয়ার্কস্পেস পর্যালোচনা করা হচ্ছে', + 'conversations.tools.reviewWorkspace.done': 'ওয়ার্কস্পেস পর্যালোচনা করা হয়েছে', + 'conversations.tools.configureProxy.active': 'প্রক্সি কনফিগার করা হচ্ছে', + 'conversations.tools.configureProxy.done': 'প্রক্সি কনফিগার করা হয়েছে', + 'conversations.tools.checkUpdates.active': 'আপডেট খোঁজা হচ্ছে', + 'conversations.tools.checkUpdates.done': 'আপডেট খোঁজা হয়েছে', + 'conversations.tools.installUpdate.active': 'আপডেট ইনস্টল করা হচ্ছে', + 'conversations.tools.installUpdate.done': 'আপডেট ইনস্টল করা হয়েছে', + 'conversations.tools.sendNotification.active': 'বিজ্ঞপ্তি পাঠানো হচ্ছে', + 'conversations.tools.sendNotification.done': 'বিজ্ঞপ্তি পাঠানো হয়েছে', + 'conversations.tools.reviewToolUsage.active': 'টুল ব্যবহার পর্যালোচনা করা হচ্ছে', + 'conversations.tools.reviewToolUsage.done': 'টুল ব্যবহার পর্যালোচনা করা হয়েছে', + 'conversations.tools.typeKeys.active': 'টাইপ করা হচ্ছে', + 'conversations.tools.typeKeys.done': 'টাইপ করা হয়েছে', + 'conversations.tools.click.active': 'ক্লিক করা হচ্ছে', + 'conversations.tools.click.done': 'ক্লিক করা হয়েছে', + 'conversations.tools.searchWeb.active': 'ওয়েবে খোঁজা হচ্ছে', + 'conversations.tools.searchWeb.done': 'ওয়েবে খোঁজা হয়েছে', + 'conversations.tools.searchNews.active': 'খবর খোঁজা হচ্ছে', + 'conversations.tools.searchNews.done': 'খবর খোঁজা হয়েছে', + 'conversations.tools.searchImages.active': 'ছবি খোঁজা হচ্ছে', + 'conversations.tools.searchImages.done': 'ছবি খোঁজা হয়েছে', + 'conversations.tools.searchVideos.active': 'ভিডিও খোঁজা হচ্ছে', + 'conversations.tools.searchVideos.done': 'ভিডিও খোঁজা হয়েছে', + 'conversations.tools.findSimilarPages.active': 'অনুরূপ পেজ খোঁজা হচ্ছে', + 'conversations.tools.findSimilarPages.done': 'অনুরূপ পেজ পাওয়া গেছে', + 'conversations.tools.readPages.active': 'পেজগুলো পড়া হচ্ছে', + 'conversations.tools.readPages.done': 'পেজগুলো পড়া হয়েছে', + 'conversations.tools.readWebpage.active': 'ওয়েবপেজ পড়া হচ্ছে', + 'conversations.tools.readWebpage.done': 'ওয়েবপেজ পড়া হয়েছে', + 'conversations.tools.research.active': 'গবেষণা করা হচ্ছে', + 'conversations.tools.research.done': 'গবেষণা করা হয়েছে', + 'conversations.tools.enrichData.active': 'ডেটা সমৃদ্ধ করা হচ্ছে', + 'conversations.tools.enrichData.done': 'ডেটা সমৃদ্ধ করা হয়েছে', + 'conversations.tools.buildDataset.active': 'ডেটাসেট তৈরি হচ্ছে', + 'conversations.tools.buildDataset.done': 'ডেটাসেট তৈরি হয়েছে', + 'conversations.tools.askTheWeb.active': 'ওয়েবে জিজ্ঞাসা করা হচ্ছে', + 'conversations.tools.askTheWeb.done': 'ওয়েবে জিজ্ঞাসা করা হয়েছে', + 'conversations.tools.browseForYou.active': 'আপনার জন্য ব্রাউজ করা হচ্ছে', + 'conversations.tools.browseForYou.done': 'আপনার জন্য ব্রাউজ করা হয়েছে', + 'conversations.tools.callApi.active': 'API কল করা হচ্ছে', + 'conversations.tools.callApi.done': 'API কল করা হয়েছে', + 'conversations.tools.downloadFile.active': 'ফাইল ডাউনলোড হচ্ছে', + 'conversations.tools.downloadFile.done': 'ফাইল ডাউনলোড হয়েছে', + 'conversations.tools.makePaidRequest.active': 'পেইড অনুরোধ পাঠানো হচ্ছে', + 'conversations.tools.makePaidRequest.done': 'পেইড অনুরোধ পাঠানো হয়েছে', + 'conversations.tools.searchDocs.active': 'ডকুমেন্টেশন খোঁজা হচ্ছে', + 'conversations.tools.searchDocs.done': 'ডকুমেন্টেশন খোঁজা হয়েছে', + 'conversations.tools.readDocs.active': 'ডকুমেন্টেশন পড়া হচ্ছে', + 'conversations.tools.readDocs.done': 'ডকুমেন্টেশন পড়া হয়েছে', + 'conversations.tools.useBrowser.active': 'ব্রাউজার ব্যবহার করা হচ্ছে', + 'conversations.tools.useBrowser.done': 'ব্রাউজার ব্যবহার করা হয়েছে', + 'conversations.tools.openPage.active': 'পেজ খোলা হচ্ছে', + 'conversations.tools.openPage.done': 'পেজ খোলা হয়েছে', + 'conversations.tools.navigate.active': 'নেভিগেট করা হচ্ছে', + 'conversations.tools.navigate.done': 'নেভিগেট করা হয়েছে', + 'conversations.tools.takeScreenshot.active': 'স্ক্রিনশট নেওয়া হচ্ছে', + 'conversations.tools.takeScreenshot.done': 'স্ক্রিনশট নেওয়া হয়েছে', + 'conversations.tools.scrollPage.active': 'স্ক্রল করা হচ্ছে', + 'conversations.tools.scrollPage.done': 'স্ক্রল করা হয়েছে', + 'conversations.tools.readPage.active': 'পেজ পড়া হচ্ছে', + 'conversations.tools.readPage.done': 'পেজ পড়া হয়েছে', + 'conversations.tools.analyzeImage.active': 'ছবি বিশ্লেষণ করা হচ্ছে', + 'conversations.tools.analyzeImage.done': 'ছবি বিশ্লেষণ করা হয়েছে', + 'conversations.tools.generateImage.active': 'ছবি তৈরি হচ্ছে', + 'conversations.tools.generateImage.done': 'ছবি তৈরি হয়েছে', + 'conversations.tools.generateVideo.active': 'ভিডিও তৈরি হচ্ছে', + 'conversations.tools.generateVideo.done': 'ভিডিও তৈরি হয়েছে', + 'conversations.tools.checkMediaModels.active': 'মিডিয়া মডেল যাচাই করা হচ্ছে', + 'conversations.tools.checkMediaModels.done': 'মিডিয়া মডেল যাচাই করা হয়েছে', + 'conversations.tools.createDocument.active': 'ডকুমেন্ট তৈরি হচ্ছে', + 'conversations.tools.createDocument.done': 'ডকুমেন্ট তৈরি হয়েছে', + 'conversations.tools.createPresentation.active': 'প্রেজেন্টেশন তৈরি হচ্ছে', + 'conversations.tools.createPresentation.done': 'প্রেজেন্টেশন তৈরি হয়েছে', + 'conversations.tools.generatePodcast.active': 'পডকাস্ট তৈরি হচ্ছে', + 'conversations.tools.generatePodcast.done': 'পডকাস্ট তৈরি হয়েছে', + 'conversations.tools.emailPodcast.active': 'পডকাস্ট ইমেইল করা হচ্ছে', + 'conversations.tools.emailPodcast.done': 'পডকাস্ট ইমেইল করা হয়েছে', + 'conversations.tools.createAndEmailPodcast.active': 'পডকাস্ট তৈরি করে ইমেইল করা হচ্ছে', + 'conversations.tools.createAndEmailPodcast.done': 'পডকাস্ট তৈরি করে ইমেইল করা হয়েছে', + 'conversations.tools.recallMemories.active': 'স্মৃতি মনে করা হচ্ছে', + 'conversations.tools.recallMemories.done': 'স্মৃতি মনে করা হয়েছে', + 'conversations.tools.saveToMemory.active': 'মেমরিতে সংরক্ষণ করা হচ্ছে', + 'conversations.tools.saveToMemory.done': 'মেমরিতে সংরক্ষণ করা হয়েছে', + 'conversations.tools.forgetMemory.active': 'মেমরি মুছে ফেলা হচ্ছে', + 'conversations.tools.forgetMemory.done': 'মেমরি মুছে ফেলা হয়েছে', + 'conversations.tools.searchMemory.active': 'মেমরিতে খোঁজা হচ্ছে', + 'conversations.tools.searchMemory.done': 'মেমরিতে খোঁজা হয়েছে', + 'conversations.tools.inspectMemory.active': 'মেমরি পরীক্ষা করা হচ্ছে', + 'conversations.tools.inspectMemory.done': 'মেমরি পরীক্ষা করা হয়েছে', + 'conversations.tools.exploreMemory.active': 'মেমরি ঘেঁটে দেখা হচ্ছে', + 'conversations.tools.exploreMemory.done': 'মেমরি ঘেঁটে দেখা হয়েছে', + 'conversations.tools.saveDocumentToMemory.active': 'ডকুমেন্ট মেমরিতে সংরক্ষণ করা হচ্ছে', + 'conversations.tools.saveDocumentToMemory.done': 'ডকুমেন্ট মেমরিতে সংরক্ষণ করা হয়েছে', + 'conversations.tools.updateGoals.active': 'লক্ষ্য হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateGoals.done': 'লক্ষ্য হালনাগাদ করা হয়েছে', + 'conversations.tools.reviewGoals.active': 'লক্ষ্য পর্যালোচনা করা হচ্ছে', + 'conversations.tools.reviewGoals.done': 'লক্ষ্য পর্যালোচনা করা হয়েছে', + 'conversations.tools.savePreference.active': 'পছন্দ সংরক্ষণ করা হচ্ছে', + 'conversations.tools.savePreference.done': 'পছন্দ সংরক্ষণ করা হয়েছে', + 'conversations.tools.reviewLearnings.active': 'যা শিখেছি তা পর্যালোচনা করা হচ্ছে', + 'conversations.tools.reviewLearnings.done': 'যা শিখেছি তা পর্যালোচনা করা হয়েছে', + 'conversations.tools.updateLearnings.active': 'যা শিখেছি তা হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateLearnings.done': 'যা শিখেছি তা হালনাগাদ করা হয়েছে', + 'conversations.tools.delegateTask.active': 'কাজ অর্পণ করা হচ্ছে', + 'conversations.tools.delegateTask.done': 'কাজ অর্পণ করা হয়েছে', + 'conversations.tools.runAgentsInParallel.active': 'এজেন্টগুলো একসাথে চালানো হচ্ছে', + 'conversations.tools.runAgentsInParallel.done': 'এজেন্টগুলো একসাথে চালানো হয়েছে', + 'conversations.tools.messageAgent.active': 'এজেন্টকে বার্তা পাঠানো হচ্ছে', + 'conversations.tools.messageAgent.done': 'এজেন্টকে বার্তা পাঠানো হয়েছে', + 'conversations.tools.waitForAgent.active': 'এজেন্টের জন্য অপেক্ষা করা হচ্ছে', + 'conversations.tools.waitForAgent.done': 'এজেন্টের জন্য অপেক্ষা করা হয়েছে', + 'conversations.tools.wait.active': 'অপেক্ষা করা হচ্ছে', + 'conversations.tools.wait.done': 'অপেক্ষা করা হয়েছে', + 'conversations.tools.closeAgent.active': 'এজেন্ট বন্ধ করা হচ্ছে', + 'conversations.tools.closeAgent.done': 'এজেন্ট বন্ধ করা হয়েছে', + 'conversations.tools.checkAgents.active': 'এজেন্টগুলো যাচাই করা হচ্ছে', + 'conversations.tools.checkAgents.done': 'এজেন্টগুলো যাচাই করা হয়েছে', + 'conversations.tools.askQuestion.active': 'আপনাকে একটি প্রশ্ন করা হচ্ছে', + 'conversations.tools.askQuestion.done': 'আপনাকে একটি প্রশ্ন করা হয়েছে', + 'conversations.tools.prepareContext.active': 'প্রসঙ্গ প্রস্তুত করা হচ্ছে', + 'conversations.tools.prepareContext.done': 'প্রসঙ্গ প্রস্তুত করা হয়েছে', + 'conversations.tools.extractDetails.active': 'বিস্তারিত বের করা হচ্ছে', + 'conversations.tools.extractDetails.done': 'বিস্তারিত বের করা হয়েছে', + 'conversations.tools.planNextSteps.active': 'পরবর্তী ধাপের পরিকল্পনা করা হচ্ছে', + 'conversations.tools.planNextSteps.done': 'পরবর্তী ধাপের পরিকল্পনা করা হয়েছে', + 'conversations.tools.reviewWork.active': 'কাজ পর্যালোচনা করা হচ্ছে', + 'conversations.tools.reviewWork.done': 'কাজ পর্যালোচনা করা হয়েছে', + 'conversations.tools.scoutContext.active': 'প্রসঙ্গ অনুসন্ধান করা হচ্ছে', + 'conversations.tools.scoutContext.done': 'প্রসঙ্গ অনুসন্ধান করা হয়েছে', + 'conversations.tools.useTools.active': 'টুল ব্যবহার করা হচ্ছে', + 'conversations.tools.useTools.done': 'টুল ব্যবহার করা হয়েছে', + 'conversations.tools.checkConnectedApp.active': 'আপনার সংযুক্ত অ্যাপ যাচাই করা হচ্ছে', + 'conversations.tools.checkConnectedApp.done': 'আপনার সংযুক্ত অ্যাপ যাচাই করা হয়েছে', + 'conversations.tools.updateTodos.active': 'করণীয় তালিকা হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateTodos.done': 'করণীয় তালিকা হালনাগাদ করা হয়েছে', + 'conversations.tools.requestPlanReview.active': 'পরিকল্পনা পর্যালোচনার অনুরোধ করা হচ্ছে', + 'conversations.tools.requestPlanReview.done': 'পরিকল্পনা পর্যালোচনার অনুরোধ করা হয়েছে', + 'conversations.tools.finishPlan.active': 'পরিকল্পনা চূড়ান্ত করা হচ্ছে', + 'conversations.tools.finishPlan.done': 'পরিকল্পনা চূড়ান্ত করা হয়েছে', + 'conversations.tools.setGoal.active': 'লক্ষ্য নির্ধারণ করা হচ্ছে', + 'conversations.tools.setGoal.done': 'লক্ষ্য নির্ধারণ করা হয়েছে', + 'conversations.tools.checkGoal.active': 'লক্ষ্য যাচাই করা হচ্ছে', + 'conversations.tools.checkGoal.done': 'লক্ষ্য যাচাই করা হয়েছে', + 'conversations.tools.completeGoal.active': 'লক্ষ্য সম্পন্ন করা হচ্ছে', + 'conversations.tools.completeGoal.done': 'লক্ষ্য সম্পন্ন করা হয়েছে', + 'conversations.tools.scheduleTask.active': 'কাজের সময়সূচি নির্ধারণ করা হচ্ছে', + 'conversations.tools.scheduleTask.done': 'কাজের সময়সূচি নির্ধারণ করা হয়েছে', + 'conversations.tools.checkSchedules.active': 'সময়সূচি যাচাই করা হচ্ছে', + 'conversations.tools.checkSchedules.done': 'সময়সূচি যাচাই করা হয়েছে', + 'conversations.tools.updateSchedule.active': 'নির্ধারিত কাজ হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateSchedule.done': 'নির্ধারিত কাজ হালনাগাদ করা হয়েছে', + 'conversations.tools.removeSchedule.active': 'নির্ধারিত কাজ সরানো হচ্ছে', + 'conversations.tools.removeSchedule.done': 'নির্ধারিত কাজ সরানো হয়েছে', + 'conversations.tools.runScheduledTask.active': 'নির্ধারিত কাজ চালানো হচ্ছে', + 'conversations.tools.runScheduledTask.done': 'নির্ধারিত কাজ চালানো হয়েছে', + 'conversations.tools.checkRunHistory.active': 'চালানোর ইতিহাস যাচাই করা হচ্ছে', + 'conversations.tools.checkRunHistory.done': 'চালানোর ইতিহাস যাচাই করা হয়েছে', + 'conversations.tools.useApp.active': '{app} ব্যবহার করা হচ্ছে', + 'conversations.tools.useApp.done': '{app} ব্যবহার করা হয়েছে', + 'conversations.tools.checkAvailableApps.active': 'উপলব্ধ অ্যাপ যাচাই করা হচ্ছে', + 'conversations.tools.checkAvailableApps.done': 'উপলব্ধ অ্যাপ যাচাই করা হয়েছে', + 'conversations.tools.checkConnections.active': 'আপনার সংযোগ যাচাই করা হচ্ছে', + 'conversations.tools.checkConnections.done': 'আপনার সংযোগ যাচাই করা হয়েছে', + 'conversations.tools.connectApp.active': 'অ্যাপ সংযুক্ত করা হচ্ছে', + 'conversations.tools.connectApp.done': 'অ্যাপ সংযুক্ত করা হয়েছে', + 'conversations.tools.authorizeApp.active': 'অ্যাপ অনুমোদন করা হচ্ছে', + 'conversations.tools.authorizeApp.done': 'অ্যাপ অনুমোদন করা হয়েছে', + 'conversations.tools.findAppActions.active': 'অ্যাপের অ্যাকশন খোঁজা হচ্ছে', + 'conversations.tools.findAppActions.done': 'অ্যাপের অ্যাকশন পাওয়া গেছে', + 'conversations.tools.runAppAction.active': 'অ্যাপের অ্যাকশন চালানো হচ্ছে', + 'conversations.tools.runAppAction.done': 'অ্যাপের অ্যাকশন চালানো হয়েছে', + 'conversations.tools.findTools.active': 'টুল খোঁজা হচ্ছে', + 'conversations.tools.findTools.done': 'টুল পাওয়া গেছে', + 'conversations.tools.useTool.active': '{tool} ব্যবহার করা হচ্ছে', + 'conversations.tools.useTool.done': '{tool} ব্যবহার করা হয়েছে', + 'conversations.tools.unsubscribe.active': 'সদস্যতা বাতিল করা হচ্ছে', + 'conversations.tools.unsubscribe.done': 'সদস্যতা বাতিল করা হয়েছে', + 'conversations.tools.searchPlaces.active': 'স্থান খোঁজা হচ্ছে', + 'conversations.tools.searchPlaces.done': 'স্থান খোঁজা হয়েছে', + 'conversations.tools.lookUpPlace.active': 'স্থানের তথ্য দেখা হচ্ছে', + 'conversations.tools.lookUpPlace.done': 'স্থানের তথ্য দেখা হয়েছে', + 'conversations.tools.checkMarkets.active': 'বাজার দেখা হচ্ছে', + 'conversations.tools.checkMarkets.done': 'বাজার দেখা হয়েছে', + 'conversations.tools.placeCall.active': 'কল করা হচ্ছে', + 'conversations.tools.placeCall.done': 'কল করা হয়েছে', + 'conversations.tools.checkTaskSources.active': 'কাজের উৎস যাচাই করা হচ্ছে', + 'conversations.tools.checkTaskSources.done': 'কাজের উৎস যাচাই করা হয়েছে', + 'conversations.tools.updateTaskSources.active': 'কাজের উৎস হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateTaskSources.done': 'কাজের উৎস হালনাগাদ করা হয়েছে', + 'conversations.tools.fetchTasks.active': 'কাজ আনা হচ্ছে', + 'conversations.tools.fetchTasks.done': 'কাজ আনা হয়েছে', + 'conversations.tools.checkMcpServers.active': 'MCP সার্ভার যাচাই করা হচ্ছে', + 'conversations.tools.checkMcpServers.done': 'MCP সার্ভার যাচাই করা হয়েছে', + 'conversations.tools.checkMcpTools.active': 'MCP টুল যাচাই করা হচ্ছে', + 'conversations.tools.checkMcpTools.done': 'MCP টুল যাচাই করা হয়েছে', + 'conversations.tools.callMcpTool.active': '{tool} কল করা হচ্ছে', + 'conversations.tools.callMcpTool.done': '{tool} কল করা হয়েছে', + 'conversations.tools.searchMcpServers.active': 'MCP সার্ভার খোঁজা হচ্ছে', + 'conversations.tools.searchMcpServers.done': 'MCP সার্ভার খোঁজা হয়েছে', + 'conversations.tools.connectMcpServer.active': 'MCP সার্ভার সংযুক্ত করা হচ্ছে', + 'conversations.tools.connectMcpServer.done': 'MCP সার্ভার সংযুক্ত করা হয়েছে', + 'conversations.tools.disconnectMcpServer.active': 'MCP সার্ভার বিচ্ছিন্ন করা হচ্ছে', + 'conversations.tools.disconnectMcpServer.done': 'MCP সার্ভার বিচ্ছিন্ন করা হয়েছে', + 'conversations.tools.removeMcpServer.active': 'MCP সার্ভার সরানো হচ্ছে', + 'conversations.tools.removeMcpServer.done': 'MCP সার্ভার সরানো হয়েছে', + 'conversations.tools.uploadFile.active': 'ফাইল আপলোড হচ্ছে', + 'conversations.tools.uploadFile.done': 'ফাইল আপলোড হয়েছে', + 'conversations.tools.listStoredFiles.active': 'সংরক্ষিত ফাইলের তালিকা তৈরি হচ্ছে', + 'conversations.tools.listStoredFiles.done': 'সংরক্ষিত ফাইলের তালিকা তৈরি হয়েছে', + 'conversations.tools.createShareLink.active': 'শেয়ার লিংক তৈরি হচ্ছে', + 'conversations.tools.createShareLink.done': 'শেয়ার লিংক তৈরি হয়েছে', + 'conversations.tools.deleteFile.active': 'ফাইল মোছা হচ্ছে', + 'conversations.tools.deleteFile.done': 'ফাইল মোছা হয়েছে', + 'conversations.tools.updateFileAccess.active': 'ফাইলের অ্যাক্সেস হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateFileAccess.done': 'ফাইলের অ্যাক্সেস হালনাগাদ করা হয়েছে', + 'conversations.tools.deploySite.active': 'সাইট ডিপ্লয় করা হচ্ছে', + 'conversations.tools.deploySite.done': 'সাইট ডিপ্লয় করা হয়েছে', + 'conversations.tools.checkHosting.active': 'হোস্টিং যাচাই করা হচ্ছে', + 'conversations.tools.checkHosting.done': 'হোস্টিং যাচাই করা হয়েছে', + 'conversations.tools.updateHosting.active': 'হোস্টিং হালনাগাদ করা হচ্ছে', + 'conversations.tools.updateHosting.done': 'হোস্টিং হালনাগাদ করা হয়েছে', + 'conversations.tools.rollBackDeployment.active': 'ডিপ্লয়মেন্ট আগের অবস্থায় ফেরানো হচ্ছে', + 'conversations.tools.rollBackDeployment.done': 'ডিপ্লয়মেন্ট আগের অবস্থায় ফেরানো হয়েছে', + 'conversations.tools.checkWallet.active': 'ওয়ালেট যাচাই করা হচ্ছে', + 'conversations.tools.checkWallet.done': 'ওয়ালেট যাচাই করা হয়েছে', + 'conversations.tools.prepareTransfer.active': 'ট্রান্সফার প্রস্তুত করা হচ্ছে', + 'conversations.tools.prepareTransfer.done': 'ট্রান্সফার প্রস্তুত করা হয়েছে', + 'conversations.tools.checkTransaction.active': 'লেনদেন যাচাই করা হচ্ছে', + 'conversations.tools.checkTransaction.done': 'লেনদেন যাচাই করা হয়েছে', + 'conversations.tools.getSwapQuote.active': 'সোয়াপ কোট আনা হচ্ছে', + 'conversations.tools.getSwapQuote.done': 'সোয়াপ কোট আনা হয়েছে', + 'conversations.tools.swapTokens.active': 'টোকেন সোয়াপ করা হচ্ছে', + 'conversations.tools.swapTokens.done': 'টোকেন সোয়াপ করা হয়েছে', + 'conversations.tools.getBridgeQuote.active': 'ব্রিজ কোট আনা হচ্ছে', + 'conversations.tools.getBridgeQuote.done': 'ব্রিজ কোট আনা হয়েছে', + 'conversations.tools.bridgeTokens.active': 'টোকেন ব্রিজ করা হচ্ছে', + 'conversations.tools.bridgeTokens.done': 'টোকেন ব্রিজ করা হয়েছে', + 'conversations.tools.callDapp.active': 'অ্যাপ কন্ট্র্যাক্ট কল করা হচ্ছে', + 'conversations.tools.callDapp.done': 'অ্যাপ কন্ট্র্যাক্ট কল করা হয়েছে', + 'conversations.tools.useSkill.active': 'স্কিল ব্যবহার করা হচ্ছে', + 'conversations.tools.useSkill.done': 'স্কিল ব্যবহার করা হয়েছে', + 'conversations.tools.searchSkills.active': 'স্কিল খোঁজা হচ্ছে', + 'conversations.tools.searchSkills.done': 'স্কিল খোঁজা হয়েছে', + 'conversations.tools.checkSkills.active': 'স্কিল যাচাই করা হচ্ছে', + 'conversations.tools.checkSkills.done': 'স্কিল যাচাই করা হয়েছে', + 'conversations.tools.installSkill.active': 'স্কিল ইনস্টল করা হচ্ছে', + 'conversations.tools.installSkill.done': 'স্কিল ইনস্টল করা হয়েছে', + 'conversations.tools.removeSkill.active': 'স্কিল সরানো হচ্ছে', + 'conversations.tools.removeSkill.done': 'স্কিল সরানো হয়েছে', + 'conversations.tools.createSkill.active': 'স্কিল তৈরি হচ্ছে', + 'conversations.tools.createSkill.done': 'স্কিল তৈরি হয়েছে', + 'conversations.tools.runWorkflow.active': 'ওয়ার্কফ্লো চালানো হচ্ছে', + 'conversations.tools.runWorkflow.done': 'ওয়ার্কফ্লো চালানো হয়েছে', + 'conversations.tools.waitForWorkflow.active': 'ওয়ার্কফ্লোর জন্য অপেক্ষা করা হচ্ছে', + 'conversations.tools.waitForWorkflow.done': 'ওয়ার্কফ্লোর জন্য অপেক্ষা করা হয়েছে', + 'conversations.tools.designWorkflow.active': 'ওয়ার্কফ্লো ডিজাইন করা হচ্ছে', + 'conversations.tools.designWorkflow.done': 'ওয়ার্কফ্লো ডিজাইন করা হয়েছে', + 'conversations.tools.saveWorkflow.active': 'ওয়ার্কফ্লো সংরক্ষণ করা হচ্ছে', + 'conversations.tools.saveWorkflow.done': 'ওয়ার্কফ্লো সংরক্ষণ করা হয়েছে', + 'conversations.tools.validateWorkflow.active': 'ওয়ার্কফ্লো যাচাই করা হচ্ছে', + 'conversations.tools.validateWorkflow.done': 'ওয়ার্কফ্লো যাচাই করা হয়েছে', + 'conversations.tools.testWorkflow.active': 'ওয়ার্কফ্লো পরীক্ষা করা হচ্ছে', + 'conversations.tools.testWorkflow.done': 'ওয়ার্কফ্লো পরীক্ষা করা হয়েছে', + 'conversations.tools.checkWorkflows.active': 'ওয়ার্কফ্লোগুলো দেখা হচ্ছে', + 'conversations.tools.checkWorkflows.done': 'ওয়ার্কফ্লোগুলো দেখা হয়েছে', + 'conversations.tools.cancelWorkflow.active': 'ওয়ার্কফ্লো রান বাতিল করা হচ্ছে', + 'conversations.tools.cancelWorkflow.done': 'ওয়ার্কফ্লো রান বাতিল করা হয়েছে', + 'conversations.tools.suggestWorkflows.active': 'ওয়ার্কফ্লো প্রস্তাব করা হচ্ছে', + 'conversations.tools.suggestWorkflows.done': 'ওয়ার্কফ্লো প্রস্তাব করা হয়েছে', + 'conversations.tools.checkSettings.active': 'সেটিংস যাচাই করা হচ্ছে', + 'conversations.tools.checkSettings.done': 'সেটিংস যাচাই করা হয়েছে', + 'conversations.tools.checkSecurity.active': 'নিরাপত্তা যাচাই করা হচ্ছে', + 'conversations.tools.checkSecurity.done': 'নিরাপত্তা যাচাই করা হয়েছে', + 'conversations.tools.runDiagnostics.active': 'ডায়াগনস্টিক চালানো হচ্ছে', + 'conversations.tools.runDiagnostics.done': 'ডায়াগনস্টিক চালানো হয়েছে', + 'conversations.tools.checkUsageCosts.active': 'ব্যবহারের খরচ যাচাই করা হচ্ছে', + 'conversations.tools.checkUsageCosts.done': 'ব্যবহারের খরচ যাচাই করা হয়েছে', + 'conversations.tools.manageService.active': 'ব্যাকগ্রাউন্ড সার্ভিস পরিচালনা করা হচ্ছে', + 'conversations.tools.manageService.done': 'ব্যাকগ্রাউন্ড সার্ভিস পরিচালনা করা হয়েছে', + 'conversations.tools.readPersona.active': 'পারসোনা পড়া হচ্ছে', + 'conversations.tools.readPersona.done': 'পারসোনা পড়া হয়েছে', + 'conversations.tools.updatePersona.active': 'পারসোনা হালনাগাদ করা হচ্ছে', + 'conversations.tools.updatePersona.done': 'পারসোনা হালনাগাদ করা হয়েছে', + 'conversations.tools.setUpWorkspace.active': 'ওয়ার্কস্পেস সেট আপ করা হচ্ছে', + 'conversations.tools.setUpWorkspace.done': 'ওয়ার্কস্পেস সেট আপ করা হয়েছে', + 'conversations.tools.checkArtifacts.active': 'আর্টিফ্যাক্ট যাচাই করা হচ্ছে', + 'conversations.tools.checkArtifacts.done': 'আর্টিফ্যাক্ট যাচাই করা হয়েছে', + 'conversations.tools.deleteArtifact.active': 'আর্টিফ্যাক্ট মোছা হচ্ছে', + 'conversations.tools.deleteArtifact.done': 'আর্টিফ্যাক্ট মোছা হয়েছে', 'conversations.subagent.noOutput': 'কোনো আউটপুট ফেরত আসেনি', 'conversations.subagent.close': 'বন্ধ করুন', 'conversations.subagent.cancel': 'কাজ বাতিল করুন', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index b6e617482ef..b3305c903d2 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -3410,6 +3410,360 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Noch keine Ausgabe', 'conversations.subagent.input': 'Eingabe', 'conversations.subagent.output': 'Ausgabe', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} Schritt', + 'conversations.tools.steps.other': '{count} Schritte', + 'conversations.tools.working': 'Arbeitet', + 'conversations.tools.noOutput': 'Keine Ausgabe', + 'conversations.tools.delegatedTo': 'An {agent} delegiert', + 'conversations.tools.openInBrowser': 'Im Browser öffnen', + 'conversations.tools.status.running': 'läuft', + 'conversations.tools.status.done': 'fertig', + 'conversations.tools.status.failed': 'fehlgeschlagen', + 'conversations.tools.status.cancelled': 'abgebrochen', + 'conversations.tools.status.awaiting': 'wartet auf Eingabe', + 'conversations.tools.search.searching': 'Suche läuft', + 'conversations.tools.search.none': 'Keine Ergebnisse', + 'conversations.tools.search.found.one': '{count} Ergebnis gefunden', + 'conversations.tools.search.found.other': '{count} Ergebnisse gefunden', + 'conversations.tools.search.via': 'über {provider}', + 'conversations.tools.readFile.active': 'Datei wird gelesen', + 'conversations.tools.readFile.done': 'Datei gelesen', + 'conversations.tools.writeFile.active': 'Datei wird geschrieben', + 'conversations.tools.writeFile.done': 'Datei geschrieben', + 'conversations.tools.editFile.active': 'Datei wird bearbeitet', + 'conversations.tools.editFile.done': 'Datei bearbeitet', + 'conversations.tools.applyEdits.active': 'Änderungen werden angewendet', + 'conversations.tools.applyEdits.done': 'Änderungen angewendet', + 'conversations.tools.searchCode.active': 'Code wird durchsucht', + 'conversations.tools.searchCode.done': 'Code durchsucht', + 'conversations.tools.findFiles.active': 'Dateien werden gesucht', + 'conversations.tools.findFiles.done': 'Dateien gefunden', + 'conversations.tools.listFolder.active': 'Ordner wird aufgelistet', + 'conversations.tools.listFolder.done': 'Ordner aufgelistet', + 'conversations.tools.exportCsv.active': 'CSV wird exportiert', + 'conversations.tools.exportCsv.done': 'CSV exportiert', + 'conversations.tools.updateMemoryNotes.active': 'Gedächtnisnotizen werden aktualisiert', + 'conversations.tools.updateMemoryNotes.done': 'Gedächtnisnotizen aktualisiert', + 'conversations.tools.runGit.active': 'git wird ausgeführt', + 'conversations.tools.runGit.done': 'git ausgeführt', + 'conversations.tools.readChanges.active': 'Änderungen werden gelesen', + 'conversations.tools.readChanges.done': 'Änderungen gelesen', + 'conversations.tools.runLinter.active': 'Linter wird ausgeführt', + 'conversations.tools.runLinter.done': 'Linter ausgeführt', + 'conversations.tools.runTests.active': 'Tests werden ausgeführt', + 'conversations.tools.runTests.done': 'Tests ausgeführt', + 'conversations.tools.analyzeCode.active': 'Code wird analysiert', + 'conversations.tools.analyzeCode.done': 'Code analysiert', + 'conversations.tools.insertRecord.active': 'Datensatz wird eingefügt', + 'conversations.tools.insertRecord.done': 'Datensatz eingefügt', + 'conversations.tools.runCommand.active': 'Befehl wird ausgeführt', + 'conversations.tools.runCommand.done': 'Befehl ausgeführt', + 'conversations.tools.runCode.active': 'Code wird ausgeführt', + 'conversations.tools.runCode.done': 'Code ausgeführt', + 'conversations.tools.runPackageManager.active': 'npm wird ausgeführt', + 'conversations.tools.runPackageManager.done': 'npm ausgeführt', + 'conversations.tools.checkInstalledTools.active': 'Installierte Tools werden geprüft', + 'conversations.tools.checkInstalledTools.done': 'Installierte Tools geprüft', + 'conversations.tools.installTool.active': 'Tool wird installiert', + 'conversations.tools.installTool.done': 'Tool installiert', + 'conversations.tools.checkTime.active': 'Uhrzeit wird geprüft', + 'conversations.tools.checkTime.done': 'Uhrzeit geprüft', + 'conversations.tools.resolveDate.active': 'Datum wird ermittelt', + 'conversations.tools.resolveDate.done': 'Datum ermittelt', + 'conversations.tools.retrieveOutput.active': 'Vollständige Ausgabe wird abgerufen', + 'conversations.tools.retrieveOutput.done': 'Vollständige Ausgabe abgerufen', + 'conversations.tools.reviewWorkspace.active': 'Arbeitsbereich wird geprüft', + 'conversations.tools.reviewWorkspace.done': 'Arbeitsbereich geprüft', + 'conversations.tools.configureProxy.active': 'Proxy wird konfiguriert', + 'conversations.tools.configureProxy.done': 'Proxy konfiguriert', + 'conversations.tools.checkUpdates.active': 'Nach Updates wird gesucht', + 'conversations.tools.checkUpdates.done': 'Nach Updates gesucht', + 'conversations.tools.installUpdate.active': 'Update wird installiert', + 'conversations.tools.installUpdate.done': 'Update installiert', + 'conversations.tools.sendNotification.active': 'Benachrichtigung wird gesendet', + 'conversations.tools.sendNotification.done': 'Benachrichtigung gesendet', + 'conversations.tools.reviewToolUsage.active': 'Tool-Nutzung wird geprüft', + 'conversations.tools.reviewToolUsage.done': 'Tool-Nutzung geprüft', + 'conversations.tools.typeKeys.active': 'Tippt', + 'conversations.tools.typeKeys.done': 'Getippt', + 'conversations.tools.click.active': 'Klickt', + 'conversations.tools.click.done': 'Geklickt', + 'conversations.tools.searchWeb.active': 'Web wird durchsucht', + 'conversations.tools.searchWeb.done': 'Web durchsucht', + 'conversations.tools.searchNews.active': 'Nachrichten werden durchsucht', + 'conversations.tools.searchNews.done': 'Nachrichten durchsucht', + 'conversations.tools.searchImages.active': 'Bilder werden gesucht', + 'conversations.tools.searchImages.done': 'Bilder gesucht', + 'conversations.tools.searchVideos.active': 'Videos werden gesucht', + 'conversations.tools.searchVideos.done': 'Videos gesucht', + 'conversations.tools.findSimilarPages.active': 'Ähnliche Seiten werden gesucht', + 'conversations.tools.findSimilarPages.done': 'Ähnliche Seiten gefunden', + 'conversations.tools.readPages.active': 'Seiten werden gelesen', + 'conversations.tools.readPages.done': 'Seiten gelesen', + 'conversations.tools.readWebpage.active': 'Webseite wird gelesen', + 'conversations.tools.readWebpage.done': 'Webseite gelesen', + 'conversations.tools.research.active': 'Recherche läuft', + 'conversations.tools.research.done': 'Recherche abgeschlossen', + 'conversations.tools.enrichData.active': 'Daten werden angereichert', + 'conversations.tools.enrichData.done': 'Daten angereichert', + 'conversations.tools.buildDataset.active': 'Datensatz wird erstellt', + 'conversations.tools.buildDataset.done': 'Datensatz erstellt', + 'conversations.tools.askTheWeb.active': 'Web wird befragt', + 'conversations.tools.askTheWeb.done': 'Web befragt', + 'conversations.tools.browseForYou.active': 'Surft für dich', + 'conversations.tools.browseForYou.done': 'Für dich gesurft', + 'conversations.tools.callApi.active': 'API wird aufgerufen', + 'conversations.tools.callApi.done': 'API aufgerufen', + 'conversations.tools.downloadFile.active': 'Datei wird heruntergeladen', + 'conversations.tools.downloadFile.done': 'Datei heruntergeladen', + 'conversations.tools.makePaidRequest.active': 'Kostenpflichtige Anfrage läuft', + 'conversations.tools.makePaidRequest.done': 'Kostenpflichtige Anfrage gesendet', + 'conversations.tools.searchDocs.active': 'Dokumentation wird durchsucht', + 'conversations.tools.searchDocs.done': 'Dokumentation durchsucht', + 'conversations.tools.readDocs.active': 'Dokumentation wird gelesen', + 'conversations.tools.readDocs.done': 'Dokumentation gelesen', + 'conversations.tools.useBrowser.active': 'Browser wird verwendet', + 'conversations.tools.useBrowser.done': 'Browser verwendet', + 'conversations.tools.openPage.active': 'Seite wird geöffnet', + 'conversations.tools.openPage.done': 'Seite geöffnet', + 'conversations.tools.navigate.active': 'Navigation läuft', + 'conversations.tools.navigate.done': 'Navigiert', + 'conversations.tools.takeScreenshot.active': 'Screenshot wird erstellt', + 'conversations.tools.takeScreenshot.done': 'Screenshot erstellt', + 'conversations.tools.scrollPage.active': 'Scrollt', + 'conversations.tools.scrollPage.done': 'Gescrollt', + 'conversations.tools.readPage.active': 'Seite wird gelesen', + 'conversations.tools.readPage.done': 'Seite gelesen', + 'conversations.tools.analyzeImage.active': 'Bild wird analysiert', + 'conversations.tools.analyzeImage.done': 'Bild analysiert', + 'conversations.tools.generateImage.active': 'Bild wird generiert', + 'conversations.tools.generateImage.done': 'Bild generiert', + 'conversations.tools.generateVideo.active': 'Video wird generiert', + 'conversations.tools.generateVideo.done': 'Video generiert', + 'conversations.tools.checkMediaModels.active': 'Medienmodelle werden geprüft', + 'conversations.tools.checkMediaModels.done': 'Medienmodelle geprüft', + 'conversations.tools.createDocument.active': 'Dokument wird erstellt', + 'conversations.tools.createDocument.done': 'Dokument erstellt', + 'conversations.tools.createPresentation.active': 'Präsentation wird erstellt', + 'conversations.tools.createPresentation.done': 'Präsentation erstellt', + 'conversations.tools.generatePodcast.active': 'Podcast wird generiert', + 'conversations.tools.generatePodcast.done': 'Podcast generiert', + 'conversations.tools.emailPodcast.active': 'Podcast wird per E-Mail gesendet', + 'conversations.tools.emailPodcast.done': 'Podcast per E-Mail gesendet', + 'conversations.tools.createAndEmailPodcast.active': + 'Podcast wird erstellt und per E-Mail gesendet', + 'conversations.tools.createAndEmailPodcast.done': 'Podcast erstellt und per E-Mail gesendet', + 'conversations.tools.recallMemories.active': 'Erinnerungen werden abgerufen', + 'conversations.tools.recallMemories.done': 'Erinnerungen abgerufen', + 'conversations.tools.saveToMemory.active': 'Wird im Gedächtnis gespeichert', + 'conversations.tools.saveToMemory.done': 'Im Gedächtnis gespeichert', + 'conversations.tools.forgetMemory.active': 'Erinnerung wird vergessen', + 'conversations.tools.forgetMemory.done': 'Erinnerung vergessen', + 'conversations.tools.searchMemory.active': 'Gedächtnis wird durchsucht', + 'conversations.tools.searchMemory.done': 'Gedächtnis durchsucht', + 'conversations.tools.inspectMemory.active': 'Gedächtnis wird untersucht', + 'conversations.tools.inspectMemory.done': 'Gedächtnis untersucht', + 'conversations.tools.exploreMemory.active': 'Gedächtnis wird erkundet', + 'conversations.tools.exploreMemory.done': 'Gedächtnis erkundet', + 'conversations.tools.saveDocumentToMemory.active': 'Dokument wird im Gedächtnis gespeichert', + 'conversations.tools.saveDocumentToMemory.done': 'Dokument im Gedächtnis gespeichert', + 'conversations.tools.updateGoals.active': 'Ziele werden aktualisiert', + 'conversations.tools.updateGoals.done': 'Ziele aktualisiert', + 'conversations.tools.reviewGoals.active': 'Ziele werden geprüft', + 'conversations.tools.reviewGoals.done': 'Ziele geprüft', + 'conversations.tools.savePreference.active': 'Einstellung wird gespeichert', + 'conversations.tools.savePreference.done': 'Einstellung gespeichert', + 'conversations.tools.reviewLearnings.active': 'Gelerntes wird geprüft', + 'conversations.tools.reviewLearnings.done': 'Gelerntes geprüft', + 'conversations.tools.updateLearnings.active': 'Gelerntes wird aktualisiert', + 'conversations.tools.updateLearnings.done': 'Gelerntes aktualisiert', + 'conversations.tools.delegateTask.active': 'Aufgabe wird delegiert', + 'conversations.tools.delegateTask.done': 'Aufgabe delegiert', + 'conversations.tools.runAgentsInParallel.active': 'Agenten laufen parallel', + 'conversations.tools.runAgentsInParallel.done': 'Agenten parallel ausgeführt', + 'conversations.tools.messageAgent.active': 'Nachricht an Agenten wird gesendet', + 'conversations.tools.messageAgent.done': 'Nachricht an Agenten gesendet', + 'conversations.tools.waitForAgent.active': 'Wartet auf Agenten', + 'conversations.tools.waitForAgent.done': 'Auf Agenten gewartet', + 'conversations.tools.wait.active': 'Wartet', + 'conversations.tools.wait.done': 'Gewartet', + 'conversations.tools.closeAgent.active': 'Agent wird geschlossen', + 'conversations.tools.closeAgent.done': 'Agent geschlossen', + 'conversations.tools.checkAgents.active': 'Agenten werden geprüft', + 'conversations.tools.checkAgents.done': 'Agenten geprüft', + 'conversations.tools.askQuestion.active': 'Stellt dir eine Frage', + 'conversations.tools.askQuestion.done': 'Dir eine Frage gestellt', + 'conversations.tools.prepareContext.active': 'Kontext wird vorbereitet', + 'conversations.tools.prepareContext.done': 'Kontext vorbereitet', + 'conversations.tools.extractDetails.active': 'Details werden extrahiert', + 'conversations.tools.extractDetails.done': 'Details extrahiert', + 'conversations.tools.planNextSteps.active': 'Nächste Schritte werden geplant', + 'conversations.tools.planNextSteps.done': 'Nächste Schritte geplant', + 'conversations.tools.reviewWork.active': 'Arbeit wird geprüft', + 'conversations.tools.reviewWork.done': 'Arbeit geprüft', + 'conversations.tools.scoutContext.active': 'Kontext wird erkundet', + 'conversations.tools.scoutContext.done': 'Kontext erkundet', + 'conversations.tools.useTools.active': 'Tools werden verwendet', + 'conversations.tools.useTools.done': 'Tools verwendet', + 'conversations.tools.checkConnectedApp.active': 'Verbundene App wird geprüft', + 'conversations.tools.checkConnectedApp.done': 'Verbundene App geprüft', + 'conversations.tools.updateTodos.active': 'To-do-Liste wird aktualisiert', + 'conversations.tools.updateTodos.done': 'To-do-Liste aktualisiert', + 'conversations.tools.requestPlanReview.active': 'Planprüfung wird angefordert', + 'conversations.tools.requestPlanReview.done': 'Planprüfung angefordert', + 'conversations.tools.finishPlan.active': 'Plan wird abgeschlossen', + 'conversations.tools.finishPlan.done': 'Plan abgeschlossen', + 'conversations.tools.setGoal.active': 'Ziel wird festgelegt', + 'conversations.tools.setGoal.done': 'Ziel festgelegt', + 'conversations.tools.checkGoal.active': 'Ziel wird geprüft', + 'conversations.tools.checkGoal.done': 'Ziel geprüft', + 'conversations.tools.completeGoal.active': 'Ziel wird abgeschlossen', + 'conversations.tools.completeGoal.done': 'Ziel abgeschlossen', + 'conversations.tools.scheduleTask.active': 'Aufgabe wird geplant', + 'conversations.tools.scheduleTask.done': 'Aufgabe geplant', + 'conversations.tools.checkSchedules.active': 'Zeitpläne werden geprüft', + 'conversations.tools.checkSchedules.done': 'Zeitpläne geprüft', + 'conversations.tools.updateSchedule.active': 'Geplante Aufgabe wird aktualisiert', + 'conversations.tools.updateSchedule.done': 'Geplante Aufgabe aktualisiert', + 'conversations.tools.removeSchedule.active': 'Geplante Aufgabe wird entfernt', + 'conversations.tools.removeSchedule.done': 'Geplante Aufgabe entfernt', + 'conversations.tools.runScheduledTask.active': 'Geplante Aufgabe wird ausgeführt', + 'conversations.tools.runScheduledTask.done': 'Geplante Aufgabe ausgeführt', + 'conversations.tools.checkRunHistory.active': 'Ausführungsverlauf wird geprüft', + 'conversations.tools.checkRunHistory.done': 'Ausführungsverlauf geprüft', + 'conversations.tools.useApp.active': '{app} wird verwendet', + 'conversations.tools.useApp.done': '{app} verwendet', + 'conversations.tools.checkAvailableApps.active': 'Verfügbare Apps werden geprüft', + 'conversations.tools.checkAvailableApps.done': 'Verfügbare Apps geprüft', + 'conversations.tools.checkConnections.active': 'Deine Verbindungen werden geprüft', + 'conversations.tools.checkConnections.done': 'Deine Verbindungen geprüft', + 'conversations.tools.connectApp.active': 'App wird verbunden', + 'conversations.tools.connectApp.done': 'App verbunden', + 'conversations.tools.authorizeApp.active': 'App wird autorisiert', + 'conversations.tools.authorizeApp.done': 'App autorisiert', + 'conversations.tools.findAppActions.active': 'App-Aktionen werden gesucht', + 'conversations.tools.findAppActions.done': 'App-Aktionen gefunden', + 'conversations.tools.runAppAction.active': 'App-Aktion wird ausgeführt', + 'conversations.tools.runAppAction.done': 'App-Aktion ausgeführt', + 'conversations.tools.findTools.active': 'Tools werden gesucht', + 'conversations.tools.findTools.done': 'Tools gefunden', + 'conversations.tools.useTool.active': '{tool} wird verwendet', + 'conversations.tools.useTool.done': '{tool} verwendet', + 'conversations.tools.unsubscribe.active': 'Abmeldung läuft', + 'conversations.tools.unsubscribe.done': 'Abgemeldet', + 'conversations.tools.searchPlaces.active': 'Orte werden gesucht', + 'conversations.tools.searchPlaces.done': 'Orte gesucht', + 'conversations.tools.lookUpPlace.active': 'Ort wird nachgeschlagen', + 'conversations.tools.lookUpPlace.done': 'Ort nachgeschlagen', + 'conversations.tools.checkMarkets.active': 'Märkte werden geprüft', + 'conversations.tools.checkMarkets.done': 'Märkte geprüft', + 'conversations.tools.placeCall.active': 'Anruf wird getätigt', + 'conversations.tools.placeCall.done': 'Anruf getätigt', + 'conversations.tools.checkTaskSources.active': 'Aufgabenquellen werden geprüft', + 'conversations.tools.checkTaskSources.done': 'Aufgabenquellen geprüft', + 'conversations.tools.updateTaskSources.active': 'Aufgabenquellen werden aktualisiert', + 'conversations.tools.updateTaskSources.done': 'Aufgabenquellen aktualisiert', + 'conversations.tools.fetchTasks.active': 'Aufgaben werden abgerufen', + 'conversations.tools.fetchTasks.done': 'Aufgaben abgerufen', + 'conversations.tools.checkMcpServers.active': 'MCP-Server werden geprüft', + 'conversations.tools.checkMcpServers.done': 'MCP-Server geprüft', + 'conversations.tools.checkMcpTools.active': 'MCP-Tools werden geprüft', + 'conversations.tools.checkMcpTools.done': 'MCP-Tools geprüft', + 'conversations.tools.callMcpTool.active': '{tool} wird aufgerufen', + 'conversations.tools.callMcpTool.done': '{tool} aufgerufen', + 'conversations.tools.searchMcpServers.active': 'MCP-Server werden gesucht', + 'conversations.tools.searchMcpServers.done': 'MCP-Server gesucht', + 'conversations.tools.connectMcpServer.active': 'MCP-Server wird verbunden', + 'conversations.tools.connectMcpServer.done': 'MCP-Server verbunden', + 'conversations.tools.disconnectMcpServer.active': 'MCP-Server wird getrennt', + 'conversations.tools.disconnectMcpServer.done': 'MCP-Server getrennt', + 'conversations.tools.removeMcpServer.active': 'MCP-Server wird entfernt', + 'conversations.tools.removeMcpServer.done': 'MCP-Server entfernt', + 'conversations.tools.uploadFile.active': 'Datei wird hochgeladen', + 'conversations.tools.uploadFile.done': 'Datei hochgeladen', + 'conversations.tools.listStoredFiles.active': 'Gespeicherte Dateien werden aufgelistet', + 'conversations.tools.listStoredFiles.done': 'Gespeicherte Dateien aufgelistet', + 'conversations.tools.createShareLink.active': 'Freigabelink wird erstellt', + 'conversations.tools.createShareLink.done': 'Freigabelink erstellt', + 'conversations.tools.deleteFile.active': 'Datei wird gelöscht', + 'conversations.tools.deleteFile.done': 'Datei gelöscht', + 'conversations.tools.updateFileAccess.active': 'Dateizugriff wird aktualisiert', + 'conversations.tools.updateFileAccess.done': 'Dateizugriff aktualisiert', + 'conversations.tools.deploySite.active': 'Website wird bereitgestellt', + 'conversations.tools.deploySite.done': 'Website bereitgestellt', + 'conversations.tools.checkHosting.active': 'Hosting wird geprüft', + 'conversations.tools.checkHosting.done': 'Hosting geprüft', + 'conversations.tools.updateHosting.active': 'Hosting wird aktualisiert', + 'conversations.tools.updateHosting.done': 'Hosting aktualisiert', + 'conversations.tools.rollBackDeployment.active': 'Bereitstellung wird zurückgesetzt', + 'conversations.tools.rollBackDeployment.done': 'Bereitstellung zurückgesetzt', + 'conversations.tools.checkWallet.active': 'Wallet wird geprüft', + 'conversations.tools.checkWallet.done': 'Wallet geprüft', + 'conversations.tools.prepareTransfer.active': 'Überweisung wird vorbereitet', + 'conversations.tools.prepareTransfer.done': 'Überweisung vorbereitet', + 'conversations.tools.checkTransaction.active': 'Transaktion wird geprüft', + 'conversations.tools.checkTransaction.done': 'Transaktion geprüft', + 'conversations.tools.getSwapQuote.active': 'Swap-Angebot wird abgerufen', + 'conversations.tools.getSwapQuote.done': 'Swap-Angebot abgerufen', + 'conversations.tools.swapTokens.active': 'Tokens werden getauscht', + 'conversations.tools.swapTokens.done': 'Tokens getauscht', + 'conversations.tools.getBridgeQuote.active': 'Bridge-Angebot wird abgerufen', + 'conversations.tools.getBridgeQuote.done': 'Bridge-Angebot abgerufen', + 'conversations.tools.bridgeTokens.active': 'Tokens werden übertragen', + 'conversations.tools.bridgeTokens.done': 'Tokens übertragen', + 'conversations.tools.callDapp.active': 'App-Vertrag wird aufgerufen', + 'conversations.tools.callDapp.done': 'App-Vertrag aufgerufen', + 'conversations.tools.useSkill.active': 'Skill wird verwendet', + 'conversations.tools.useSkill.done': 'Skill verwendet', + 'conversations.tools.searchSkills.active': 'Skills werden gesucht', + 'conversations.tools.searchSkills.done': 'Skills gesucht', + 'conversations.tools.checkSkills.active': 'Skills werden geprüft', + 'conversations.tools.checkSkills.done': 'Skills geprüft', + 'conversations.tools.installSkill.active': 'Skill wird installiert', + 'conversations.tools.installSkill.done': 'Skill installiert', + 'conversations.tools.removeSkill.active': 'Skill wird entfernt', + 'conversations.tools.removeSkill.done': 'Skill entfernt', + 'conversations.tools.createSkill.active': 'Skill wird erstellt', + 'conversations.tools.createSkill.done': 'Skill erstellt', + 'conversations.tools.runWorkflow.active': 'Workflow wird ausgeführt', + 'conversations.tools.runWorkflow.done': 'Workflow ausgeführt', + 'conversations.tools.waitForWorkflow.active': 'Wartet auf Workflow', + 'conversations.tools.waitForWorkflow.done': 'Auf Workflow gewartet', + 'conversations.tools.designWorkflow.active': 'Workflow wird entworfen', + 'conversations.tools.designWorkflow.done': 'Workflow entworfen', + 'conversations.tools.saveWorkflow.active': 'Workflow wird gespeichert', + 'conversations.tools.saveWorkflow.done': 'Workflow gespeichert', + 'conversations.tools.validateWorkflow.active': 'Workflow wird validiert', + 'conversations.tools.validateWorkflow.done': 'Workflow validiert', + 'conversations.tools.testWorkflow.active': 'Workflow wird getestet', + 'conversations.tools.testWorkflow.done': 'Workflow getestet', + 'conversations.tools.checkWorkflows.active': 'Workflows werden geprüft', + 'conversations.tools.checkWorkflows.done': 'Workflows geprüft', + 'conversations.tools.cancelWorkflow.active': 'Workflow-Ausführung wird abgebrochen', + 'conversations.tools.cancelWorkflow.done': 'Workflow-Ausführung abgebrochen', + 'conversations.tools.suggestWorkflows.active': 'Workflows werden vorgeschlagen', + 'conversations.tools.suggestWorkflows.done': 'Workflows vorgeschlagen', + 'conversations.tools.checkSettings.active': 'Einstellungen werden geprüft', + 'conversations.tools.checkSettings.done': 'Einstellungen geprüft', + 'conversations.tools.checkSecurity.active': 'Sicherheit wird geprüft', + 'conversations.tools.checkSecurity.done': 'Sicherheit geprüft', + 'conversations.tools.runDiagnostics.active': 'Diagnose wird ausgeführt', + 'conversations.tools.runDiagnostics.done': 'Diagnose ausgeführt', + 'conversations.tools.checkUsageCosts.active': 'Nutzungskosten werden geprüft', + 'conversations.tools.checkUsageCosts.done': 'Nutzungskosten geprüft', + 'conversations.tools.manageService.active': 'Hintergrunddienst wird verwaltet', + 'conversations.tools.manageService.done': 'Hintergrunddienst verwaltet', + 'conversations.tools.readPersona.active': 'Persona wird gelesen', + 'conversations.tools.readPersona.done': 'Persona gelesen', + 'conversations.tools.updatePersona.active': 'Persona wird aktualisiert', + 'conversations.tools.updatePersona.done': 'Persona aktualisiert', + 'conversations.tools.setUpWorkspace.active': 'Arbeitsbereich wird eingerichtet', + 'conversations.tools.setUpWorkspace.done': 'Arbeitsbereich eingerichtet', + 'conversations.tools.checkArtifacts.active': 'Artefakte werden geprüft', + 'conversations.tools.checkArtifacts.done': 'Artefakte geprüft', + 'conversations.tools.deleteArtifact.active': 'Artefakt wird gelöscht', + 'conversations.tools.deleteArtifact.done': 'Artefakt gelöscht', 'conversations.subagent.noOutput': 'Keine Ausgabe zurückgegeben', 'conversations.subagent.close': 'Schließen', 'conversations.subagent.cancel': 'Aufgabe abbrechen', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 13f58f18356..ee8b7893b2f 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -3740,6 +3740,359 @@ const en: TranslationMap = { 'conversations.subagent.noOutputYet': 'No output yet', 'conversations.subagent.input': 'Input', 'conversations.subagent.output': 'Output', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} step', + 'conversations.tools.steps.other': '{count} steps', + 'conversations.tools.working': 'Working', + 'conversations.tools.noOutput': 'No output', + 'conversations.tools.delegatedTo': 'Delegated to {agent}', + 'conversations.tools.openInBrowser': 'Open in browser', + 'conversations.tools.status.running': 'running', + 'conversations.tools.status.done': 'done', + 'conversations.tools.status.failed': 'failed', + 'conversations.tools.status.cancelled': 'cancelled', + 'conversations.tools.status.awaiting': 'awaiting input', + 'conversations.tools.search.searching': 'Searching', + 'conversations.tools.search.none': 'No results', + 'conversations.tools.search.found.one': 'Found {count} result', + 'conversations.tools.search.found.other': 'Found {count} results', + 'conversations.tools.search.via': 'via {provider}', + 'conversations.tools.readFile.active': 'Reading file', + 'conversations.tools.readFile.done': 'Read file', + 'conversations.tools.writeFile.active': 'Writing file', + 'conversations.tools.writeFile.done': 'Wrote file', + 'conversations.tools.editFile.active': 'Editing file', + 'conversations.tools.editFile.done': 'Edited file', + 'conversations.tools.applyEdits.active': 'Applying edits', + 'conversations.tools.applyEdits.done': 'Applied edits', + 'conversations.tools.searchCode.active': 'Searching code', + 'conversations.tools.searchCode.done': 'Searched code', + 'conversations.tools.findFiles.active': 'Finding files', + 'conversations.tools.findFiles.done': 'Found files', + 'conversations.tools.listFolder.active': 'Listing folder', + 'conversations.tools.listFolder.done': 'Listed folder', + 'conversations.tools.exportCsv.active': 'Exporting CSV', + 'conversations.tools.exportCsv.done': 'Exported CSV', + 'conversations.tools.updateMemoryNotes.active': 'Updating memory notes', + 'conversations.tools.updateMemoryNotes.done': 'Updated memory notes', + 'conversations.tools.runGit.active': 'Running git', + 'conversations.tools.runGit.done': 'Ran git', + 'conversations.tools.readChanges.active': 'Reading changes', + 'conversations.tools.readChanges.done': 'Read changes', + 'conversations.tools.runLinter.active': 'Running linter', + 'conversations.tools.runLinter.done': 'Ran linter', + 'conversations.tools.runTests.active': 'Running tests', + 'conversations.tools.runTests.done': 'Ran tests', + 'conversations.tools.analyzeCode.active': 'Analyzing code', + 'conversations.tools.analyzeCode.done': 'Analyzed code', + 'conversations.tools.insertRecord.active': 'Inserting record', + 'conversations.tools.insertRecord.done': 'Inserted record', + 'conversations.tools.runCommand.active': 'Running command', + 'conversations.tools.runCommand.done': 'Ran command', + 'conversations.tools.runCode.active': 'Running code', + 'conversations.tools.runCode.done': 'Ran code', + 'conversations.tools.runPackageManager.active': 'Running npm', + 'conversations.tools.runPackageManager.done': 'Ran npm', + 'conversations.tools.checkInstalledTools.active': 'Checking installed tools', + 'conversations.tools.checkInstalledTools.done': 'Checked installed tools', + 'conversations.tools.installTool.active': 'Installing tool', + 'conversations.tools.installTool.done': 'Installed tool', + 'conversations.tools.checkTime.active': 'Checking the time', + 'conversations.tools.checkTime.done': 'Checked the time', + 'conversations.tools.resolveDate.active': 'Working out the date', + 'conversations.tools.resolveDate.done': 'Worked out the date', + 'conversations.tools.retrieveOutput.active': 'Retrieving full output', + 'conversations.tools.retrieveOutput.done': 'Retrieved full output', + 'conversations.tools.reviewWorkspace.active': 'Reviewing workspace', + 'conversations.tools.reviewWorkspace.done': 'Reviewed workspace', + 'conversations.tools.configureProxy.active': 'Configuring proxy', + 'conversations.tools.configureProxy.done': 'Configured proxy', + 'conversations.tools.checkUpdates.active': 'Checking for updates', + 'conversations.tools.checkUpdates.done': 'Checked for updates', + 'conversations.tools.installUpdate.active': 'Installing update', + 'conversations.tools.installUpdate.done': 'Installed update', + 'conversations.tools.sendNotification.active': 'Sending notification', + 'conversations.tools.sendNotification.done': 'Sent notification', + 'conversations.tools.reviewToolUsage.active': 'Reviewing tool usage', + 'conversations.tools.reviewToolUsage.done': 'Reviewed tool usage', + 'conversations.tools.typeKeys.active': 'Typing', + 'conversations.tools.typeKeys.done': 'Typed', + 'conversations.tools.click.active': 'Clicking', + 'conversations.tools.click.done': 'Clicked', + 'conversations.tools.searchWeb.active': 'Searching the web', + 'conversations.tools.searchWeb.done': 'Searched the web', + 'conversations.tools.searchNews.active': 'Searching news', + 'conversations.tools.searchNews.done': 'Searched news', + 'conversations.tools.searchImages.active': 'Searching images', + 'conversations.tools.searchImages.done': 'Searched images', + 'conversations.tools.searchVideos.active': 'Searching videos', + 'conversations.tools.searchVideos.done': 'Searched videos', + 'conversations.tools.findSimilarPages.active': 'Finding similar pages', + 'conversations.tools.findSimilarPages.done': 'Found similar pages', + 'conversations.tools.readPages.active': 'Reading pages', + 'conversations.tools.readPages.done': 'Read pages', + 'conversations.tools.readWebpage.active': 'Reading webpage', + 'conversations.tools.readWebpage.done': 'Read webpage', + 'conversations.tools.research.active': 'Researching', + 'conversations.tools.research.done': 'Researched', + 'conversations.tools.enrichData.active': 'Enriching data', + 'conversations.tools.enrichData.done': 'Enriched data', + 'conversations.tools.buildDataset.active': 'Building dataset', + 'conversations.tools.buildDataset.done': 'Built dataset', + 'conversations.tools.askTheWeb.active': 'Asking the web', + 'conversations.tools.askTheWeb.done': 'Asked the web', + 'conversations.tools.browseForYou.active': 'Browsing for you', + 'conversations.tools.browseForYou.done': 'Browsed for you', + 'conversations.tools.callApi.active': 'Calling API', + 'conversations.tools.callApi.done': 'Called API', + 'conversations.tools.downloadFile.active': 'Downloading file', + 'conversations.tools.downloadFile.done': 'Downloaded file', + 'conversations.tools.makePaidRequest.active': 'Making paid request', + 'conversations.tools.makePaidRequest.done': 'Made paid request', + 'conversations.tools.searchDocs.active': 'Searching docs', + 'conversations.tools.searchDocs.done': 'Searched docs', + 'conversations.tools.readDocs.active': 'Reading docs', + 'conversations.tools.readDocs.done': 'Read docs', + 'conversations.tools.useBrowser.active': 'Using browser', + 'conversations.tools.useBrowser.done': 'Used browser', + 'conversations.tools.openPage.active': 'Opening page', + 'conversations.tools.openPage.done': 'Opened page', + 'conversations.tools.navigate.active': 'Navigating', + 'conversations.tools.navigate.done': 'Navigated', + 'conversations.tools.takeScreenshot.active': 'Taking screenshot', + 'conversations.tools.takeScreenshot.done': 'Took screenshot', + 'conversations.tools.scrollPage.active': 'Scrolling', + 'conversations.tools.scrollPage.done': 'Scrolled', + 'conversations.tools.readPage.active': 'Reading page', + 'conversations.tools.readPage.done': 'Read page', + 'conversations.tools.analyzeImage.active': 'Analyzing image', + 'conversations.tools.analyzeImage.done': 'Analyzed image', + 'conversations.tools.generateImage.active': 'Generating image', + 'conversations.tools.generateImage.done': 'Generated image', + 'conversations.tools.generateVideo.active': 'Generating video', + 'conversations.tools.generateVideo.done': 'Generated video', + 'conversations.tools.checkMediaModels.active': 'Checking media models', + 'conversations.tools.checkMediaModels.done': 'Checked media models', + 'conversations.tools.createDocument.active': 'Creating document', + 'conversations.tools.createDocument.done': 'Created document', + 'conversations.tools.createPresentation.active': 'Creating presentation', + 'conversations.tools.createPresentation.done': 'Created presentation', + 'conversations.tools.generatePodcast.active': 'Generating podcast', + 'conversations.tools.generatePodcast.done': 'Generated podcast', + 'conversations.tools.emailPodcast.active': 'Emailing podcast', + 'conversations.tools.emailPodcast.done': 'Emailed podcast', + 'conversations.tools.createAndEmailPodcast.active': 'Creating and emailing podcast', + 'conversations.tools.createAndEmailPodcast.done': 'Created and emailed podcast', + 'conversations.tools.recallMemories.active': 'Recalling memories', + 'conversations.tools.recallMemories.done': 'Recalled memories', + 'conversations.tools.saveToMemory.active': 'Saving to memory', + 'conversations.tools.saveToMemory.done': 'Saved to memory', + 'conversations.tools.forgetMemory.active': 'Forgetting memory', + 'conversations.tools.forgetMemory.done': 'Forgot memory', + 'conversations.tools.searchMemory.active': 'Searching memory', + 'conversations.tools.searchMemory.done': 'Searched memory', + 'conversations.tools.inspectMemory.active': 'Inspecting memory', + 'conversations.tools.inspectMemory.done': 'Inspected memory', + 'conversations.tools.exploreMemory.active': 'Exploring memory', + 'conversations.tools.exploreMemory.done': 'Explored memory', + 'conversations.tools.saveDocumentToMemory.active': 'Saving document to memory', + 'conversations.tools.saveDocumentToMemory.done': 'Saved document to memory', + 'conversations.tools.updateGoals.active': 'Updating goals', + 'conversations.tools.updateGoals.done': 'Updated goals', + 'conversations.tools.reviewGoals.active': 'Reviewing goals', + 'conversations.tools.reviewGoals.done': 'Reviewed goals', + 'conversations.tools.savePreference.active': 'Saving preference', + 'conversations.tools.savePreference.done': 'Saved preference', + 'conversations.tools.reviewLearnings.active': 'Reviewing what I learned', + 'conversations.tools.reviewLearnings.done': 'Reviewed what I learned', + 'conversations.tools.updateLearnings.active': 'Updating what I learned', + 'conversations.tools.updateLearnings.done': 'Updated what I learned', + 'conversations.tools.delegateTask.active': 'Delegating task', + 'conversations.tools.delegateTask.done': 'Delegated task', + 'conversations.tools.runAgentsInParallel.active': 'Running agents in parallel', + 'conversations.tools.runAgentsInParallel.done': 'Ran agents in parallel', + 'conversations.tools.messageAgent.active': 'Messaging agent', + 'conversations.tools.messageAgent.done': 'Messaged agent', + 'conversations.tools.waitForAgent.active': 'Waiting for agent', + 'conversations.tools.waitForAgent.done': 'Waited for agent', + 'conversations.tools.wait.active': 'Waiting', + 'conversations.tools.wait.done': 'Waited', + 'conversations.tools.closeAgent.active': 'Closing agent', + 'conversations.tools.closeAgent.done': 'Closed agent', + 'conversations.tools.checkAgents.active': 'Checking agents', + 'conversations.tools.checkAgents.done': 'Checked agents', + 'conversations.tools.askQuestion.active': 'Asking you a question', + 'conversations.tools.askQuestion.done': 'Asked you a question', + 'conversations.tools.prepareContext.active': 'Preparing context', + 'conversations.tools.prepareContext.done': 'Prepared context', + 'conversations.tools.extractDetails.active': 'Extracting details', + 'conversations.tools.extractDetails.done': 'Extracted details', + 'conversations.tools.planNextSteps.active': 'Planning next steps', + 'conversations.tools.planNextSteps.done': 'Planned next steps', + 'conversations.tools.reviewWork.active': 'Reviewing the work', + 'conversations.tools.reviewWork.done': 'Reviewed the work', + 'conversations.tools.scoutContext.active': 'Scouting context', + 'conversations.tools.scoutContext.done': 'Scouted context', + 'conversations.tools.useTools.active': 'Using tools', + 'conversations.tools.useTools.done': 'Used tools', + 'conversations.tools.checkConnectedApp.active': 'Checking your connected app', + 'conversations.tools.checkConnectedApp.done': 'Checked your connected app', + 'conversations.tools.updateTodos.active': 'Updating to-do list', + 'conversations.tools.updateTodos.done': 'Updated to-do list', + 'conversations.tools.requestPlanReview.active': 'Requesting plan review', + 'conversations.tools.requestPlanReview.done': 'Requested plan review', + 'conversations.tools.finishPlan.active': 'Finishing plan', + 'conversations.tools.finishPlan.done': 'Finished plan', + 'conversations.tools.setGoal.active': 'Setting goal', + 'conversations.tools.setGoal.done': 'Set goal', + 'conversations.tools.checkGoal.active': 'Checking goal', + 'conversations.tools.checkGoal.done': 'Checked goal', + 'conversations.tools.completeGoal.active': 'Completing goal', + 'conversations.tools.completeGoal.done': 'Completed goal', + 'conversations.tools.scheduleTask.active': 'Scheduling task', + 'conversations.tools.scheduleTask.done': 'Scheduled task', + 'conversations.tools.checkSchedules.active': 'Checking schedules', + 'conversations.tools.checkSchedules.done': 'Checked schedules', + 'conversations.tools.updateSchedule.active': 'Updating scheduled task', + 'conversations.tools.updateSchedule.done': 'Updated scheduled task', + 'conversations.tools.removeSchedule.active': 'Removing scheduled task', + 'conversations.tools.removeSchedule.done': 'Removed scheduled task', + 'conversations.tools.runScheduledTask.active': 'Running scheduled task', + 'conversations.tools.runScheduledTask.done': 'Ran scheduled task', + 'conversations.tools.checkRunHistory.active': 'Checking run history', + 'conversations.tools.checkRunHistory.done': 'Checked run history', + 'conversations.tools.useApp.active': 'Using {app}', + 'conversations.tools.useApp.done': 'Used {app}', + 'conversations.tools.checkAvailableApps.active': 'Checking available apps', + 'conversations.tools.checkAvailableApps.done': 'Checked available apps', + 'conversations.tools.checkConnections.active': 'Checking your connections', + 'conversations.tools.checkConnections.done': 'Checked your connections', + 'conversations.tools.connectApp.active': 'Connecting app', + 'conversations.tools.connectApp.done': 'Connected app', + 'conversations.tools.authorizeApp.active': 'Authorizing app', + 'conversations.tools.authorizeApp.done': 'Authorized app', + 'conversations.tools.findAppActions.active': 'Finding app actions', + 'conversations.tools.findAppActions.done': 'Found app actions', + 'conversations.tools.runAppAction.active': 'Running app action', + 'conversations.tools.runAppAction.done': 'Ran app action', + 'conversations.tools.findTools.active': 'Finding tools', + 'conversations.tools.findTools.done': 'Found tools', + 'conversations.tools.useTool.active': 'Using {tool}', + 'conversations.tools.useTool.done': 'Used {tool}', + 'conversations.tools.unsubscribe.active': 'Unsubscribing', + 'conversations.tools.unsubscribe.done': 'Unsubscribed', + 'conversations.tools.searchPlaces.active': 'Searching places', + 'conversations.tools.searchPlaces.done': 'Searched places', + 'conversations.tools.lookUpPlace.active': 'Looking up place', + 'conversations.tools.lookUpPlace.done': 'Looked up place', + 'conversations.tools.checkMarkets.active': 'Checking markets', + 'conversations.tools.checkMarkets.done': 'Checked markets', + 'conversations.tools.placeCall.active': 'Placing call', + 'conversations.tools.placeCall.done': 'Placed call', + 'conversations.tools.checkTaskSources.active': 'Checking task sources', + 'conversations.tools.checkTaskSources.done': 'Checked task sources', + 'conversations.tools.updateTaskSources.active': 'Updating task sources', + 'conversations.tools.updateTaskSources.done': 'Updated task sources', + 'conversations.tools.fetchTasks.active': 'Fetching tasks', + 'conversations.tools.fetchTasks.done': 'Fetched tasks', + 'conversations.tools.checkMcpServers.active': 'Checking MCP servers', + 'conversations.tools.checkMcpServers.done': 'Checked MCP servers', + 'conversations.tools.checkMcpTools.active': 'Checking MCP tools', + 'conversations.tools.checkMcpTools.done': 'Checked MCP tools', + 'conversations.tools.callMcpTool.active': 'Calling {tool}', + 'conversations.tools.callMcpTool.done': 'Called {tool}', + 'conversations.tools.searchMcpServers.active': 'Searching MCP servers', + 'conversations.tools.searchMcpServers.done': 'Searched MCP servers', + 'conversations.tools.connectMcpServer.active': 'Connecting MCP server', + 'conversations.tools.connectMcpServer.done': 'Connected MCP server', + 'conversations.tools.disconnectMcpServer.active': 'Disconnecting MCP server', + 'conversations.tools.disconnectMcpServer.done': 'Disconnected MCP server', + 'conversations.tools.removeMcpServer.active': 'Removing MCP server', + 'conversations.tools.removeMcpServer.done': 'Removed MCP server', + 'conversations.tools.uploadFile.active': 'Uploading file', + 'conversations.tools.uploadFile.done': 'Uploaded file', + 'conversations.tools.listStoredFiles.active': 'Listing stored files', + 'conversations.tools.listStoredFiles.done': 'Listed stored files', + 'conversations.tools.createShareLink.active': 'Creating share link', + 'conversations.tools.createShareLink.done': 'Created share link', + 'conversations.tools.deleteFile.active': 'Deleting file', + 'conversations.tools.deleteFile.done': 'Deleted file', + 'conversations.tools.updateFileAccess.active': 'Updating file access', + 'conversations.tools.updateFileAccess.done': 'Updated file access', + 'conversations.tools.deploySite.active': 'Deploying site', + 'conversations.tools.deploySite.done': 'Deployed site', + 'conversations.tools.checkHosting.active': 'Checking hosting', + 'conversations.tools.checkHosting.done': 'Checked hosting', + 'conversations.tools.updateHosting.active': 'Updating hosting', + 'conversations.tools.updateHosting.done': 'Updated hosting', + 'conversations.tools.rollBackDeployment.active': 'Rolling back deployment', + 'conversations.tools.rollBackDeployment.done': 'Rolled back deployment', + 'conversations.tools.checkWallet.active': 'Checking wallet', + 'conversations.tools.checkWallet.done': 'Checked wallet', + 'conversations.tools.prepareTransfer.active': 'Preparing transfer', + 'conversations.tools.prepareTransfer.done': 'Prepared transfer', + 'conversations.tools.checkTransaction.active': 'Checking transaction', + 'conversations.tools.checkTransaction.done': 'Checked transaction', + 'conversations.tools.getSwapQuote.active': 'Getting swap quote', + 'conversations.tools.getSwapQuote.done': 'Got swap quote', + 'conversations.tools.swapTokens.active': 'Swapping tokens', + 'conversations.tools.swapTokens.done': 'Swapped tokens', + 'conversations.tools.getBridgeQuote.active': 'Getting bridge quote', + 'conversations.tools.getBridgeQuote.done': 'Got bridge quote', + 'conversations.tools.bridgeTokens.active': 'Bridging tokens', + 'conversations.tools.bridgeTokens.done': 'Bridged tokens', + 'conversations.tools.callDapp.active': 'Calling app contract', + 'conversations.tools.callDapp.done': 'Called app contract', + 'conversations.tools.useSkill.active': 'Using skill', + 'conversations.tools.useSkill.done': 'Used skill', + 'conversations.tools.searchSkills.active': 'Searching skills', + 'conversations.tools.searchSkills.done': 'Searched skills', + 'conversations.tools.checkSkills.active': 'Checking skills', + 'conversations.tools.checkSkills.done': 'Checked skills', + 'conversations.tools.installSkill.active': 'Installing skill', + 'conversations.tools.installSkill.done': 'Installed skill', + 'conversations.tools.removeSkill.active': 'Removing skill', + 'conversations.tools.removeSkill.done': 'Removed skill', + 'conversations.tools.createSkill.active': 'Creating skill', + 'conversations.tools.createSkill.done': 'Created skill', + 'conversations.tools.runWorkflow.active': 'Running workflow', + 'conversations.tools.runWorkflow.done': 'Ran workflow', + 'conversations.tools.waitForWorkflow.active': 'Waiting for workflow', + 'conversations.tools.waitForWorkflow.done': 'Waited for workflow', + 'conversations.tools.designWorkflow.active': 'Designing workflow', + 'conversations.tools.designWorkflow.done': 'Designed workflow', + 'conversations.tools.saveWorkflow.active': 'Saving workflow', + 'conversations.tools.saveWorkflow.done': 'Saved workflow', + 'conversations.tools.validateWorkflow.active': 'Validating workflow', + 'conversations.tools.validateWorkflow.done': 'Validated workflow', + 'conversations.tools.testWorkflow.active': 'Testing workflow', + 'conversations.tools.testWorkflow.done': 'Tested workflow', + 'conversations.tools.checkWorkflows.active': 'Checking workflows', + 'conversations.tools.checkWorkflows.done': 'Checked workflows', + 'conversations.tools.cancelWorkflow.active': 'Cancelling workflow run', + 'conversations.tools.cancelWorkflow.done': 'Cancelled workflow run', + 'conversations.tools.suggestWorkflows.active': 'Suggesting workflows', + 'conversations.tools.suggestWorkflows.done': 'Suggested workflows', + 'conversations.tools.checkSettings.active': 'Checking settings', + 'conversations.tools.checkSettings.done': 'Checked settings', + 'conversations.tools.checkSecurity.active': 'Checking security', + 'conversations.tools.checkSecurity.done': 'Checked security', + 'conversations.tools.runDiagnostics.active': 'Running diagnostics', + 'conversations.tools.runDiagnostics.done': 'Ran diagnostics', + 'conversations.tools.checkUsageCosts.active': 'Checking usage costs', + 'conversations.tools.checkUsageCosts.done': 'Checked usage costs', + 'conversations.tools.manageService.active': 'Managing background service', + 'conversations.tools.manageService.done': 'Managed background service', + 'conversations.tools.readPersona.active': 'Reading persona', + 'conversations.tools.readPersona.done': 'Read persona', + 'conversations.tools.updatePersona.active': 'Updating persona', + 'conversations.tools.updatePersona.done': 'Updated persona', + 'conversations.tools.setUpWorkspace.active': 'Setting up workspace', + 'conversations.tools.setUpWorkspace.done': 'Set up workspace', + 'conversations.tools.checkArtifacts.active': 'Checking artifacts', + 'conversations.tools.checkArtifacts.done': 'Checked artifacts', + 'conversations.tools.deleteArtifact.active': 'Deleting artifact', + 'conversations.tools.deleteArtifact.done': 'Deleted artifact', 'conversations.subagent.noOutput': 'No output returned', 'conversations.subagent.close': 'Close', 'conversations.subagent.cancel': 'Cancel task', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index fcbf565d354..74b90d0fdda 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -3374,6 +3374,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Aún no hay resultados', 'conversations.subagent.input': 'Entrada', 'conversations.subagent.output': 'Salida', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} paso', + 'conversations.tools.steps.other': '{count} pasos', + 'conversations.tools.working': 'Trabajando', + 'conversations.tools.noOutput': 'Sin salida', + 'conversations.tools.delegatedTo': 'Delegado a {agent}', + 'conversations.tools.openInBrowser': 'Abrir en el navegador', + 'conversations.tools.status.running': 'en curso', + 'conversations.tools.status.done': 'hecho', + 'conversations.tools.status.failed': 'fallido', + 'conversations.tools.status.cancelled': 'cancelado', + 'conversations.tools.status.awaiting': 'esperando respuesta', + 'conversations.tools.search.searching': 'Buscando', + 'conversations.tools.search.none': 'Sin resultados', + 'conversations.tools.search.found.one': '{count} resultado encontrado', + 'conversations.tools.search.found.other': '{count} resultados encontrados', + 'conversations.tools.search.via': 'vía {provider}', + 'conversations.tools.readFile.active': 'Leyendo archivo', + 'conversations.tools.readFile.done': 'Archivo leído', + 'conversations.tools.writeFile.active': 'Escribiendo archivo', + 'conversations.tools.writeFile.done': 'Archivo escrito', + 'conversations.tools.editFile.active': 'Editando archivo', + 'conversations.tools.editFile.done': 'Archivo editado', + 'conversations.tools.applyEdits.active': 'Aplicando cambios', + 'conversations.tools.applyEdits.done': 'Cambios aplicados', + 'conversations.tools.searchCode.active': 'Buscando en el código', + 'conversations.tools.searchCode.done': 'Código buscado', + 'conversations.tools.findFiles.active': 'Buscando archivos', + 'conversations.tools.findFiles.done': 'Archivos encontrados', + 'conversations.tools.listFolder.active': 'Listando carpeta', + 'conversations.tools.listFolder.done': 'Carpeta listada', + 'conversations.tools.exportCsv.active': 'Exportando CSV', + 'conversations.tools.exportCsv.done': 'CSV exportado', + 'conversations.tools.updateMemoryNotes.active': 'Actualizando notas de memoria', + 'conversations.tools.updateMemoryNotes.done': 'Notas de memoria actualizadas', + 'conversations.tools.runGit.active': 'Ejecutando git', + 'conversations.tools.runGit.done': 'git ejecutado', + 'conversations.tools.readChanges.active': 'Leyendo cambios', + 'conversations.tools.readChanges.done': 'Cambios leídos', + 'conversations.tools.runLinter.active': 'Ejecutando linter', + 'conversations.tools.runLinter.done': 'Linter ejecutado', + 'conversations.tools.runTests.active': 'Ejecutando pruebas', + 'conversations.tools.runTests.done': 'Pruebas ejecutadas', + 'conversations.tools.analyzeCode.active': 'Analizando código', + 'conversations.tools.analyzeCode.done': 'Código analizado', + 'conversations.tools.insertRecord.active': 'Insertando registro', + 'conversations.tools.insertRecord.done': 'Registro insertado', + 'conversations.tools.runCommand.active': 'Ejecutando comando', + 'conversations.tools.runCommand.done': 'Comando ejecutado', + 'conversations.tools.runCode.active': 'Ejecutando código', + 'conversations.tools.runCode.done': 'Código ejecutado', + 'conversations.tools.runPackageManager.active': 'Ejecutando npm', + 'conversations.tools.runPackageManager.done': 'npm ejecutado', + 'conversations.tools.checkInstalledTools.active': 'Comprobando herramientas instaladas', + 'conversations.tools.checkInstalledTools.done': 'Herramientas instaladas comprobadas', + 'conversations.tools.installTool.active': 'Instalando herramienta', + 'conversations.tools.installTool.done': 'Herramienta instalada', + 'conversations.tools.checkTime.active': 'Consultando la hora', + 'conversations.tools.checkTime.done': 'Hora consultada', + 'conversations.tools.resolveDate.active': 'Calculando la fecha', + 'conversations.tools.resolveDate.done': 'Fecha calculada', + 'conversations.tools.retrieveOutput.active': 'Recuperando la salida completa', + 'conversations.tools.retrieveOutput.done': 'Salida completa recuperada', + 'conversations.tools.reviewWorkspace.active': 'Revisando el espacio de trabajo', + 'conversations.tools.reviewWorkspace.done': 'Espacio de trabajo revisado', + 'conversations.tools.configureProxy.active': 'Configurando proxy', + 'conversations.tools.configureProxy.done': 'Proxy configurado', + 'conversations.tools.checkUpdates.active': 'Buscando actualizaciones', + 'conversations.tools.checkUpdates.done': 'Actualizaciones comprobadas', + 'conversations.tools.installUpdate.active': 'Instalando actualización', + 'conversations.tools.installUpdate.done': 'Actualización instalada', + 'conversations.tools.sendNotification.active': 'Enviando notificación', + 'conversations.tools.sendNotification.done': 'Notificación enviada', + 'conversations.tools.reviewToolUsage.active': 'Revisando el uso de herramientas', + 'conversations.tools.reviewToolUsage.done': 'Uso de herramientas revisado', + 'conversations.tools.typeKeys.active': 'Escribiendo', + 'conversations.tools.typeKeys.done': 'Texto escrito', + 'conversations.tools.click.active': 'Haciendo clic', + 'conversations.tools.click.done': 'Clic hecho', + 'conversations.tools.searchWeb.active': 'Buscando en la web', + 'conversations.tools.searchWeb.done': 'Búsqueda web hecha', + 'conversations.tools.searchNews.active': 'Buscando noticias', + 'conversations.tools.searchNews.done': 'Noticias buscadas', + 'conversations.tools.searchImages.active': 'Buscando imágenes', + 'conversations.tools.searchImages.done': 'Imágenes buscadas', + 'conversations.tools.searchVideos.active': 'Buscando vídeos', + 'conversations.tools.searchVideos.done': 'Vídeos buscados', + 'conversations.tools.findSimilarPages.active': 'Buscando páginas similares', + 'conversations.tools.findSimilarPages.done': 'Páginas similares encontradas', + 'conversations.tools.readPages.active': 'Leyendo páginas', + 'conversations.tools.readPages.done': 'Páginas leídas', + 'conversations.tools.readWebpage.active': 'Leyendo página web', + 'conversations.tools.readWebpage.done': 'Página web leída', + 'conversations.tools.research.active': 'Investigando', + 'conversations.tools.research.done': 'Investigación hecha', + 'conversations.tools.enrichData.active': 'Enriqueciendo datos', + 'conversations.tools.enrichData.done': 'Datos enriquecidos', + 'conversations.tools.buildDataset.active': 'Creando conjunto de datos', + 'conversations.tools.buildDataset.done': 'Conjunto de datos creado', + 'conversations.tools.askTheWeb.active': 'Consultando la web', + 'conversations.tools.askTheWeb.done': 'Web consultada', + 'conversations.tools.browseForYou.active': 'Navegando por ti', + 'conversations.tools.browseForYou.done': 'Navegación hecha por ti', + 'conversations.tools.callApi.active': 'Llamando a la API', + 'conversations.tools.callApi.done': 'API llamada', + 'conversations.tools.downloadFile.active': 'Descargando archivo', + 'conversations.tools.downloadFile.done': 'Archivo descargado', + 'conversations.tools.makePaidRequest.active': 'Haciendo solicitud de pago', + 'conversations.tools.makePaidRequest.done': 'Solicitud de pago hecha', + 'conversations.tools.searchDocs.active': 'Buscando en la documentación', + 'conversations.tools.searchDocs.done': 'Documentación consultada', + 'conversations.tools.readDocs.active': 'Leyendo la documentación', + 'conversations.tools.readDocs.done': 'Documentación leída', + 'conversations.tools.useBrowser.active': 'Usando el navegador', + 'conversations.tools.useBrowser.done': 'Navegador usado', + 'conversations.tools.openPage.active': 'Abriendo página', + 'conversations.tools.openPage.done': 'Página abierta', + 'conversations.tools.navigate.active': 'Navegando', + 'conversations.tools.navigate.done': 'Navegación hecha', + 'conversations.tools.takeScreenshot.active': 'Tomando captura de pantalla', + 'conversations.tools.takeScreenshot.done': 'Captura de pantalla tomada', + 'conversations.tools.scrollPage.active': 'Desplazando', + 'conversations.tools.scrollPage.done': 'Desplazamiento hecho', + 'conversations.tools.readPage.active': 'Leyendo página', + 'conversations.tools.readPage.done': 'Página leída', + 'conversations.tools.analyzeImage.active': 'Analizando imagen', + 'conversations.tools.analyzeImage.done': 'Imagen analizada', + 'conversations.tools.generateImage.active': 'Generando imagen', + 'conversations.tools.generateImage.done': 'Imagen generada', + 'conversations.tools.generateVideo.active': 'Generando vídeo', + 'conversations.tools.generateVideo.done': 'Vídeo generado', + 'conversations.tools.checkMediaModels.active': 'Comprobando modelos multimedia', + 'conversations.tools.checkMediaModels.done': 'Modelos multimedia comprobados', + 'conversations.tools.createDocument.active': 'Creando documento', + 'conversations.tools.createDocument.done': 'Documento creado', + 'conversations.tools.createPresentation.active': 'Creando presentación', + 'conversations.tools.createPresentation.done': 'Presentación creada', + 'conversations.tools.generatePodcast.active': 'Generando pódcast', + 'conversations.tools.generatePodcast.done': 'Pódcast generado', + 'conversations.tools.emailPodcast.active': 'Enviando pódcast por correo', + 'conversations.tools.emailPodcast.done': 'Pódcast enviado por correo', + 'conversations.tools.createAndEmailPodcast.active': 'Creando y enviando pódcast por correo', + 'conversations.tools.createAndEmailPodcast.done': 'Pódcast creado y enviado por correo', + 'conversations.tools.recallMemories.active': 'Recordando memorias', + 'conversations.tools.recallMemories.done': 'Memorias recordadas', + 'conversations.tools.saveToMemory.active': 'Guardando en la memoria', + 'conversations.tools.saveToMemory.done': 'Guardado en la memoria', + 'conversations.tools.forgetMemory.active': 'Olvidando recuerdo', + 'conversations.tools.forgetMemory.done': 'Recuerdo olvidado', + 'conversations.tools.searchMemory.active': 'Buscando en la memoria', + 'conversations.tools.searchMemory.done': 'Memoria consultada', + 'conversations.tools.inspectMemory.active': 'Inspeccionando la memoria', + 'conversations.tools.inspectMemory.done': 'Memoria inspeccionada', + 'conversations.tools.exploreMemory.active': 'Explorando la memoria', + 'conversations.tools.exploreMemory.done': 'Memoria explorada', + 'conversations.tools.saveDocumentToMemory.active': 'Guardando documento en la memoria', + 'conversations.tools.saveDocumentToMemory.done': 'Documento guardado en la memoria', + 'conversations.tools.updateGoals.active': 'Actualizando objetivos', + 'conversations.tools.updateGoals.done': 'Objetivos actualizados', + 'conversations.tools.reviewGoals.active': 'Revisando objetivos', + 'conversations.tools.reviewGoals.done': 'Objetivos revisados', + 'conversations.tools.savePreference.active': 'Guardando preferencia', + 'conversations.tools.savePreference.done': 'Preferencia guardada', + 'conversations.tools.reviewLearnings.active': 'Revisando lo aprendido', + 'conversations.tools.reviewLearnings.done': 'Aprendizajes revisados', + 'conversations.tools.updateLearnings.active': 'Actualizando lo aprendido', + 'conversations.tools.updateLearnings.done': 'Aprendizajes actualizados', + 'conversations.tools.delegateTask.active': 'Delegando tarea', + 'conversations.tools.delegateTask.done': 'Tarea delegada', + 'conversations.tools.runAgentsInParallel.active': 'Ejecutando agentes en paralelo', + 'conversations.tools.runAgentsInParallel.done': 'Agentes ejecutados en paralelo', + 'conversations.tools.messageAgent.active': 'Enviando mensaje al agente', + 'conversations.tools.messageAgent.done': 'Mensaje enviado al agente', + 'conversations.tools.waitForAgent.active': 'Esperando al agente', + 'conversations.tools.waitForAgent.done': 'Espera al agente terminada', + 'conversations.tools.wait.active': 'Esperando', + 'conversations.tools.wait.done': 'Espera terminada', + 'conversations.tools.closeAgent.active': 'Cerrando agente', + 'conversations.tools.closeAgent.done': 'Agente cerrado', + 'conversations.tools.checkAgents.active': 'Comprobando agentes', + 'conversations.tools.checkAgents.done': 'Agentes comprobados', + 'conversations.tools.askQuestion.active': 'Haciéndote una pregunta', + 'conversations.tools.askQuestion.done': 'Te hice una pregunta', + 'conversations.tools.prepareContext.active': 'Preparando contexto', + 'conversations.tools.prepareContext.done': 'Contexto preparado', + 'conversations.tools.extractDetails.active': 'Extrayendo detalles', + 'conversations.tools.extractDetails.done': 'Detalles extraídos', + 'conversations.tools.planNextSteps.active': 'Planificando próximos pasos', + 'conversations.tools.planNextSteps.done': 'Próximos pasos planificados', + 'conversations.tools.reviewWork.active': 'Revisando el trabajo', + 'conversations.tools.reviewWork.done': 'Trabajo revisado', + 'conversations.tools.scoutContext.active': 'Explorando el contexto', + 'conversations.tools.scoutContext.done': 'Contexto explorado', + 'conversations.tools.useTools.active': 'Usando herramientas', + 'conversations.tools.useTools.done': 'Herramientas usadas', + 'conversations.tools.checkConnectedApp.active': 'Comprobando tu app conectada', + 'conversations.tools.checkConnectedApp.done': 'App conectada comprobada', + 'conversations.tools.updateTodos.active': 'Actualizando lista de tareas', + 'conversations.tools.updateTodos.done': 'Lista de tareas actualizada', + 'conversations.tools.requestPlanReview.active': 'Solicitando revisión del plan', + 'conversations.tools.requestPlanReview.done': 'Revisión del plan solicitada', + 'conversations.tools.finishPlan.active': 'Terminando el plan', + 'conversations.tools.finishPlan.done': 'Plan terminado', + 'conversations.tools.setGoal.active': 'Estableciendo objetivo', + 'conversations.tools.setGoal.done': 'Objetivo establecido', + 'conversations.tools.checkGoal.active': 'Comprobando objetivo', + 'conversations.tools.checkGoal.done': 'Objetivo comprobado', + 'conversations.tools.completeGoal.active': 'Completando objetivo', + 'conversations.tools.completeGoal.done': 'Objetivo completado', + 'conversations.tools.scheduleTask.active': 'Programando tarea', + 'conversations.tools.scheduleTask.done': 'Tarea programada', + 'conversations.tools.checkSchedules.active': 'Comprobando programaciones', + 'conversations.tools.checkSchedules.done': 'Programaciones comprobadas', + 'conversations.tools.updateSchedule.active': 'Actualizando tarea programada', + 'conversations.tools.updateSchedule.done': 'Tarea programada actualizada', + 'conversations.tools.removeSchedule.active': 'Eliminando tarea programada', + 'conversations.tools.removeSchedule.done': 'Tarea programada eliminada', + 'conversations.tools.runScheduledTask.active': 'Ejecutando tarea programada', + 'conversations.tools.runScheduledTask.done': 'Tarea programada ejecutada', + 'conversations.tools.checkRunHistory.active': 'Comprobando historial de ejecuciones', + 'conversations.tools.checkRunHistory.done': 'Historial de ejecuciones comprobado', + 'conversations.tools.useApp.active': 'Usando {app}', + 'conversations.tools.useApp.done': '{app} usado', + 'conversations.tools.checkAvailableApps.active': 'Comprobando apps disponibles', + 'conversations.tools.checkAvailableApps.done': 'Apps disponibles comprobadas', + 'conversations.tools.checkConnections.active': 'Comprobando tus conexiones', + 'conversations.tools.checkConnections.done': 'Conexiones comprobadas', + 'conversations.tools.connectApp.active': 'Conectando app', + 'conversations.tools.connectApp.done': 'App conectada', + 'conversations.tools.authorizeApp.active': 'Autorizando app', + 'conversations.tools.authorizeApp.done': 'App autorizada', + 'conversations.tools.findAppActions.active': 'Buscando acciones de la app', + 'conversations.tools.findAppActions.done': 'Acciones de la app encontradas', + 'conversations.tools.runAppAction.active': 'Ejecutando acción de la app', + 'conversations.tools.runAppAction.done': 'Acción de la app ejecutada', + 'conversations.tools.findTools.active': 'Buscando herramientas', + 'conversations.tools.findTools.done': 'Herramientas encontradas', + 'conversations.tools.useTool.active': 'Usando {tool}', + 'conversations.tools.useTool.done': '{tool} usado', + 'conversations.tools.unsubscribe.active': 'Cancelando suscripción', + 'conversations.tools.unsubscribe.done': 'Suscripción cancelada', + 'conversations.tools.searchPlaces.active': 'Buscando lugares', + 'conversations.tools.searchPlaces.done': 'Lugares buscados', + 'conversations.tools.lookUpPlace.active': 'Consultando lugar', + 'conversations.tools.lookUpPlace.done': 'Lugar consultado', + 'conversations.tools.checkMarkets.active': 'Consultando mercados', + 'conversations.tools.checkMarkets.done': 'Mercados consultados', + 'conversations.tools.placeCall.active': 'Realizando llamada', + 'conversations.tools.placeCall.done': 'Llamada realizada', + 'conversations.tools.checkTaskSources.active': 'Comprobando fuentes de tareas', + 'conversations.tools.checkTaskSources.done': 'Fuentes de tareas comprobadas', + 'conversations.tools.updateTaskSources.active': 'Actualizando fuentes de tareas', + 'conversations.tools.updateTaskSources.done': 'Fuentes de tareas actualizadas', + 'conversations.tools.fetchTasks.active': 'Obteniendo tareas', + 'conversations.tools.fetchTasks.done': 'Tareas obtenidas', + 'conversations.tools.checkMcpServers.active': 'Comprobando servidores MCP', + 'conversations.tools.checkMcpServers.done': 'Servidores MCP comprobados', + 'conversations.tools.checkMcpTools.active': 'Comprobando herramientas MCP', + 'conversations.tools.checkMcpTools.done': 'Herramientas MCP comprobadas', + 'conversations.tools.callMcpTool.active': 'Llamando a {tool}', + 'conversations.tools.callMcpTool.done': '{tool} llamado', + 'conversations.tools.searchMcpServers.active': 'Buscando servidores MCP', + 'conversations.tools.searchMcpServers.done': 'Servidores MCP buscados', + 'conversations.tools.connectMcpServer.active': 'Conectando servidor MCP', + 'conversations.tools.connectMcpServer.done': 'Servidor MCP conectado', + 'conversations.tools.disconnectMcpServer.active': 'Desconectando servidor MCP', + 'conversations.tools.disconnectMcpServer.done': 'Servidor MCP desconectado', + 'conversations.tools.removeMcpServer.active': 'Eliminando servidor MCP', + 'conversations.tools.removeMcpServer.done': 'Servidor MCP eliminado', + 'conversations.tools.uploadFile.active': 'Subiendo archivo', + 'conversations.tools.uploadFile.done': 'Archivo subido', + 'conversations.tools.listStoredFiles.active': 'Listando archivos guardados', + 'conversations.tools.listStoredFiles.done': 'Archivos guardados listados', + 'conversations.tools.createShareLink.active': 'Creando enlace para compartir', + 'conversations.tools.createShareLink.done': 'Enlace para compartir creado', + 'conversations.tools.deleteFile.active': 'Eliminando archivo', + 'conversations.tools.deleteFile.done': 'Archivo eliminado', + 'conversations.tools.updateFileAccess.active': 'Actualizando acceso al archivo', + 'conversations.tools.updateFileAccess.done': 'Acceso al archivo actualizado', + 'conversations.tools.deploySite.active': 'Desplegando sitio', + 'conversations.tools.deploySite.done': 'Sitio desplegado', + 'conversations.tools.checkHosting.active': 'Comprobando alojamiento', + 'conversations.tools.checkHosting.done': 'Alojamiento comprobado', + 'conversations.tools.updateHosting.active': 'Actualizando alojamiento', + 'conversations.tools.updateHosting.done': 'Alojamiento actualizado', + 'conversations.tools.rollBackDeployment.active': 'Revirtiendo despliegue', + 'conversations.tools.rollBackDeployment.done': 'Despliegue revertido', + 'conversations.tools.checkWallet.active': 'Comprobando billetera', + 'conversations.tools.checkWallet.done': 'Billetera comprobada', + 'conversations.tools.prepareTransfer.active': 'Preparando transferencia', + 'conversations.tools.prepareTransfer.done': 'Transferencia preparada', + 'conversations.tools.checkTransaction.active': 'Comprobando transacción', + 'conversations.tools.checkTransaction.done': 'Transacción comprobada', + 'conversations.tools.getSwapQuote.active': 'Obteniendo cotización de intercambio', + 'conversations.tools.getSwapQuote.done': 'Cotización de intercambio obtenida', + 'conversations.tools.swapTokens.active': 'Intercambiando tokens', + 'conversations.tools.swapTokens.done': 'Tokens intercambiados', + 'conversations.tools.getBridgeQuote.active': 'Obteniendo cotización de puente', + 'conversations.tools.getBridgeQuote.done': 'Cotización de puente obtenida', + 'conversations.tools.bridgeTokens.active': 'Transfiriendo tokens por puente', + 'conversations.tools.bridgeTokens.done': 'Tokens transferidos por puente', + 'conversations.tools.callDapp.active': 'Llamando al contrato de la app', + 'conversations.tools.callDapp.done': 'Contrato de la app llamado', + 'conversations.tools.useSkill.active': 'Usando habilidad', + 'conversations.tools.useSkill.done': 'Habilidad usada', + 'conversations.tools.searchSkills.active': 'Buscando habilidades', + 'conversations.tools.searchSkills.done': 'Habilidades buscadas', + 'conversations.tools.checkSkills.active': 'Comprobando habilidades', + 'conversations.tools.checkSkills.done': 'Habilidades comprobadas', + 'conversations.tools.installSkill.active': 'Instalando habilidad', + 'conversations.tools.installSkill.done': 'Habilidad instalada', + 'conversations.tools.removeSkill.active': 'Eliminando habilidad', + 'conversations.tools.removeSkill.done': 'Habilidad eliminada', + 'conversations.tools.createSkill.active': 'Creando habilidad', + 'conversations.tools.createSkill.done': 'Habilidad creada', + 'conversations.tools.runWorkflow.active': 'Ejecutando flujo de trabajo', + 'conversations.tools.runWorkflow.done': 'Flujo de trabajo ejecutado', + 'conversations.tools.waitForWorkflow.active': 'Esperando el flujo de trabajo', + 'conversations.tools.waitForWorkflow.done': 'Espera del flujo de trabajo terminada', + 'conversations.tools.designWorkflow.active': 'Diseñando flujo de trabajo', + 'conversations.tools.designWorkflow.done': 'Flujo de trabajo diseñado', + 'conversations.tools.saveWorkflow.active': 'Guardando flujo de trabajo', + 'conversations.tools.saveWorkflow.done': 'Flujo de trabajo guardado', + 'conversations.tools.validateWorkflow.active': 'Validando flujo de trabajo', + 'conversations.tools.validateWorkflow.done': 'Flujo de trabajo validado', + 'conversations.tools.testWorkflow.active': 'Probando flujo de trabajo', + 'conversations.tools.testWorkflow.done': 'Flujo de trabajo probado', + 'conversations.tools.checkWorkflows.active': 'Comprobando flujos de trabajo', + 'conversations.tools.checkWorkflows.done': 'Flujos de trabajo comprobados', + 'conversations.tools.cancelWorkflow.active': 'Cancelando ejecución del flujo de trabajo', + 'conversations.tools.cancelWorkflow.done': 'Ejecución del flujo de trabajo cancelada', + 'conversations.tools.suggestWorkflows.active': 'Sugiriendo flujos de trabajo', + 'conversations.tools.suggestWorkflows.done': 'Flujos de trabajo sugeridos', + 'conversations.tools.checkSettings.active': 'Comprobando ajustes', + 'conversations.tools.checkSettings.done': 'Ajustes comprobados', + 'conversations.tools.checkSecurity.active': 'Comprobando seguridad', + 'conversations.tools.checkSecurity.done': 'Seguridad comprobada', + 'conversations.tools.runDiagnostics.active': 'Ejecutando diagnósticos', + 'conversations.tools.runDiagnostics.done': 'Diagnósticos ejecutados', + 'conversations.tools.checkUsageCosts.active': 'Comprobando costes de uso', + 'conversations.tools.checkUsageCosts.done': 'Costes de uso comprobados', + 'conversations.tools.manageService.active': 'Gestionando servicio en segundo plano', + 'conversations.tools.manageService.done': 'Servicio en segundo plano gestionado', + 'conversations.tools.readPersona.active': 'Leyendo personalidad', + 'conversations.tools.readPersona.done': 'Personalidad leída', + 'conversations.tools.updatePersona.active': 'Actualizando personalidad', + 'conversations.tools.updatePersona.done': 'Personalidad actualizada', + 'conversations.tools.setUpWorkspace.active': 'Configurando espacio de trabajo', + 'conversations.tools.setUpWorkspace.done': 'Espacio de trabajo configurado', + 'conversations.tools.checkArtifacts.active': 'Comprobando artefactos', + 'conversations.tools.checkArtifacts.done': 'Artefactos comprobados', + 'conversations.tools.deleteArtifact.active': 'Eliminando artefacto', + 'conversations.tools.deleteArtifact.done': 'Artefacto eliminado', 'conversations.subagent.noOutput': 'No se devolvió ninguna salida', 'conversations.subagent.close': 'Cerrar', 'conversations.subagent.cancel': 'Cancelar tarea', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 9b9f4e9f97b..4c45a3aac01 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -3398,6 +3398,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Aucun résultat pour l’instant', 'conversations.subagent.input': 'Entrée', 'conversations.subagent.output': 'Sortie', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} étape', + 'conversations.tools.steps.other': '{count} étapes', + 'conversations.tools.working': 'En cours', + 'conversations.tools.noOutput': 'Aucune sortie', + 'conversations.tools.delegatedTo': 'Délégué à {agent}', + 'conversations.tools.openInBrowser': 'Ouvrir dans le navigateur', + 'conversations.tools.status.running': 'en cours', + 'conversations.tools.status.done': 'terminé', + 'conversations.tools.status.failed': 'échec', + 'conversations.tools.status.cancelled': 'annulé', + 'conversations.tools.status.awaiting': 'en attente de réponse', + 'conversations.tools.search.searching': 'Recherche en cours', + 'conversations.tools.search.none': 'Aucun résultat', + 'conversations.tools.search.found.one': '{count} résultat trouvé', + 'conversations.tools.search.found.other': '{count} résultats trouvés', + 'conversations.tools.search.via': 'par {provider}', + 'conversations.tools.readFile.active': 'Lecture du fichier', + 'conversations.tools.readFile.done': 'Fichier lu', + 'conversations.tools.writeFile.active': 'Écriture du fichier', + 'conversations.tools.writeFile.done': 'Fichier écrit', + 'conversations.tools.editFile.active': 'Modification du fichier', + 'conversations.tools.editFile.done': 'Fichier modifié', + 'conversations.tools.applyEdits.active': 'Application des modifications', + 'conversations.tools.applyEdits.done': 'Modifications appliquées', + 'conversations.tools.searchCode.active': 'Recherche dans le code', + 'conversations.tools.searchCode.done': 'Code parcouru', + 'conversations.tools.findFiles.active': 'Recherche de fichiers', + 'conversations.tools.findFiles.done': 'Fichiers trouvés', + 'conversations.tools.listFolder.active': 'Affichage du dossier', + 'conversations.tools.listFolder.done': 'Dossier affiché', + 'conversations.tools.exportCsv.active': 'Export du CSV', + 'conversations.tools.exportCsv.done': 'CSV exporté', + 'conversations.tools.updateMemoryNotes.active': 'Mise à jour des notes de mémoire', + 'conversations.tools.updateMemoryNotes.done': 'Notes de mémoire mises à jour', + 'conversations.tools.runGit.active': 'Exécution de git', + 'conversations.tools.runGit.done': 'git exécuté', + 'conversations.tools.readChanges.active': 'Lecture des modifications', + 'conversations.tools.readChanges.done': 'Modifications lues', + 'conversations.tools.runLinter.active': 'Exécution du linter', + 'conversations.tools.runLinter.done': 'Linter exécuté', + 'conversations.tools.runTests.active': 'Exécution des tests', + 'conversations.tools.runTests.done': 'Tests exécutés', + 'conversations.tools.analyzeCode.active': 'Analyse du code', + 'conversations.tools.analyzeCode.done': 'Code analysé', + 'conversations.tools.insertRecord.active': "Insertion de l'enregistrement", + 'conversations.tools.insertRecord.done': 'Enregistrement inséré', + 'conversations.tools.runCommand.active': 'Exécution de la commande', + 'conversations.tools.runCommand.done': 'Commande exécutée', + 'conversations.tools.runCode.active': 'Exécution du code', + 'conversations.tools.runCode.done': 'Code exécuté', + 'conversations.tools.runPackageManager.active': 'Exécution de npm', + 'conversations.tools.runPackageManager.done': 'npm exécuté', + 'conversations.tools.checkInstalledTools.active': 'Vérification des outils installés', + 'conversations.tools.checkInstalledTools.done': 'Outils installés vérifiés', + 'conversations.tools.installTool.active': "Installation de l'outil", + 'conversations.tools.installTool.done': 'Outil installé', + 'conversations.tools.checkTime.active': "Vérification de l'heure", + 'conversations.tools.checkTime.done': 'Heure vérifiée', + 'conversations.tools.resolveDate.active': 'Calcul de la date', + 'conversations.tools.resolveDate.done': 'Date calculée', + 'conversations.tools.retrieveOutput.active': 'Récupération de la sortie complète', + 'conversations.tools.retrieveOutput.done': 'Sortie complète récupérée', + 'conversations.tools.reviewWorkspace.active': "Examen de l'espace de travail", + 'conversations.tools.reviewWorkspace.done': 'Espace de travail examiné', + 'conversations.tools.configureProxy.active': 'Configuration du proxy', + 'conversations.tools.configureProxy.done': 'Proxy configuré', + 'conversations.tools.checkUpdates.active': 'Recherche de mises à jour', + 'conversations.tools.checkUpdates.done': 'Mises à jour vérifiées', + 'conversations.tools.installUpdate.active': 'Installation de la mise à jour', + 'conversations.tools.installUpdate.done': 'Mise à jour installée', + 'conversations.tools.sendNotification.active': 'Envoi de la notification', + 'conversations.tools.sendNotification.done': 'Notification envoyée', + 'conversations.tools.reviewToolUsage.active': "Examen de l'utilisation des outils", + 'conversations.tools.reviewToolUsage.done': 'Utilisation des outils examinée', + 'conversations.tools.typeKeys.active': 'Saisie en cours', + 'conversations.tools.typeKeys.done': 'Texte saisi', + 'conversations.tools.click.active': 'Clic en cours', + 'conversations.tools.click.done': 'Clic effectué', + 'conversations.tools.searchWeb.active': 'Recherche sur le web', + 'conversations.tools.searchWeb.done': 'Recherche web effectuée', + 'conversations.tools.searchNews.active': "Recherche d'actualités", + 'conversations.tools.searchNews.done': 'Actualités recherchées', + 'conversations.tools.searchImages.active': "Recherche d'images", + 'conversations.tools.searchImages.done': 'Images recherchées', + 'conversations.tools.searchVideos.active': 'Recherche de vidéos', + 'conversations.tools.searchVideos.done': 'Vidéos recherchées', + 'conversations.tools.findSimilarPages.active': 'Recherche de pages similaires', + 'conversations.tools.findSimilarPages.done': 'Pages similaires trouvées', + 'conversations.tools.readPages.active': 'Lecture des pages', + 'conversations.tools.readPages.done': 'Pages lues', + 'conversations.tools.readWebpage.active': 'Lecture de la page web', + 'conversations.tools.readWebpage.done': 'Page web lue', + 'conversations.tools.research.active': 'Recherche approfondie', + 'conversations.tools.research.done': 'Recherche approfondie terminée', + 'conversations.tools.enrichData.active': 'Enrichissement des données', + 'conversations.tools.enrichData.done': 'Données enrichies', + 'conversations.tools.buildDataset.active': 'Création du jeu de données', + 'conversations.tools.buildDataset.done': 'Jeu de données créé', + 'conversations.tools.askTheWeb.active': 'Interrogation du web', + 'conversations.tools.askTheWeb.done': 'Web interrogé', + 'conversations.tools.browseForYou.active': 'Navigation pour vous', + 'conversations.tools.browseForYou.done': 'Navigation effectuée pour vous', + 'conversations.tools.callApi.active': "Appel de l'API", + 'conversations.tools.callApi.done': 'API appelée', + 'conversations.tools.downloadFile.active': 'Téléchargement du fichier', + 'conversations.tools.downloadFile.done': 'Fichier téléchargé', + 'conversations.tools.makePaidRequest.active': "Envoi d'une requête payante", + 'conversations.tools.makePaidRequest.done': 'Requête payante envoyée', + 'conversations.tools.searchDocs.active': 'Recherche dans la documentation', + 'conversations.tools.searchDocs.done': 'Documentation parcourue', + 'conversations.tools.readDocs.active': 'Lecture de la documentation', + 'conversations.tools.readDocs.done': 'Documentation lue', + 'conversations.tools.useBrowser.active': 'Utilisation du navigateur', + 'conversations.tools.useBrowser.done': 'Navigateur utilisé', + 'conversations.tools.openPage.active': 'Ouverture de la page', + 'conversations.tools.openPage.done': 'Page ouverte', + 'conversations.tools.navigate.active': 'Navigation en cours', + 'conversations.tools.navigate.done': 'Navigation effectuée', + 'conversations.tools.takeScreenshot.active': "Capture d'écran en cours", + 'conversations.tools.takeScreenshot.done': "Capture d'écran effectuée", + 'conversations.tools.scrollPage.active': 'Défilement en cours', + 'conversations.tools.scrollPage.done': 'Défilement effectué', + 'conversations.tools.readPage.active': 'Lecture de la page', + 'conversations.tools.readPage.done': 'Page lue', + 'conversations.tools.analyzeImage.active': "Analyse de l'image", + 'conversations.tools.analyzeImage.done': 'Image analysée', + 'conversations.tools.generateImage.active': "Génération de l'image", + 'conversations.tools.generateImage.done': 'Image générée', + 'conversations.tools.generateVideo.active': 'Génération de la vidéo', + 'conversations.tools.generateVideo.done': 'Vidéo générée', + 'conversations.tools.checkMediaModels.active': 'Vérification des modèles multimédias', + 'conversations.tools.checkMediaModels.done': 'Modèles multimédias vérifiés', + 'conversations.tools.createDocument.active': 'Création du document', + 'conversations.tools.createDocument.done': 'Document créé', + 'conversations.tools.createPresentation.active': 'Création de la présentation', + 'conversations.tools.createPresentation.done': 'Présentation créée', + 'conversations.tools.generatePodcast.active': 'Génération du podcast', + 'conversations.tools.generatePodcast.done': 'Podcast généré', + 'conversations.tools.emailPodcast.active': 'Envoi du podcast par e-mail', + 'conversations.tools.emailPodcast.done': 'Podcast envoyé par e-mail', + 'conversations.tools.createAndEmailPodcast.active': 'Création et envoi du podcast par e-mail', + 'conversations.tools.createAndEmailPodcast.done': 'Podcast créé et envoyé par e-mail', + 'conversations.tools.recallMemories.active': 'Rappel des souvenirs', + 'conversations.tools.recallMemories.done': 'Souvenirs rappelés', + 'conversations.tools.saveToMemory.active': 'Enregistrement en mémoire', + 'conversations.tools.saveToMemory.done': 'Enregistré en mémoire', + 'conversations.tools.forgetMemory.active': 'Oubli du souvenir', + 'conversations.tools.forgetMemory.done': 'Souvenir oublié', + 'conversations.tools.searchMemory.active': 'Recherche dans la mémoire', + 'conversations.tools.searchMemory.done': 'Mémoire parcourue', + 'conversations.tools.inspectMemory.active': 'Inspection de la mémoire', + 'conversations.tools.inspectMemory.done': 'Mémoire inspectée', + 'conversations.tools.exploreMemory.active': 'Exploration de la mémoire', + 'conversations.tools.exploreMemory.done': 'Mémoire explorée', + 'conversations.tools.saveDocumentToMemory.active': 'Enregistrement du document en mémoire', + 'conversations.tools.saveDocumentToMemory.done': 'Document enregistré en mémoire', + 'conversations.tools.updateGoals.active': 'Mise à jour des objectifs', + 'conversations.tools.updateGoals.done': 'Objectifs mis à jour', + 'conversations.tools.reviewGoals.active': 'Examen des objectifs', + 'conversations.tools.reviewGoals.done': 'Objectifs examinés', + 'conversations.tools.savePreference.active': 'Enregistrement de la préférence', + 'conversations.tools.savePreference.done': 'Préférence enregistrée', + 'conversations.tools.reviewLearnings.active': 'Examen de mes apprentissages', + 'conversations.tools.reviewLearnings.done': 'Apprentissages examinés', + 'conversations.tools.updateLearnings.active': 'Mise à jour de mes apprentissages', + 'conversations.tools.updateLearnings.done': 'Apprentissages mis à jour', + 'conversations.tools.delegateTask.active': 'Délégation de la tâche', + 'conversations.tools.delegateTask.done': 'Tâche déléguée', + 'conversations.tools.runAgentsInParallel.active': "Exécution d'agents en parallèle", + 'conversations.tools.runAgentsInParallel.done': 'Agents exécutés en parallèle', + 'conversations.tools.messageAgent.active': "Envoi d'un message à l'agent", + 'conversations.tools.messageAgent.done': "Message envoyé à l'agent", + 'conversations.tools.waitForAgent.active': "Attente de l'agent", + 'conversations.tools.waitForAgent.done': "Attente de l'agent terminée", + 'conversations.tools.wait.active': 'Attente en cours', + 'conversations.tools.wait.done': 'Attente terminée', + 'conversations.tools.closeAgent.active': "Fermeture de l'agent", + 'conversations.tools.closeAgent.done': 'Agent fermé', + 'conversations.tools.checkAgents.active': 'Vérification des agents', + 'conversations.tools.checkAgents.done': 'Agents vérifiés', + 'conversations.tools.askQuestion.active': 'Question en cours pour vous', + 'conversations.tools.askQuestion.done': 'Question posée', + 'conversations.tools.prepareContext.active': 'Préparation du contexte', + 'conversations.tools.prepareContext.done': 'Contexte préparé', + 'conversations.tools.extractDetails.active': 'Extraction des détails', + 'conversations.tools.extractDetails.done': 'Détails extraits', + 'conversations.tools.planNextSteps.active': 'Planification des prochaines étapes', + 'conversations.tools.planNextSteps.done': 'Prochaines étapes planifiées', + 'conversations.tools.reviewWork.active': 'Relecture du travail', + 'conversations.tools.reviewWork.done': 'Travail relu', + 'conversations.tools.scoutContext.active': 'Repérage du contexte', + 'conversations.tools.scoutContext.done': 'Contexte repéré', + 'conversations.tools.useTools.active': 'Utilisation des outils', + 'conversations.tools.useTools.done': 'Outils utilisés', + 'conversations.tools.checkConnectedApp.active': 'Vérification de votre app connectée', + 'conversations.tools.checkConnectedApp.done': 'App connectée vérifiée', + 'conversations.tools.updateTodos.active': 'Mise à jour de la liste de tâches', + 'conversations.tools.updateTodos.done': 'Liste de tâches mise à jour', + 'conversations.tools.requestPlanReview.active': 'Demande de relecture du plan', + 'conversations.tools.requestPlanReview.done': 'Relecture du plan demandée', + 'conversations.tools.finishPlan.active': 'Finalisation du plan', + 'conversations.tools.finishPlan.done': 'Plan finalisé', + 'conversations.tools.setGoal.active': "Définition de l'objectif", + 'conversations.tools.setGoal.done': 'Objectif défini', + 'conversations.tools.checkGoal.active': "Vérification de l'objectif", + 'conversations.tools.checkGoal.done': 'Objectif vérifié', + 'conversations.tools.completeGoal.active': "Réalisation de l'objectif", + 'conversations.tools.completeGoal.done': 'Objectif atteint', + 'conversations.tools.scheduleTask.active': 'Planification de la tâche', + 'conversations.tools.scheduleTask.done': 'Tâche planifiée', + 'conversations.tools.checkSchedules.active': 'Vérification des planifications', + 'conversations.tools.checkSchedules.done': 'Planifications vérifiées', + 'conversations.tools.updateSchedule.active': 'Mise à jour de la tâche planifiée', + 'conversations.tools.updateSchedule.done': 'Tâche planifiée mise à jour', + 'conversations.tools.removeSchedule.active': 'Suppression de la tâche planifiée', + 'conversations.tools.removeSchedule.done': 'Tâche planifiée supprimée', + 'conversations.tools.runScheduledTask.active': 'Exécution de la tâche planifiée', + 'conversations.tools.runScheduledTask.done': 'Tâche planifiée exécutée', + 'conversations.tools.checkRunHistory.active': "Vérification de l'historique d'exécution", + 'conversations.tools.checkRunHistory.done': "Historique d'exécution vérifié", + 'conversations.tools.useApp.active': 'Utilisation de {app}', + 'conversations.tools.useApp.done': '{app} utilisé', + 'conversations.tools.checkAvailableApps.active': 'Vérification des apps disponibles', + 'conversations.tools.checkAvailableApps.done': 'Apps disponibles vérifiées', + 'conversations.tools.checkConnections.active': 'Vérification de vos connexions', + 'conversations.tools.checkConnections.done': 'Connexions vérifiées', + 'conversations.tools.connectApp.active': "Connexion de l'app", + 'conversations.tools.connectApp.done': 'App connectée', + 'conversations.tools.authorizeApp.active': "Autorisation de l'app", + 'conversations.tools.authorizeApp.done': 'App autorisée', + 'conversations.tools.findAppActions.active': "Recherche d'actions de l'app", + 'conversations.tools.findAppActions.done': "Actions de l'app trouvées", + 'conversations.tools.runAppAction.active': "Exécution de l'action de l'app", + 'conversations.tools.runAppAction.done': "Action de l'app exécutée", + 'conversations.tools.findTools.active': "Recherche d'outils", + 'conversations.tools.findTools.done': 'Outils trouvés', + 'conversations.tools.useTool.active': 'Utilisation de {tool}', + 'conversations.tools.useTool.done': '{tool} utilisé', + 'conversations.tools.unsubscribe.active': 'Désabonnement en cours', + 'conversations.tools.unsubscribe.done': 'Désabonnement effectué', + 'conversations.tools.searchPlaces.active': 'Recherche de lieux', + 'conversations.tools.searchPlaces.done': 'Lieux recherchés', + 'conversations.tools.lookUpPlace.active': 'Recherche du lieu', + 'conversations.tools.lookUpPlace.done': 'Lieu trouvé', + 'conversations.tools.checkMarkets.active': 'Consultation des marchés', + 'conversations.tools.checkMarkets.done': 'Marchés consultés', + 'conversations.tools.placeCall.active': 'Appel en cours', + 'conversations.tools.placeCall.done': 'Appel passé', + 'conversations.tools.checkTaskSources.active': 'Vérification des sources de tâches', + 'conversations.tools.checkTaskSources.done': 'Sources de tâches vérifiées', + 'conversations.tools.updateTaskSources.active': 'Mise à jour des sources de tâches', + 'conversations.tools.updateTaskSources.done': 'Sources de tâches mises à jour', + 'conversations.tools.fetchTasks.active': 'Récupération des tâches', + 'conversations.tools.fetchTasks.done': 'Tâches récupérées', + 'conversations.tools.checkMcpServers.active': 'Vérification des serveurs MCP', + 'conversations.tools.checkMcpServers.done': 'Serveurs MCP vérifiés', + 'conversations.tools.checkMcpTools.active': 'Vérification des outils MCP', + 'conversations.tools.checkMcpTools.done': 'Outils MCP vérifiés', + 'conversations.tools.callMcpTool.active': 'Appel de {tool}', + 'conversations.tools.callMcpTool.done': '{tool} appelé', + 'conversations.tools.searchMcpServers.active': 'Recherche de serveurs MCP', + 'conversations.tools.searchMcpServers.done': 'Serveurs MCP recherchés', + 'conversations.tools.connectMcpServer.active': 'Connexion du serveur MCP', + 'conversations.tools.connectMcpServer.done': 'Serveur MCP connecté', + 'conversations.tools.disconnectMcpServer.active': 'Déconnexion du serveur MCP', + 'conversations.tools.disconnectMcpServer.done': 'Serveur MCP déconnecté', + 'conversations.tools.removeMcpServer.active': 'Suppression du serveur MCP', + 'conversations.tools.removeMcpServer.done': 'Serveur MCP supprimé', + 'conversations.tools.uploadFile.active': 'Envoi du fichier', + 'conversations.tools.uploadFile.done': 'Fichier envoyé', + 'conversations.tools.listStoredFiles.active': 'Liste des fichiers stockés', + 'conversations.tools.listStoredFiles.done': 'Fichiers stockés listés', + 'conversations.tools.createShareLink.active': 'Création du lien de partage', + 'conversations.tools.createShareLink.done': 'Lien de partage créé', + 'conversations.tools.deleteFile.active': 'Suppression du fichier', + 'conversations.tools.deleteFile.done': 'Fichier supprimé', + 'conversations.tools.updateFileAccess.active': "Mise à jour de l'accès au fichier", + 'conversations.tools.updateFileAccess.done': 'Accès au fichier mis à jour', + 'conversations.tools.deploySite.active': 'Déploiement du site', + 'conversations.tools.deploySite.done': 'Site déployé', + 'conversations.tools.checkHosting.active': "Vérification de l'hébergement", + 'conversations.tools.checkHosting.done': 'Hébergement vérifié', + 'conversations.tools.updateHosting.active': "Mise à jour de l'hébergement", + 'conversations.tools.updateHosting.done': 'Hébergement mis à jour', + 'conversations.tools.rollBackDeployment.active': 'Annulation du déploiement', + 'conversations.tools.rollBackDeployment.done': 'Déploiement annulé', + 'conversations.tools.checkWallet.active': 'Vérification du portefeuille', + 'conversations.tools.checkWallet.done': 'Portefeuille vérifié', + 'conversations.tools.prepareTransfer.active': 'Préparation du transfert', + 'conversations.tools.prepareTransfer.done': 'Transfert préparé', + 'conversations.tools.checkTransaction.active': 'Vérification de la transaction', + 'conversations.tools.checkTransaction.done': 'Transaction vérifiée', + 'conversations.tools.getSwapQuote.active': "Obtention du devis d'échange", + 'conversations.tools.getSwapQuote.done': "Devis d'échange obtenu", + 'conversations.tools.swapTokens.active': 'Échange de jetons', + 'conversations.tools.swapTokens.done': 'Jetons échangés', + 'conversations.tools.getBridgeQuote.active': 'Obtention du devis de pont', + 'conversations.tools.getBridgeQuote.done': 'Devis de pont obtenu', + 'conversations.tools.bridgeTokens.active': 'Transfert de jetons par pont', + 'conversations.tools.bridgeTokens.done': 'Jetons transférés par pont', + 'conversations.tools.callDapp.active': "Appel du contrat de l'app", + 'conversations.tools.callDapp.done': "Contrat de l'app appelé", + 'conversations.tools.useSkill.active': 'Utilisation de la compétence', + 'conversations.tools.useSkill.done': 'Compétence utilisée', + 'conversations.tools.searchSkills.active': 'Recherche de compétences', + 'conversations.tools.searchSkills.done': 'Compétences recherchées', + 'conversations.tools.checkSkills.active': 'Vérification des compétences', + 'conversations.tools.checkSkills.done': 'Compétences vérifiées', + 'conversations.tools.installSkill.active': 'Installation de la compétence', + 'conversations.tools.installSkill.done': 'Compétence installée', + 'conversations.tools.removeSkill.active': 'Suppression de la compétence', + 'conversations.tools.removeSkill.done': 'Compétence supprimée', + 'conversations.tools.createSkill.active': 'Création de la compétence', + 'conversations.tools.createSkill.done': 'Compétence créée', + 'conversations.tools.runWorkflow.active': 'Exécution du workflow', + 'conversations.tools.runWorkflow.done': 'Workflow exécuté', + 'conversations.tools.waitForWorkflow.active': 'Attente du workflow', + 'conversations.tools.waitForWorkflow.done': 'Attente du workflow terminée', + 'conversations.tools.designWorkflow.active': 'Conception du workflow', + 'conversations.tools.designWorkflow.done': 'Workflow conçu', + 'conversations.tools.saveWorkflow.active': 'Enregistrement du workflow', + 'conversations.tools.saveWorkflow.done': 'Workflow enregistré', + 'conversations.tools.validateWorkflow.active': 'Validation du workflow', + 'conversations.tools.validateWorkflow.done': 'Workflow validé', + 'conversations.tools.testWorkflow.active': 'Test du workflow', + 'conversations.tools.testWorkflow.done': 'Workflow testé', + 'conversations.tools.checkWorkflows.active': 'Vérification des workflows', + 'conversations.tools.checkWorkflows.done': 'Workflows vérifiés', + 'conversations.tools.cancelWorkflow.active': "Annulation de l'exécution du workflow", + 'conversations.tools.cancelWorkflow.done': 'Exécution du workflow annulée', + 'conversations.tools.suggestWorkflows.active': 'Suggestion de workflows', + 'conversations.tools.suggestWorkflows.done': 'Workflows suggérés', + 'conversations.tools.checkSettings.active': 'Vérification des paramètres', + 'conversations.tools.checkSettings.done': 'Paramètres vérifiés', + 'conversations.tools.checkSecurity.active': 'Vérification de la sécurité', + 'conversations.tools.checkSecurity.done': 'Sécurité vérifiée', + 'conversations.tools.runDiagnostics.active': 'Exécution des diagnostics', + 'conversations.tools.runDiagnostics.done': 'Diagnostics exécutés', + 'conversations.tools.checkUsageCosts.active': "Vérification des coûts d'utilisation", + 'conversations.tools.checkUsageCosts.done': "Coûts d'utilisation vérifiés", + 'conversations.tools.manageService.active': 'Gestion du service en arrière-plan', + 'conversations.tools.manageService.done': 'Service en arrière-plan géré', + 'conversations.tools.readPersona.active': 'Lecture du persona', + 'conversations.tools.readPersona.done': 'Persona lu', + 'conversations.tools.updatePersona.active': 'Mise à jour du persona', + 'conversations.tools.updatePersona.done': 'Persona mis à jour', + 'conversations.tools.setUpWorkspace.active': "Configuration de l'espace de travail", + 'conversations.tools.setUpWorkspace.done': 'Espace de travail configuré', + 'conversations.tools.checkArtifacts.active': 'Vérification des artefacts', + 'conversations.tools.checkArtifacts.done': 'Artefacts vérifiés', + 'conversations.tools.deleteArtifact.active': "Suppression de l'artefact", + 'conversations.tools.deleteArtifact.done': 'Artefact supprimé', 'conversations.subagent.noOutput': 'Aucune sortie renvoyée', 'conversations.subagent.close': 'Fermer', 'conversations.subagent.cancel': 'Annuler la tâche', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 695dee834bf..89e04762628 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -3317,6 +3317,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'अभी तक कोई आउटपुट नहीं', 'conversations.subagent.input': 'इनपुट', 'conversations.subagent.output': 'आउटपुट', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} चरण', + 'conversations.tools.steps.other': '{count} चरण', + 'conversations.tools.working': 'काम जारी है', + 'conversations.tools.noOutput': 'कोई आउटपुट नहीं', + 'conversations.tools.delegatedTo': '{agent} को सौंपा गया', + 'conversations.tools.openInBrowser': 'ब्राउज़र में खोलें', + 'conversations.tools.status.running': 'चल रहा है', + 'conversations.tools.status.done': 'पूरा', + 'conversations.tools.status.failed': 'विफल', + 'conversations.tools.status.cancelled': 'रद्द', + 'conversations.tools.status.awaiting': 'इनपुट की प्रतीक्षा', + 'conversations.tools.search.searching': 'खोज रहा है', + 'conversations.tools.search.none': 'कोई परिणाम नहीं', + 'conversations.tools.search.found.one': '{count} परिणाम मिला', + 'conversations.tools.search.found.other': '{count} परिणाम मिले', + 'conversations.tools.search.via': '{provider} के ज़रिए', + 'conversations.tools.readFile.active': 'फ़ाइल पढ़ रहा है', + 'conversations.tools.readFile.done': 'फ़ाइल पढ़ी', + 'conversations.tools.writeFile.active': 'फ़ाइल लिख रहा है', + 'conversations.tools.writeFile.done': 'फ़ाइल लिखी', + 'conversations.tools.editFile.active': 'फ़ाइल संपादित कर रहा है', + 'conversations.tools.editFile.done': 'फ़ाइल संपादित की', + 'conversations.tools.applyEdits.active': 'बदलाव लागू कर रहा है', + 'conversations.tools.applyEdits.done': 'बदलाव लागू किए', + 'conversations.tools.searchCode.active': 'कोड खोज रहा है', + 'conversations.tools.searchCode.done': 'कोड खोजा', + 'conversations.tools.findFiles.active': 'फ़ाइलें ढूँढ रहा है', + 'conversations.tools.findFiles.done': 'फ़ाइलें ढूँढीं', + 'conversations.tools.listFolder.active': 'फ़ोल्डर की सूची बना रहा है', + 'conversations.tools.listFolder.done': 'फ़ोल्डर की सूची बनाई', + 'conversations.tools.exportCsv.active': 'CSV निर्यात कर रहा है', + 'conversations.tools.exportCsv.done': 'CSV निर्यात किया', + 'conversations.tools.updateMemoryNotes.active': 'मेमोरी नोट्स अपडेट कर रहा है', + 'conversations.tools.updateMemoryNotes.done': 'मेमोरी नोट्स अपडेट किए', + 'conversations.tools.runGit.active': 'git चला रहा है', + 'conversations.tools.runGit.done': 'git चलाया', + 'conversations.tools.readChanges.active': 'बदलाव पढ़ रहा है', + 'conversations.tools.readChanges.done': 'बदलाव पढ़े', + 'conversations.tools.runLinter.active': 'लिंटर चला रहा है', + 'conversations.tools.runLinter.done': 'लिंटर चलाया', + 'conversations.tools.runTests.active': 'टेस्ट चला रहा है', + 'conversations.tools.runTests.done': 'टेस्ट चलाए', + 'conversations.tools.analyzeCode.active': 'कोड का विश्लेषण कर रहा है', + 'conversations.tools.analyzeCode.done': 'कोड का विश्लेषण किया', + 'conversations.tools.insertRecord.active': 'रिकॉर्ड जोड़ रहा है', + 'conversations.tools.insertRecord.done': 'रिकॉर्ड जोड़ा', + 'conversations.tools.runCommand.active': 'कमांड चला रहा है', + 'conversations.tools.runCommand.done': 'कमांड चलाई', + 'conversations.tools.runCode.active': 'कोड चला रहा है', + 'conversations.tools.runCode.done': 'कोड चलाया', + 'conversations.tools.runPackageManager.active': 'npm चला रहा है', + 'conversations.tools.runPackageManager.done': 'npm चलाया', + 'conversations.tools.checkInstalledTools.active': 'इंस्टॉल किए गए टूल जाँच रहा है', + 'conversations.tools.checkInstalledTools.done': 'इंस्टॉल किए गए टूल जाँचे', + 'conversations.tools.installTool.active': 'टूल इंस्टॉल कर रहा है', + 'conversations.tools.installTool.done': 'टूल इंस्टॉल किया', + 'conversations.tools.checkTime.active': 'समय देख रहा है', + 'conversations.tools.checkTime.done': 'समय देखा', + 'conversations.tools.resolveDate.active': 'तारीख़ निकाल रहा है', + 'conversations.tools.resolveDate.done': 'तारीख़ निकाली', + 'conversations.tools.retrieveOutput.active': 'पूरा आउटपुट ला रहा है', + 'conversations.tools.retrieveOutput.done': 'पूरा आउटपुट लाया', + 'conversations.tools.reviewWorkspace.active': 'वर्कस्पेस की समीक्षा कर रहा है', + 'conversations.tools.reviewWorkspace.done': 'वर्कस्पेस की समीक्षा की', + 'conversations.tools.configureProxy.active': 'प्रॉक्सी कॉन्फ़िगर कर रहा है', + 'conversations.tools.configureProxy.done': 'प्रॉक्सी कॉन्फ़िगर की', + 'conversations.tools.checkUpdates.active': 'अपडेट जाँच रहा है', + 'conversations.tools.checkUpdates.done': 'अपडेट जाँचे', + 'conversations.tools.installUpdate.active': 'अपडेट इंस्टॉल कर रहा है', + 'conversations.tools.installUpdate.done': 'अपडेट इंस्टॉल किया', + 'conversations.tools.sendNotification.active': 'सूचना भेज रहा है', + 'conversations.tools.sendNotification.done': 'सूचना भेजी', + 'conversations.tools.reviewToolUsage.active': 'टूल उपयोग की समीक्षा कर रहा है', + 'conversations.tools.reviewToolUsage.done': 'टूल उपयोग की समीक्षा की', + 'conversations.tools.typeKeys.active': 'टाइप कर रहा है', + 'conversations.tools.typeKeys.done': 'टाइप किया', + 'conversations.tools.click.active': 'क्लिक कर रहा है', + 'conversations.tools.click.done': 'क्लिक किया', + 'conversations.tools.searchWeb.active': 'वेब पर खोज रहा है', + 'conversations.tools.searchWeb.done': 'वेब पर खोजा', + 'conversations.tools.searchNews.active': 'समाचार खोज रहा है', + 'conversations.tools.searchNews.done': 'समाचार खोजे', + 'conversations.tools.searchImages.active': 'चित्र खोज रहा है', + 'conversations.tools.searchImages.done': 'चित्र खोजे', + 'conversations.tools.searchVideos.active': 'वीडियो खोज रहा है', + 'conversations.tools.searchVideos.done': 'वीडियो खोजे', + 'conversations.tools.findSimilarPages.active': 'मिलते-जुलते पेज ढूँढ रहा है', + 'conversations.tools.findSimilarPages.done': 'मिलते-जुलते पेज ढूँढे', + 'conversations.tools.readPages.active': 'पेज पढ़ रहा है', + 'conversations.tools.readPages.done': 'पेज पढ़े', + 'conversations.tools.readWebpage.active': 'वेबपेज पढ़ रहा है', + 'conversations.tools.readWebpage.done': 'वेबपेज पढ़ा', + 'conversations.tools.research.active': 'शोध कर रहा है', + 'conversations.tools.research.done': 'शोध किया', + 'conversations.tools.enrichData.active': 'डेटा समृद्ध कर रहा है', + 'conversations.tools.enrichData.done': 'डेटा समृद्ध किया', + 'conversations.tools.buildDataset.active': 'डेटासेट बना रहा है', + 'conversations.tools.buildDataset.done': 'डेटासेट बनाया', + 'conversations.tools.askTheWeb.active': 'वेब से पूछ रहा है', + 'conversations.tools.askTheWeb.done': 'वेब से पूछा', + 'conversations.tools.browseForYou.active': 'आपके लिए ब्राउज़ कर रहा है', + 'conversations.tools.browseForYou.done': 'आपके लिए ब्राउज़ किया', + 'conversations.tools.callApi.active': 'API कॉल कर रहा है', + 'conversations.tools.callApi.done': 'API कॉल किया', + 'conversations.tools.downloadFile.active': 'फ़ाइल डाउनलोड कर रहा है', + 'conversations.tools.downloadFile.done': 'फ़ाइल डाउनलोड की', + 'conversations.tools.makePaidRequest.active': 'सशुल्क अनुरोध भेज रहा है', + 'conversations.tools.makePaidRequest.done': 'सशुल्क अनुरोध भेजा', + 'conversations.tools.searchDocs.active': 'दस्तावेज़ खोज रहा है', + 'conversations.tools.searchDocs.done': 'दस्तावेज़ खोजे', + 'conversations.tools.readDocs.active': 'दस्तावेज़ पढ़ रहा है', + 'conversations.tools.readDocs.done': 'दस्तावेज़ पढ़े', + 'conversations.tools.useBrowser.active': 'ब्राउज़र का उपयोग कर रहा है', + 'conversations.tools.useBrowser.done': 'ब्राउज़र का उपयोग किया', + 'conversations.tools.openPage.active': 'पेज खोल रहा है', + 'conversations.tools.openPage.done': 'पेज खोला', + 'conversations.tools.navigate.active': 'नेविगेट कर रहा है', + 'conversations.tools.navigate.done': 'नेविगेट किया', + 'conversations.tools.takeScreenshot.active': 'स्क्रीनशॉट ले रहा है', + 'conversations.tools.takeScreenshot.done': 'स्क्रीनशॉट लिया', + 'conversations.tools.scrollPage.active': 'स्क्रॉल कर रहा है', + 'conversations.tools.scrollPage.done': 'स्क्रॉल किया', + 'conversations.tools.readPage.active': 'पेज पढ़ रहा है', + 'conversations.tools.readPage.done': 'पेज पढ़ा', + 'conversations.tools.analyzeImage.active': 'चित्र का विश्लेषण कर रहा है', + 'conversations.tools.analyzeImage.done': 'चित्र का विश्लेषण किया', + 'conversations.tools.generateImage.active': 'चित्र बना रहा है', + 'conversations.tools.generateImage.done': 'चित्र बनाया', + 'conversations.tools.generateVideo.active': 'वीडियो बना रहा है', + 'conversations.tools.generateVideo.done': 'वीडियो बनाया', + 'conversations.tools.checkMediaModels.active': 'मीडिया मॉडल जाँच रहा है', + 'conversations.tools.checkMediaModels.done': 'मीडिया मॉडल जाँचे', + 'conversations.tools.createDocument.active': 'दस्तावेज़ बना रहा है', + 'conversations.tools.createDocument.done': 'दस्तावेज़ बनाया', + 'conversations.tools.createPresentation.active': 'प्रेज़ेंटेशन बना रहा है', + 'conversations.tools.createPresentation.done': 'प्रेज़ेंटेशन बनाया', + 'conversations.tools.generatePodcast.active': 'पॉडकास्ट बना रहा है', + 'conversations.tools.generatePodcast.done': 'पॉडकास्ट बनाया', + 'conversations.tools.emailPodcast.active': 'पॉडकास्ट ईमेल कर रहा है', + 'conversations.tools.emailPodcast.done': 'पॉडकास्ट ईमेल किया', + 'conversations.tools.createAndEmailPodcast.active': 'पॉडकास्ट बनाकर ईमेल कर रहा है', + 'conversations.tools.createAndEmailPodcast.done': 'पॉडकास्ट बनाकर ईमेल किया', + 'conversations.tools.recallMemories.active': 'यादें खोज रहा है', + 'conversations.tools.recallMemories.done': 'यादें खोजीं', + 'conversations.tools.saveToMemory.active': 'मेमोरी में सहेज रहा है', + 'conversations.tools.saveToMemory.done': 'मेमोरी में सहेजा', + 'conversations.tools.forgetMemory.active': 'मेमोरी भुला रहा है', + 'conversations.tools.forgetMemory.done': 'मेमोरी भुलाई', + 'conversations.tools.searchMemory.active': 'मेमोरी में खोज रहा है', + 'conversations.tools.searchMemory.done': 'मेमोरी में खोजा', + 'conversations.tools.inspectMemory.active': 'मेमोरी की जाँच कर रहा है', + 'conversations.tools.inspectMemory.done': 'मेमोरी की जाँच की', + 'conversations.tools.exploreMemory.active': 'मेमोरी देख रहा है', + 'conversations.tools.exploreMemory.done': 'मेमोरी देखी', + 'conversations.tools.saveDocumentToMemory.active': 'दस्तावेज़ मेमोरी में सहेज रहा है', + 'conversations.tools.saveDocumentToMemory.done': 'दस्तावेज़ मेमोरी में सहेजा', + 'conversations.tools.updateGoals.active': 'लक्ष्य अपडेट कर रहा है', + 'conversations.tools.updateGoals.done': 'लक्ष्य अपडेट किए', + 'conversations.tools.reviewGoals.active': 'लक्ष्यों की समीक्षा कर रहा है', + 'conversations.tools.reviewGoals.done': 'लक्ष्यों की समीक्षा की', + 'conversations.tools.savePreference.active': 'पसंद सहेज रहा है', + 'conversations.tools.savePreference.done': 'पसंद सहेजी', + 'conversations.tools.reviewLearnings.active': 'सीखी बातों की समीक्षा कर रहा है', + 'conversations.tools.reviewLearnings.done': 'सीखी बातों की समीक्षा की', + 'conversations.tools.updateLearnings.active': 'सीखी बातें अपडेट कर रहा है', + 'conversations.tools.updateLearnings.done': 'सीखी बातें अपडेट कीं', + 'conversations.tools.delegateTask.active': 'कार्य सौंप रहा है', + 'conversations.tools.delegateTask.done': 'कार्य सौंपा', + 'conversations.tools.runAgentsInParallel.active': 'एजेंट समानांतर चला रहा है', + 'conversations.tools.runAgentsInParallel.done': 'एजेंट समानांतर चलाए', + 'conversations.tools.messageAgent.active': 'एजेंट को संदेश भेज रहा है', + 'conversations.tools.messageAgent.done': 'एजेंट को संदेश भेजा', + 'conversations.tools.waitForAgent.active': 'एजेंट की प्रतीक्षा कर रहा है', + 'conversations.tools.waitForAgent.done': 'एजेंट की प्रतीक्षा की', + 'conversations.tools.wait.active': 'प्रतीक्षा कर रहा है', + 'conversations.tools.wait.done': 'प्रतीक्षा की', + 'conversations.tools.closeAgent.active': 'एजेंट बंद कर रहा है', + 'conversations.tools.closeAgent.done': 'एजेंट बंद किया', + 'conversations.tools.checkAgents.active': 'एजेंट जाँच रहा है', + 'conversations.tools.checkAgents.done': 'एजेंट जाँचे', + 'conversations.tools.askQuestion.active': 'आपसे सवाल पूछ रहा है', + 'conversations.tools.askQuestion.done': 'आपसे सवाल पूछा', + 'conversations.tools.prepareContext.active': 'संदर्भ तैयार कर रहा है', + 'conversations.tools.prepareContext.done': 'संदर्भ तैयार किया', + 'conversations.tools.extractDetails.active': 'विवरण निकाल रहा है', + 'conversations.tools.extractDetails.done': 'विवरण निकाले', + 'conversations.tools.planNextSteps.active': 'अगले कदमों की योजना बना रहा है', + 'conversations.tools.planNextSteps.done': 'अगले कदमों की योजना बनाई', + 'conversations.tools.reviewWork.active': 'काम की समीक्षा कर रहा है', + 'conversations.tools.reviewWork.done': 'काम की समीक्षा की', + 'conversations.tools.scoutContext.active': 'संदर्भ टटोल रहा है', + 'conversations.tools.scoutContext.done': 'संदर्भ टटोला', + 'conversations.tools.useTools.active': 'टूल का उपयोग कर रहा है', + 'conversations.tools.useTools.done': 'टूल का उपयोग किया', + 'conversations.tools.checkConnectedApp.active': 'आपका कनेक्टेड ऐप जाँच रहा है', + 'conversations.tools.checkConnectedApp.done': 'आपका कनेक्टेड ऐप जाँचा', + 'conversations.tools.updateTodos.active': 'कार्य सूची अपडेट कर रहा है', + 'conversations.tools.updateTodos.done': 'कार्य सूची अपडेट की', + 'conversations.tools.requestPlanReview.active': 'योजना की समीक्षा का अनुरोध कर रहा है', + 'conversations.tools.requestPlanReview.done': 'योजना की समीक्षा का अनुरोध किया', + 'conversations.tools.finishPlan.active': 'योजना पूरी कर रहा है', + 'conversations.tools.finishPlan.done': 'योजना पूरी की', + 'conversations.tools.setGoal.active': 'लक्ष्य तय कर रहा है', + 'conversations.tools.setGoal.done': 'लक्ष्य तय किया', + 'conversations.tools.checkGoal.active': 'लक्ष्य जाँच रहा है', + 'conversations.tools.checkGoal.done': 'लक्ष्य जाँचा', + 'conversations.tools.completeGoal.active': 'लक्ष्य पूरा कर रहा है', + 'conversations.tools.completeGoal.done': 'लक्ष्य पूरा किया', + 'conversations.tools.scheduleTask.active': 'कार्य शेड्यूल कर रहा है', + 'conversations.tools.scheduleTask.done': 'कार्य शेड्यूल किया', + 'conversations.tools.checkSchedules.active': 'शेड्यूल जाँच रहा है', + 'conversations.tools.checkSchedules.done': 'शेड्यूल जाँचे', + 'conversations.tools.updateSchedule.active': 'शेड्यूल किया गया कार्य अपडेट कर रहा है', + 'conversations.tools.updateSchedule.done': 'शेड्यूल किया गया कार्य अपडेट किया', + 'conversations.tools.removeSchedule.active': 'शेड्यूल किया गया कार्य हटा रहा है', + 'conversations.tools.removeSchedule.done': 'शेड्यूल किया गया कार्य हटाया', + 'conversations.tools.runScheduledTask.active': 'शेड्यूल किया गया कार्य चला रहा है', + 'conversations.tools.runScheduledTask.done': 'शेड्यूल किया गया कार्य चलाया', + 'conversations.tools.checkRunHistory.active': 'रन इतिहास जाँच रहा है', + 'conversations.tools.checkRunHistory.done': 'रन इतिहास जाँचा', + 'conversations.tools.useApp.active': '{app} का उपयोग कर रहा है', + 'conversations.tools.useApp.done': '{app} का उपयोग किया', + 'conversations.tools.checkAvailableApps.active': 'उपलब्ध ऐप जाँच रहा है', + 'conversations.tools.checkAvailableApps.done': 'उपलब्ध ऐप जाँचे', + 'conversations.tools.checkConnections.active': 'आपके कनेक्शन जाँच रहा है', + 'conversations.tools.checkConnections.done': 'आपके कनेक्शन जाँचे', + 'conversations.tools.connectApp.active': 'ऐप कनेक्ट कर रहा है', + 'conversations.tools.connectApp.done': 'ऐप कनेक्ट किया', + 'conversations.tools.authorizeApp.active': 'ऐप को अधिकृत कर रहा है', + 'conversations.tools.authorizeApp.done': 'ऐप को अधिकृत किया', + 'conversations.tools.findAppActions.active': 'ऐप क्रियाएँ ढूँढ रहा है', + 'conversations.tools.findAppActions.done': 'ऐप क्रियाएँ ढूँढीं', + 'conversations.tools.runAppAction.active': 'ऐप क्रिया चला रहा है', + 'conversations.tools.runAppAction.done': 'ऐप क्रिया चलाई', + 'conversations.tools.findTools.active': 'टूल ढूँढ रहा है', + 'conversations.tools.findTools.done': 'टूल ढूँढे', + 'conversations.tools.useTool.active': '{tool} का उपयोग कर रहा है', + 'conversations.tools.useTool.done': '{tool} का उपयोग किया', + 'conversations.tools.unsubscribe.active': 'सदस्यता रद्द कर रहा है', + 'conversations.tools.unsubscribe.done': 'सदस्यता रद्द की', + 'conversations.tools.searchPlaces.active': 'स्थान खोज रहा है', + 'conversations.tools.searchPlaces.done': 'स्थान खोजे', + 'conversations.tools.lookUpPlace.active': 'स्थान की जानकारी ले रहा है', + 'conversations.tools.lookUpPlace.done': 'स्थान की जानकारी ली', + 'conversations.tools.checkMarkets.active': 'बाज़ार देख रहा है', + 'conversations.tools.checkMarkets.done': 'बाज़ार देखे', + 'conversations.tools.placeCall.active': 'कॉल कर रहा है', + 'conversations.tools.placeCall.done': 'कॉल किया', + 'conversations.tools.checkTaskSources.active': 'कार्य स्रोत जाँच रहा है', + 'conversations.tools.checkTaskSources.done': 'कार्य स्रोत जाँचे', + 'conversations.tools.updateTaskSources.active': 'कार्य स्रोत अपडेट कर रहा है', + 'conversations.tools.updateTaskSources.done': 'कार्य स्रोत अपडेट किए', + 'conversations.tools.fetchTasks.active': 'कार्य ला रहा है', + 'conversations.tools.fetchTasks.done': 'कार्य लाए', + 'conversations.tools.checkMcpServers.active': 'MCP सर्वर जाँच रहा है', + 'conversations.tools.checkMcpServers.done': 'MCP सर्वर जाँचे', + 'conversations.tools.checkMcpTools.active': 'MCP टूल जाँच रहा है', + 'conversations.tools.checkMcpTools.done': 'MCP टूल जाँचे', + 'conversations.tools.callMcpTool.active': '{tool} कॉल कर रहा है', + 'conversations.tools.callMcpTool.done': '{tool} कॉल किया', + 'conversations.tools.searchMcpServers.active': 'MCP सर्वर खोज रहा है', + 'conversations.tools.searchMcpServers.done': 'MCP सर्वर खोजे', + 'conversations.tools.connectMcpServer.active': 'MCP सर्वर कनेक्ट कर रहा है', + 'conversations.tools.connectMcpServer.done': 'MCP सर्वर कनेक्ट किया', + 'conversations.tools.disconnectMcpServer.active': 'MCP सर्वर डिस्कनेक्ट कर रहा है', + 'conversations.tools.disconnectMcpServer.done': 'MCP सर्वर डिस्कनेक्ट किया', + 'conversations.tools.removeMcpServer.active': 'MCP सर्वर हटा रहा है', + 'conversations.tools.removeMcpServer.done': 'MCP सर्वर हटाया', + 'conversations.tools.uploadFile.active': 'फ़ाइल अपलोड कर रहा है', + 'conversations.tools.uploadFile.done': 'फ़ाइल अपलोड की', + 'conversations.tools.listStoredFiles.active': 'सहेजी गई फ़ाइलों की सूची बना रहा है', + 'conversations.tools.listStoredFiles.done': 'सहेजी गई फ़ाइलों की सूची बनाई', + 'conversations.tools.createShareLink.active': 'शेयर लिंक बना रहा है', + 'conversations.tools.createShareLink.done': 'शेयर लिंक बनाया', + 'conversations.tools.deleteFile.active': 'फ़ाइल हटा रहा है', + 'conversations.tools.deleteFile.done': 'फ़ाइल हटाई', + 'conversations.tools.updateFileAccess.active': 'फ़ाइल एक्सेस अपडेट कर रहा है', + 'conversations.tools.updateFileAccess.done': 'फ़ाइल एक्सेस अपडेट किया', + 'conversations.tools.deploySite.active': 'साइट डिप्लॉय कर रहा है', + 'conversations.tools.deploySite.done': 'साइट डिप्लॉय की', + 'conversations.tools.checkHosting.active': 'होस्टिंग जाँच रहा है', + 'conversations.tools.checkHosting.done': 'होस्टिंग जाँची', + 'conversations.tools.updateHosting.active': 'होस्टिंग अपडेट कर रहा है', + 'conversations.tools.updateHosting.done': 'होस्टिंग अपडेट की', + 'conversations.tools.rollBackDeployment.active': 'डिप्लॉयमेंट वापस ले रहा है', + 'conversations.tools.rollBackDeployment.done': 'डिप्लॉयमेंट वापस लिया', + 'conversations.tools.checkWallet.active': 'वॉलेट जाँच रहा है', + 'conversations.tools.checkWallet.done': 'वॉलेट जाँचा', + 'conversations.tools.prepareTransfer.active': 'ट्रांसफ़र तैयार कर रहा है', + 'conversations.tools.prepareTransfer.done': 'ट्रांसफ़र तैयार किया', + 'conversations.tools.checkTransaction.active': 'लेन-देन जाँच रहा है', + 'conversations.tools.checkTransaction.done': 'लेन-देन जाँचा', + 'conversations.tools.getSwapQuote.active': 'स्वैप कोट ले रहा है', + 'conversations.tools.getSwapQuote.done': 'स्वैप कोट लिया', + 'conversations.tools.swapTokens.active': 'टोकन स्वैप कर रहा है', + 'conversations.tools.swapTokens.done': 'टोकन स्वैप किए', + 'conversations.tools.getBridgeQuote.active': 'ब्रिज कोट ले रहा है', + 'conversations.tools.getBridgeQuote.done': 'ब्रिज कोट लिया', + 'conversations.tools.bridgeTokens.active': 'टोकन ब्रिज कर रहा है', + 'conversations.tools.bridgeTokens.done': 'टोकन ब्रिज किए', + 'conversations.tools.callDapp.active': 'ऐप कॉन्ट्रैक्ट कॉल कर रहा है', + 'conversations.tools.callDapp.done': 'ऐप कॉन्ट्रैक्ट कॉल किया', + 'conversations.tools.useSkill.active': 'स्किल का उपयोग कर रहा है', + 'conversations.tools.useSkill.done': 'स्किल का उपयोग किया', + 'conversations.tools.searchSkills.active': 'स्किल खोज रहा है', + 'conversations.tools.searchSkills.done': 'स्किल खोजीं', + 'conversations.tools.checkSkills.active': 'स्किल जाँच रहा है', + 'conversations.tools.checkSkills.done': 'स्किल जाँचीं', + 'conversations.tools.installSkill.active': 'स्किल इंस्टॉल कर रहा है', + 'conversations.tools.installSkill.done': 'स्किल इंस्टॉल की', + 'conversations.tools.removeSkill.active': 'स्किल हटा रहा है', + 'conversations.tools.removeSkill.done': 'स्किल हटाई', + 'conversations.tools.createSkill.active': 'स्किल बना रहा है', + 'conversations.tools.createSkill.done': 'स्किल बनाई', + 'conversations.tools.runWorkflow.active': 'वर्कफ़्लो चला रहा है', + 'conversations.tools.runWorkflow.done': 'वर्कफ़्लो चलाया', + 'conversations.tools.waitForWorkflow.active': 'वर्कफ़्लो की प्रतीक्षा कर रहा है', + 'conversations.tools.waitForWorkflow.done': 'वर्कफ़्लो की प्रतीक्षा की', + 'conversations.tools.designWorkflow.active': 'वर्कफ़्लो डिज़ाइन कर रहा है', + 'conversations.tools.designWorkflow.done': 'वर्कफ़्लो डिज़ाइन किया', + 'conversations.tools.saveWorkflow.active': 'वर्कफ़्लो सहेज रहा है', + 'conversations.tools.saveWorkflow.done': 'वर्कफ़्लो सहेजा', + 'conversations.tools.validateWorkflow.active': 'वर्कफ़्लो सत्यापित कर रहा है', + 'conversations.tools.validateWorkflow.done': 'वर्कफ़्लो सत्यापित किया', + 'conversations.tools.testWorkflow.active': 'वर्कफ़्लो टेस्ट कर रहा है', + 'conversations.tools.testWorkflow.done': 'वर्कफ़्लो टेस्ट किया', + 'conversations.tools.checkWorkflows.active': 'वर्कफ़्लो जाँच रहा है', + 'conversations.tools.checkWorkflows.done': 'वर्कफ़्लो जाँचे', + 'conversations.tools.cancelWorkflow.active': 'वर्कफ़्लो रन रद्द कर रहा है', + 'conversations.tools.cancelWorkflow.done': 'वर्कफ़्लो रन रद्द किया', + 'conversations.tools.suggestWorkflows.active': 'वर्कफ़्लो सुझा रहा है', + 'conversations.tools.suggestWorkflows.done': 'वर्कफ़्लो सुझाए', + 'conversations.tools.checkSettings.active': 'सेटिंग्स जाँच रहा है', + 'conversations.tools.checkSettings.done': 'सेटिंग्स जाँचीं', + 'conversations.tools.checkSecurity.active': 'सुरक्षा जाँच रहा है', + 'conversations.tools.checkSecurity.done': 'सुरक्षा जाँची', + 'conversations.tools.runDiagnostics.active': 'डायग्नोस्टिक्स चला रहा है', + 'conversations.tools.runDiagnostics.done': 'डायग्नोस्टिक्स चलाए', + 'conversations.tools.checkUsageCosts.active': 'उपयोग लागत जाँच रहा है', + 'conversations.tools.checkUsageCosts.done': 'उपयोग लागत जाँची', + 'conversations.tools.manageService.active': 'बैकग्राउंड सेवा प्रबंधित कर रहा है', + 'conversations.tools.manageService.done': 'बैकग्राउंड सेवा प्रबंधित की', + 'conversations.tools.readPersona.active': 'पर्सोना पढ़ रहा है', + 'conversations.tools.readPersona.done': 'पर्सोना पढ़ा', + 'conversations.tools.updatePersona.active': 'पर्सोना अपडेट कर रहा है', + 'conversations.tools.updatePersona.done': 'पर्सोना अपडेट किया', + 'conversations.tools.setUpWorkspace.active': 'वर्कस्पेस सेट अप कर रहा है', + 'conversations.tools.setUpWorkspace.done': 'वर्कस्पेस सेट अप किया', + 'conversations.tools.checkArtifacts.active': 'आर्टिफैक्ट जाँच रहा है', + 'conversations.tools.checkArtifacts.done': 'आर्टिफैक्ट जाँचे', + 'conversations.tools.deleteArtifact.active': 'आर्टिफैक्ट हटा रहा है', + 'conversations.tools.deleteArtifact.done': 'आर्टिफैक्ट हटाया', 'conversations.subagent.noOutput': 'कोई आउटपुट नहीं मिला', 'conversations.subagent.close': 'बंद करें', 'conversations.subagent.cancel': 'कार्य रद्द करें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index f660d28fce5..9f44b5b3279 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -3332,6 +3332,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Belum ada keluaran', 'conversations.subagent.input': 'Masukan', 'conversations.subagent.output': 'Keluaran', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} langkah', + 'conversations.tools.steps.other': '{count} langkah', + 'conversations.tools.working': 'Sedang bekerja', + 'conversations.tools.noOutput': 'Tidak ada keluaran', + 'conversations.tools.delegatedTo': 'Didelegasikan ke {agent}', + 'conversations.tools.openInBrowser': 'Buka di peramban', + 'conversations.tools.status.running': 'berjalan', + 'conversations.tools.status.done': 'selesai', + 'conversations.tools.status.failed': 'gagal', + 'conversations.tools.status.cancelled': 'dibatalkan', + 'conversations.tools.status.awaiting': 'menunggu masukan', + 'conversations.tools.search.searching': 'Mencari', + 'conversations.tools.search.none': 'Tidak ada hasil', + 'conversations.tools.search.found.one': '{count} hasil ditemukan', + 'conversations.tools.search.found.other': '{count} hasil ditemukan', + 'conversations.tools.search.via': 'melalui {provider}', + 'conversations.tools.readFile.active': 'Membaca file', + 'conversations.tools.readFile.done': 'File dibaca', + 'conversations.tools.writeFile.active': 'Menulis file', + 'conversations.tools.writeFile.done': 'File ditulis', + 'conversations.tools.editFile.active': 'Mengedit file', + 'conversations.tools.editFile.done': 'File diedit', + 'conversations.tools.applyEdits.active': 'Menerapkan perubahan', + 'conversations.tools.applyEdits.done': 'Perubahan diterapkan', + 'conversations.tools.searchCode.active': 'Mencari kode', + 'conversations.tools.searchCode.done': 'Kode dicari', + 'conversations.tools.findFiles.active': 'Mencari file', + 'conversations.tools.findFiles.done': 'File ditemukan', + 'conversations.tools.listFolder.active': 'Menampilkan isi folder', + 'conversations.tools.listFolder.done': 'Isi folder ditampilkan', + 'conversations.tools.exportCsv.active': 'Mengekspor CSV', + 'conversations.tools.exportCsv.done': 'CSV diekspor', + 'conversations.tools.updateMemoryNotes.active': 'Memperbarui catatan memori', + 'conversations.tools.updateMemoryNotes.done': 'Catatan memori diperbarui', + 'conversations.tools.runGit.active': 'Menjalankan git', + 'conversations.tools.runGit.done': 'git dijalankan', + 'conversations.tools.readChanges.active': 'Membaca perubahan', + 'conversations.tools.readChanges.done': 'Perubahan dibaca', + 'conversations.tools.runLinter.active': 'Menjalankan linter', + 'conversations.tools.runLinter.done': 'Linter dijalankan', + 'conversations.tools.runTests.active': 'Menjalankan pengujian', + 'conversations.tools.runTests.done': 'Pengujian dijalankan', + 'conversations.tools.analyzeCode.active': 'Menganalisis kode', + 'conversations.tools.analyzeCode.done': 'Kode dianalisis', + 'conversations.tools.insertRecord.active': 'Menambahkan catatan', + 'conversations.tools.insertRecord.done': 'Catatan ditambahkan', + 'conversations.tools.runCommand.active': 'Menjalankan perintah', + 'conversations.tools.runCommand.done': 'Perintah dijalankan', + 'conversations.tools.runCode.active': 'Menjalankan kode', + 'conversations.tools.runCode.done': 'Kode dijalankan', + 'conversations.tools.runPackageManager.active': 'Menjalankan npm', + 'conversations.tools.runPackageManager.done': 'npm dijalankan', + 'conversations.tools.checkInstalledTools.active': 'Memeriksa alat terpasang', + 'conversations.tools.checkInstalledTools.done': 'Alat terpasang diperiksa', + 'conversations.tools.installTool.active': 'Memasang alat', + 'conversations.tools.installTool.done': 'Alat dipasang', + 'conversations.tools.checkTime.active': 'Memeriksa waktu', + 'conversations.tools.checkTime.done': 'Waktu diperiksa', + 'conversations.tools.resolveDate.active': 'Menentukan tanggal', + 'conversations.tools.resolveDate.done': 'Tanggal ditentukan', + 'conversations.tools.retrieveOutput.active': 'Mengambil keluaran lengkap', + 'conversations.tools.retrieveOutput.done': 'Keluaran lengkap diambil', + 'conversations.tools.reviewWorkspace.active': 'Meninjau ruang kerja', + 'conversations.tools.reviewWorkspace.done': 'Ruang kerja ditinjau', + 'conversations.tools.configureProxy.active': 'Mengonfigurasi proxy', + 'conversations.tools.configureProxy.done': 'Proxy dikonfigurasi', + 'conversations.tools.checkUpdates.active': 'Memeriksa pembaruan', + 'conversations.tools.checkUpdates.done': 'Pembaruan diperiksa', + 'conversations.tools.installUpdate.active': 'Memasang pembaruan', + 'conversations.tools.installUpdate.done': 'Pembaruan dipasang', + 'conversations.tools.sendNotification.active': 'Mengirim notifikasi', + 'conversations.tools.sendNotification.done': 'Notifikasi dikirim', + 'conversations.tools.reviewToolUsage.active': 'Meninjau penggunaan alat', + 'conversations.tools.reviewToolUsage.done': 'Penggunaan alat ditinjau', + 'conversations.tools.typeKeys.active': 'Mengetik', + 'conversations.tools.typeKeys.done': 'Selesai mengetik', + 'conversations.tools.click.active': 'Mengeklik', + 'conversations.tools.click.done': 'Diklik', + 'conversations.tools.searchWeb.active': 'Mencari di web', + 'conversations.tools.searchWeb.done': 'Pencarian web selesai', + 'conversations.tools.searchNews.active': 'Mencari berita', + 'conversations.tools.searchNews.done': 'Berita dicari', + 'conversations.tools.searchImages.active': 'Mencari gambar', + 'conversations.tools.searchImages.done': 'Gambar dicari', + 'conversations.tools.searchVideos.active': 'Mencari video', + 'conversations.tools.searchVideos.done': 'Video dicari', + 'conversations.tools.findSimilarPages.active': 'Mencari halaman serupa', + 'conversations.tools.findSimilarPages.done': 'Halaman serupa ditemukan', + 'conversations.tools.readPages.active': 'Membaca halaman', + 'conversations.tools.readPages.done': 'Halaman dibaca', + 'conversations.tools.readWebpage.active': 'Membaca halaman web', + 'conversations.tools.readWebpage.done': 'Halaman web dibaca', + 'conversations.tools.research.active': 'Meriset', + 'conversations.tools.research.done': 'Riset selesai', + 'conversations.tools.enrichData.active': 'Memperkaya data', + 'conversations.tools.enrichData.done': 'Data diperkaya', + 'conversations.tools.buildDataset.active': 'Membangun dataset', + 'conversations.tools.buildDataset.done': 'Dataset dibangun', + 'conversations.tools.askTheWeb.active': 'Bertanya ke web', + 'conversations.tools.askTheWeb.done': 'Sudah bertanya ke web', + 'conversations.tools.browseForYou.active': 'Menjelajah untuk Anda', + 'conversations.tools.browseForYou.done': 'Selesai menjelajah untuk Anda', + 'conversations.tools.callApi.active': 'Memanggil API', + 'conversations.tools.callApi.done': 'API dipanggil', + 'conversations.tools.downloadFile.active': 'Mengunduh file', + 'conversations.tools.downloadFile.done': 'File diunduh', + 'conversations.tools.makePaidRequest.active': 'Mengirim permintaan berbayar', + 'conversations.tools.makePaidRequest.done': 'Permintaan berbayar dikirim', + 'conversations.tools.searchDocs.active': 'Mencari dokumentasi', + 'conversations.tools.searchDocs.done': 'Dokumentasi dicari', + 'conversations.tools.readDocs.active': 'Membaca dokumentasi', + 'conversations.tools.readDocs.done': 'Dokumentasi dibaca', + 'conversations.tools.useBrowser.active': 'Menggunakan peramban', + 'conversations.tools.useBrowser.done': 'Peramban digunakan', + 'conversations.tools.openPage.active': 'Membuka halaman', + 'conversations.tools.openPage.done': 'Halaman dibuka', + 'conversations.tools.navigate.active': 'Bernavigasi', + 'conversations.tools.navigate.done': 'Navigasi selesai', + 'conversations.tools.takeScreenshot.active': 'Mengambil tangkapan layar', + 'conversations.tools.takeScreenshot.done': 'Tangkapan layar diambil', + 'conversations.tools.scrollPage.active': 'Menggulir', + 'conversations.tools.scrollPage.done': 'Selesai menggulir', + 'conversations.tools.readPage.active': 'Membaca halaman', + 'conversations.tools.readPage.done': 'Halaman dibaca', + 'conversations.tools.analyzeImage.active': 'Menganalisis gambar', + 'conversations.tools.analyzeImage.done': 'Gambar dianalisis', + 'conversations.tools.generateImage.active': 'Membuat gambar', + 'conversations.tools.generateImage.done': 'Gambar dibuat', + 'conversations.tools.generateVideo.active': 'Membuat video', + 'conversations.tools.generateVideo.done': 'Video dibuat', + 'conversations.tools.checkMediaModels.active': 'Memeriksa model media', + 'conversations.tools.checkMediaModels.done': 'Model media diperiksa', + 'conversations.tools.createDocument.active': 'Membuat dokumen', + 'conversations.tools.createDocument.done': 'Dokumen dibuat', + 'conversations.tools.createPresentation.active': 'Membuat presentasi', + 'conversations.tools.createPresentation.done': 'Presentasi dibuat', + 'conversations.tools.generatePodcast.active': 'Membuat podcast', + 'conversations.tools.generatePodcast.done': 'Podcast dibuat', + 'conversations.tools.emailPodcast.active': 'Mengirim podcast lewat email', + 'conversations.tools.emailPodcast.done': 'Podcast dikirim lewat email', + 'conversations.tools.createAndEmailPodcast.active': 'Membuat dan mengirim podcast lewat email', + 'conversations.tools.createAndEmailPodcast.done': 'Podcast dibuat dan dikirim lewat email', + 'conversations.tools.recallMemories.active': 'Mengingat kembali memori', + 'conversations.tools.recallMemories.done': 'Memori diingat kembali', + 'conversations.tools.saveToMemory.active': 'Menyimpan ke memori', + 'conversations.tools.saveToMemory.done': 'Disimpan ke memori', + 'conversations.tools.forgetMemory.active': 'Melupakan memori', + 'conversations.tools.forgetMemory.done': 'Memori dilupakan', + 'conversations.tools.searchMemory.active': 'Mencari di memori', + 'conversations.tools.searchMemory.done': 'Memori dicari', + 'conversations.tools.inspectMemory.active': 'Memeriksa memori', + 'conversations.tools.inspectMemory.done': 'Memori diperiksa', + 'conversations.tools.exploreMemory.active': 'Menjelajahi memori', + 'conversations.tools.exploreMemory.done': 'Memori dijelajahi', + 'conversations.tools.saveDocumentToMemory.active': 'Menyimpan dokumen ke memori', + 'conversations.tools.saveDocumentToMemory.done': 'Dokumen disimpan ke memori', + 'conversations.tools.updateGoals.active': 'Memperbarui tujuan', + 'conversations.tools.updateGoals.done': 'Tujuan diperbarui', + 'conversations.tools.reviewGoals.active': 'Meninjau tujuan', + 'conversations.tools.reviewGoals.done': 'Tujuan ditinjau', + 'conversations.tools.savePreference.active': 'Menyimpan preferensi', + 'conversations.tools.savePreference.done': 'Preferensi disimpan', + 'conversations.tools.reviewLearnings.active': 'Meninjau hal yang saya pelajari', + 'conversations.tools.reviewLearnings.done': 'Hal yang saya pelajari ditinjau', + 'conversations.tools.updateLearnings.active': 'Memperbarui hal yang saya pelajari', + 'conversations.tools.updateLearnings.done': 'Hal yang saya pelajari diperbarui', + 'conversations.tools.delegateTask.active': 'Mendelegasikan tugas', + 'conversations.tools.delegateTask.done': 'Tugas didelegasikan', + 'conversations.tools.runAgentsInParallel.active': 'Menjalankan agen secara paralel', + 'conversations.tools.runAgentsInParallel.done': 'Agen dijalankan secara paralel', + 'conversations.tools.messageAgent.active': 'Mengirim pesan ke agen', + 'conversations.tools.messageAgent.done': 'Pesan dikirim ke agen', + 'conversations.tools.waitForAgent.active': 'Menunggu agen', + 'conversations.tools.waitForAgent.done': 'Selesai menunggu agen', + 'conversations.tools.wait.active': 'Menunggu', + 'conversations.tools.wait.done': 'Selesai menunggu', + 'conversations.tools.closeAgent.active': 'Menutup agen', + 'conversations.tools.closeAgent.done': 'Agen ditutup', + 'conversations.tools.checkAgents.active': 'Memeriksa agen', + 'conversations.tools.checkAgents.done': 'Agen diperiksa', + 'conversations.tools.askQuestion.active': 'Mengajukan pertanyaan kepada Anda', + 'conversations.tools.askQuestion.done': 'Pertanyaan diajukan kepada Anda', + 'conversations.tools.prepareContext.active': 'Menyiapkan konteks', + 'conversations.tools.prepareContext.done': 'Konteks disiapkan', + 'conversations.tools.extractDetails.active': 'Mengekstrak detail', + 'conversations.tools.extractDetails.done': 'Detail diekstrak', + 'conversations.tools.planNextSteps.active': 'Merencanakan langkah berikutnya', + 'conversations.tools.planNextSteps.done': 'Langkah berikutnya direncanakan', + 'conversations.tools.reviewWork.active': 'Meninjau pekerjaan', + 'conversations.tools.reviewWork.done': 'Pekerjaan ditinjau', + 'conversations.tools.scoutContext.active': 'Menelusuri konteks', + 'conversations.tools.scoutContext.done': 'Konteks ditelusuri', + 'conversations.tools.useTools.active': 'Menggunakan alat', + 'conversations.tools.useTools.done': 'Alat digunakan', + 'conversations.tools.checkConnectedApp.active': 'Memeriksa aplikasi terhubung Anda', + 'conversations.tools.checkConnectedApp.done': 'Aplikasi terhubung Anda diperiksa', + 'conversations.tools.updateTodos.active': 'Memperbarui daftar tugas', + 'conversations.tools.updateTodos.done': 'Daftar tugas diperbarui', + 'conversations.tools.requestPlanReview.active': 'Meminta tinjauan rencana', + 'conversations.tools.requestPlanReview.done': 'Tinjauan rencana diminta', + 'conversations.tools.finishPlan.active': 'Menyelesaikan rencana', + 'conversations.tools.finishPlan.done': 'Rencana diselesaikan', + 'conversations.tools.setGoal.active': 'Menetapkan tujuan', + 'conversations.tools.setGoal.done': 'Tujuan ditetapkan', + 'conversations.tools.checkGoal.active': 'Memeriksa tujuan', + 'conversations.tools.checkGoal.done': 'Tujuan diperiksa', + 'conversations.tools.completeGoal.active': 'Menuntaskan tujuan', + 'conversations.tools.completeGoal.done': 'Tujuan dituntaskan', + 'conversations.tools.scheduleTask.active': 'Menjadwalkan tugas', + 'conversations.tools.scheduleTask.done': 'Tugas dijadwalkan', + 'conversations.tools.checkSchedules.active': 'Memeriksa jadwal', + 'conversations.tools.checkSchedules.done': 'Jadwal diperiksa', + 'conversations.tools.updateSchedule.active': 'Memperbarui tugas terjadwal', + 'conversations.tools.updateSchedule.done': 'Tugas terjadwal diperbarui', + 'conversations.tools.removeSchedule.active': 'Menghapus tugas terjadwal', + 'conversations.tools.removeSchedule.done': 'Tugas terjadwal dihapus', + 'conversations.tools.runScheduledTask.active': 'Menjalankan tugas terjadwal', + 'conversations.tools.runScheduledTask.done': 'Tugas terjadwal dijalankan', + 'conversations.tools.checkRunHistory.active': 'Memeriksa riwayat eksekusi', + 'conversations.tools.checkRunHistory.done': 'Riwayat eksekusi diperiksa', + 'conversations.tools.useApp.active': 'Menggunakan {app}', + 'conversations.tools.useApp.done': '{app} digunakan', + 'conversations.tools.checkAvailableApps.active': 'Memeriksa aplikasi yang tersedia', + 'conversations.tools.checkAvailableApps.done': 'Aplikasi yang tersedia diperiksa', + 'conversations.tools.checkConnections.active': 'Memeriksa koneksi Anda', + 'conversations.tools.checkConnections.done': 'Koneksi Anda diperiksa', + 'conversations.tools.connectApp.active': 'Menghubungkan aplikasi', + 'conversations.tools.connectApp.done': 'Aplikasi terhubung', + 'conversations.tools.authorizeApp.active': 'Mengotorisasi aplikasi', + 'conversations.tools.authorizeApp.done': 'Aplikasi diotorisasi', + 'conversations.tools.findAppActions.active': 'Mencari tindakan aplikasi', + 'conversations.tools.findAppActions.done': 'Tindakan aplikasi ditemukan', + 'conversations.tools.runAppAction.active': 'Menjalankan tindakan aplikasi', + 'conversations.tools.runAppAction.done': 'Tindakan aplikasi dijalankan', + 'conversations.tools.findTools.active': 'Mencari alat', + 'conversations.tools.findTools.done': 'Alat ditemukan', + 'conversations.tools.useTool.active': 'Menggunakan {tool}', + 'conversations.tools.useTool.done': '{tool} digunakan', + 'conversations.tools.unsubscribe.active': 'Berhenti berlangganan', + 'conversations.tools.unsubscribe.done': 'Langganan dihentikan', + 'conversations.tools.searchPlaces.active': 'Mencari tempat', + 'conversations.tools.searchPlaces.done': 'Tempat dicari', + 'conversations.tools.lookUpPlace.active': 'Mencari info tempat', + 'conversations.tools.lookUpPlace.done': 'Info tempat ditemukan', + 'conversations.tools.checkMarkets.active': 'Memeriksa pasar', + 'conversations.tools.checkMarkets.done': 'Pasar diperiksa', + 'conversations.tools.placeCall.active': 'Melakukan panggilan', + 'conversations.tools.placeCall.done': 'Panggilan dilakukan', + 'conversations.tools.checkTaskSources.active': 'Memeriksa sumber tugas', + 'conversations.tools.checkTaskSources.done': 'Sumber tugas diperiksa', + 'conversations.tools.updateTaskSources.active': 'Memperbarui sumber tugas', + 'conversations.tools.updateTaskSources.done': 'Sumber tugas diperbarui', + 'conversations.tools.fetchTasks.active': 'Mengambil tugas', + 'conversations.tools.fetchTasks.done': 'Tugas diambil', + 'conversations.tools.checkMcpServers.active': 'Memeriksa server MCP', + 'conversations.tools.checkMcpServers.done': 'Server MCP diperiksa', + 'conversations.tools.checkMcpTools.active': 'Memeriksa alat MCP', + 'conversations.tools.checkMcpTools.done': 'Alat MCP diperiksa', + 'conversations.tools.callMcpTool.active': 'Memanggil {tool}', + 'conversations.tools.callMcpTool.done': '{tool} dipanggil', + 'conversations.tools.searchMcpServers.active': 'Mencari server MCP', + 'conversations.tools.searchMcpServers.done': 'Server MCP dicari', + 'conversations.tools.connectMcpServer.active': 'Menghubungkan server MCP', + 'conversations.tools.connectMcpServer.done': 'Server MCP terhubung', + 'conversations.tools.disconnectMcpServer.active': 'Memutuskan server MCP', + 'conversations.tools.disconnectMcpServer.done': 'Server MCP diputuskan', + 'conversations.tools.removeMcpServer.active': 'Menghapus server MCP', + 'conversations.tools.removeMcpServer.done': 'Server MCP dihapus', + 'conversations.tools.uploadFile.active': 'Mengunggah file', + 'conversations.tools.uploadFile.done': 'File diunggah', + 'conversations.tools.listStoredFiles.active': 'Menampilkan file tersimpan', + 'conversations.tools.listStoredFiles.done': 'File tersimpan ditampilkan', + 'conversations.tools.createShareLink.active': 'Membuat tautan berbagi', + 'conversations.tools.createShareLink.done': 'Tautan berbagi dibuat', + 'conversations.tools.deleteFile.active': 'Menghapus file', + 'conversations.tools.deleteFile.done': 'File dihapus', + 'conversations.tools.updateFileAccess.active': 'Memperbarui akses file', + 'conversations.tools.updateFileAccess.done': 'Akses file diperbarui', + 'conversations.tools.deploySite.active': 'Menerapkan situs', + 'conversations.tools.deploySite.done': 'Situs diterapkan', + 'conversations.tools.checkHosting.active': 'Memeriksa hosting', + 'conversations.tools.checkHosting.done': 'Hosting diperiksa', + 'conversations.tools.updateHosting.active': 'Memperbarui hosting', + 'conversations.tools.updateHosting.done': 'Hosting diperbarui', + 'conversations.tools.rollBackDeployment.active': 'Membatalkan penerapan', + 'conversations.tools.rollBackDeployment.done': 'Penerapan dibatalkan', + 'conversations.tools.checkWallet.active': 'Memeriksa dompet', + 'conversations.tools.checkWallet.done': 'Dompet diperiksa', + 'conversations.tools.prepareTransfer.active': 'Menyiapkan transfer', + 'conversations.tools.prepareTransfer.done': 'Transfer disiapkan', + 'conversations.tools.checkTransaction.active': 'Memeriksa transaksi', + 'conversations.tools.checkTransaction.done': 'Transaksi diperiksa', + 'conversations.tools.getSwapQuote.active': 'Mengambil kuotasi swap', + 'conversations.tools.getSwapQuote.done': 'Kuotasi swap diambil', + 'conversations.tools.swapTokens.active': 'Menukar token', + 'conversations.tools.swapTokens.done': 'Token ditukar', + 'conversations.tools.getBridgeQuote.active': 'Mengambil kuotasi bridge', + 'conversations.tools.getBridgeQuote.done': 'Kuotasi bridge diambil', + 'conversations.tools.bridgeTokens.active': 'Memindahkan token lewat bridge', + 'conversations.tools.bridgeTokens.done': 'Token dipindahkan lewat bridge', + 'conversations.tools.callDapp.active': 'Memanggil kontrak aplikasi', + 'conversations.tools.callDapp.done': 'Kontrak aplikasi dipanggil', + 'conversations.tools.useSkill.active': 'Menggunakan keahlian', + 'conversations.tools.useSkill.done': 'Keahlian digunakan', + 'conversations.tools.searchSkills.active': 'Mencari keahlian', + 'conversations.tools.searchSkills.done': 'Keahlian dicari', + 'conversations.tools.checkSkills.active': 'Memeriksa keahlian', + 'conversations.tools.checkSkills.done': 'Keahlian diperiksa', + 'conversations.tools.installSkill.active': 'Memasang keahlian', + 'conversations.tools.installSkill.done': 'Keahlian dipasang', + 'conversations.tools.removeSkill.active': 'Menghapus keahlian', + 'conversations.tools.removeSkill.done': 'Keahlian dihapus', + 'conversations.tools.createSkill.active': 'Membuat keahlian', + 'conversations.tools.createSkill.done': 'Keahlian dibuat', + 'conversations.tools.runWorkflow.active': 'Menjalankan alur kerja', + 'conversations.tools.runWorkflow.done': 'Alur kerja dijalankan', + 'conversations.tools.waitForWorkflow.active': 'Menunggu alur kerja', + 'conversations.tools.waitForWorkflow.done': 'Selesai menunggu alur kerja', + 'conversations.tools.designWorkflow.active': 'Merancang alur kerja', + 'conversations.tools.designWorkflow.done': 'Alur kerja dirancang', + 'conversations.tools.saveWorkflow.active': 'Menyimpan alur kerja', + 'conversations.tools.saveWorkflow.done': 'Alur kerja disimpan', + 'conversations.tools.validateWorkflow.active': 'Memvalidasi alur kerja', + 'conversations.tools.validateWorkflow.done': 'Alur kerja divalidasi', + 'conversations.tools.testWorkflow.active': 'Menguji alur kerja', + 'conversations.tools.testWorkflow.done': 'Alur kerja diuji', + 'conversations.tools.checkWorkflows.active': 'Memeriksa alur kerja', + 'conversations.tools.checkWorkflows.done': 'Alur kerja diperiksa', + 'conversations.tools.cancelWorkflow.active': 'Membatalkan eksekusi alur kerja', + 'conversations.tools.cancelWorkflow.done': 'Eksekusi alur kerja dibatalkan', + 'conversations.tools.suggestWorkflows.active': 'Menyarankan alur kerja', + 'conversations.tools.suggestWorkflows.done': 'Alur kerja disarankan', + 'conversations.tools.checkSettings.active': 'Memeriksa pengaturan', + 'conversations.tools.checkSettings.done': 'Pengaturan diperiksa', + 'conversations.tools.checkSecurity.active': 'Memeriksa keamanan', + 'conversations.tools.checkSecurity.done': 'Keamanan diperiksa', + 'conversations.tools.runDiagnostics.active': 'Menjalankan diagnostik', + 'conversations.tools.runDiagnostics.done': 'Diagnostik dijalankan', + 'conversations.tools.checkUsageCosts.active': 'Memeriksa biaya penggunaan', + 'conversations.tools.checkUsageCosts.done': 'Biaya penggunaan diperiksa', + 'conversations.tools.manageService.active': 'Mengelola layanan latar belakang', + 'conversations.tools.manageService.done': 'Layanan latar belakang dikelola', + 'conversations.tools.readPersona.active': 'Membaca persona', + 'conversations.tools.readPersona.done': 'Persona dibaca', + 'conversations.tools.updatePersona.active': 'Memperbarui persona', + 'conversations.tools.updatePersona.done': 'Persona diperbarui', + 'conversations.tools.setUpWorkspace.active': 'Menyiapkan ruang kerja', + 'conversations.tools.setUpWorkspace.done': 'Ruang kerja disiapkan', + 'conversations.tools.checkArtifacts.active': 'Memeriksa artefak', + 'conversations.tools.checkArtifacts.done': 'Artefak diperiksa', + 'conversations.tools.deleteArtifact.active': 'Menghapus artefak', + 'conversations.tools.deleteArtifact.done': 'Artefak dihapus', 'conversations.subagent.noOutput': 'Tidak ada keluaran', 'conversations.subagent.close': 'Tutup', 'conversations.subagent.cancel': 'Batalkan tugas', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 30f5b5c36ca..3ed6e95c660 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -3373,6 +3373,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Ancora nessun output', 'conversations.subagent.input': 'Input', 'conversations.subagent.output': 'Output', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} passaggio', + 'conversations.tools.steps.other': '{count} passaggi', + 'conversations.tools.working': 'In corso', + 'conversations.tools.noOutput': 'Nessun output', + 'conversations.tools.delegatedTo': 'Delegato a {agent}', + 'conversations.tools.openInBrowser': 'Apri nel browser', + 'conversations.tools.status.running': 'in corso', + 'conversations.tools.status.done': 'completato', + 'conversations.tools.status.failed': 'non riuscito', + 'conversations.tools.status.cancelled': 'annullato', + 'conversations.tools.status.awaiting': 'in attesa di risposta', + 'conversations.tools.search.searching': 'Ricerca in corso', + 'conversations.tools.search.none': 'Nessun risultato', + 'conversations.tools.search.found.one': '{count} risultato trovato', + 'conversations.tools.search.found.other': '{count} risultati trovati', + 'conversations.tools.search.via': 'tramite {provider}', + 'conversations.tools.readFile.active': 'Lettura del file', + 'conversations.tools.readFile.done': 'File letto', + 'conversations.tools.writeFile.active': 'Scrittura del file', + 'conversations.tools.writeFile.done': 'File scritto', + 'conversations.tools.editFile.active': 'Modifica del file', + 'conversations.tools.editFile.done': 'File modificato', + 'conversations.tools.applyEdits.active': 'Applicazione delle modifiche', + 'conversations.tools.applyEdits.done': 'Modifiche applicate', + 'conversations.tools.searchCode.active': 'Ricerca nel codice', + 'conversations.tools.searchCode.done': 'Codice esaminato', + 'conversations.tools.findFiles.active': 'Ricerca dei file', + 'conversations.tools.findFiles.done': 'File trovati', + 'conversations.tools.listFolder.active': 'Elenco della cartella', + 'conversations.tools.listFolder.done': 'Cartella elencata', + 'conversations.tools.exportCsv.active': 'Esportazione CSV', + 'conversations.tools.exportCsv.done': 'CSV esportato', + 'conversations.tools.updateMemoryNotes.active': 'Aggiornamento delle note di memoria', + 'conversations.tools.updateMemoryNotes.done': 'Note di memoria aggiornate', + 'conversations.tools.runGit.active': 'Esecuzione di git', + 'conversations.tools.runGit.done': 'git eseguito', + 'conversations.tools.readChanges.active': 'Lettura delle modifiche', + 'conversations.tools.readChanges.done': 'Modifiche lette', + 'conversations.tools.runLinter.active': 'Esecuzione del linter', + 'conversations.tools.runLinter.done': 'Linter eseguito', + 'conversations.tools.runTests.active': 'Esecuzione dei test', + 'conversations.tools.runTests.done': 'Test eseguiti', + 'conversations.tools.analyzeCode.active': 'Analisi del codice', + 'conversations.tools.analyzeCode.done': 'Codice analizzato', + 'conversations.tools.insertRecord.active': 'Inserimento del record', + 'conversations.tools.insertRecord.done': 'Record inserito', + 'conversations.tools.runCommand.active': 'Esecuzione del comando', + 'conversations.tools.runCommand.done': 'Comando eseguito', + 'conversations.tools.runCode.active': 'Esecuzione del codice', + 'conversations.tools.runCode.done': 'Codice eseguito', + 'conversations.tools.runPackageManager.active': 'Esecuzione di npm', + 'conversations.tools.runPackageManager.done': 'npm eseguito', + 'conversations.tools.checkInstalledTools.active': 'Verifica degli strumenti installati', + 'conversations.tools.checkInstalledTools.done': 'Strumenti installati verificati', + 'conversations.tools.installTool.active': 'Installazione dello strumento', + 'conversations.tools.installTool.done': 'Strumento installato', + 'conversations.tools.checkTime.active': "Controllo dell'ora", + 'conversations.tools.checkTime.done': 'Ora controllata', + 'conversations.tools.resolveDate.active': 'Calcolo della data', + 'conversations.tools.resolveDate.done': 'Data calcolata', + 'conversations.tools.retrieveOutput.active': "Recupero dell'output completo", + 'conversations.tools.retrieveOutput.done': 'Output completo recuperato', + 'conversations.tools.reviewWorkspace.active': "Esame dell'area di lavoro", + 'conversations.tools.reviewWorkspace.done': 'Area di lavoro esaminata', + 'conversations.tools.configureProxy.active': 'Configurazione del proxy', + 'conversations.tools.configureProxy.done': 'Proxy configurato', + 'conversations.tools.checkUpdates.active': 'Ricerca di aggiornamenti', + 'conversations.tools.checkUpdates.done': 'Aggiornamenti verificati', + 'conversations.tools.installUpdate.active': "Installazione dell'aggiornamento", + 'conversations.tools.installUpdate.done': 'Aggiornamento installato', + 'conversations.tools.sendNotification.active': 'Invio della notifica', + 'conversations.tools.sendNotification.done': 'Notifica inviata', + 'conversations.tools.reviewToolUsage.active': "Esame dell'uso degli strumenti", + 'conversations.tools.reviewToolUsage.done': 'Uso degli strumenti esaminato', + 'conversations.tools.typeKeys.active': 'Digitazione in corso', + 'conversations.tools.typeKeys.done': 'Testo digitato', + 'conversations.tools.click.active': 'Clic in corso', + 'conversations.tools.click.done': 'Clic eseguito', + 'conversations.tools.searchWeb.active': 'Ricerca sul web', + 'conversations.tools.searchWeb.done': 'Ricerca web completata', + 'conversations.tools.searchNews.active': 'Ricerca di notizie', + 'conversations.tools.searchNews.done': 'Notizie cercate', + 'conversations.tools.searchImages.active': 'Ricerca di immagini', + 'conversations.tools.searchImages.done': 'Immagini cercate', + 'conversations.tools.searchVideos.active': 'Ricerca di video', + 'conversations.tools.searchVideos.done': 'Video cercati', + 'conversations.tools.findSimilarPages.active': 'Ricerca di pagine simili', + 'conversations.tools.findSimilarPages.done': 'Pagine simili trovate', + 'conversations.tools.readPages.active': 'Lettura delle pagine', + 'conversations.tools.readPages.done': 'Pagine lette', + 'conversations.tools.readWebpage.active': 'Lettura della pagina web', + 'conversations.tools.readWebpage.done': 'Pagina web letta', + 'conversations.tools.research.active': 'Approfondimento in corso', + 'conversations.tools.research.done': 'Approfondimento completato', + 'conversations.tools.enrichData.active': 'Arricchimento dei dati', + 'conversations.tools.enrichData.done': 'Dati arricchiti', + 'conversations.tools.buildDataset.active': 'Creazione del set di dati', + 'conversations.tools.buildDataset.done': 'Set di dati creato', + 'conversations.tools.askTheWeb.active': 'Interrogazione del web', + 'conversations.tools.askTheWeb.done': 'Web interrogato', + 'conversations.tools.browseForYou.active': 'Navigazione per te', + 'conversations.tools.browseForYou.done': 'Navigazione completata per te', + 'conversations.tools.callApi.active': "Chiamata all'API", + 'conversations.tools.callApi.done': 'API chiamata', + 'conversations.tools.downloadFile.active': 'Download del file', + 'conversations.tools.downloadFile.done': 'File scaricato', + 'conversations.tools.makePaidRequest.active': 'Invio di una richiesta a pagamento', + 'conversations.tools.makePaidRequest.done': 'Richiesta a pagamento inviata', + 'conversations.tools.searchDocs.active': 'Ricerca nella documentazione', + 'conversations.tools.searchDocs.done': 'Documentazione consultata', + 'conversations.tools.readDocs.active': 'Lettura della documentazione', + 'conversations.tools.readDocs.done': 'Documentazione letta', + 'conversations.tools.useBrowser.active': 'Uso del browser', + 'conversations.tools.useBrowser.done': 'Browser utilizzato', + 'conversations.tools.openPage.active': 'Apertura della pagina', + 'conversations.tools.openPage.done': 'Pagina aperta', + 'conversations.tools.navigate.active': 'Navigazione in corso', + 'conversations.tools.navigate.done': 'Navigazione completata', + 'conversations.tools.takeScreenshot.active': 'Acquisizione dello screenshot', + 'conversations.tools.takeScreenshot.done': 'Screenshot acquisito', + 'conversations.tools.scrollPage.active': 'Scorrimento in corso', + 'conversations.tools.scrollPage.done': 'Scorrimento completato', + 'conversations.tools.readPage.active': 'Lettura della pagina', + 'conversations.tools.readPage.done': 'Pagina letta', + 'conversations.tools.analyzeImage.active': "Analisi dell'immagine", + 'conversations.tools.analyzeImage.done': 'Immagine analizzata', + 'conversations.tools.generateImage.active': "Generazione dell'immagine", + 'conversations.tools.generateImage.done': 'Immagine generata', + 'conversations.tools.generateVideo.active': 'Generazione del video', + 'conversations.tools.generateVideo.done': 'Video generato', + 'conversations.tools.checkMediaModels.active': 'Verifica dei modelli multimediali', + 'conversations.tools.checkMediaModels.done': 'Modelli multimediali verificati', + 'conversations.tools.createDocument.active': 'Creazione del documento', + 'conversations.tools.createDocument.done': 'Documento creato', + 'conversations.tools.createPresentation.active': 'Creazione della presentazione', + 'conversations.tools.createPresentation.done': 'Presentazione creata', + 'conversations.tools.generatePodcast.active': 'Generazione del podcast', + 'conversations.tools.generatePodcast.done': 'Podcast generato', + 'conversations.tools.emailPodcast.active': 'Invio del podcast via email', + 'conversations.tools.emailPodcast.done': 'Podcast inviato via email', + 'conversations.tools.createAndEmailPodcast.active': 'Creazione e invio del podcast via email', + 'conversations.tools.createAndEmailPodcast.done': 'Podcast creato e inviato via email', + 'conversations.tools.recallMemories.active': 'Recupero dei ricordi', + 'conversations.tools.recallMemories.done': 'Ricordi recuperati', + 'conversations.tools.saveToMemory.active': 'Salvataggio in memoria', + 'conversations.tools.saveToMemory.done': 'Salvato in memoria', + 'conversations.tools.forgetMemory.active': 'Rimozione del ricordo', + 'conversations.tools.forgetMemory.done': 'Ricordo rimosso', + 'conversations.tools.searchMemory.active': 'Ricerca nella memoria', + 'conversations.tools.searchMemory.done': 'Memoria consultata', + 'conversations.tools.inspectMemory.active': 'Ispezione della memoria', + 'conversations.tools.inspectMemory.done': 'Memoria ispezionata', + 'conversations.tools.exploreMemory.active': 'Esplorazione della memoria', + 'conversations.tools.exploreMemory.done': 'Memoria esplorata', + 'conversations.tools.saveDocumentToMemory.active': 'Salvataggio del documento in memoria', + 'conversations.tools.saveDocumentToMemory.done': 'Documento salvato in memoria', + 'conversations.tools.updateGoals.active': 'Aggiornamento degli obiettivi', + 'conversations.tools.updateGoals.done': 'Obiettivi aggiornati', + 'conversations.tools.reviewGoals.active': 'Esame degli obiettivi', + 'conversations.tools.reviewGoals.done': 'Obiettivi esaminati', + 'conversations.tools.savePreference.active': 'Salvataggio della preferenza', + 'conversations.tools.savePreference.done': 'Preferenza salvata', + 'conversations.tools.reviewLearnings.active': 'Esame di ciò che ho imparato', + 'conversations.tools.reviewLearnings.done': 'Apprendimenti esaminati', + 'conversations.tools.updateLearnings.active': 'Aggiornamento di ciò che ho imparato', + 'conversations.tools.updateLearnings.done': 'Apprendimenti aggiornati', + 'conversations.tools.delegateTask.active': "Delega dell'attività", + 'conversations.tools.delegateTask.done': 'Attività delegata', + 'conversations.tools.runAgentsInParallel.active': 'Esecuzione di agenti in parallelo', + 'conversations.tools.runAgentsInParallel.done': 'Agenti eseguiti in parallelo', + 'conversations.tools.messageAgent.active': "Invio di un messaggio all'agente", + 'conversations.tools.messageAgent.done': "Messaggio inviato all'agente", + 'conversations.tools.waitForAgent.active': "Attesa dell'agente", + 'conversations.tools.waitForAgent.done': "Attesa dell'agente terminata", + 'conversations.tools.wait.active': 'Attesa in corso', + 'conversations.tools.wait.done': 'Attesa terminata', + 'conversations.tools.closeAgent.active': "Chiusura dell'agente", + 'conversations.tools.closeAgent.done': 'Agente chiuso', + 'conversations.tools.checkAgents.active': 'Verifica degli agenti', + 'conversations.tools.checkAgents.done': 'Agenti verificati', + 'conversations.tools.askQuestion.active': 'Ti sto facendo una domanda', + 'conversations.tools.askQuestion.done': 'Domanda posta', + 'conversations.tools.prepareContext.active': 'Preparazione del contesto', + 'conversations.tools.prepareContext.done': 'Contesto preparato', + 'conversations.tools.extractDetails.active': 'Estrazione dei dettagli', + 'conversations.tools.extractDetails.done': 'Dettagli estratti', + 'conversations.tools.planNextSteps.active': 'Pianificazione dei prossimi passi', + 'conversations.tools.planNextSteps.done': 'Prossimi passi pianificati', + 'conversations.tools.reviewWork.active': 'Revisione del lavoro', + 'conversations.tools.reviewWork.done': 'Lavoro revisionato', + 'conversations.tools.scoutContext.active': 'Esplorazione del contesto', + 'conversations.tools.scoutContext.done': 'Contesto esplorato', + 'conversations.tools.useTools.active': 'Uso degli strumenti', + 'conversations.tools.useTools.done': 'Strumenti utilizzati', + 'conversations.tools.checkConnectedApp.active': 'Verifica della tua app collegata', + 'conversations.tools.checkConnectedApp.done': 'App collegata verificata', + 'conversations.tools.updateTodos.active': 'Aggiornamento della lista di cose da fare', + 'conversations.tools.updateTodos.done': 'Lista di cose da fare aggiornata', + 'conversations.tools.requestPlanReview.active': 'Richiesta di revisione del piano', + 'conversations.tools.requestPlanReview.done': 'Revisione del piano richiesta', + 'conversations.tools.finishPlan.active': 'Completamento del piano', + 'conversations.tools.finishPlan.done': 'Piano completato', + 'conversations.tools.setGoal.active': "Impostazione dell'obiettivo", + 'conversations.tools.setGoal.done': 'Obiettivo impostato', + 'conversations.tools.checkGoal.active': "Verifica dell'obiettivo", + 'conversations.tools.checkGoal.done': 'Obiettivo verificato', + 'conversations.tools.completeGoal.active': "Completamento dell'obiettivo", + 'conversations.tools.completeGoal.done': 'Obiettivo completato', + 'conversations.tools.scheduleTask.active': "Pianificazione dell'attività", + 'conversations.tools.scheduleTask.done': 'Attività pianificata', + 'conversations.tools.checkSchedules.active': 'Verifica delle pianificazioni', + 'conversations.tools.checkSchedules.done': 'Pianificazioni verificate', + 'conversations.tools.updateSchedule.active': "Aggiornamento dell'attività pianificata", + 'conversations.tools.updateSchedule.done': 'Attività pianificata aggiornata', + 'conversations.tools.removeSchedule.active': "Rimozione dell'attività pianificata", + 'conversations.tools.removeSchedule.done': 'Attività pianificata rimossa', + 'conversations.tools.runScheduledTask.active': "Esecuzione dell'attività pianificata", + 'conversations.tools.runScheduledTask.done': 'Attività pianificata eseguita', + 'conversations.tools.checkRunHistory.active': 'Verifica della cronologia delle esecuzioni', + 'conversations.tools.checkRunHistory.done': 'Cronologia delle esecuzioni verificata', + 'conversations.tools.useApp.active': 'Uso di {app}', + 'conversations.tools.useApp.done': '{app} utilizzato', + 'conversations.tools.checkAvailableApps.active': 'Verifica delle app disponibili', + 'conversations.tools.checkAvailableApps.done': 'App disponibili verificate', + 'conversations.tools.checkConnections.active': 'Verifica dei tuoi collegamenti', + 'conversations.tools.checkConnections.done': 'Collegamenti verificati', + 'conversations.tools.connectApp.active': "Collegamento dell'app", + 'conversations.tools.connectApp.done': 'App collegata', + 'conversations.tools.authorizeApp.active': "Autorizzazione dell'app", + 'conversations.tools.authorizeApp.done': 'App autorizzata', + 'conversations.tools.findAppActions.active': "Ricerca delle azioni dell'app", + 'conversations.tools.findAppActions.done': "Azioni dell'app trovate", + 'conversations.tools.runAppAction.active': "Esecuzione dell'azione dell'app", + 'conversations.tools.runAppAction.done': "Azione dell'app eseguita", + 'conversations.tools.findTools.active': 'Ricerca degli strumenti', + 'conversations.tools.findTools.done': 'Strumenti trovati', + 'conversations.tools.useTool.active': 'Uso di {tool}', + 'conversations.tools.useTool.done': '{tool} utilizzato', + 'conversations.tools.unsubscribe.active': "Annullamento dell'iscrizione", + 'conversations.tools.unsubscribe.done': 'Iscrizione annullata', + 'conversations.tools.searchPlaces.active': 'Ricerca di luoghi', + 'conversations.tools.searchPlaces.done': 'Luoghi cercati', + 'conversations.tools.lookUpPlace.active': 'Ricerca del luogo', + 'conversations.tools.lookUpPlace.done': 'Luogo trovato', + 'conversations.tools.checkMarkets.active': 'Controllo dei mercati', + 'conversations.tools.checkMarkets.done': 'Mercati controllati', + 'conversations.tools.placeCall.active': 'Chiamata in corso', + 'conversations.tools.placeCall.done': 'Chiamata effettuata', + 'conversations.tools.checkTaskSources.active': 'Verifica delle fonti delle attività', + 'conversations.tools.checkTaskSources.done': 'Fonti delle attività verificate', + 'conversations.tools.updateTaskSources.active': 'Aggiornamento delle fonti delle attività', + 'conversations.tools.updateTaskSources.done': 'Fonti delle attività aggiornate', + 'conversations.tools.fetchTasks.active': 'Recupero delle attività', + 'conversations.tools.fetchTasks.done': 'Attività recuperate', + 'conversations.tools.checkMcpServers.active': 'Verifica dei server MCP', + 'conversations.tools.checkMcpServers.done': 'Server MCP verificati', + 'conversations.tools.checkMcpTools.active': 'Verifica degli strumenti MCP', + 'conversations.tools.checkMcpTools.done': 'Strumenti MCP verificati', + 'conversations.tools.callMcpTool.active': 'Chiamata a {tool}', + 'conversations.tools.callMcpTool.done': '{tool} chiamato', + 'conversations.tools.searchMcpServers.active': 'Ricerca di server MCP', + 'conversations.tools.searchMcpServers.done': 'Server MCP cercati', + 'conversations.tools.connectMcpServer.active': 'Collegamento del server MCP', + 'conversations.tools.connectMcpServer.done': 'Server MCP collegato', + 'conversations.tools.disconnectMcpServer.active': 'Scollegamento del server MCP', + 'conversations.tools.disconnectMcpServer.done': 'Server MCP scollegato', + 'conversations.tools.removeMcpServer.active': 'Rimozione del server MCP', + 'conversations.tools.removeMcpServer.done': 'Server MCP rimosso', + 'conversations.tools.uploadFile.active': 'Caricamento del file', + 'conversations.tools.uploadFile.done': 'File caricato', + 'conversations.tools.listStoredFiles.active': 'Elenco dei file archiviati', + 'conversations.tools.listStoredFiles.done': 'File archiviati elencati', + 'conversations.tools.createShareLink.active': 'Creazione del link di condivisione', + 'conversations.tools.createShareLink.done': 'Link di condivisione creato', + 'conversations.tools.deleteFile.active': 'Eliminazione del file', + 'conversations.tools.deleteFile.done': 'File eliminato', + 'conversations.tools.updateFileAccess.active': "Aggiornamento dell'accesso al file", + 'conversations.tools.updateFileAccess.done': 'Accesso al file aggiornato', + 'conversations.tools.deploySite.active': 'Distribuzione del sito', + 'conversations.tools.deploySite.done': 'Sito distribuito', + 'conversations.tools.checkHosting.active': "Verifica dell'hosting", + 'conversations.tools.checkHosting.done': 'Hosting verificato', + 'conversations.tools.updateHosting.active': "Aggiornamento dell'hosting", + 'conversations.tools.updateHosting.done': 'Hosting aggiornato', + 'conversations.tools.rollBackDeployment.active': 'Ripristino della distribuzione', + 'conversations.tools.rollBackDeployment.done': 'Distribuzione ripristinata', + 'conversations.tools.checkWallet.active': 'Verifica del portafoglio', + 'conversations.tools.checkWallet.done': 'Portafoglio verificato', + 'conversations.tools.prepareTransfer.active': 'Preparazione del trasferimento', + 'conversations.tools.prepareTransfer.done': 'Trasferimento preparato', + 'conversations.tools.checkTransaction.active': 'Verifica della transazione', + 'conversations.tools.checkTransaction.done': 'Transazione verificata', + 'conversations.tools.getSwapQuote.active': 'Richiesta del preventivo di scambio', + 'conversations.tools.getSwapQuote.done': 'Preventivo di scambio ottenuto', + 'conversations.tools.swapTokens.active': 'Scambio di token', + 'conversations.tools.swapTokens.done': 'Token scambiati', + 'conversations.tools.getBridgeQuote.active': 'Richiesta del preventivo di bridge', + 'conversations.tools.getBridgeQuote.done': 'Preventivo di bridge ottenuto', + 'conversations.tools.bridgeTokens.active': 'Trasferimento di token tramite bridge', + 'conversations.tools.bridgeTokens.done': 'Token trasferiti tramite bridge', + 'conversations.tools.callDapp.active': "Chiamata al contratto dell'app", + 'conversations.tools.callDapp.done': "Contratto dell'app chiamato", + 'conversations.tools.useSkill.active': 'Uso della skill', + 'conversations.tools.useSkill.done': 'Skill utilizzata', + 'conversations.tools.searchSkills.active': 'Ricerca di skill', + 'conversations.tools.searchSkills.done': 'Skill cercate', + 'conversations.tools.checkSkills.active': 'Verifica delle skill', + 'conversations.tools.checkSkills.done': 'Skill verificate', + 'conversations.tools.installSkill.active': 'Installazione della skill', + 'conversations.tools.installSkill.done': 'Skill installata', + 'conversations.tools.removeSkill.active': 'Rimozione della skill', + 'conversations.tools.removeSkill.done': 'Skill rimossa', + 'conversations.tools.createSkill.active': 'Creazione della skill', + 'conversations.tools.createSkill.done': 'Skill creata', + 'conversations.tools.runWorkflow.active': 'Esecuzione del flusso di lavoro', + 'conversations.tools.runWorkflow.done': 'Flusso di lavoro eseguito', + 'conversations.tools.waitForWorkflow.active': 'Attesa del flusso di lavoro', + 'conversations.tools.waitForWorkflow.done': 'Attesa del flusso di lavoro terminata', + 'conversations.tools.designWorkflow.active': 'Progettazione del flusso di lavoro', + 'conversations.tools.designWorkflow.done': 'Flusso di lavoro progettato', + 'conversations.tools.saveWorkflow.active': 'Salvataggio del flusso di lavoro', + 'conversations.tools.saveWorkflow.done': 'Flusso di lavoro salvato', + 'conversations.tools.validateWorkflow.active': 'Convalida del flusso di lavoro', + 'conversations.tools.validateWorkflow.done': 'Flusso di lavoro convalidato', + 'conversations.tools.testWorkflow.active': 'Test del flusso di lavoro', + 'conversations.tools.testWorkflow.done': 'Flusso di lavoro testato', + 'conversations.tools.checkWorkflows.active': 'Verifica dei flussi di lavoro', + 'conversations.tools.checkWorkflows.done': 'Flussi di lavoro verificati', + 'conversations.tools.cancelWorkflow.active': "Annullamento dell'esecuzione del flusso di lavoro", + 'conversations.tools.cancelWorkflow.done': 'Esecuzione del flusso di lavoro annullata', + 'conversations.tools.suggestWorkflows.active': 'Suggerimento di flussi di lavoro', + 'conversations.tools.suggestWorkflows.done': 'Flussi di lavoro suggeriti', + 'conversations.tools.checkSettings.active': 'Verifica delle impostazioni', + 'conversations.tools.checkSettings.done': 'Impostazioni verificate', + 'conversations.tools.checkSecurity.active': 'Verifica della sicurezza', + 'conversations.tools.checkSecurity.done': 'Sicurezza verificata', + 'conversations.tools.runDiagnostics.active': 'Esecuzione della diagnostica', + 'conversations.tools.runDiagnostics.done': 'Diagnostica eseguita', + 'conversations.tools.checkUsageCosts.active': 'Verifica dei costi di utilizzo', + 'conversations.tools.checkUsageCosts.done': 'Costi di utilizzo verificati', + 'conversations.tools.manageService.active': 'Gestione del servizio in background', + 'conversations.tools.manageService.done': 'Servizio in background gestito', + 'conversations.tools.readPersona.active': 'Lettura della persona', + 'conversations.tools.readPersona.done': 'Persona letta', + 'conversations.tools.updatePersona.active': 'Aggiornamento della persona', + 'conversations.tools.updatePersona.done': 'Persona aggiornata', + 'conversations.tools.setUpWorkspace.active': "Configurazione dell'area di lavoro", + 'conversations.tools.setUpWorkspace.done': 'Area di lavoro configurata', + 'conversations.tools.checkArtifacts.active': 'Verifica degli artefatti', + 'conversations.tools.checkArtifacts.done': 'Artefatti verificati', + 'conversations.tools.deleteArtifact.active': "Eliminazione dell'artefatto", + 'conversations.tools.deleteArtifact.done': 'Artefatto eliminato', 'conversations.subagent.noOutput': 'Nessun output restituito', 'conversations.subagent.close': 'Chiudi', 'conversations.subagent.cancel': 'Annulla attività', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index e8da4ed5255..0681c9670c6 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -3282,6 +3282,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': '아직 출력이 없습니다', 'conversations.subagent.input': '입력', 'conversations.subagent.output': '출력', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count}단계', + 'conversations.tools.steps.other': '{count}단계', + 'conversations.tools.working': '작업 중', + 'conversations.tools.noOutput': '출력 없음', + 'conversations.tools.delegatedTo': '{agent}에게 위임함', + 'conversations.tools.openInBrowser': '브라우저에서 열기', + 'conversations.tools.status.running': '실행 중', + 'conversations.tools.status.done': '완료', + 'conversations.tools.status.failed': '실패', + 'conversations.tools.status.cancelled': '취소됨', + 'conversations.tools.status.awaiting': '입력 대기 중', + 'conversations.tools.search.searching': '검색 중', + 'conversations.tools.search.none': '결과 없음', + 'conversations.tools.search.found.one': '결과 {count}개 찾음', + 'conversations.tools.search.found.other': '결과 {count}개 찾음', + 'conversations.tools.search.via': '{provider} 사용', + 'conversations.tools.readFile.active': '파일 읽는 중', + 'conversations.tools.readFile.done': '파일 읽음', + 'conversations.tools.writeFile.active': '파일 쓰는 중', + 'conversations.tools.writeFile.done': '파일 작성함', + 'conversations.tools.editFile.active': '파일 편집 중', + 'conversations.tools.editFile.done': '파일 편집함', + 'conversations.tools.applyEdits.active': '변경 사항 적용 중', + 'conversations.tools.applyEdits.done': '변경 사항 적용함', + 'conversations.tools.searchCode.active': '코드 검색 중', + 'conversations.tools.searchCode.done': '코드 검색함', + 'conversations.tools.findFiles.active': '파일 찾는 중', + 'conversations.tools.findFiles.done': '파일 찾음', + 'conversations.tools.listFolder.active': '폴더 목록 확인 중', + 'conversations.tools.listFolder.done': '폴더 목록 확인함', + 'conversations.tools.exportCsv.active': 'CSV 내보내는 중', + 'conversations.tools.exportCsv.done': 'CSV 내보냄', + 'conversations.tools.updateMemoryNotes.active': '메모리 노트 업데이트 중', + 'conversations.tools.updateMemoryNotes.done': '메모리 노트 업데이트함', + 'conversations.tools.runGit.active': 'git 실행 중', + 'conversations.tools.runGit.done': 'git 실행함', + 'conversations.tools.readChanges.active': '변경 사항 읽는 중', + 'conversations.tools.readChanges.done': '변경 사항 읽음', + 'conversations.tools.runLinter.active': '린터 실행 중', + 'conversations.tools.runLinter.done': '린터 실행함', + 'conversations.tools.runTests.active': '테스트 실행 중', + 'conversations.tools.runTests.done': '테스트 실행함', + 'conversations.tools.analyzeCode.active': '코드 분석 중', + 'conversations.tools.analyzeCode.done': '코드 분석함', + 'conversations.tools.insertRecord.active': '레코드 추가 중', + 'conversations.tools.insertRecord.done': '레코드 추가함', + 'conversations.tools.runCommand.active': '명령 실행 중', + 'conversations.tools.runCommand.done': '명령 실행함', + 'conversations.tools.runCode.active': '코드 실행 중', + 'conversations.tools.runCode.done': '코드 실행함', + 'conversations.tools.runPackageManager.active': 'npm 실행 중', + 'conversations.tools.runPackageManager.done': 'npm 실행함', + 'conversations.tools.checkInstalledTools.active': '설치된 도구 확인 중', + 'conversations.tools.checkInstalledTools.done': '설치된 도구 확인함', + 'conversations.tools.installTool.active': '도구 설치 중', + 'conversations.tools.installTool.done': '도구 설치함', + 'conversations.tools.checkTime.active': '시간 확인 중', + 'conversations.tools.checkTime.done': '시간 확인함', + 'conversations.tools.resolveDate.active': '날짜 계산 중', + 'conversations.tools.resolveDate.done': '날짜 계산함', + 'conversations.tools.retrieveOutput.active': '전체 출력 가져오는 중', + 'conversations.tools.retrieveOutput.done': '전체 출력 가져옴', + 'conversations.tools.reviewWorkspace.active': '작업 공간 검토 중', + 'conversations.tools.reviewWorkspace.done': '작업 공간 검토함', + 'conversations.tools.configureProxy.active': '프록시 구성 중', + 'conversations.tools.configureProxy.done': '프록시 구성함', + 'conversations.tools.checkUpdates.active': '업데이트 확인 중', + 'conversations.tools.checkUpdates.done': '업데이트 확인함', + 'conversations.tools.installUpdate.active': '업데이트 설치 중', + 'conversations.tools.installUpdate.done': '업데이트 설치함', + 'conversations.tools.sendNotification.active': '알림 보내는 중', + 'conversations.tools.sendNotification.done': '알림 보냄', + 'conversations.tools.reviewToolUsage.active': '도구 사용 내역 검토 중', + 'conversations.tools.reviewToolUsage.done': '도구 사용 내역 검토함', + 'conversations.tools.typeKeys.active': '입력 중', + 'conversations.tools.typeKeys.done': '입력함', + 'conversations.tools.click.active': '클릭 중', + 'conversations.tools.click.done': '클릭함', + 'conversations.tools.searchWeb.active': '웹 검색 중', + 'conversations.tools.searchWeb.done': '웹 검색함', + 'conversations.tools.searchNews.active': '뉴스 검색 중', + 'conversations.tools.searchNews.done': '뉴스 검색함', + 'conversations.tools.searchImages.active': '이미지 검색 중', + 'conversations.tools.searchImages.done': '이미지 검색함', + 'conversations.tools.searchVideos.active': '동영상 검색 중', + 'conversations.tools.searchVideos.done': '동영상 검색함', + 'conversations.tools.findSimilarPages.active': '유사한 페이지 찾는 중', + 'conversations.tools.findSimilarPages.done': '유사한 페이지 찾음', + 'conversations.tools.readPages.active': '페이지 읽는 중', + 'conversations.tools.readPages.done': '페이지 읽음', + 'conversations.tools.readWebpage.active': '웹페이지 읽는 중', + 'conversations.tools.readWebpage.done': '웹페이지 읽음', + 'conversations.tools.research.active': '조사 중', + 'conversations.tools.research.done': '조사함', + 'conversations.tools.enrichData.active': '데이터 보강 중', + 'conversations.tools.enrichData.done': '데이터 보강함', + 'conversations.tools.buildDataset.active': '데이터셋 구축 중', + 'conversations.tools.buildDataset.done': '데이터셋 구축함', + 'conversations.tools.askTheWeb.active': '웹에 질문하는 중', + 'conversations.tools.askTheWeb.done': '웹에 질문함', + 'conversations.tools.browseForYou.active': '대신 탐색하는 중', + 'conversations.tools.browseForYou.done': '대신 탐색함', + 'conversations.tools.callApi.active': 'API 호출 중', + 'conversations.tools.callApi.done': 'API 호출함', + 'conversations.tools.downloadFile.active': '파일 다운로드 중', + 'conversations.tools.downloadFile.done': '파일 다운로드함', + 'conversations.tools.makePaidRequest.active': '유료 요청 보내는 중', + 'conversations.tools.makePaidRequest.done': '유료 요청 보냄', + 'conversations.tools.searchDocs.active': '문서 검색 중', + 'conversations.tools.searchDocs.done': '문서 검색함', + 'conversations.tools.readDocs.active': '문서 읽는 중', + 'conversations.tools.readDocs.done': '문서 읽음', + 'conversations.tools.useBrowser.active': '브라우저 사용 중', + 'conversations.tools.useBrowser.done': '브라우저 사용함', + 'conversations.tools.openPage.active': '페이지 여는 중', + 'conversations.tools.openPage.done': '페이지 열었음', + 'conversations.tools.navigate.active': '이동 중', + 'conversations.tools.navigate.done': '이동함', + 'conversations.tools.takeScreenshot.active': '스크린샷 찍는 중', + 'conversations.tools.takeScreenshot.done': '스크린샷 찍음', + 'conversations.tools.scrollPage.active': '스크롤 중', + 'conversations.tools.scrollPage.done': '스크롤함', + 'conversations.tools.readPage.active': '페이지 읽는 중', + 'conversations.tools.readPage.done': '페이지 읽음', + 'conversations.tools.analyzeImage.active': '이미지 분석 중', + 'conversations.tools.analyzeImage.done': '이미지 분석함', + 'conversations.tools.generateImage.active': '이미지 생성 중', + 'conversations.tools.generateImage.done': '이미지 생성함', + 'conversations.tools.generateVideo.active': '동영상 생성 중', + 'conversations.tools.generateVideo.done': '동영상 생성함', + 'conversations.tools.checkMediaModels.active': '미디어 모델 확인 중', + 'conversations.tools.checkMediaModels.done': '미디어 모델 확인함', + 'conversations.tools.createDocument.active': '문서 만드는 중', + 'conversations.tools.createDocument.done': '문서 만듦', + 'conversations.tools.createPresentation.active': '프레젠테이션 만드는 중', + 'conversations.tools.createPresentation.done': '프레젠테이션 만듦', + 'conversations.tools.generatePodcast.active': '팟캐스트 생성 중', + 'conversations.tools.generatePodcast.done': '팟캐스트 생성함', + 'conversations.tools.emailPodcast.active': '팟캐스트 이메일 보내는 중', + 'conversations.tools.emailPodcast.done': '팟캐스트 이메일 보냄', + 'conversations.tools.createAndEmailPodcast.active': '팟캐스트 만들어 이메일 보내는 중', + 'conversations.tools.createAndEmailPodcast.done': '팟캐스트 만들어 이메일 보냄', + 'conversations.tools.recallMemories.active': '기억 떠올리는 중', + 'conversations.tools.recallMemories.done': '기억 떠올림', + 'conversations.tools.saveToMemory.active': '메모리에 저장 중', + 'conversations.tools.saveToMemory.done': '메모리에 저장함', + 'conversations.tools.forgetMemory.active': '기억 삭제 중', + 'conversations.tools.forgetMemory.done': '기억 삭제함', + 'conversations.tools.searchMemory.active': '메모리 검색 중', + 'conversations.tools.searchMemory.done': '메모리 검색함', + 'conversations.tools.inspectMemory.active': '메모리 살펴보는 중', + 'conversations.tools.inspectMemory.done': '메모리 살펴봄', + 'conversations.tools.exploreMemory.active': '메모리 탐색 중', + 'conversations.tools.exploreMemory.done': '메모리 탐색함', + 'conversations.tools.saveDocumentToMemory.active': '문서를 메모리에 저장 중', + 'conversations.tools.saveDocumentToMemory.done': '문서를 메모리에 저장함', + 'conversations.tools.updateGoals.active': '목표 업데이트 중', + 'conversations.tools.updateGoals.done': '목표 업데이트함', + 'conversations.tools.reviewGoals.active': '목표 검토 중', + 'conversations.tools.reviewGoals.done': '목표 검토함', + 'conversations.tools.savePreference.active': '선호 설정 저장 중', + 'conversations.tools.savePreference.done': '선호 설정 저장함', + 'conversations.tools.reviewLearnings.active': '배운 내용 검토 중', + 'conversations.tools.reviewLearnings.done': '배운 내용 검토함', + 'conversations.tools.updateLearnings.active': '배운 내용 업데이트 중', + 'conversations.tools.updateLearnings.done': '배운 내용 업데이트함', + 'conversations.tools.delegateTask.active': '작업 위임 중', + 'conversations.tools.delegateTask.done': '작업 위임함', + 'conversations.tools.runAgentsInParallel.active': '에이전트 병렬 실행 중', + 'conversations.tools.runAgentsInParallel.done': '에이전트 병렬 실행함', + 'conversations.tools.messageAgent.active': '에이전트에게 메시지 보내는 중', + 'conversations.tools.messageAgent.done': '에이전트에게 메시지 보냄', + 'conversations.tools.waitForAgent.active': '에이전트 기다리는 중', + 'conversations.tools.waitForAgent.done': '에이전트 기다림', + 'conversations.tools.wait.active': '대기 중', + 'conversations.tools.wait.done': '대기함', + 'conversations.tools.closeAgent.active': '에이전트 닫는 중', + 'conversations.tools.closeAgent.done': '에이전트 닫음', + 'conversations.tools.checkAgents.active': '에이전트 확인 중', + 'conversations.tools.checkAgents.done': '에이전트 확인함', + 'conversations.tools.askQuestion.active': '질문하는 중', + 'conversations.tools.askQuestion.done': '질문함', + 'conversations.tools.prepareContext.active': '컨텍스트 준비 중', + 'conversations.tools.prepareContext.done': '컨텍스트 준비함', + 'conversations.tools.extractDetails.active': '세부 정보 추출 중', + 'conversations.tools.extractDetails.done': '세부 정보 추출함', + 'conversations.tools.planNextSteps.active': '다음 단계 계획 중', + 'conversations.tools.planNextSteps.done': '다음 단계 계획함', + 'conversations.tools.reviewWork.active': '작업 검토 중', + 'conversations.tools.reviewWork.done': '작업 검토함', + 'conversations.tools.scoutContext.active': '컨텍스트 파악 중', + 'conversations.tools.scoutContext.done': '컨텍스트 파악함', + 'conversations.tools.useTools.active': '도구 사용 중', + 'conversations.tools.useTools.done': '도구 사용함', + 'conversations.tools.checkConnectedApp.active': '연결된 앱 확인 중', + 'conversations.tools.checkConnectedApp.done': '연결된 앱 확인함', + 'conversations.tools.updateTodos.active': '할 일 목록 업데이트 중', + 'conversations.tools.updateTodos.done': '할 일 목록 업데이트함', + 'conversations.tools.requestPlanReview.active': '계획 검토 요청 중', + 'conversations.tools.requestPlanReview.done': '계획 검토 요청함', + 'conversations.tools.finishPlan.active': '계획 마무리 중', + 'conversations.tools.finishPlan.done': '계획 마무리함', + 'conversations.tools.setGoal.active': '목표 설정 중', + 'conversations.tools.setGoal.done': '목표 설정함', + 'conversations.tools.checkGoal.active': '목표 확인 중', + 'conversations.tools.checkGoal.done': '목표 확인함', + 'conversations.tools.completeGoal.active': '목표 완료 처리 중', + 'conversations.tools.completeGoal.done': '목표 완료함', + 'conversations.tools.scheduleTask.active': '작업 예약 중', + 'conversations.tools.scheduleTask.done': '작업 예약함', + 'conversations.tools.checkSchedules.active': '일정 확인 중', + 'conversations.tools.checkSchedules.done': '일정 확인함', + 'conversations.tools.updateSchedule.active': '예약된 작업 업데이트 중', + 'conversations.tools.updateSchedule.done': '예약된 작업 업데이트함', + 'conversations.tools.removeSchedule.active': '예약된 작업 삭제 중', + 'conversations.tools.removeSchedule.done': '예약된 작업 삭제함', + 'conversations.tools.runScheduledTask.active': '예약된 작업 실행 중', + 'conversations.tools.runScheduledTask.done': '예약된 작업 실행함', + 'conversations.tools.checkRunHistory.active': '실행 기록 확인 중', + 'conversations.tools.checkRunHistory.done': '실행 기록 확인함', + 'conversations.tools.useApp.active': '{app} 사용 중', + 'conversations.tools.useApp.done': '{app} 사용함', + 'conversations.tools.checkAvailableApps.active': '사용 가능한 앱 확인 중', + 'conversations.tools.checkAvailableApps.done': '사용 가능한 앱 확인함', + 'conversations.tools.checkConnections.active': '연결 확인 중', + 'conversations.tools.checkConnections.done': '연결 확인함', + 'conversations.tools.connectApp.active': '앱 연결 중', + 'conversations.tools.connectApp.done': '앱 연결함', + 'conversations.tools.authorizeApp.active': '앱 승인 중', + 'conversations.tools.authorizeApp.done': '앱 승인함', + 'conversations.tools.findAppActions.active': '앱 작업 찾는 중', + 'conversations.tools.findAppActions.done': '앱 작업 찾음', + 'conversations.tools.runAppAction.active': '앱 작업 실행 중', + 'conversations.tools.runAppAction.done': '앱 작업 실행함', + 'conversations.tools.findTools.active': '도구 찾는 중', + 'conversations.tools.findTools.done': '도구 찾음', + 'conversations.tools.useTool.active': '{tool} 사용 중', + 'conversations.tools.useTool.done': '{tool} 사용함', + 'conversations.tools.unsubscribe.active': '구독 취소 중', + 'conversations.tools.unsubscribe.done': '구독 취소함', + 'conversations.tools.searchPlaces.active': '장소 검색 중', + 'conversations.tools.searchPlaces.done': '장소 검색함', + 'conversations.tools.lookUpPlace.active': '장소 조회 중', + 'conversations.tools.lookUpPlace.done': '장소 조회함', + 'conversations.tools.checkMarkets.active': '시장 확인 중', + 'conversations.tools.checkMarkets.done': '시장 확인함', + 'conversations.tools.placeCall.active': '전화 거는 중', + 'conversations.tools.placeCall.done': '전화 걸었음', + 'conversations.tools.checkTaskSources.active': '작업 소스 확인 중', + 'conversations.tools.checkTaskSources.done': '작업 소스 확인함', + 'conversations.tools.updateTaskSources.active': '작업 소스 업데이트 중', + 'conversations.tools.updateTaskSources.done': '작업 소스 업데이트함', + 'conversations.tools.fetchTasks.active': '작업 가져오는 중', + 'conversations.tools.fetchTasks.done': '작업 가져옴', + 'conversations.tools.checkMcpServers.active': 'MCP 서버 확인 중', + 'conversations.tools.checkMcpServers.done': 'MCP 서버 확인함', + 'conversations.tools.checkMcpTools.active': 'MCP 도구 확인 중', + 'conversations.tools.checkMcpTools.done': 'MCP 도구 확인함', + 'conversations.tools.callMcpTool.active': '{tool} 호출 중', + 'conversations.tools.callMcpTool.done': '{tool} 호출함', + 'conversations.tools.searchMcpServers.active': 'MCP 서버 검색 중', + 'conversations.tools.searchMcpServers.done': 'MCP 서버 검색함', + 'conversations.tools.connectMcpServer.active': 'MCP 서버 연결 중', + 'conversations.tools.connectMcpServer.done': 'MCP 서버 연결함', + 'conversations.tools.disconnectMcpServer.active': 'MCP 서버 연결 해제 중', + 'conversations.tools.disconnectMcpServer.done': 'MCP 서버 연결 해제함', + 'conversations.tools.removeMcpServer.active': 'MCP 서버 삭제 중', + 'conversations.tools.removeMcpServer.done': 'MCP 서버 삭제함', + 'conversations.tools.uploadFile.active': '파일 업로드 중', + 'conversations.tools.uploadFile.done': '파일 업로드함', + 'conversations.tools.listStoredFiles.active': '저장된 파일 목록 확인 중', + 'conversations.tools.listStoredFiles.done': '저장된 파일 목록 확인함', + 'conversations.tools.createShareLink.active': '공유 링크 만드는 중', + 'conversations.tools.createShareLink.done': '공유 링크 만듦', + 'conversations.tools.deleteFile.active': '파일 삭제 중', + 'conversations.tools.deleteFile.done': '파일 삭제함', + 'conversations.tools.updateFileAccess.active': '파일 접근 권한 업데이트 중', + 'conversations.tools.updateFileAccess.done': '파일 접근 권한 업데이트함', + 'conversations.tools.deploySite.active': '사이트 배포 중', + 'conversations.tools.deploySite.done': '사이트 배포함', + 'conversations.tools.checkHosting.active': '호스팅 확인 중', + 'conversations.tools.checkHosting.done': '호스팅 확인함', + 'conversations.tools.updateHosting.active': '호스팅 업데이트 중', + 'conversations.tools.updateHosting.done': '호스팅 업데이트함', + 'conversations.tools.rollBackDeployment.active': '배포 롤백 중', + 'conversations.tools.rollBackDeployment.done': '배포 롤백함', + 'conversations.tools.checkWallet.active': '지갑 확인 중', + 'conversations.tools.checkWallet.done': '지갑 확인함', + 'conversations.tools.prepareTransfer.active': '송금 준비 중', + 'conversations.tools.prepareTransfer.done': '송금 준비함', + 'conversations.tools.checkTransaction.active': '거래 확인 중', + 'conversations.tools.checkTransaction.done': '거래 확인함', + 'conversations.tools.getSwapQuote.active': '스왑 견적 가져오는 중', + 'conversations.tools.getSwapQuote.done': '스왑 견적 가져옴', + 'conversations.tools.swapTokens.active': '토큰 스왑 중', + 'conversations.tools.swapTokens.done': '토큰 스왑함', + 'conversations.tools.getBridgeQuote.active': '브리지 견적 가져오는 중', + 'conversations.tools.getBridgeQuote.done': '브리지 견적 가져옴', + 'conversations.tools.bridgeTokens.active': '토큰 브리지 중', + 'conversations.tools.bridgeTokens.done': '토큰 브리지함', + 'conversations.tools.callDapp.active': '앱 컨트랙트 호출 중', + 'conversations.tools.callDapp.done': '앱 컨트랙트 호출함', + 'conversations.tools.useSkill.active': '스킬 사용 중', + 'conversations.tools.useSkill.done': '스킬 사용함', + 'conversations.tools.searchSkills.active': '스킬 검색 중', + 'conversations.tools.searchSkills.done': '스킬 검색함', + 'conversations.tools.checkSkills.active': '스킬 확인 중', + 'conversations.tools.checkSkills.done': '스킬 확인함', + 'conversations.tools.installSkill.active': '스킬 설치 중', + 'conversations.tools.installSkill.done': '스킬 설치함', + 'conversations.tools.removeSkill.active': '스킬 삭제 중', + 'conversations.tools.removeSkill.done': '스킬 삭제함', + 'conversations.tools.createSkill.active': '스킬 만드는 중', + 'conversations.tools.createSkill.done': '스킬 만듦', + 'conversations.tools.runWorkflow.active': '워크플로 실행 중', + 'conversations.tools.runWorkflow.done': '워크플로 실행함', + 'conversations.tools.waitForWorkflow.active': '워크플로 기다리는 중', + 'conversations.tools.waitForWorkflow.done': '워크플로 기다림', + 'conversations.tools.designWorkflow.active': '워크플로 설계 중', + 'conversations.tools.designWorkflow.done': '워크플로 설계함', + 'conversations.tools.saveWorkflow.active': '워크플로 저장 중', + 'conversations.tools.saveWorkflow.done': '워크플로 저장함', + 'conversations.tools.validateWorkflow.active': '워크플로 검증 중', + 'conversations.tools.validateWorkflow.done': '워크플로 검증함', + 'conversations.tools.testWorkflow.active': '워크플로 테스트 중', + 'conversations.tools.testWorkflow.done': '워크플로 테스트함', + 'conversations.tools.checkWorkflows.active': '워크플로 확인 중', + 'conversations.tools.checkWorkflows.done': '워크플로 확인함', + 'conversations.tools.cancelWorkflow.active': '워크플로 실행 취소 중', + 'conversations.tools.cancelWorkflow.done': '워크플로 실행 취소함', + 'conversations.tools.suggestWorkflows.active': '워크플로 제안 중', + 'conversations.tools.suggestWorkflows.done': '워크플로 제안함', + 'conversations.tools.checkSettings.active': '설정 확인 중', + 'conversations.tools.checkSettings.done': '설정 확인함', + 'conversations.tools.checkSecurity.active': '보안 확인 중', + 'conversations.tools.checkSecurity.done': '보안 확인함', + 'conversations.tools.runDiagnostics.active': '진단 실행 중', + 'conversations.tools.runDiagnostics.done': '진단 실행함', + 'conversations.tools.checkUsageCosts.active': '사용 비용 확인 중', + 'conversations.tools.checkUsageCosts.done': '사용 비용 확인함', + 'conversations.tools.manageService.active': '백그라운드 서비스 관리 중', + 'conversations.tools.manageService.done': '백그라운드 서비스 관리함', + 'conversations.tools.readPersona.active': '페르소나 읽는 중', + 'conversations.tools.readPersona.done': '페르소나 읽음', + 'conversations.tools.updatePersona.active': '페르소나 업데이트 중', + 'conversations.tools.updatePersona.done': '페르소나 업데이트함', + 'conversations.tools.setUpWorkspace.active': '작업 공간 설정 중', + 'conversations.tools.setUpWorkspace.done': '작업 공간 설정함', + 'conversations.tools.checkArtifacts.active': '아티팩트 확인 중', + 'conversations.tools.checkArtifacts.done': '아티팩트 확인함', + 'conversations.tools.deleteArtifact.active': '아티팩트 삭제 중', + 'conversations.tools.deleteArtifact.done': '아티팩트 삭제함', 'conversations.subagent.noOutput': '반환된 출력 없음', 'conversations.subagent.close': '닫기', 'conversations.subagent.cancel': '작업 취소', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index cdf3c79fd49..724a910ea69 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -3356,6 +3356,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Brak wyników', 'conversations.subagent.input': 'Wejście', 'conversations.subagent.output': 'Wyjście', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} krok', + 'conversations.tools.steps.other': 'Kroki: {count}', + 'conversations.tools.working': 'Pracuje', + 'conversations.tools.noOutput': 'Brak wyniku', + 'conversations.tools.delegatedTo': 'Przekazano do {agent}', + 'conversations.tools.openInBrowser': 'Otwórz w przeglądarce', + 'conversations.tools.status.running': 'w toku', + 'conversations.tools.status.done': 'gotowe', + 'conversations.tools.status.failed': 'niepowodzenie', + 'conversations.tools.status.cancelled': 'anulowano', + 'conversations.tools.status.awaiting': 'czeka na dane', + 'conversations.tools.search.searching': 'Wyszukiwanie', + 'conversations.tools.search.none': 'Brak wyników', + 'conversations.tools.search.found.one': 'Znaleziono {count} wynik', + 'conversations.tools.search.found.other': 'Znalezione wyniki: {count}', + 'conversations.tools.search.via': 'przez {provider}', + 'conversations.tools.readFile.active': 'Czytanie pliku', + 'conversations.tools.readFile.done': 'Odczytano plik', + 'conversations.tools.writeFile.active': 'Zapisywanie pliku', + 'conversations.tools.writeFile.done': 'Zapisano plik', + 'conversations.tools.editFile.active': 'Edytowanie pliku', + 'conversations.tools.editFile.done': 'Edytowano plik', + 'conversations.tools.applyEdits.active': 'Wprowadzanie zmian', + 'conversations.tools.applyEdits.done': 'Wprowadzono zmiany', + 'conversations.tools.searchCode.active': 'Przeszukiwanie kodu', + 'conversations.tools.searchCode.done': 'Przeszukano kod', + 'conversations.tools.findFiles.active': 'Wyszukiwanie plików', + 'conversations.tools.findFiles.done': 'Znaleziono pliki', + 'conversations.tools.listFolder.active': 'Wyświetlanie folderu', + 'conversations.tools.listFolder.done': 'Wyświetlono folder', + 'conversations.tools.exportCsv.active': 'Eksportowanie CSV', + 'conversations.tools.exportCsv.done': 'Wyeksportowano CSV', + 'conversations.tools.updateMemoryNotes.active': 'Aktualizowanie notatek pamięci', + 'conversations.tools.updateMemoryNotes.done': 'Zaktualizowano notatki pamięci', + 'conversations.tools.runGit.active': 'Uruchamianie git', + 'conversations.tools.runGit.done': 'Uruchomiono git', + 'conversations.tools.readChanges.active': 'Czytanie zmian', + 'conversations.tools.readChanges.done': 'Odczytano zmiany', + 'conversations.tools.runLinter.active': 'Uruchamianie lintera', + 'conversations.tools.runLinter.done': 'Uruchomiono linter', + 'conversations.tools.runTests.active': 'Uruchamianie testów', + 'conversations.tools.runTests.done': 'Uruchomiono testy', + 'conversations.tools.analyzeCode.active': 'Analizowanie kodu', + 'conversations.tools.analyzeCode.done': 'Przeanalizowano kod', + 'conversations.tools.insertRecord.active': 'Wstawianie rekordu', + 'conversations.tools.insertRecord.done': 'Wstawiono rekord', + 'conversations.tools.runCommand.active': 'Wykonywanie polecenia', + 'conversations.tools.runCommand.done': 'Wykonano polecenie', + 'conversations.tools.runCode.active': 'Wykonywanie kodu', + 'conversations.tools.runCode.done': 'Wykonano kod', + 'conversations.tools.runPackageManager.active': 'Uruchamianie npm', + 'conversations.tools.runPackageManager.done': 'Uruchomiono npm', + 'conversations.tools.checkInstalledTools.active': 'Sprawdzanie zainstalowanych narzędzi', + 'conversations.tools.checkInstalledTools.done': 'Sprawdzono zainstalowane narzędzia', + 'conversations.tools.installTool.active': 'Instalowanie narzędzia', + 'conversations.tools.installTool.done': 'Zainstalowano narzędzie', + 'conversations.tools.checkTime.active': 'Sprawdzanie godziny', + 'conversations.tools.checkTime.done': 'Sprawdzono godzinę', + 'conversations.tools.resolveDate.active': 'Ustalanie daty', + 'conversations.tools.resolveDate.done': 'Ustalono datę', + 'conversations.tools.retrieveOutput.active': 'Pobieranie pełnego wyniku', + 'conversations.tools.retrieveOutput.done': 'Pobrano pełny wynik', + 'conversations.tools.reviewWorkspace.active': 'Przeglądanie obszaru roboczego', + 'conversations.tools.reviewWorkspace.done': 'Przejrzano obszar roboczy', + 'conversations.tools.configureProxy.active': 'Konfigurowanie proxy', + 'conversations.tools.configureProxy.done': 'Skonfigurowano proxy', + 'conversations.tools.checkUpdates.active': 'Sprawdzanie aktualizacji', + 'conversations.tools.checkUpdates.done': 'Sprawdzono aktualizacje', + 'conversations.tools.installUpdate.active': 'Instalowanie aktualizacji', + 'conversations.tools.installUpdate.done': 'Zainstalowano aktualizację', + 'conversations.tools.sendNotification.active': 'Wysyłanie powiadomienia', + 'conversations.tools.sendNotification.done': 'Wysłano powiadomienie', + 'conversations.tools.reviewToolUsage.active': 'Przeglądanie użycia narzędzi', + 'conversations.tools.reviewToolUsage.done': 'Przejrzano użycie narzędzi', + 'conversations.tools.typeKeys.active': 'Wpisywanie', + 'conversations.tools.typeKeys.done': 'Wpisano', + 'conversations.tools.click.active': 'Klikanie', + 'conversations.tools.click.done': 'Kliknięto', + 'conversations.tools.searchWeb.active': 'Przeszukiwanie internetu', + 'conversations.tools.searchWeb.done': 'Przeszukano internet', + 'conversations.tools.searchNews.active': 'Wyszukiwanie wiadomości', + 'conversations.tools.searchNews.done': 'Wyszukano wiadomości', + 'conversations.tools.searchImages.active': 'Wyszukiwanie obrazów', + 'conversations.tools.searchImages.done': 'Wyszukano obrazy', + 'conversations.tools.searchVideos.active': 'Wyszukiwanie filmów', + 'conversations.tools.searchVideos.done': 'Wyszukano filmy', + 'conversations.tools.findSimilarPages.active': 'Szukanie podobnych stron', + 'conversations.tools.findSimilarPages.done': 'Znaleziono podobne strony', + 'conversations.tools.readPages.active': 'Czytanie stron', + 'conversations.tools.readPages.done': 'Odczytano strony', + 'conversations.tools.readWebpage.active': 'Czytanie strony internetowej', + 'conversations.tools.readWebpage.done': 'Odczytano stronę internetową', + 'conversations.tools.research.active': 'Badanie tematu', + 'conversations.tools.research.done': 'Zbadano temat', + 'conversations.tools.enrichData.active': 'Wzbogacanie danych', + 'conversations.tools.enrichData.done': 'Wzbogacono dane', + 'conversations.tools.buildDataset.active': 'Tworzenie zbioru danych', + 'conversations.tools.buildDataset.done': 'Utworzono zbiór danych', + 'conversations.tools.askTheWeb.active': 'Pytanie internetu', + 'conversations.tools.askTheWeb.done': 'Zapytano internet', + 'conversations.tools.browseForYou.active': 'Przeglądanie stron za Ciebie', + 'conversations.tools.browseForYou.done': 'Przejrzano strony za Ciebie', + 'conversations.tools.callApi.active': 'Wywoływanie API', + 'conversations.tools.callApi.done': 'Wywołano API', + 'conversations.tools.downloadFile.active': 'Pobieranie pliku', + 'conversations.tools.downloadFile.done': 'Pobrano plik', + 'conversations.tools.makePaidRequest.active': 'Wysyłanie płatnego żądania', + 'conversations.tools.makePaidRequest.done': 'Wysłano płatne żądanie', + 'conversations.tools.searchDocs.active': 'Przeszukiwanie dokumentacji', + 'conversations.tools.searchDocs.done': 'Przeszukano dokumentację', + 'conversations.tools.readDocs.active': 'Czytanie dokumentacji', + 'conversations.tools.readDocs.done': 'Odczytano dokumentację', + 'conversations.tools.useBrowser.active': 'Korzystanie z przeglądarki', + 'conversations.tools.useBrowser.done': 'Skorzystano z przeglądarki', + 'conversations.tools.openPage.active': 'Otwieranie strony', + 'conversations.tools.openPage.done': 'Otwarto stronę', + 'conversations.tools.navigate.active': 'Przechodzenie', + 'conversations.tools.navigate.done': 'Przejście zakończone', + 'conversations.tools.takeScreenshot.active': 'Robienie zrzutu ekranu', + 'conversations.tools.takeScreenshot.done': 'Zrobiono zrzut ekranu', + 'conversations.tools.scrollPage.active': 'Przewijanie', + 'conversations.tools.scrollPage.done': 'Przewinięto', + 'conversations.tools.readPage.active': 'Czytanie strony', + 'conversations.tools.readPage.done': 'Odczytano stronę', + 'conversations.tools.analyzeImage.active': 'Analizowanie obrazu', + 'conversations.tools.analyzeImage.done': 'Przeanalizowano obraz', + 'conversations.tools.generateImage.active': 'Generowanie obrazu', + 'conversations.tools.generateImage.done': 'Wygenerowano obraz', + 'conversations.tools.generateVideo.active': 'Generowanie filmu', + 'conversations.tools.generateVideo.done': 'Wygenerowano film', + 'conversations.tools.checkMediaModels.active': 'Sprawdzanie modeli multimediów', + 'conversations.tools.checkMediaModels.done': 'Sprawdzono modele multimediów', + 'conversations.tools.createDocument.active': 'Tworzenie dokumentu', + 'conversations.tools.createDocument.done': 'Utworzono dokument', + 'conversations.tools.createPresentation.active': 'Tworzenie prezentacji', + 'conversations.tools.createPresentation.done': 'Utworzono prezentację', + 'conversations.tools.generatePodcast.active': 'Generowanie podcastu', + 'conversations.tools.generatePodcast.done': 'Wygenerowano podcast', + 'conversations.tools.emailPodcast.active': 'Wysyłanie podcastu e-mailem', + 'conversations.tools.emailPodcast.done': 'Wysłano podcast e-mailem', + 'conversations.tools.createAndEmailPodcast.active': 'Tworzenie i wysyłanie podcastu e-mailem', + 'conversations.tools.createAndEmailPodcast.done': 'Utworzono i wysłano podcast e-mailem', + 'conversations.tools.recallMemories.active': 'Przywoływanie wspomnień', + 'conversations.tools.recallMemories.done': 'Przywołano wspomnienia', + 'conversations.tools.saveToMemory.active': 'Zapisywanie w pamięci', + 'conversations.tools.saveToMemory.done': 'Zapisano w pamięci', + 'conversations.tools.forgetMemory.active': 'Usuwanie wspomnienia', + 'conversations.tools.forgetMemory.done': 'Usunięto wspomnienie', + 'conversations.tools.searchMemory.active': 'Przeszukiwanie pamięci', + 'conversations.tools.searchMemory.done': 'Przeszukano pamięć', + 'conversations.tools.inspectMemory.active': 'Sprawdzanie pamięci', + 'conversations.tools.inspectMemory.done': 'Sprawdzono pamięć', + 'conversations.tools.exploreMemory.active': 'Eksplorowanie pamięci', + 'conversations.tools.exploreMemory.done': 'Przejrzano pamięć', + 'conversations.tools.saveDocumentToMemory.active': 'Zapisywanie dokumentu w pamięci', + 'conversations.tools.saveDocumentToMemory.done': 'Zapisano dokument w pamięci', + 'conversations.tools.updateGoals.active': 'Aktualizowanie celów', + 'conversations.tools.updateGoals.done': 'Zaktualizowano cele', + 'conversations.tools.reviewGoals.active': 'Przeglądanie celów', + 'conversations.tools.reviewGoals.done': 'Przejrzano cele', + 'conversations.tools.savePreference.active': 'Zapisywanie preferencji', + 'conversations.tools.savePreference.done': 'Zapisano preferencję', + 'conversations.tools.reviewLearnings.active': 'Przeglądanie wniosków', + 'conversations.tools.reviewLearnings.done': 'Przejrzano wnioski', + 'conversations.tools.updateLearnings.active': 'Aktualizowanie wniosków', + 'conversations.tools.updateLearnings.done': 'Zaktualizowano wnioski', + 'conversations.tools.delegateTask.active': 'Przekazywanie zadania', + 'conversations.tools.delegateTask.done': 'Przekazano zadanie', + 'conversations.tools.runAgentsInParallel.active': 'Równoległe uruchamianie agentów', + 'conversations.tools.runAgentsInParallel.done': 'Uruchomiono agentów równolegle', + 'conversations.tools.messageAgent.active': 'Wysyłanie wiadomości do agenta', + 'conversations.tools.messageAgent.done': 'Wysłano wiadomość do agenta', + 'conversations.tools.waitForAgent.active': 'Oczekiwanie na agenta', + 'conversations.tools.waitForAgent.done': 'Agent odpowiedział', + 'conversations.tools.wait.active': 'Oczekiwanie', + 'conversations.tools.wait.done': 'Zakończono oczekiwanie', + 'conversations.tools.closeAgent.active': 'Zamykanie agenta', + 'conversations.tools.closeAgent.done': 'Zamknięto agenta', + 'conversations.tools.checkAgents.active': 'Sprawdzanie agentów', + 'conversations.tools.checkAgents.done': 'Sprawdzono agentów', + 'conversations.tools.askQuestion.active': 'Zadawanie Ci pytania', + 'conversations.tools.askQuestion.done': 'Zadano Ci pytanie', + 'conversations.tools.prepareContext.active': 'Przygotowywanie kontekstu', + 'conversations.tools.prepareContext.done': 'Przygotowano kontekst', + 'conversations.tools.extractDetails.active': 'Wyodrębnianie szczegółów', + 'conversations.tools.extractDetails.done': 'Wyodrębniono szczegóły', + 'conversations.tools.planNextSteps.active': 'Planowanie kolejnych kroków', + 'conversations.tools.planNextSteps.done': 'Zaplanowano kolejne kroki', + 'conversations.tools.reviewWork.active': 'Przeglądanie pracy', + 'conversations.tools.reviewWork.done': 'Przejrzano pracę', + 'conversations.tools.scoutContext.active': 'Rozpoznawanie kontekstu', + 'conversations.tools.scoutContext.done': 'Rozpoznano kontekst', + 'conversations.tools.useTools.active': 'Korzystanie z narzędzi', + 'conversations.tools.useTools.done': 'Skorzystano z narzędzi', + 'conversations.tools.checkConnectedApp.active': 'Sprawdzanie połączonej aplikacji', + 'conversations.tools.checkConnectedApp.done': 'Sprawdzono połączoną aplikację', + 'conversations.tools.updateTodos.active': 'Aktualizowanie listy zadań', + 'conversations.tools.updateTodos.done': 'Zaktualizowano listę zadań', + 'conversations.tools.requestPlanReview.active': 'Prośba o przegląd planu', + 'conversations.tools.requestPlanReview.done': 'Poproszono o przegląd planu', + 'conversations.tools.finishPlan.active': 'Kończenie planu', + 'conversations.tools.finishPlan.done': 'Ukończono plan', + 'conversations.tools.setGoal.active': 'Ustawianie celu', + 'conversations.tools.setGoal.done': 'Ustawiono cel', + 'conversations.tools.checkGoal.active': 'Sprawdzanie celu', + 'conversations.tools.checkGoal.done': 'Sprawdzono cel', + 'conversations.tools.completeGoal.active': 'Realizowanie celu', + 'conversations.tools.completeGoal.done': 'Zrealizowano cel', + 'conversations.tools.scheduleTask.active': 'Planowanie zadania', + 'conversations.tools.scheduleTask.done': 'Zaplanowano zadanie', + 'conversations.tools.checkSchedules.active': 'Sprawdzanie harmonogramów', + 'conversations.tools.checkSchedules.done': 'Sprawdzono harmonogramy', + 'conversations.tools.updateSchedule.active': 'Aktualizowanie zaplanowanego zadania', + 'conversations.tools.updateSchedule.done': 'Zaktualizowano zaplanowane zadanie', + 'conversations.tools.removeSchedule.active': 'Usuwanie zaplanowanego zadania', + 'conversations.tools.removeSchedule.done': 'Usunięto zaplanowane zadanie', + 'conversations.tools.runScheduledTask.active': 'Uruchamianie zaplanowanego zadania', + 'conversations.tools.runScheduledTask.done': 'Uruchomiono zaplanowane zadanie', + 'conversations.tools.checkRunHistory.active': 'Sprawdzanie historii uruchomień', + 'conversations.tools.checkRunHistory.done': 'Sprawdzono historię uruchomień', + 'conversations.tools.useApp.active': 'Korzystanie z {app}', + 'conversations.tools.useApp.done': 'Skorzystano z {app}', + 'conversations.tools.checkAvailableApps.active': 'Sprawdzanie dostępnych aplikacji', + 'conversations.tools.checkAvailableApps.done': 'Sprawdzono dostępne aplikacje', + 'conversations.tools.checkConnections.active': 'Sprawdzanie Twoich połączeń', + 'conversations.tools.checkConnections.done': 'Sprawdzono Twoje połączenia', + 'conversations.tools.connectApp.active': 'Łączenie aplikacji', + 'conversations.tools.connectApp.done': 'Połączono aplikację', + 'conversations.tools.authorizeApp.active': 'Autoryzowanie aplikacji', + 'conversations.tools.authorizeApp.done': 'Autoryzowano aplikację', + 'conversations.tools.findAppActions.active': 'Szukanie akcji aplikacji', + 'conversations.tools.findAppActions.done': 'Znaleziono akcje aplikacji', + 'conversations.tools.runAppAction.active': 'Uruchamianie akcji aplikacji', + 'conversations.tools.runAppAction.done': 'Uruchomiono akcję aplikacji', + 'conversations.tools.findTools.active': 'Szukanie narzędzi', + 'conversations.tools.findTools.done': 'Znaleziono narzędzia', + 'conversations.tools.useTool.active': 'Korzystanie z {tool}', + 'conversations.tools.useTool.done': 'Skorzystano z {tool}', + 'conversations.tools.unsubscribe.active': 'Wypisywanie z subskrypcji', + 'conversations.tools.unsubscribe.done': 'Wypisano z subskrypcji', + 'conversations.tools.searchPlaces.active': 'Wyszukiwanie miejsc', + 'conversations.tools.searchPlaces.done': 'Wyszukano miejsca', + 'conversations.tools.lookUpPlace.active': 'Sprawdzanie miejsca', + 'conversations.tools.lookUpPlace.done': 'Sprawdzono miejsce', + 'conversations.tools.checkMarkets.active': 'Sprawdzanie rynków', + 'conversations.tools.checkMarkets.done': 'Sprawdzono rynki', + 'conversations.tools.placeCall.active': 'Wykonywanie połączenia', + 'conversations.tools.placeCall.done': 'Wykonano połączenie', + 'conversations.tools.checkTaskSources.active': 'Sprawdzanie źródeł zadań', + 'conversations.tools.checkTaskSources.done': 'Sprawdzono źródła zadań', + 'conversations.tools.updateTaskSources.active': 'Aktualizowanie źródeł zadań', + 'conversations.tools.updateTaskSources.done': 'Zaktualizowano źródła zadań', + 'conversations.tools.fetchTasks.active': 'Pobieranie zadań', + 'conversations.tools.fetchTasks.done': 'Pobrano zadania', + 'conversations.tools.checkMcpServers.active': 'Sprawdzanie serwerów MCP', + 'conversations.tools.checkMcpServers.done': 'Sprawdzono serwery MCP', + 'conversations.tools.checkMcpTools.active': 'Sprawdzanie narzędzi MCP', + 'conversations.tools.checkMcpTools.done': 'Sprawdzono narzędzia MCP', + 'conversations.tools.callMcpTool.active': 'Wywoływanie {tool}', + 'conversations.tools.callMcpTool.done': 'Wywołano {tool}', + 'conversations.tools.searchMcpServers.active': 'Wyszukiwanie serwerów MCP', + 'conversations.tools.searchMcpServers.done': 'Wyszukano serwery MCP', + 'conversations.tools.connectMcpServer.active': 'Łączenie serwera MCP', + 'conversations.tools.connectMcpServer.done': 'Połączono serwer MCP', + 'conversations.tools.disconnectMcpServer.active': 'Rozłączanie serwera MCP', + 'conversations.tools.disconnectMcpServer.done': 'Rozłączono serwer MCP', + 'conversations.tools.removeMcpServer.active': 'Usuwanie serwera MCP', + 'conversations.tools.removeMcpServer.done': 'Usunięto serwer MCP', + 'conversations.tools.uploadFile.active': 'Przesyłanie pliku', + 'conversations.tools.uploadFile.done': 'Przesłano plik', + 'conversations.tools.listStoredFiles.active': 'Wyświetlanie zapisanych plików', + 'conversations.tools.listStoredFiles.done': 'Wyświetlono zapisane pliki', + 'conversations.tools.createShareLink.active': 'Tworzenie linku do udostępnienia', + 'conversations.tools.createShareLink.done': 'Utworzono link do udostępnienia', + 'conversations.tools.deleteFile.active': 'Usuwanie pliku', + 'conversations.tools.deleteFile.done': 'Usunięto plik', + 'conversations.tools.updateFileAccess.active': 'Aktualizowanie dostępu do pliku', + 'conversations.tools.updateFileAccess.done': 'Zaktualizowano dostęp do pliku', + 'conversations.tools.deploySite.active': 'Wdrażanie witryny', + 'conversations.tools.deploySite.done': 'Wdrożono witrynę', + 'conversations.tools.checkHosting.active': 'Sprawdzanie hostingu', + 'conversations.tools.checkHosting.done': 'Sprawdzono hosting', + 'conversations.tools.updateHosting.active': 'Aktualizowanie hostingu', + 'conversations.tools.updateHosting.done': 'Zaktualizowano hosting', + 'conversations.tools.rollBackDeployment.active': 'Wycofywanie wdrożenia', + 'conversations.tools.rollBackDeployment.done': 'Wycofano wdrożenie', + 'conversations.tools.checkWallet.active': 'Sprawdzanie portfela', + 'conversations.tools.checkWallet.done': 'Sprawdzono portfel', + 'conversations.tools.prepareTransfer.active': 'Przygotowywanie przelewu', + 'conversations.tools.prepareTransfer.done': 'Przygotowano przelew', + 'conversations.tools.checkTransaction.active': 'Sprawdzanie transakcji', + 'conversations.tools.checkTransaction.done': 'Sprawdzono transakcję', + 'conversations.tools.getSwapQuote.active': 'Pobieranie wyceny wymiany', + 'conversations.tools.getSwapQuote.done': 'Pobrano wycenę wymiany', + 'conversations.tools.swapTokens.active': 'Wymiana tokenów', + 'conversations.tools.swapTokens.done': 'Wymieniono tokeny', + 'conversations.tools.getBridgeQuote.active': 'Pobieranie wyceny mostu', + 'conversations.tools.getBridgeQuote.done': 'Pobrano wycenę mostu', + 'conversations.tools.bridgeTokens.active': 'Przenoszenie tokenów przez most', + 'conversations.tools.bridgeTokens.done': 'Przeniesiono tokeny przez most', + 'conversations.tools.callDapp.active': 'Wywoływanie kontraktu aplikacji', + 'conversations.tools.callDapp.done': 'Wywołano kontrakt aplikacji', + 'conversations.tools.useSkill.active': 'Korzystanie z umiejętności', + 'conversations.tools.useSkill.done': 'Skorzystano z umiejętności', + 'conversations.tools.searchSkills.active': 'Wyszukiwanie umiejętności', + 'conversations.tools.searchSkills.done': 'Wyszukano umiejętności', + 'conversations.tools.checkSkills.active': 'Sprawdzanie umiejętności', + 'conversations.tools.checkSkills.done': 'Sprawdzono umiejętności', + 'conversations.tools.installSkill.active': 'Instalowanie umiejętności', + 'conversations.tools.installSkill.done': 'Zainstalowano umiejętność', + 'conversations.tools.removeSkill.active': 'Usuwanie umiejętności', + 'conversations.tools.removeSkill.done': 'Usunięto umiejętność', + 'conversations.tools.createSkill.active': 'Tworzenie umiejętności', + 'conversations.tools.createSkill.done': 'Utworzono umiejętność', + 'conversations.tools.runWorkflow.active': 'Uruchamianie przepływu pracy', + 'conversations.tools.runWorkflow.done': 'Uruchomiono przepływ pracy', + 'conversations.tools.waitForWorkflow.active': 'Oczekiwanie na przepływ pracy', + 'conversations.tools.waitForWorkflow.done': 'Przepływ pracy zakończony', + 'conversations.tools.designWorkflow.active': 'Projektowanie przepływu pracy', + 'conversations.tools.designWorkflow.done': 'Zaprojektowano przepływ pracy', + 'conversations.tools.saveWorkflow.active': 'Zapisywanie przepływu pracy', + 'conversations.tools.saveWorkflow.done': 'Zapisano przepływ pracy', + 'conversations.tools.validateWorkflow.active': 'Weryfikowanie przepływu pracy', + 'conversations.tools.validateWorkflow.done': 'Zweryfikowano przepływ pracy', + 'conversations.tools.testWorkflow.active': 'Testowanie przepływu pracy', + 'conversations.tools.testWorkflow.done': 'Przetestowano przepływ pracy', + 'conversations.tools.checkWorkflows.active': 'Sprawdzanie przepływów pracy', + 'conversations.tools.checkWorkflows.done': 'Sprawdzono przepływy pracy', + 'conversations.tools.cancelWorkflow.active': 'Anulowanie uruchomienia przepływu pracy', + 'conversations.tools.cancelWorkflow.done': 'Anulowano uruchomienie przepływu pracy', + 'conversations.tools.suggestWorkflows.active': 'Proponowanie przepływów pracy', + 'conversations.tools.suggestWorkflows.done': 'Zaproponowano przepływy pracy', + 'conversations.tools.checkSettings.active': 'Sprawdzanie ustawień', + 'conversations.tools.checkSettings.done': 'Sprawdzono ustawienia', + 'conversations.tools.checkSecurity.active': 'Sprawdzanie zabezpieczeń', + 'conversations.tools.checkSecurity.done': 'Sprawdzono zabezpieczenia', + 'conversations.tools.runDiagnostics.active': 'Uruchamianie diagnostyki', + 'conversations.tools.runDiagnostics.done': 'Uruchomiono diagnostykę', + 'conversations.tools.checkUsageCosts.active': 'Sprawdzanie kosztów użycia', + 'conversations.tools.checkUsageCosts.done': 'Sprawdzono koszty użycia', + 'conversations.tools.manageService.active': 'Zarządzanie usługą w tle', + 'conversations.tools.manageService.done': 'Zarządzono usługą w tle', + 'conversations.tools.readPersona.active': 'Czytanie persony', + 'conversations.tools.readPersona.done': 'Odczytano personę', + 'conversations.tools.updatePersona.active': 'Aktualizowanie persony', + 'conversations.tools.updatePersona.done': 'Zaktualizowano personę', + 'conversations.tools.setUpWorkspace.active': 'Konfigurowanie obszaru roboczego', + 'conversations.tools.setUpWorkspace.done': 'Skonfigurowano obszar roboczy', + 'conversations.tools.checkArtifacts.active': 'Sprawdzanie artefaktów', + 'conversations.tools.checkArtifacts.done': 'Sprawdzono artefakty', + 'conversations.tools.deleteArtifact.active': 'Usuwanie artefaktu', + 'conversations.tools.deleteArtifact.done': 'Usunięto artefakt', 'conversations.subagent.noOutput': 'Brak zwróconych danych wyjściowych', 'conversations.subagent.close': 'Zamknij', 'conversations.subagent.cancel': 'Anuluj zadanie', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index dc6668e06c7..059b591edc9 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -3370,6 +3370,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Ainda sem resultado', 'conversations.subagent.input': 'Entrada', 'conversations.subagent.output': 'Saída', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} etapa', + 'conversations.tools.steps.other': '{count} etapas', + 'conversations.tools.working': 'Trabalhando', + 'conversations.tools.noOutput': 'Sem saída', + 'conversations.tools.delegatedTo': 'Delegado a {agent}', + 'conversations.tools.openInBrowser': 'Abrir no navegador', + 'conversations.tools.status.running': 'em execução', + 'conversations.tools.status.done': 'concluído', + 'conversations.tools.status.failed': 'falhou', + 'conversations.tools.status.cancelled': 'cancelado', + 'conversations.tools.status.awaiting': 'aguardando resposta', + 'conversations.tools.search.searching': 'Pesquisando', + 'conversations.tools.search.none': 'Nenhum resultado', + 'conversations.tools.search.found.one': '{count} resultado encontrado', + 'conversations.tools.search.found.other': '{count} resultados encontrados', + 'conversations.tools.search.via': 'por meio de {provider}', + 'conversations.tools.readFile.active': 'Lendo arquivo', + 'conversations.tools.readFile.done': 'Arquivo lido', + 'conversations.tools.writeFile.active': 'Escrevendo arquivo', + 'conversations.tools.writeFile.done': 'Arquivo escrito', + 'conversations.tools.editFile.active': 'Editando arquivo', + 'conversations.tools.editFile.done': 'Arquivo editado', + 'conversations.tools.applyEdits.active': 'Aplicando alterações', + 'conversations.tools.applyEdits.done': 'Alterações aplicadas', + 'conversations.tools.searchCode.active': 'Pesquisando no código', + 'conversations.tools.searchCode.done': 'Código pesquisado', + 'conversations.tools.findFiles.active': 'Procurando arquivos', + 'conversations.tools.findFiles.done': 'Arquivos encontrados', + 'conversations.tools.listFolder.active': 'Listando pasta', + 'conversations.tools.listFolder.done': 'Pasta listada', + 'conversations.tools.exportCsv.active': 'Exportando CSV', + 'conversations.tools.exportCsv.done': 'CSV exportado', + 'conversations.tools.updateMemoryNotes.active': 'Atualizando notas de memória', + 'conversations.tools.updateMemoryNotes.done': 'Notas de memória atualizadas', + 'conversations.tools.runGit.active': 'Executando git', + 'conversations.tools.runGit.done': 'git executado', + 'conversations.tools.readChanges.active': 'Lendo alterações', + 'conversations.tools.readChanges.done': 'Alterações lidas', + 'conversations.tools.runLinter.active': 'Executando linter', + 'conversations.tools.runLinter.done': 'Linter executado', + 'conversations.tools.runTests.active': 'Executando testes', + 'conversations.tools.runTests.done': 'Testes executados', + 'conversations.tools.analyzeCode.active': 'Analisando código', + 'conversations.tools.analyzeCode.done': 'Código analisado', + 'conversations.tools.insertRecord.active': 'Inserindo registro', + 'conversations.tools.insertRecord.done': 'Registro inserido', + 'conversations.tools.runCommand.active': 'Executando comando', + 'conversations.tools.runCommand.done': 'Comando executado', + 'conversations.tools.runCode.active': 'Executando código', + 'conversations.tools.runCode.done': 'Código executado', + 'conversations.tools.runPackageManager.active': 'Executando npm', + 'conversations.tools.runPackageManager.done': 'npm executado', + 'conversations.tools.checkInstalledTools.active': 'Verificando ferramentas instaladas', + 'conversations.tools.checkInstalledTools.done': 'Ferramentas instaladas verificadas', + 'conversations.tools.installTool.active': 'Instalando ferramenta', + 'conversations.tools.installTool.done': 'Ferramenta instalada', + 'conversations.tools.checkTime.active': 'Verificando a hora', + 'conversations.tools.checkTime.done': 'Hora verificada', + 'conversations.tools.resolveDate.active': 'Calculando a data', + 'conversations.tools.resolveDate.done': 'Data calculada', + 'conversations.tools.retrieveOutput.active': 'Recuperando a saída completa', + 'conversations.tools.retrieveOutput.done': 'Saída completa recuperada', + 'conversations.tools.reviewWorkspace.active': 'Revisando o espaço de trabalho', + 'conversations.tools.reviewWorkspace.done': 'Espaço de trabalho revisado', + 'conversations.tools.configureProxy.active': 'Configurando proxy', + 'conversations.tools.configureProxy.done': 'Proxy configurado', + 'conversations.tools.checkUpdates.active': 'Procurando atualizações', + 'conversations.tools.checkUpdates.done': 'Atualizações verificadas', + 'conversations.tools.installUpdate.active': 'Instalando atualização', + 'conversations.tools.installUpdate.done': 'Atualização instalada', + 'conversations.tools.sendNotification.active': 'Enviando notificação', + 'conversations.tools.sendNotification.done': 'Notificação enviada', + 'conversations.tools.reviewToolUsage.active': 'Revisando o uso de ferramentas', + 'conversations.tools.reviewToolUsage.done': 'Uso de ferramentas revisado', + 'conversations.tools.typeKeys.active': 'Digitando', + 'conversations.tools.typeKeys.done': 'Texto digitado', + 'conversations.tools.click.active': 'Clicando', + 'conversations.tools.click.done': 'Clique feito', + 'conversations.tools.searchWeb.active': 'Pesquisando na web', + 'conversations.tools.searchWeb.done': 'Pesquisa na web concluída', + 'conversations.tools.searchNews.active': 'Pesquisando notícias', + 'conversations.tools.searchNews.done': 'Notícias pesquisadas', + 'conversations.tools.searchImages.active': 'Pesquisando imagens', + 'conversations.tools.searchImages.done': 'Imagens pesquisadas', + 'conversations.tools.searchVideos.active': 'Pesquisando vídeos', + 'conversations.tools.searchVideos.done': 'Vídeos pesquisados', + 'conversations.tools.findSimilarPages.active': 'Procurando páginas semelhantes', + 'conversations.tools.findSimilarPages.done': 'Páginas semelhantes encontradas', + 'conversations.tools.readPages.active': 'Lendo páginas', + 'conversations.tools.readPages.done': 'Páginas lidas', + 'conversations.tools.readWebpage.active': 'Lendo página da web', + 'conversations.tools.readWebpage.done': 'Página da web lida', + 'conversations.tools.research.active': 'Pesquisando a fundo', + 'conversations.tools.research.done': 'Pesquisa aprofundada concluída', + 'conversations.tools.enrichData.active': 'Enriquecendo dados', + 'conversations.tools.enrichData.done': 'Dados enriquecidos', + 'conversations.tools.buildDataset.active': 'Criando conjunto de dados', + 'conversations.tools.buildDataset.done': 'Conjunto de dados criado', + 'conversations.tools.askTheWeb.active': 'Consultando a web', + 'conversations.tools.askTheWeb.done': 'Web consultada', + 'conversations.tools.browseForYou.active': 'Navegando por você', + 'conversations.tools.browseForYou.done': 'Navegação feita por você', + 'conversations.tools.callApi.active': 'Chamando a API', + 'conversations.tools.callApi.done': 'API chamada', + 'conversations.tools.downloadFile.active': 'Baixando arquivo', + 'conversations.tools.downloadFile.done': 'Arquivo baixado', + 'conversations.tools.makePaidRequest.active': 'Fazendo solicitação paga', + 'conversations.tools.makePaidRequest.done': 'Solicitação paga feita', + 'conversations.tools.searchDocs.active': 'Pesquisando na documentação', + 'conversations.tools.searchDocs.done': 'Documentação pesquisada', + 'conversations.tools.readDocs.active': 'Lendo a documentação', + 'conversations.tools.readDocs.done': 'Documentação lida', + 'conversations.tools.useBrowser.active': 'Usando o navegador', + 'conversations.tools.useBrowser.done': 'Navegador usado', + 'conversations.tools.openPage.active': 'Abrindo página', + 'conversations.tools.openPage.done': 'Página aberta', + 'conversations.tools.navigate.active': 'Navegando', + 'conversations.tools.navigate.done': 'Navegação concluída', + 'conversations.tools.takeScreenshot.active': 'Fazendo captura de tela', + 'conversations.tools.takeScreenshot.done': 'Captura de tela feita', + 'conversations.tools.scrollPage.active': 'Rolando', + 'conversations.tools.scrollPage.done': 'Rolagem concluída', + 'conversations.tools.readPage.active': 'Lendo página', + 'conversations.tools.readPage.done': 'Página lida', + 'conversations.tools.analyzeImage.active': 'Analisando imagem', + 'conversations.tools.analyzeImage.done': 'Imagem analisada', + 'conversations.tools.generateImage.active': 'Gerando imagem', + 'conversations.tools.generateImage.done': 'Imagem gerada', + 'conversations.tools.generateVideo.active': 'Gerando vídeo', + 'conversations.tools.generateVideo.done': 'Vídeo gerado', + 'conversations.tools.checkMediaModels.active': 'Verificando modelos de mídia', + 'conversations.tools.checkMediaModels.done': 'Modelos de mídia verificados', + 'conversations.tools.createDocument.active': 'Criando documento', + 'conversations.tools.createDocument.done': 'Documento criado', + 'conversations.tools.createPresentation.active': 'Criando apresentação', + 'conversations.tools.createPresentation.done': 'Apresentação criada', + 'conversations.tools.generatePodcast.active': 'Gerando podcast', + 'conversations.tools.generatePodcast.done': 'Podcast gerado', + 'conversations.tools.emailPodcast.active': 'Enviando podcast por e-mail', + 'conversations.tools.emailPodcast.done': 'Podcast enviado por e-mail', + 'conversations.tools.createAndEmailPodcast.active': 'Criando e enviando podcast por e-mail', + 'conversations.tools.createAndEmailPodcast.done': 'Podcast criado e enviado por e-mail', + 'conversations.tools.recallMemories.active': 'Relembrando memórias', + 'conversations.tools.recallMemories.done': 'Memórias relembradas', + 'conversations.tools.saveToMemory.active': 'Salvando na memória', + 'conversations.tools.saveToMemory.done': 'Salvo na memória', + 'conversations.tools.forgetMemory.active': 'Esquecendo memória', + 'conversations.tools.forgetMemory.done': 'Memória esquecida', + 'conversations.tools.searchMemory.active': 'Pesquisando na memória', + 'conversations.tools.searchMemory.done': 'Memória pesquisada', + 'conversations.tools.inspectMemory.active': 'Inspecionando a memória', + 'conversations.tools.inspectMemory.done': 'Memória inspecionada', + 'conversations.tools.exploreMemory.active': 'Explorando a memória', + 'conversations.tools.exploreMemory.done': 'Memória explorada', + 'conversations.tools.saveDocumentToMemory.active': 'Salvando documento na memória', + 'conversations.tools.saveDocumentToMemory.done': 'Documento salvo na memória', + 'conversations.tools.updateGoals.active': 'Atualizando metas', + 'conversations.tools.updateGoals.done': 'Metas atualizadas', + 'conversations.tools.reviewGoals.active': 'Revisando metas', + 'conversations.tools.reviewGoals.done': 'Metas revisadas', + 'conversations.tools.savePreference.active': 'Salvando preferência', + 'conversations.tools.savePreference.done': 'Preferência salva', + 'conversations.tools.reviewLearnings.active': 'Revisando o que aprendi', + 'conversations.tools.reviewLearnings.done': 'Aprendizados revisados', + 'conversations.tools.updateLearnings.active': 'Atualizando o que aprendi', + 'conversations.tools.updateLearnings.done': 'Aprendizados atualizados', + 'conversations.tools.delegateTask.active': 'Delegando tarefa', + 'conversations.tools.delegateTask.done': 'Tarefa delegada', + 'conversations.tools.runAgentsInParallel.active': 'Executando agentes em paralelo', + 'conversations.tools.runAgentsInParallel.done': 'Agentes executados em paralelo', + 'conversations.tools.messageAgent.active': 'Enviando mensagem ao agente', + 'conversations.tools.messageAgent.done': 'Mensagem enviada ao agente', + 'conversations.tools.waitForAgent.active': 'Aguardando o agente', + 'conversations.tools.waitForAgent.done': 'Espera pelo agente concluída', + 'conversations.tools.wait.active': 'Aguardando', + 'conversations.tools.wait.done': 'Espera concluída', + 'conversations.tools.closeAgent.active': 'Fechando agente', + 'conversations.tools.closeAgent.done': 'Agente fechado', + 'conversations.tools.checkAgents.active': 'Verificando agentes', + 'conversations.tools.checkAgents.done': 'Agentes verificados', + 'conversations.tools.askQuestion.active': 'Fazendo uma pergunta a você', + 'conversations.tools.askQuestion.done': 'Pergunta feita a você', + 'conversations.tools.prepareContext.active': 'Preparando contexto', + 'conversations.tools.prepareContext.done': 'Contexto preparado', + 'conversations.tools.extractDetails.active': 'Extraindo detalhes', + 'conversations.tools.extractDetails.done': 'Detalhes extraídos', + 'conversations.tools.planNextSteps.active': 'Planejando próximos passos', + 'conversations.tools.planNextSteps.done': 'Próximos passos planejados', + 'conversations.tools.reviewWork.active': 'Revisando o trabalho', + 'conversations.tools.reviewWork.done': 'Trabalho revisado', + 'conversations.tools.scoutContext.active': 'Explorando o contexto', + 'conversations.tools.scoutContext.done': 'Contexto explorado', + 'conversations.tools.useTools.active': 'Usando ferramentas', + 'conversations.tools.useTools.done': 'Ferramentas usadas', + 'conversations.tools.checkConnectedApp.active': 'Verificando seu app conectado', + 'conversations.tools.checkConnectedApp.done': 'App conectado verificado', + 'conversations.tools.updateTodos.active': 'Atualizando lista de tarefas', + 'conversations.tools.updateTodos.done': 'Lista de tarefas atualizada', + 'conversations.tools.requestPlanReview.active': 'Solicitando revisão do plano', + 'conversations.tools.requestPlanReview.done': 'Revisão do plano solicitada', + 'conversations.tools.finishPlan.active': 'Finalizando o plano', + 'conversations.tools.finishPlan.done': 'Plano finalizado', + 'conversations.tools.setGoal.active': 'Definindo meta', + 'conversations.tools.setGoal.done': 'Meta definida', + 'conversations.tools.checkGoal.active': 'Verificando meta', + 'conversations.tools.checkGoal.done': 'Meta verificada', + 'conversations.tools.completeGoal.active': 'Concluindo meta', + 'conversations.tools.completeGoal.done': 'Meta concluída', + 'conversations.tools.scheduleTask.active': 'Agendando tarefa', + 'conversations.tools.scheduleTask.done': 'Tarefa agendada', + 'conversations.tools.checkSchedules.active': 'Verificando agendamentos', + 'conversations.tools.checkSchedules.done': 'Agendamentos verificados', + 'conversations.tools.updateSchedule.active': 'Atualizando tarefa agendada', + 'conversations.tools.updateSchedule.done': 'Tarefa agendada atualizada', + 'conversations.tools.removeSchedule.active': 'Removendo tarefa agendada', + 'conversations.tools.removeSchedule.done': 'Tarefa agendada removida', + 'conversations.tools.runScheduledTask.active': 'Executando tarefa agendada', + 'conversations.tools.runScheduledTask.done': 'Tarefa agendada executada', + 'conversations.tools.checkRunHistory.active': 'Verificando histórico de execuções', + 'conversations.tools.checkRunHistory.done': 'Histórico de execuções verificado', + 'conversations.tools.useApp.active': 'Usando {app}', + 'conversations.tools.useApp.done': '{app} usado', + 'conversations.tools.checkAvailableApps.active': 'Verificando apps disponíveis', + 'conversations.tools.checkAvailableApps.done': 'Apps disponíveis verificados', + 'conversations.tools.checkConnections.active': 'Verificando suas conexões', + 'conversations.tools.checkConnections.done': 'Conexões verificadas', + 'conversations.tools.connectApp.active': 'Conectando app', + 'conversations.tools.connectApp.done': 'App conectado', + 'conversations.tools.authorizeApp.active': 'Autorizando app', + 'conversations.tools.authorizeApp.done': 'App autorizado', + 'conversations.tools.findAppActions.active': 'Procurando ações do app', + 'conversations.tools.findAppActions.done': 'Ações do app encontradas', + 'conversations.tools.runAppAction.active': 'Executando ação do app', + 'conversations.tools.runAppAction.done': 'Ação do app executada', + 'conversations.tools.findTools.active': 'Procurando ferramentas', + 'conversations.tools.findTools.done': 'Ferramentas encontradas', + 'conversations.tools.useTool.active': 'Usando {tool}', + 'conversations.tools.useTool.done': '{tool} usado', + 'conversations.tools.unsubscribe.active': 'Cancelando inscrição', + 'conversations.tools.unsubscribe.done': 'Inscrição cancelada', + 'conversations.tools.searchPlaces.active': 'Pesquisando lugares', + 'conversations.tools.searchPlaces.done': 'Lugares pesquisados', + 'conversations.tools.lookUpPlace.active': 'Consultando local', + 'conversations.tools.lookUpPlace.done': 'Local consultado', + 'conversations.tools.checkMarkets.active': 'Consultando mercados', + 'conversations.tools.checkMarkets.done': 'Mercados consultados', + 'conversations.tools.placeCall.active': 'Fazendo ligação', + 'conversations.tools.placeCall.done': 'Ligação feita', + 'conversations.tools.checkTaskSources.active': 'Verificando fontes de tarefas', + 'conversations.tools.checkTaskSources.done': 'Fontes de tarefas verificadas', + 'conversations.tools.updateTaskSources.active': 'Atualizando fontes de tarefas', + 'conversations.tools.updateTaskSources.done': 'Fontes de tarefas atualizadas', + 'conversations.tools.fetchTasks.active': 'Buscando tarefas', + 'conversations.tools.fetchTasks.done': 'Tarefas obtidas', + 'conversations.tools.checkMcpServers.active': 'Verificando servidores MCP', + 'conversations.tools.checkMcpServers.done': 'Servidores MCP verificados', + 'conversations.tools.checkMcpTools.active': 'Verificando ferramentas MCP', + 'conversations.tools.checkMcpTools.done': 'Ferramentas MCP verificadas', + 'conversations.tools.callMcpTool.active': 'Chamando {tool}', + 'conversations.tools.callMcpTool.done': '{tool} chamado', + 'conversations.tools.searchMcpServers.active': 'Pesquisando servidores MCP', + 'conversations.tools.searchMcpServers.done': 'Servidores MCP pesquisados', + 'conversations.tools.connectMcpServer.active': 'Conectando servidor MCP', + 'conversations.tools.connectMcpServer.done': 'Servidor MCP conectado', + 'conversations.tools.disconnectMcpServer.active': 'Desconectando servidor MCP', + 'conversations.tools.disconnectMcpServer.done': 'Servidor MCP desconectado', + 'conversations.tools.removeMcpServer.active': 'Removendo servidor MCP', + 'conversations.tools.removeMcpServer.done': 'Servidor MCP removido', + 'conversations.tools.uploadFile.active': 'Enviando arquivo', + 'conversations.tools.uploadFile.done': 'Arquivo enviado', + 'conversations.tools.listStoredFiles.active': 'Listando arquivos armazenados', + 'conversations.tools.listStoredFiles.done': 'Arquivos armazenados listados', + 'conversations.tools.createShareLink.active': 'Criando link de compartilhamento', + 'conversations.tools.createShareLink.done': 'Link de compartilhamento criado', + 'conversations.tools.deleteFile.active': 'Excluindo arquivo', + 'conversations.tools.deleteFile.done': 'Arquivo excluído', + 'conversations.tools.updateFileAccess.active': 'Atualizando acesso ao arquivo', + 'conversations.tools.updateFileAccess.done': 'Acesso ao arquivo atualizado', + 'conversations.tools.deploySite.active': 'Implantando site', + 'conversations.tools.deploySite.done': 'Site implantado', + 'conversations.tools.checkHosting.active': 'Verificando hospedagem', + 'conversations.tools.checkHosting.done': 'Hospedagem verificada', + 'conversations.tools.updateHosting.active': 'Atualizando hospedagem', + 'conversations.tools.updateHosting.done': 'Hospedagem atualizada', + 'conversations.tools.rollBackDeployment.active': 'Revertendo implantação', + 'conversations.tools.rollBackDeployment.done': 'Implantação revertida', + 'conversations.tools.checkWallet.active': 'Verificando carteira', + 'conversations.tools.checkWallet.done': 'Carteira verificada', + 'conversations.tools.prepareTransfer.active': 'Preparando transferência', + 'conversations.tools.prepareTransfer.done': 'Transferência preparada', + 'conversations.tools.checkTransaction.active': 'Verificando transação', + 'conversations.tools.checkTransaction.done': 'Transação verificada', + 'conversations.tools.getSwapQuote.active': 'Obtendo cotação de troca', + 'conversations.tools.getSwapQuote.done': 'Cotação de troca obtida', + 'conversations.tools.swapTokens.active': 'Trocando tokens', + 'conversations.tools.swapTokens.done': 'Tokens trocados', + 'conversations.tools.getBridgeQuote.active': 'Obtendo cotação de ponte', + 'conversations.tools.getBridgeQuote.done': 'Cotação de ponte obtida', + 'conversations.tools.bridgeTokens.active': 'Transferindo tokens por ponte', + 'conversations.tools.bridgeTokens.done': 'Tokens transferidos por ponte', + 'conversations.tools.callDapp.active': 'Chamando o contrato do app', + 'conversations.tools.callDapp.done': 'Contrato do app chamado', + 'conversations.tools.useSkill.active': 'Usando habilidade', + 'conversations.tools.useSkill.done': 'Habilidade usada', + 'conversations.tools.searchSkills.active': 'Pesquisando habilidades', + 'conversations.tools.searchSkills.done': 'Habilidades pesquisadas', + 'conversations.tools.checkSkills.active': 'Verificando habilidades', + 'conversations.tools.checkSkills.done': 'Habilidades verificadas', + 'conversations.tools.installSkill.active': 'Instalando habilidade', + 'conversations.tools.installSkill.done': 'Habilidade instalada', + 'conversations.tools.removeSkill.active': 'Removendo habilidade', + 'conversations.tools.removeSkill.done': 'Habilidade removida', + 'conversations.tools.createSkill.active': 'Criando habilidade', + 'conversations.tools.createSkill.done': 'Habilidade criada', + 'conversations.tools.runWorkflow.active': 'Executando fluxo de trabalho', + 'conversations.tools.runWorkflow.done': 'Fluxo de trabalho executado', + 'conversations.tools.waitForWorkflow.active': 'Aguardando o fluxo de trabalho', + 'conversations.tools.waitForWorkflow.done': 'Espera pelo fluxo de trabalho concluída', + 'conversations.tools.designWorkflow.active': 'Projetando fluxo de trabalho', + 'conversations.tools.designWorkflow.done': 'Fluxo de trabalho projetado', + 'conversations.tools.saveWorkflow.active': 'Salvando fluxo de trabalho', + 'conversations.tools.saveWorkflow.done': 'Fluxo de trabalho salvo', + 'conversations.tools.validateWorkflow.active': 'Validando fluxo de trabalho', + 'conversations.tools.validateWorkflow.done': 'Fluxo de trabalho validado', + 'conversations.tools.testWorkflow.active': 'Testando fluxo de trabalho', + 'conversations.tools.testWorkflow.done': 'Fluxo de trabalho testado', + 'conversations.tools.checkWorkflows.active': 'Verificando fluxos de trabalho', + 'conversations.tools.checkWorkflows.done': 'Fluxos de trabalho verificados', + 'conversations.tools.cancelWorkflow.active': 'Cancelando execução do fluxo de trabalho', + 'conversations.tools.cancelWorkflow.done': 'Execução do fluxo de trabalho cancelada', + 'conversations.tools.suggestWorkflows.active': 'Sugerindo fluxos de trabalho', + 'conversations.tools.suggestWorkflows.done': 'Fluxos de trabalho sugeridos', + 'conversations.tools.checkSettings.active': 'Verificando configurações', + 'conversations.tools.checkSettings.done': 'Configurações verificadas', + 'conversations.tools.checkSecurity.active': 'Verificando segurança', + 'conversations.tools.checkSecurity.done': 'Segurança verificada', + 'conversations.tools.runDiagnostics.active': 'Executando diagnósticos', + 'conversations.tools.runDiagnostics.done': 'Diagnósticos executados', + 'conversations.tools.checkUsageCosts.active': 'Verificando custos de uso', + 'conversations.tools.checkUsageCosts.done': 'Custos de uso verificados', + 'conversations.tools.manageService.active': 'Gerenciando serviço em segundo plano', + 'conversations.tools.manageService.done': 'Serviço em segundo plano gerenciado', + 'conversations.tools.readPersona.active': 'Lendo persona', + 'conversations.tools.readPersona.done': 'Persona lida', + 'conversations.tools.updatePersona.active': 'Atualizando persona', + 'conversations.tools.updatePersona.done': 'Persona atualizada', + 'conversations.tools.setUpWorkspace.active': 'Configurando espaço de trabalho', + 'conversations.tools.setUpWorkspace.done': 'Espaço de trabalho configurado', + 'conversations.tools.checkArtifacts.active': 'Verificando artefatos', + 'conversations.tools.checkArtifacts.done': 'Artefatos verificados', + 'conversations.tools.deleteArtifact.active': 'Excluindo artefato', + 'conversations.tools.deleteArtifact.done': 'Artefato excluído', 'conversations.subagent.noOutput': 'Nenhuma saída retornada', 'conversations.subagent.close': 'Fechar', 'conversations.subagent.cancel': 'Cancelar tarefa', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 4d95c0dfa2e..f2a9d74520a 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -3345,6 +3345,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': 'Пока нет результата', 'conversations.subagent.input': 'Ввод', 'conversations.subagent.output': 'Вывод', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} шаг', + 'conversations.tools.steps.other': 'Шагов: {count}', + 'conversations.tools.working': 'Выполняется', + 'conversations.tools.noOutput': 'Нет вывода', + 'conversations.tools.delegatedTo': 'Передано агенту {agent}', + 'conversations.tools.openInBrowser': 'Открыть в браузере', + 'conversations.tools.status.running': 'выполняется', + 'conversations.tools.status.done': 'готово', + 'conversations.tools.status.failed': 'ошибка', + 'conversations.tools.status.cancelled': 'отменено', + 'conversations.tools.status.awaiting': 'ожидает ввода', + 'conversations.tools.search.searching': 'Поиск', + 'conversations.tools.search.none': 'Нет результатов', + 'conversations.tools.search.found.one': 'Найден {count} результат', + 'conversations.tools.search.found.other': 'Найдено результатов: {count}', + 'conversations.tools.search.via': 'через {provider}', + 'conversations.tools.readFile.active': 'Чтение файла', + 'conversations.tools.readFile.done': 'Файл прочитан', + 'conversations.tools.writeFile.active': 'Запись файла', + 'conversations.tools.writeFile.done': 'Файл записан', + 'conversations.tools.editFile.active': 'Редактирование файла', + 'conversations.tools.editFile.done': 'Файл отредактирован', + 'conversations.tools.applyEdits.active': 'Применение правок', + 'conversations.tools.applyEdits.done': 'Правки применены', + 'conversations.tools.searchCode.active': 'Поиск по коду', + 'conversations.tools.searchCode.done': 'Поиск по коду выполнен', + 'conversations.tools.findFiles.active': 'Поиск файлов', + 'conversations.tools.findFiles.done': 'Файлы найдены', + 'conversations.tools.listFolder.active': 'Просмотр папки', + 'conversations.tools.listFolder.done': 'Папка просмотрена', + 'conversations.tools.exportCsv.active': 'Экспорт CSV', + 'conversations.tools.exportCsv.done': 'CSV экспортирован', + 'conversations.tools.updateMemoryNotes.active': 'Обновление заметок памяти', + 'conversations.tools.updateMemoryNotes.done': 'Заметки памяти обновлены', + 'conversations.tools.runGit.active': 'Запуск git', + 'conversations.tools.runGit.done': 'git выполнен', + 'conversations.tools.readChanges.active': 'Чтение изменений', + 'conversations.tools.readChanges.done': 'Изменения прочитаны', + 'conversations.tools.runLinter.active': 'Запуск линтера', + 'conversations.tools.runLinter.done': 'Линтер выполнен', + 'conversations.tools.runTests.active': 'Запуск тестов', + 'conversations.tools.runTests.done': 'Тесты выполнены', + 'conversations.tools.analyzeCode.active': 'Анализ кода', + 'conversations.tools.analyzeCode.done': 'Код проанализирован', + 'conversations.tools.insertRecord.active': 'Добавление записи', + 'conversations.tools.insertRecord.done': 'Запись добавлена', + 'conversations.tools.runCommand.active': 'Выполнение команды', + 'conversations.tools.runCommand.done': 'Команда выполнена', + 'conversations.tools.runCode.active': 'Выполнение кода', + 'conversations.tools.runCode.done': 'Код выполнен', + 'conversations.tools.runPackageManager.active': 'Запуск npm', + 'conversations.tools.runPackageManager.done': 'npm выполнен', + 'conversations.tools.checkInstalledTools.active': 'Проверка установленных инструментов', + 'conversations.tools.checkInstalledTools.done': 'Установленные инструменты проверены', + 'conversations.tools.installTool.active': 'Установка инструмента', + 'conversations.tools.installTool.done': 'Инструмент установлен', + 'conversations.tools.checkTime.active': 'Проверка времени', + 'conversations.tools.checkTime.done': 'Время проверено', + 'conversations.tools.resolveDate.active': 'Определение даты', + 'conversations.tools.resolveDate.done': 'Дата определена', + 'conversations.tools.retrieveOutput.active': 'Получение полного вывода', + 'conversations.tools.retrieveOutput.done': 'Полный вывод получен', + 'conversations.tools.reviewWorkspace.active': 'Обзор рабочей области', + 'conversations.tools.reviewWorkspace.done': 'Рабочая область просмотрена', + 'conversations.tools.configureProxy.active': 'Настройка прокси', + 'conversations.tools.configureProxy.done': 'Прокси настроен', + 'conversations.tools.checkUpdates.active': 'Проверка обновлений', + 'conversations.tools.checkUpdates.done': 'Обновления проверены', + 'conversations.tools.installUpdate.active': 'Установка обновления', + 'conversations.tools.installUpdate.done': 'Обновление установлено', + 'conversations.tools.sendNotification.active': 'Отправка уведомления', + 'conversations.tools.sendNotification.done': 'Уведомление отправлено', + 'conversations.tools.reviewToolUsage.active': 'Анализ использования инструментов', + 'conversations.tools.reviewToolUsage.done': 'Использование инструментов проанализировано', + 'conversations.tools.typeKeys.active': 'Ввод текста', + 'conversations.tools.typeKeys.done': 'Текст введён', + 'conversations.tools.click.active': 'Нажатие', + 'conversations.tools.click.done': 'Нажато', + 'conversations.tools.searchWeb.active': 'Поиск в интернете', + 'conversations.tools.searchWeb.done': 'Поиск в интернете выполнен', + 'conversations.tools.searchNews.active': 'Поиск новостей', + 'conversations.tools.searchNews.done': 'Новости найдены', + 'conversations.tools.searchImages.active': 'Поиск изображений', + 'conversations.tools.searchImages.done': 'Изображения найдены', + 'conversations.tools.searchVideos.active': 'Поиск видео', + 'conversations.tools.searchVideos.done': 'Видео найдены', + 'conversations.tools.findSimilarPages.active': 'Поиск похожих страниц', + 'conversations.tools.findSimilarPages.done': 'Похожие страницы найдены', + 'conversations.tools.readPages.active': 'Чтение страниц', + 'conversations.tools.readPages.done': 'Страницы прочитаны', + 'conversations.tools.readWebpage.active': 'Чтение веб-страницы', + 'conversations.tools.readWebpage.done': 'Веб-страница прочитана', + 'conversations.tools.research.active': 'Исследование', + 'conversations.tools.research.done': 'Исследование завершено', + 'conversations.tools.enrichData.active': 'Обогащение данных', + 'conversations.tools.enrichData.done': 'Данные обогащены', + 'conversations.tools.buildDataset.active': 'Создание набора данных', + 'conversations.tools.buildDataset.done': 'Набор данных создан', + 'conversations.tools.askTheWeb.active': 'Запрос к интернету', + 'conversations.tools.askTheWeb.done': 'Запрос к интернету выполнен', + 'conversations.tools.browseForYou.active': 'Просмотр сайтов за вас', + 'conversations.tools.browseForYou.done': 'Сайты просмотрены за вас', + 'conversations.tools.callApi.active': 'Вызов API', + 'conversations.tools.callApi.done': 'API вызван', + 'conversations.tools.downloadFile.active': 'Загрузка файла', + 'conversations.tools.downloadFile.done': 'Файл загружен', + 'conversations.tools.makePaidRequest.active': 'Платный запрос', + 'conversations.tools.makePaidRequest.done': 'Платный запрос выполнен', + 'conversations.tools.searchDocs.active': 'Поиск по документации', + 'conversations.tools.searchDocs.done': 'Поиск по документации выполнен', + 'conversations.tools.readDocs.active': 'Чтение документации', + 'conversations.tools.readDocs.done': 'Документация прочитана', + 'conversations.tools.useBrowser.active': 'Работа с браузером', + 'conversations.tools.useBrowser.done': 'Браузер использован', + 'conversations.tools.openPage.active': 'Открытие страницы', + 'conversations.tools.openPage.done': 'Страница открыта', + 'conversations.tools.navigate.active': 'Переход', + 'conversations.tools.navigate.done': 'Переход выполнен', + 'conversations.tools.takeScreenshot.active': 'Создание снимка экрана', + 'conversations.tools.takeScreenshot.done': 'Снимок экрана создан', + 'conversations.tools.scrollPage.active': 'Прокрутка', + 'conversations.tools.scrollPage.done': 'Прокручено', + 'conversations.tools.readPage.active': 'Чтение страницы', + 'conversations.tools.readPage.done': 'Страница прочитана', + 'conversations.tools.analyzeImage.active': 'Анализ изображения', + 'conversations.tools.analyzeImage.done': 'Изображение проанализировано', + 'conversations.tools.generateImage.active': 'Создание изображения', + 'conversations.tools.generateImage.done': 'Изображение создано', + 'conversations.tools.generateVideo.active': 'Создание видео', + 'conversations.tools.generateVideo.done': 'Видео создано', + 'conversations.tools.checkMediaModels.active': 'Проверка медиамоделей', + 'conversations.tools.checkMediaModels.done': 'Медиамодели проверены', + 'conversations.tools.createDocument.active': 'Создание документа', + 'conversations.tools.createDocument.done': 'Документ создан', + 'conversations.tools.createPresentation.active': 'Создание презентации', + 'conversations.tools.createPresentation.done': 'Презентация создана', + 'conversations.tools.generatePodcast.active': 'Создание подкаста', + 'conversations.tools.generatePodcast.done': 'Подкаст создан', + 'conversations.tools.emailPodcast.active': 'Отправка подкаста по почте', + 'conversations.tools.emailPodcast.done': 'Подкаст отправлен по почте', + 'conversations.tools.createAndEmailPodcast.active': 'Создание и отправка подкаста по почте', + 'conversations.tools.createAndEmailPodcast.done': 'Подкаст создан и отправлен по почте', + 'conversations.tools.recallMemories.active': 'Извлечение воспоминаний', + 'conversations.tools.recallMemories.done': 'Воспоминания извлечены', + 'conversations.tools.saveToMemory.active': 'Сохранение в память', + 'conversations.tools.saveToMemory.done': 'Сохранено в память', + 'conversations.tools.forgetMemory.active': 'Удаление из памяти', + 'conversations.tools.forgetMemory.done': 'Удалено из памяти', + 'conversations.tools.searchMemory.active': 'Поиск в памяти', + 'conversations.tools.searchMemory.done': 'Поиск в памяти выполнен', + 'conversations.tools.inspectMemory.active': 'Проверка памяти', + 'conversations.tools.inspectMemory.done': 'Память проверена', + 'conversations.tools.exploreMemory.active': 'Изучение памяти', + 'conversations.tools.exploreMemory.done': 'Память изучена', + 'conversations.tools.saveDocumentToMemory.active': 'Сохранение документа в память', + 'conversations.tools.saveDocumentToMemory.done': 'Документ сохранён в память', + 'conversations.tools.updateGoals.active': 'Обновление целей', + 'conversations.tools.updateGoals.done': 'Цели обновлены', + 'conversations.tools.reviewGoals.active': 'Просмотр целей', + 'conversations.tools.reviewGoals.done': 'Цели просмотрены', + 'conversations.tools.savePreference.active': 'Сохранение предпочтения', + 'conversations.tools.savePreference.done': 'Предпочтение сохранено', + 'conversations.tools.reviewLearnings.active': 'Просмотр изученного', + 'conversations.tools.reviewLearnings.done': 'Изученное просмотрено', + 'conversations.tools.updateLearnings.active': 'Обновление изученного', + 'conversations.tools.updateLearnings.done': 'Изученное обновлено', + 'conversations.tools.delegateTask.active': 'Передача задачи', + 'conversations.tools.delegateTask.done': 'Задача передана', + 'conversations.tools.runAgentsInParallel.active': 'Параллельный запуск агентов', + 'conversations.tools.runAgentsInParallel.done': 'Агенты запущены параллельно', + 'conversations.tools.messageAgent.active': 'Отправка сообщения агенту', + 'conversations.tools.messageAgent.done': 'Сообщение агенту отправлено', + 'conversations.tools.waitForAgent.active': 'Ожидание агента', + 'conversations.tools.waitForAgent.done': 'Агент ответил', + 'conversations.tools.wait.active': 'Ожидание', + 'conversations.tools.wait.done': 'Ожидание завершено', + 'conversations.tools.closeAgent.active': 'Закрытие агента', + 'conversations.tools.closeAgent.done': 'Агент закрыт', + 'conversations.tools.checkAgents.active': 'Проверка агентов', + 'conversations.tools.checkAgents.done': 'Агенты проверены', + 'conversations.tools.askQuestion.active': 'Вопрос к вам', + 'conversations.tools.askQuestion.done': 'Вопрос задан', + 'conversations.tools.prepareContext.active': 'Подготовка контекста', + 'conversations.tools.prepareContext.done': 'Контекст подготовлен', + 'conversations.tools.extractDetails.active': 'Извлечение деталей', + 'conversations.tools.extractDetails.done': 'Детали извлечены', + 'conversations.tools.planNextSteps.active': 'Планирование следующих шагов', + 'conversations.tools.planNextSteps.done': 'Следующие шаги спланированы', + 'conversations.tools.reviewWork.active': 'Проверка работы', + 'conversations.tools.reviewWork.done': 'Работа проверена', + 'conversations.tools.scoutContext.active': 'Сбор контекста', + 'conversations.tools.scoutContext.done': 'Контекст собран', + 'conversations.tools.useTools.active': 'Использование инструментов', + 'conversations.tools.useTools.done': 'Инструменты использованы', + 'conversations.tools.checkConnectedApp.active': 'Проверка подключённого приложения', + 'conversations.tools.checkConnectedApp.done': 'Подключённое приложение проверено', + 'conversations.tools.updateTodos.active': 'Обновление списка задач', + 'conversations.tools.updateTodos.done': 'Список задач обновлён', + 'conversations.tools.requestPlanReview.active': 'Запрос проверки плана', + 'conversations.tools.requestPlanReview.done': 'Проверка плана запрошена', + 'conversations.tools.finishPlan.active': 'Завершение плана', + 'conversations.tools.finishPlan.done': 'План завершён', + 'conversations.tools.setGoal.active': 'Установка цели', + 'conversations.tools.setGoal.done': 'Цель установлена', + 'conversations.tools.checkGoal.active': 'Проверка цели', + 'conversations.tools.checkGoal.done': 'Цель проверена', + 'conversations.tools.completeGoal.active': 'Выполнение цели', + 'conversations.tools.completeGoal.done': 'Цель выполнена', + 'conversations.tools.scheduleTask.active': 'Планирование задачи', + 'conversations.tools.scheduleTask.done': 'Задача запланирована', + 'conversations.tools.checkSchedules.active': 'Проверка расписаний', + 'conversations.tools.checkSchedules.done': 'Расписания проверены', + 'conversations.tools.updateSchedule.active': 'Обновление запланированной задачи', + 'conversations.tools.updateSchedule.done': 'Запланированная задача обновлена', + 'conversations.tools.removeSchedule.active': 'Удаление запланированной задачи', + 'conversations.tools.removeSchedule.done': 'Запланированная задача удалена', + 'conversations.tools.runScheduledTask.active': 'Запуск запланированной задачи', + 'conversations.tools.runScheduledTask.done': 'Запланированная задача выполнена', + 'conversations.tools.checkRunHistory.active': 'Проверка истории запусков', + 'conversations.tools.checkRunHistory.done': 'История запусков проверена', + 'conversations.tools.useApp.active': 'Использование {app}', + 'conversations.tools.useApp.done': 'Использовано: {app}', + 'conversations.tools.checkAvailableApps.active': 'Проверка доступных приложений', + 'conversations.tools.checkAvailableApps.done': 'Доступные приложения проверены', + 'conversations.tools.checkConnections.active': 'Проверка ваших подключений', + 'conversations.tools.checkConnections.done': 'Ваши подключения проверены', + 'conversations.tools.connectApp.active': 'Подключение приложения', + 'conversations.tools.connectApp.done': 'Приложение подключено', + 'conversations.tools.authorizeApp.active': 'Авторизация приложения', + 'conversations.tools.authorizeApp.done': 'Приложение авторизовано', + 'conversations.tools.findAppActions.active': 'Поиск действий приложения', + 'conversations.tools.findAppActions.done': 'Действия приложения найдены', + 'conversations.tools.runAppAction.active': 'Выполнение действия приложения', + 'conversations.tools.runAppAction.done': 'Действие приложения выполнено', + 'conversations.tools.findTools.active': 'Поиск инструментов', + 'conversations.tools.findTools.done': 'Инструменты найдены', + 'conversations.tools.useTool.active': 'Использование {tool}', + 'conversations.tools.useTool.done': 'Использовано: {tool}', + 'conversations.tools.unsubscribe.active': 'Отписка', + 'conversations.tools.unsubscribe.done': 'Отписка выполнена', + 'conversations.tools.searchPlaces.active': 'Поиск мест', + 'conversations.tools.searchPlaces.done': 'Места найдены', + 'conversations.tools.lookUpPlace.active': 'Поиск сведений о месте', + 'conversations.tools.lookUpPlace.done': 'Сведения о месте найдены', + 'conversations.tools.checkMarkets.active': 'Проверка рынков', + 'conversations.tools.checkMarkets.done': 'Рынки проверены', + 'conversations.tools.placeCall.active': 'Выполнение звонка', + 'conversations.tools.placeCall.done': 'Звонок выполнен', + 'conversations.tools.checkTaskSources.active': 'Проверка источников задач', + 'conversations.tools.checkTaskSources.done': 'Источники задач проверены', + 'conversations.tools.updateTaskSources.active': 'Обновление источников задач', + 'conversations.tools.updateTaskSources.done': 'Источники задач обновлены', + 'conversations.tools.fetchTasks.active': 'Получение задач', + 'conversations.tools.fetchTasks.done': 'Задачи получены', + 'conversations.tools.checkMcpServers.active': 'Проверка MCP-серверов', + 'conversations.tools.checkMcpServers.done': 'MCP-серверы проверены', + 'conversations.tools.checkMcpTools.active': 'Проверка инструментов MCP', + 'conversations.tools.checkMcpTools.done': 'Инструменты MCP проверены', + 'conversations.tools.callMcpTool.active': 'Вызов {tool}', + 'conversations.tools.callMcpTool.done': 'Вызвано: {tool}', + 'conversations.tools.searchMcpServers.active': 'Поиск MCP-серверов', + 'conversations.tools.searchMcpServers.done': 'Поиск MCP-серверов выполнен', + 'conversations.tools.connectMcpServer.active': 'Подключение MCP-сервера', + 'conversations.tools.connectMcpServer.done': 'MCP-сервер подключён', + 'conversations.tools.disconnectMcpServer.active': 'Отключение MCP-сервера', + 'conversations.tools.disconnectMcpServer.done': 'MCP-сервер отключён', + 'conversations.tools.removeMcpServer.active': 'Удаление MCP-сервера', + 'conversations.tools.removeMcpServer.done': 'MCP-сервер удалён', + 'conversations.tools.uploadFile.active': 'Отправка файла', + 'conversations.tools.uploadFile.done': 'Файл отправлен', + 'conversations.tools.listStoredFiles.active': 'Просмотр сохранённых файлов', + 'conversations.tools.listStoredFiles.done': 'Сохранённые файлы просмотрены', + 'conversations.tools.createShareLink.active': 'Создание ссылки для общего доступа', + 'conversations.tools.createShareLink.done': 'Ссылка для общего доступа создана', + 'conversations.tools.deleteFile.active': 'Удаление файла', + 'conversations.tools.deleteFile.done': 'Файл удалён', + 'conversations.tools.updateFileAccess.active': 'Обновление доступа к файлу', + 'conversations.tools.updateFileAccess.done': 'Доступ к файлу обновлён', + 'conversations.tools.deploySite.active': 'Развёртывание сайта', + 'conversations.tools.deploySite.done': 'Сайт развёрнут', + 'conversations.tools.checkHosting.active': 'Проверка хостинга', + 'conversations.tools.checkHosting.done': 'Хостинг проверен', + 'conversations.tools.updateHosting.active': 'Обновление хостинга', + 'conversations.tools.updateHosting.done': 'Хостинг обновлён', + 'conversations.tools.rollBackDeployment.active': 'Откат развёртывания', + 'conversations.tools.rollBackDeployment.done': 'Развёртывание откачено', + 'conversations.tools.checkWallet.active': 'Проверка кошелька', + 'conversations.tools.checkWallet.done': 'Кошелёк проверен', + 'conversations.tools.prepareTransfer.active': 'Подготовка перевода', + 'conversations.tools.prepareTransfer.done': 'Перевод подготовлен', + 'conversations.tools.checkTransaction.active': 'Проверка транзакции', + 'conversations.tools.checkTransaction.done': 'Транзакция проверена', + 'conversations.tools.getSwapQuote.active': 'Получение котировки обмена', + 'conversations.tools.getSwapQuote.done': 'Котировка обмена получена', + 'conversations.tools.swapTokens.active': 'Обмен токенов', + 'conversations.tools.swapTokens.done': 'Токены обменяны', + 'conversations.tools.getBridgeQuote.active': 'Получение котировки моста', + 'conversations.tools.getBridgeQuote.done': 'Котировка моста получена', + 'conversations.tools.bridgeTokens.active': 'Перевод токенов через мост', + 'conversations.tools.bridgeTokens.done': 'Токены переведены через мост', + 'conversations.tools.callDapp.active': 'Вызов контракта приложения', + 'conversations.tools.callDapp.done': 'Контракт приложения вызван', + 'conversations.tools.useSkill.active': 'Использование навыка', + 'conversations.tools.useSkill.done': 'Навык использован', + 'conversations.tools.searchSkills.active': 'Поиск навыков', + 'conversations.tools.searchSkills.done': 'Навыки найдены', + 'conversations.tools.checkSkills.active': 'Проверка навыков', + 'conversations.tools.checkSkills.done': 'Навыки проверены', + 'conversations.tools.installSkill.active': 'Установка навыка', + 'conversations.tools.installSkill.done': 'Навык установлен', + 'conversations.tools.removeSkill.active': 'Удаление навыка', + 'conversations.tools.removeSkill.done': 'Навык удалён', + 'conversations.tools.createSkill.active': 'Создание навыка', + 'conversations.tools.createSkill.done': 'Навык создан', + 'conversations.tools.runWorkflow.active': 'Запуск рабочего процесса', + 'conversations.tools.runWorkflow.done': 'Рабочий процесс выполнен', + 'conversations.tools.waitForWorkflow.active': 'Ожидание рабочего процесса', + 'conversations.tools.waitForWorkflow.done': 'Рабочий процесс завершён', + 'conversations.tools.designWorkflow.active': 'Проектирование рабочего процесса', + 'conversations.tools.designWorkflow.done': 'Рабочий процесс спроектирован', + 'conversations.tools.saveWorkflow.active': 'Сохранение рабочего процесса', + 'conversations.tools.saveWorkflow.done': 'Рабочий процесс сохранён', + 'conversations.tools.validateWorkflow.active': 'Проверка рабочего процесса', + 'conversations.tools.validateWorkflow.done': 'Рабочий процесс проверен', + 'conversations.tools.testWorkflow.active': 'Тестирование рабочего процесса', + 'conversations.tools.testWorkflow.done': 'Рабочий процесс протестирован', + 'conversations.tools.checkWorkflows.active': 'Проверка рабочих процессов', + 'conversations.tools.checkWorkflows.done': 'Рабочие процессы проверены', + 'conversations.tools.cancelWorkflow.active': 'Отмена запуска рабочего процесса', + 'conversations.tools.cancelWorkflow.done': 'Запуск рабочего процесса отменён', + 'conversations.tools.suggestWorkflows.active': 'Подбор рабочих процессов', + 'conversations.tools.suggestWorkflows.done': 'Рабочие процессы предложены', + 'conversations.tools.checkSettings.active': 'Проверка настроек', + 'conversations.tools.checkSettings.done': 'Настройки проверены', + 'conversations.tools.checkSecurity.active': 'Проверка безопасности', + 'conversations.tools.checkSecurity.done': 'Безопасность проверена', + 'conversations.tools.runDiagnostics.active': 'Запуск диагностики', + 'conversations.tools.runDiagnostics.done': 'Диагностика выполнена', + 'conversations.tools.checkUsageCosts.active': 'Проверка расходов', + 'conversations.tools.checkUsageCosts.done': 'Расходы проверены', + 'conversations.tools.manageService.active': 'Управление фоновой службой', + 'conversations.tools.manageService.done': 'Фоновая служба настроена', + 'conversations.tools.readPersona.active': 'Чтение персоны', + 'conversations.tools.readPersona.done': 'Персона прочитана', + 'conversations.tools.updatePersona.active': 'Обновление персоны', + 'conversations.tools.updatePersona.done': 'Персона обновлена', + 'conversations.tools.setUpWorkspace.active': 'Настройка рабочей области', + 'conversations.tools.setUpWorkspace.done': 'Рабочая область настроена', + 'conversations.tools.checkArtifacts.active': 'Проверка артефактов', + 'conversations.tools.checkArtifacts.done': 'Артефакты проверены', + 'conversations.tools.deleteArtifact.active': 'Удаление артефакта', + 'conversations.tools.deleteArtifact.done': 'Артефакт удалён', 'conversations.subagent.noOutput': 'Вывод отсутствует', 'conversations.subagent.close': 'Закрыть', 'conversations.subagent.cancel': 'Отменить задачу', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 7d626c29caa..7e7e00e67b2 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -3122,6 +3122,359 @@ const messages: TranslationMap = { 'conversations.subagent.noOutputYet': '暂无输出', 'conversations.subagent.input': '输入', 'conversations.subagent.output': '输出', + // Tool-call presentation (features/conversations/tools/toolPhrases.ts). + 'conversations.tools.steps.one': '{count} 个步骤', + 'conversations.tools.steps.other': '{count} 个步骤', + 'conversations.tools.working': '处理中', + 'conversations.tools.noOutput': '无输出', + 'conversations.tools.delegatedTo': '已委派给 {agent}', + 'conversations.tools.openInBrowser': '在浏览器中打开', + 'conversations.tools.status.running': '运行中', + 'conversations.tools.status.done': '已完成', + 'conversations.tools.status.failed': '失败', + 'conversations.tools.status.cancelled': '已取消', + 'conversations.tools.status.awaiting': '等待输入', + 'conversations.tools.search.searching': '正在搜索', + 'conversations.tools.search.none': '无结果', + 'conversations.tools.search.found.one': '找到 {count} 条结果', + 'conversations.tools.search.found.other': '找到 {count} 条结果', + 'conversations.tools.search.via': '通过 {provider}', + 'conversations.tools.readFile.active': '正在读取文件', + 'conversations.tools.readFile.done': '已读取文件', + 'conversations.tools.writeFile.active': '正在写入文件', + 'conversations.tools.writeFile.done': '已写入文件', + 'conversations.tools.editFile.active': '正在编辑文件', + 'conversations.tools.editFile.done': '已编辑文件', + 'conversations.tools.applyEdits.active': '正在应用编辑', + 'conversations.tools.applyEdits.done': '已应用编辑', + 'conversations.tools.searchCode.active': '正在搜索代码', + 'conversations.tools.searchCode.done': '已搜索代码', + 'conversations.tools.findFiles.active': '正在查找文件', + 'conversations.tools.findFiles.done': '已查找文件', + 'conversations.tools.listFolder.active': '正在列出文件夹', + 'conversations.tools.listFolder.done': '已列出文件夹', + 'conversations.tools.exportCsv.active': '正在导出 CSV', + 'conversations.tools.exportCsv.done': '已导出 CSV', + 'conversations.tools.updateMemoryNotes.active': '正在更新记忆笔记', + 'conversations.tools.updateMemoryNotes.done': '已更新记忆笔记', + 'conversations.tools.runGit.active': '正在运行 git', + 'conversations.tools.runGit.done': '已运行 git', + 'conversations.tools.readChanges.active': '正在读取更改', + 'conversations.tools.readChanges.done': '已读取更改', + 'conversations.tools.runLinter.active': '正在运行代码检查', + 'conversations.tools.runLinter.done': '已运行代码检查', + 'conversations.tools.runTests.active': '正在运行测试', + 'conversations.tools.runTests.done': '已运行测试', + 'conversations.tools.analyzeCode.active': '正在分析代码', + 'conversations.tools.analyzeCode.done': '已分析代码', + 'conversations.tools.insertRecord.active': '正在插入记录', + 'conversations.tools.insertRecord.done': '已插入记录', + 'conversations.tools.runCommand.active': '正在运行命令', + 'conversations.tools.runCommand.done': '已运行命令', + 'conversations.tools.runCode.active': '正在运行代码', + 'conversations.tools.runCode.done': '已运行代码', + 'conversations.tools.runPackageManager.active': '正在运行 npm', + 'conversations.tools.runPackageManager.done': '已运行 npm', + 'conversations.tools.checkInstalledTools.active': '正在检查已安装的工具', + 'conversations.tools.checkInstalledTools.done': '已检查已安装的工具', + 'conversations.tools.installTool.active': '正在安装工具', + 'conversations.tools.installTool.done': '已安装工具', + 'conversations.tools.checkTime.active': '正在查看时间', + 'conversations.tools.checkTime.done': '已查看时间', + 'conversations.tools.resolveDate.active': '正在推算日期', + 'conversations.tools.resolveDate.done': '已推算日期', + 'conversations.tools.retrieveOutput.active': '正在获取完整输出', + 'conversations.tools.retrieveOutput.done': '已获取完整输出', + 'conversations.tools.reviewWorkspace.active': '正在查看工作区', + 'conversations.tools.reviewWorkspace.done': '已查看工作区', + 'conversations.tools.configureProxy.active': '正在配置代理', + 'conversations.tools.configureProxy.done': '已配置代理', + 'conversations.tools.checkUpdates.active': '正在检查更新', + 'conversations.tools.checkUpdates.done': '已检查更新', + 'conversations.tools.installUpdate.active': '正在安装更新', + 'conversations.tools.installUpdate.done': '已安装更新', + 'conversations.tools.sendNotification.active': '正在发送通知', + 'conversations.tools.sendNotification.done': '已发送通知', + 'conversations.tools.reviewToolUsage.active': '正在查看工具使用情况', + 'conversations.tools.reviewToolUsage.done': '已查看工具使用情况', + 'conversations.tools.typeKeys.active': '正在输入', + 'conversations.tools.typeKeys.done': '已输入', + 'conversations.tools.click.active': '正在点击', + 'conversations.tools.click.done': '已点击', + 'conversations.tools.searchWeb.active': '正在搜索网页', + 'conversations.tools.searchWeb.done': '已搜索网页', + 'conversations.tools.searchNews.active': '正在搜索新闻', + 'conversations.tools.searchNews.done': '已搜索新闻', + 'conversations.tools.searchImages.active': '正在搜索图片', + 'conversations.tools.searchImages.done': '已搜索图片', + 'conversations.tools.searchVideos.active': '正在搜索视频', + 'conversations.tools.searchVideos.done': '已搜索视频', + 'conversations.tools.findSimilarPages.active': '正在查找相似页面', + 'conversations.tools.findSimilarPages.done': '已查找相似页面', + 'conversations.tools.readPages.active': '正在阅读页面', + 'conversations.tools.readPages.done': '已阅读页面', + 'conversations.tools.readWebpage.active': '正在阅读网页', + 'conversations.tools.readWebpage.done': '已阅读网页', + 'conversations.tools.research.active': '正在研究', + 'conversations.tools.research.done': '已研究', + 'conversations.tools.enrichData.active': '正在丰富数据', + 'conversations.tools.enrichData.done': '已丰富数据', + 'conversations.tools.buildDataset.active': '正在构建数据集', + 'conversations.tools.buildDataset.done': '已构建数据集', + 'conversations.tools.askTheWeb.active': '正在向网络提问', + 'conversations.tools.askTheWeb.done': '已向网络提问', + 'conversations.tools.browseForYou.active': '正在为你浏览', + 'conversations.tools.browseForYou.done': '已为你浏览', + 'conversations.tools.callApi.active': '正在调用 API', + 'conversations.tools.callApi.done': '已调用 API', + 'conversations.tools.downloadFile.active': '正在下载文件', + 'conversations.tools.downloadFile.done': '已下载文件', + 'conversations.tools.makePaidRequest.active': '正在发起付费请求', + 'conversations.tools.makePaidRequest.done': '已发起付费请求', + 'conversations.tools.searchDocs.active': '正在搜索文档', + 'conversations.tools.searchDocs.done': '已搜索文档', + 'conversations.tools.readDocs.active': '正在阅读文档', + 'conversations.tools.readDocs.done': '已阅读文档', + 'conversations.tools.useBrowser.active': '正在使用浏览器', + 'conversations.tools.useBrowser.done': '已使用浏览器', + 'conversations.tools.openPage.active': '正在打开页面', + 'conversations.tools.openPage.done': '已打开页面', + 'conversations.tools.navigate.active': '正在导航', + 'conversations.tools.navigate.done': '已导航', + 'conversations.tools.takeScreenshot.active': '正在截图', + 'conversations.tools.takeScreenshot.done': '已截图', + 'conversations.tools.scrollPage.active': '正在滚动', + 'conversations.tools.scrollPage.done': '已滚动', + 'conversations.tools.readPage.active': '正在阅读页面', + 'conversations.tools.readPage.done': '已阅读页面', + 'conversations.tools.analyzeImage.active': '正在分析图片', + 'conversations.tools.analyzeImage.done': '已分析图片', + 'conversations.tools.generateImage.active': '正在生成图片', + 'conversations.tools.generateImage.done': '已生成图片', + 'conversations.tools.generateVideo.active': '正在生成视频', + 'conversations.tools.generateVideo.done': '已生成视频', + 'conversations.tools.checkMediaModels.active': '正在检查媒体模型', + 'conversations.tools.checkMediaModels.done': '已检查媒体模型', + 'conversations.tools.createDocument.active': '正在创建文档', + 'conversations.tools.createDocument.done': '已创建文档', + 'conversations.tools.createPresentation.active': '正在创建演示文稿', + 'conversations.tools.createPresentation.done': '已创建演示文稿', + 'conversations.tools.generatePodcast.active': '正在生成播客', + 'conversations.tools.generatePodcast.done': '已生成播客', + 'conversations.tools.emailPodcast.active': '正在通过邮件发送播客', + 'conversations.tools.emailPodcast.done': '已通过邮件发送播客', + 'conversations.tools.createAndEmailPodcast.active': '正在创建并通过邮件发送播客', + 'conversations.tools.createAndEmailPodcast.done': '已创建并通过邮件发送播客', + 'conversations.tools.recallMemories.active': '正在回忆记忆', + 'conversations.tools.recallMemories.done': '已回忆记忆', + 'conversations.tools.saveToMemory.active': '正在保存到记忆', + 'conversations.tools.saveToMemory.done': '已保存到记忆', + 'conversations.tools.forgetMemory.active': '正在遗忘记忆', + 'conversations.tools.forgetMemory.done': '已遗忘记忆', + 'conversations.tools.searchMemory.active': '正在搜索记忆', + 'conversations.tools.searchMemory.done': '已搜索记忆', + 'conversations.tools.inspectMemory.active': '正在检查记忆', + 'conversations.tools.inspectMemory.done': '已检查记忆', + 'conversations.tools.exploreMemory.active': '正在浏览记忆', + 'conversations.tools.exploreMemory.done': '已浏览记忆', + 'conversations.tools.saveDocumentToMemory.active': '正在将文档保存到记忆', + 'conversations.tools.saveDocumentToMemory.done': '已将文档保存到记忆', + 'conversations.tools.updateGoals.active': '正在更新目标', + 'conversations.tools.updateGoals.done': '已更新目标', + 'conversations.tools.reviewGoals.active': '正在查看目标', + 'conversations.tools.reviewGoals.done': '已查看目标', + 'conversations.tools.savePreference.active': '正在保存偏好', + 'conversations.tools.savePreference.done': '已保存偏好', + 'conversations.tools.reviewLearnings.active': '正在回顾学到的内容', + 'conversations.tools.reviewLearnings.done': '已回顾学到的内容', + 'conversations.tools.updateLearnings.active': '正在更新学到的内容', + 'conversations.tools.updateLearnings.done': '已更新学到的内容', + 'conversations.tools.delegateTask.active': '正在委派任务', + 'conversations.tools.delegateTask.done': '已委派任务', + 'conversations.tools.runAgentsInParallel.active': '正在并行运行智能体', + 'conversations.tools.runAgentsInParallel.done': '已并行运行智能体', + 'conversations.tools.messageAgent.active': '正在向智能体发送消息', + 'conversations.tools.messageAgent.done': '已向智能体发送消息', + 'conversations.tools.waitForAgent.active': '正在等待智能体', + 'conversations.tools.waitForAgent.done': '已等待智能体', + 'conversations.tools.wait.active': '正在等待', + 'conversations.tools.wait.done': '已等待', + 'conversations.tools.closeAgent.active': '正在关闭智能体', + 'conversations.tools.closeAgent.done': '已关闭智能体', + 'conversations.tools.checkAgents.active': '正在检查智能体', + 'conversations.tools.checkAgents.done': '已检查智能体', + 'conversations.tools.askQuestion.active': '正在向你提问', + 'conversations.tools.askQuestion.done': '已向你提问', + 'conversations.tools.prepareContext.active': '正在准备上下文', + 'conversations.tools.prepareContext.done': '已准备上下文', + 'conversations.tools.extractDetails.active': '正在提取细节', + 'conversations.tools.extractDetails.done': '已提取细节', + 'conversations.tools.planNextSteps.active': '正在规划后续步骤', + 'conversations.tools.planNextSteps.done': '已规划后续步骤', + 'conversations.tools.reviewWork.active': '正在审查工作', + 'conversations.tools.reviewWork.done': '已审查工作', + 'conversations.tools.scoutContext.active': '正在探查上下文', + 'conversations.tools.scoutContext.done': '已探查上下文', + 'conversations.tools.useTools.active': '正在使用工具', + 'conversations.tools.useTools.done': '已使用工具', + 'conversations.tools.checkConnectedApp.active': '正在检查你已连接的应用', + 'conversations.tools.checkConnectedApp.done': '已检查你已连接的应用', + 'conversations.tools.updateTodos.active': '正在更新待办清单', + 'conversations.tools.updateTodos.done': '已更新待办清单', + 'conversations.tools.requestPlanReview.active': '正在请求审查计划', + 'conversations.tools.requestPlanReview.done': '已请求审查计划', + 'conversations.tools.finishPlan.active': '正在完成计划', + 'conversations.tools.finishPlan.done': '已完成计划', + 'conversations.tools.setGoal.active': '正在设定目标', + 'conversations.tools.setGoal.done': '已设定目标', + 'conversations.tools.checkGoal.active': '正在检查目标', + 'conversations.tools.checkGoal.done': '已检查目标', + 'conversations.tools.completeGoal.active': '正在完成目标', + 'conversations.tools.completeGoal.done': '已完成目标', + 'conversations.tools.scheduleTask.active': '正在安排任务', + 'conversations.tools.scheduleTask.done': '已安排任务', + 'conversations.tools.checkSchedules.active': '正在检查日程安排', + 'conversations.tools.checkSchedules.done': '已检查日程安排', + 'conversations.tools.updateSchedule.active': '正在更新计划任务', + 'conversations.tools.updateSchedule.done': '已更新计划任务', + 'conversations.tools.removeSchedule.active': '正在移除计划任务', + 'conversations.tools.removeSchedule.done': '已移除计划任务', + 'conversations.tools.runScheduledTask.active': '正在运行计划任务', + 'conversations.tools.runScheduledTask.done': '已运行计划任务', + 'conversations.tools.checkRunHistory.active': '正在检查运行历史', + 'conversations.tools.checkRunHistory.done': '已检查运行历史', + 'conversations.tools.useApp.active': '正在使用 {app}', + 'conversations.tools.useApp.done': '已使用 {app}', + 'conversations.tools.checkAvailableApps.active': '正在检查可用应用', + 'conversations.tools.checkAvailableApps.done': '已检查可用应用', + 'conversations.tools.checkConnections.active': '正在检查你的连接', + 'conversations.tools.checkConnections.done': '已检查你的连接', + 'conversations.tools.connectApp.active': '正在连接应用', + 'conversations.tools.connectApp.done': '已连接应用', + 'conversations.tools.authorizeApp.active': '正在授权应用', + 'conversations.tools.authorizeApp.done': '已授权应用', + 'conversations.tools.findAppActions.active': '正在查找应用操作', + 'conversations.tools.findAppActions.done': '已查找应用操作', + 'conversations.tools.runAppAction.active': '正在运行应用操作', + 'conversations.tools.runAppAction.done': '已运行应用操作', + 'conversations.tools.findTools.active': '正在查找工具', + 'conversations.tools.findTools.done': '已查找工具', + 'conversations.tools.useTool.active': '正在使用 {tool}', + 'conversations.tools.useTool.done': '已使用 {tool}', + 'conversations.tools.unsubscribe.active': '正在退订', + 'conversations.tools.unsubscribe.done': '已退订', + 'conversations.tools.searchPlaces.active': '正在搜索地点', + 'conversations.tools.searchPlaces.done': '已搜索地点', + 'conversations.tools.lookUpPlace.active': '正在查询地点', + 'conversations.tools.lookUpPlace.done': '已查询地点', + 'conversations.tools.checkMarkets.active': '正在查看市场行情', + 'conversations.tools.checkMarkets.done': '已查看市场行情', + 'conversations.tools.placeCall.active': '正在拨打电话', + 'conversations.tools.placeCall.done': '已拨打电话', + 'conversations.tools.checkTaskSources.active': '正在检查任务来源', + 'conversations.tools.checkTaskSources.done': '已检查任务来源', + 'conversations.tools.updateTaskSources.active': '正在更新任务来源', + 'conversations.tools.updateTaskSources.done': '已更新任务来源', + 'conversations.tools.fetchTasks.active': '正在获取任务', + 'conversations.tools.fetchTasks.done': '已获取任务', + 'conversations.tools.checkMcpServers.active': '正在检查 MCP 服务器', + 'conversations.tools.checkMcpServers.done': '已检查 MCP 服务器', + 'conversations.tools.checkMcpTools.active': '正在检查 MCP 工具', + 'conversations.tools.checkMcpTools.done': '已检查 MCP 工具', + 'conversations.tools.callMcpTool.active': '正在调用 {tool}', + 'conversations.tools.callMcpTool.done': '已调用 {tool}', + 'conversations.tools.searchMcpServers.active': '正在搜索 MCP 服务器', + 'conversations.tools.searchMcpServers.done': '已搜索 MCP 服务器', + 'conversations.tools.connectMcpServer.active': '正在连接 MCP 服务器', + 'conversations.tools.connectMcpServer.done': '已连接 MCP 服务器', + 'conversations.tools.disconnectMcpServer.active': '正在断开 MCP 服务器', + 'conversations.tools.disconnectMcpServer.done': '已断开 MCP 服务器', + 'conversations.tools.removeMcpServer.active': '正在移除 MCP 服务器', + 'conversations.tools.removeMcpServer.done': '已移除 MCP 服务器', + 'conversations.tools.uploadFile.active': '正在上传文件', + 'conversations.tools.uploadFile.done': '已上传文件', + 'conversations.tools.listStoredFiles.active': '正在列出已存储的文件', + 'conversations.tools.listStoredFiles.done': '已列出已存储的文件', + 'conversations.tools.createShareLink.active': '正在创建分享链接', + 'conversations.tools.createShareLink.done': '已创建分享链接', + 'conversations.tools.deleteFile.active': '正在删除文件', + 'conversations.tools.deleteFile.done': '已删除文件', + 'conversations.tools.updateFileAccess.active': '正在更新文件访问权限', + 'conversations.tools.updateFileAccess.done': '已更新文件访问权限', + 'conversations.tools.deploySite.active': '正在部署网站', + 'conversations.tools.deploySite.done': '已部署网站', + 'conversations.tools.checkHosting.active': '正在检查托管', + 'conversations.tools.checkHosting.done': '已检查托管', + 'conversations.tools.updateHosting.active': '正在更新托管', + 'conversations.tools.updateHosting.done': '已更新托管', + 'conversations.tools.rollBackDeployment.active': '正在回滚部署', + 'conversations.tools.rollBackDeployment.done': '已回滚部署', + 'conversations.tools.checkWallet.active': '正在检查钱包', + 'conversations.tools.checkWallet.done': '已检查钱包', + 'conversations.tools.prepareTransfer.active': '正在准备转账', + 'conversations.tools.prepareTransfer.done': '已准备转账', + 'conversations.tools.checkTransaction.active': '正在检查交易', + 'conversations.tools.checkTransaction.done': '已检查交易', + 'conversations.tools.getSwapQuote.active': '正在获取兑换报价', + 'conversations.tools.getSwapQuote.done': '已获取兑换报价', + 'conversations.tools.swapTokens.active': '正在兑换代币', + 'conversations.tools.swapTokens.done': '已兑换代币', + 'conversations.tools.getBridgeQuote.active': '正在获取跨链报价', + 'conversations.tools.getBridgeQuote.done': '已获取跨链报价', + 'conversations.tools.bridgeTokens.active': '正在跨链转移代币', + 'conversations.tools.bridgeTokens.done': '已跨链转移代币', + 'conversations.tools.callDapp.active': '正在调用应用合约', + 'conversations.tools.callDapp.done': '已调用应用合约', + 'conversations.tools.useSkill.active': '正在使用技能', + 'conversations.tools.useSkill.done': '已使用技能', + 'conversations.tools.searchSkills.active': '正在搜索技能', + 'conversations.tools.searchSkills.done': '已搜索技能', + 'conversations.tools.checkSkills.active': '正在检查技能', + 'conversations.tools.checkSkills.done': '已检查技能', + 'conversations.tools.installSkill.active': '正在安装技能', + 'conversations.tools.installSkill.done': '已安装技能', + 'conversations.tools.removeSkill.active': '正在移除技能', + 'conversations.tools.removeSkill.done': '已移除技能', + 'conversations.tools.createSkill.active': '正在创建技能', + 'conversations.tools.createSkill.done': '已创建技能', + 'conversations.tools.runWorkflow.active': '正在运行工作流', + 'conversations.tools.runWorkflow.done': '已运行工作流', + 'conversations.tools.waitForWorkflow.active': '正在等待工作流', + 'conversations.tools.waitForWorkflow.done': '已等待工作流', + 'conversations.tools.designWorkflow.active': '正在设计工作流', + 'conversations.tools.designWorkflow.done': '已设计工作流', + 'conversations.tools.saveWorkflow.active': '正在保存工作流', + 'conversations.tools.saveWorkflow.done': '已保存工作流', + 'conversations.tools.validateWorkflow.active': '正在验证工作流', + 'conversations.tools.validateWorkflow.done': '已验证工作流', + 'conversations.tools.testWorkflow.active': '正在测试工作流', + 'conversations.tools.testWorkflow.done': '已测试工作流', + 'conversations.tools.checkWorkflows.active': '正在检查工作流', + 'conversations.tools.checkWorkflows.done': '已检查工作流', + 'conversations.tools.cancelWorkflow.active': '正在取消工作流运行', + 'conversations.tools.cancelWorkflow.done': '已取消工作流运行', + 'conversations.tools.suggestWorkflows.active': '正在推荐工作流', + 'conversations.tools.suggestWorkflows.done': '已推荐工作流', + 'conversations.tools.checkSettings.active': '正在检查设置', + 'conversations.tools.checkSettings.done': '已检查设置', + 'conversations.tools.checkSecurity.active': '正在检查安全性', + 'conversations.tools.checkSecurity.done': '已检查安全性', + 'conversations.tools.runDiagnostics.active': '正在运行诊断', + 'conversations.tools.runDiagnostics.done': '已运行诊断', + 'conversations.tools.checkUsageCosts.active': '正在检查使用费用', + 'conversations.tools.checkUsageCosts.done': '已检查使用费用', + 'conversations.tools.manageService.active': '正在管理后台服务', + 'conversations.tools.manageService.done': '已管理后台服务', + 'conversations.tools.readPersona.active': '正在读取角色设定', + 'conversations.tools.readPersona.done': '已读取角色设定', + 'conversations.tools.updatePersona.active': '正在更新角色设定', + 'conversations.tools.updatePersona.done': '已更新角色设定', + 'conversations.tools.setUpWorkspace.active': '正在设置工作区', + 'conversations.tools.setUpWorkspace.done': '已设置工作区', + 'conversations.tools.checkArtifacts.active': '正在检查工件', + 'conversations.tools.checkArtifacts.done': '已检查工件', + 'conversations.tools.deleteArtifact.active': '正在删除工件', + 'conversations.tools.deleteArtifact.done': '已删除工件', 'conversations.subagent.noOutput': '无输出返回', 'conversations.subagent.close': '关闭', 'conversations.subagent.cancel': '取消任务', diff --git a/app/src/pages/dev/ToolCallGallery.tsx b/app/src/pages/dev/ToolCallGallery.tsx new file mode 100644 index 00000000000..d087a146e4e --- /dev/null +++ b/app/src/pages/dev/ToolCallGallery.tsx @@ -0,0 +1,194 @@ +/** + * Dev-only gallery of tool-call presentation (`/dev/tools`). + * + * Renders the chat's tool-call card and assistant-ui tool timeline with + * realistic payloads in every state, plus the whole core tool catalog with + * each tool's icon and both tenses, so a label or icon regression is visible + * at a glance. Registered only in dev builds (see `AppRoutes.tsx`). + */ +import { useState } from 'react'; + +import { ToolTimeline } from '../../components/assistant-ui/elements/tool-timeline'; +import { AssistantUiToolCallCard } from '../../features/conversations/components/AssistantUiToolCall'; +import coreToolNames from '../../features/conversations/tools/__fixtures__/coreToolNames.json'; +import { ToolIcon } from '../../features/conversations/tools/ToolIcon'; +import { describeToolCall, toolLabel } from '../../features/conversations/tools/toolPresentation'; +import { useT } from '../../lib/i18n/I18nContext'; + +const SEARCH_RESULT = [ + 'Search results for: rust async traits (via Exa)', + '1. Announcing async fn and return-position impl Trait in traits', + ' https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html', + ' Published: 2023-12-21', + ' The Rust Async Working Group is excited to announce major progress.', + '2. async-trait crate', + ' https://docs.rs/async-trait/latest/async_trait/', + ' Type erasure for async trait methods.', + '3. Async in traits: the design', + ' https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/', + '4. Tokio tutorial', + ' https://tokio.rs/tokio/tutorial', +].join('\n'); + +const SAMPLES = [ + { + toolName: 'web_search_tool', + args: { query: 'rust async traits' }, + result: SEARCH_RESULT, + status: 'success' as const, + elapsedMs: 1840, + }, + { + toolName: 'web_search_tool', + args: { query: 'tauri v2 deep links' }, + status: 'running' as const, + }, + { + toolName: 'file_read', + args: { path: 'crates/openhuman-core/src/agent/progress.rs' }, + result: 'pub enum AgentProgress {\n ToolCallStarted { .. },\n}', + status: 'success' as const, + elapsedMs: 12, + }, + { + toolName: 'edit', + args: { + path: 'app/src/App.tsx', + old_string: 'const theme = "light";', + new_string: 'const theme = useTheme();\nconst accent = theme.accent;', + }, + result: 'ok', + status: 'success' as const, + elapsedMs: 40, + }, + { + toolName: 'shell', + args: { command: 'pnpm test --run tools' }, + result: + ' ✓ toolPresentation.test.ts (22)\n ✓ parseWebSearchResult.test.ts (8)\n\n Test Files 2 passed', + status: 'success' as const, + elapsedMs: 5230, + }, + { + toolName: 'web_fetch', + args: { url: 'https://docs.rs/tokio/latest/tokio/' }, + result: + 'status=200 url=https://docs.rs/tokio/latest/tokio/ content=markdown\n# Tokio\n\nA runtime for writing **reliable** asynchronous applications with Rust.', + status: 'success' as const, + elapsedMs: 620, + }, + { + toolName: 'GMAIL_SEND_EMAIL', + args: { to: 'alex@example.com', subject: 'Q3 plan' }, + result: '{"successful":true}', + status: 'success' as const, + elapsedMs: 910, + }, + { + toolName: 'mcp_call_tool', + args: { server: 'linear', tool: 'create_issue', arguments: { title: 'Fix labels' } }, + status: 'running' as const, + }, + { + toolName: 'memory', + args: { action: 'recall', query: 'preferred meeting times' }, + result: 'Mornings before 11am.', + status: 'success' as const, + elapsedMs: 88, + }, + { + toolName: 'grep', + args: { pattern: 'display_label' }, + status: 'error' as const, + result: 'regex parse error', + failure: { + class: 'InvalidInput', + category: 'Recoverable', + recoverable: true, + causePlain: 'The search pattern was not a valid regular expression.', + nextAction: 'The agent will retry with an escaped pattern.', + }, + }, + { toolName: 'cron', args: { action: 'add', name: 'Daily digest' }, status: 'cancelled' as const }, + { toolName: 'some_new_tool', args: { name: 'widget' }, status: 'success' as const, result: 'ok' }, +]; + +function CatalogRow({ name }: { name: string }) { + const { t } = useT(); + const running = describeToolCall({ name, status: 'running' }); + const done = describeToolCall({ name, status: 'success' }); + return ( + <li className="flex items-center gap-2 py-1 text-xs" data-testid="tool-gallery-catalog-row"> + <ToolIcon presentation={done} className="text-foreground/50 size-3.5" /> + <span className="text-foreground/40 w-56 shrink-0 truncate font-mono">{name}</span> + <span className="text-foreground/80 w-56 shrink-0 truncate">{toolLabel(running, t)}</span> + <span className="text-foreground/60 truncate">{toolLabel(done, t)}</span> + </li> + ); +} + +export default function ToolCallGallery() { + const { t } = useT(); + const [streaming, setStreaming] = useState(true); + return ( + <div className="bg-background text-foreground min-h-screen overflow-auto p-8"> + <div className="mx-auto flex max-w-3xl flex-col gap-10"> + <header> + <h1 className="text-lg font-semibold">Tool calls</h1> + <p className="text-foreground/50 text-sm"> + assistant-ui tool-call, tool-timeline and web-search elements over the presentation + registry. + </p> + </header> + + <section className="flex flex-col gap-3"> + <label className="text-foreground/60 flex items-center gap-2 text-xs"> + <input + type="checkbox" + checked={streaming} + onChange={event => setStreaming(event.target.checked)} + /> + Timeline streaming + </label> + <ToolTimeline + className="max-w-none" + defaultOpen + streaming={streaming} + activeLabel={toolLabel( + describeToolCall({ name: 'web_search_tool', status: 'running' }), + t + )} + restingLabel="4 steps · Searched the web, Read file, Edited file, Ran command"> + {SAMPLES.slice(0, 5).map((sample, index) => ( + <AssistantUiToolCallCard key={index} {...sample} /> + ))} + </ToolTimeline> + </section> + + <section className="flex flex-col gap-1"> + <h2 className="text-foreground/60 mb-2 text-xs font-medium uppercase">Every state</h2> + {SAMPLES.map((sample, index) => ( + <AssistantUiToolCallCard key={index} {...sample} /> + ))} + <AssistantUiToolCallCard + toolName="composio_execute" + args={{ tool: 'SLACK_SEND_MESSAGE' }} + awaitingUser + footer={<p className="text-foreground/50 ps-5 text-xs">(approval card renders here)</p>} + /> + </section> + + <section> + <h2 className="text-foreground/60 mb-2 text-xs font-medium uppercase"> + Core catalog ({(coreToolNames as string[]).length}) + </h2> + <ul className="divide-foreground/[0.06] divide-y"> + {(coreToolNames as string[]).map(name => ( + <CatalogRow key={name} name={name} /> + ))} + </ul> + </section> + </div> + </div> + ); +} diff --git a/app/src/providers/ChatRuntimeProvider.tsx b/app/src/providers/ChatRuntimeProvider.tsx index dbde30ee9b5..e918e8d036d 100644 --- a/app/src/providers/ChatRuntimeProvider.tsx +++ b/app/src/providers/ChatRuntimeProvider.tsx @@ -762,6 +762,11 @@ const ChatRuntimeProvider = ({ children }: { children: React.ReactNode }) => { success: event.success, output: event.output, failure: event.failure, + args: event.args, + elapsedMs: event.elapsed_ms, + structured: event.structured, + displayLabel: event.tool_display_label, + displayDetail: event.tool_display_detail, }) ); diff --git a/app/src/providers/assistantUiMessages.ts b/app/src/providers/assistantUiMessages.ts index bb26a5aeca7..62c57923284 100644 --- a/app/src/providers/assistantUiMessages.ts +++ b/app/src/providers/assistantUiMessages.ts @@ -123,6 +123,41 @@ function toolResultPayload(entry: ToolTimelineEntry): unknown { }; } +/** + * Presentation data that rides a tool part's `artifact`. + * + * assistant-ui's tool-call part has no slot for a display label, a duration + * or a structured result, and this adapter used to drop all three, so the + * chat card fell back to guessing a label from the tool name and arguments. + * `artifact` is the part's UI-only field, which is exactly this. + */ +export interface OpenHumanToolArtifact { + kind: 'openhuman-tool'; + /** Server label, for dynamic tools the client registry cannot describe. */ + displayName?: string; + detail?: string; + elapsedMs?: number; + structured?: unknown; +} + +export function readOpenHumanToolArtifact(value: unknown): OpenHumanToolArtifact | undefined { + if (!value || typeof value !== 'object') return undefined; + return (value as { kind?: unknown }).kind === 'openhuman-tool' + ? (value as OpenHumanToolArtifact) + : undefined; +} + +function toolArtifact(entry: ToolTimelineEntry): OpenHumanToolArtifact | undefined { + const artifact: OpenHumanToolArtifact = { + kind: 'openhuman-tool', + ...(entry.displayName ? { displayName: entry.displayName } : {}), + ...(entry.detail ? { detail: entry.detail } : {}), + ...(entry.elapsedMs !== undefined ? { elapsedMs: entry.elapsedMs } : {}), + ...(entry.structured !== undefined ? { structured: entry.structured } : {}), + }; + return Object.keys(artifact).length > 1 ? artifact : undefined; +} + function toolPart(entry: ToolTimelineEntry): ThreadAssistantMessagePart { const running = isActiveTimelineStatus(entry.status); const isSubagent = entry.name.startsWith('subagent:') || entry.subagent !== undefined; @@ -140,6 +175,7 @@ function toolPart(entry: ToolTimelineEntry): ThreadAssistantMessagePart { toolName: isSubagent ? 'task' : entry.name, args, argsText: JSON.stringify(args, null, 2), + ...(!isSubagent && toolArtifact(entry) ? { artifact: toolArtifact(entry) } : {}), ...(!running ? { result: isSubagent diff --git a/app/src/services/chatService.ts b/app/src/services/chatService.ts index 4857b2c931a..3039e3edfbd 100644 --- a/app/src/services/chatService.ts +++ b/app/src/services/chatService.ts @@ -59,6 +59,18 @@ export interface ChatToolResultEvent { * `parseToolFailure` before it reaches the store. */ failure?: unknown; + /** The call's arguments. The start event may carry none; this is the fallback. */ + args?: unknown; + /** Wall time the call took. */ + elapsed_ms?: number; + /** + * Machine-readable result, when the tool produced one (the tool's + * `ToolResult.metadata`), e.g. `{ kind: "web_search", results: [...] }`. + */ + structured?: unknown; + /** Label / detail recomputed by the core with the call's real arguments. */ + tool_display_label?: string; + tool_display_detail?: string; } /** One sub-agent's token/cost contribution within a turn (hover breakdown). */ diff --git a/app/src/store/chatRuntimeSlice.ts b/app/src/store/chatRuntimeSlice.ts index 551474a9660..623e65db528 100644 --- a/app/src/store/chatRuntimeSlice.ts +++ b/app/src/store/chatRuntimeSlice.ts @@ -285,6 +285,37 @@ export function parseToolFailure(raw: unknown): ToolFailureExplanation | undefin }; } +/** + * Fold the optional completion fields of a `tool_result` into its row. + * + * The core's start event may carry no arguments (the harness reports them at + * completion), so `args` backfills an empty `argsBuffer`; without it a row + * could never show its target. A recomputed server label replaces the one + * sent at start, which was derived without arguments. + */ +function applyResultExtras( + entry: ToolTimelineEntry, + extras: { + args?: unknown; + elapsedMs?: number; + structured?: unknown; + displayLabel?: string; + displayDetail?: string; + } +): void { + if (!entry.argsBuffer && extras.args && typeof extras.args === 'object') { + entry.argsBuffer = JSON.stringify(extras.args); + } + if (typeof extras.elapsedMs === 'number' && Number.isFinite(extras.elapsedMs)) { + entry.elapsedMs = extras.elapsedMs; + } + if (extras.structured && typeof extras.structured === 'object') { + entry.structured = extras.structured; + } + if (extras.displayLabel?.trim()) entry.displayName = extras.displayLabel.trim(); + if (extras.displayDetail?.trim()) entry.detail = extras.displayDetail.trim(); +} + /** * Attach a human label/detail to a tool-timeline row. The server supplies a * label/detail for dynamic Composio/MCP/integration tools the client can't know @@ -293,11 +324,15 @@ export function parseToolFailure(raw: unknown): ToolFailureExplanation | undefin * caller that materialises a row. */ function decorateEntry(entry: ToolTimelineEntry): ToolTimelineEntry { + // `displayName` holds only what the server said. Baking the client title in + // here froze its tense at call time, so a finished row kept reading + // "Reading file"; every surface now resolves the title at render time. const formatted = formatTimelineEntry(entry); if (entry.displayName && !isKnownClientTool(entry.name)) { - return { ...entry, displayName: entry.displayName, detail: entry.detail ?? formatted.detail }; + return { ...entry, detail: entry.detail ?? formatted.detail }; } - return { ...entry, displayName: formatted.title, detail: formatted.detail ?? entry.detail }; + const { displayName: _serverLabel, ...rest } = entry; + return { ...rest, detail: entry.detail ?? formatted.detail }; } /** @@ -368,6 +403,15 @@ export interface ToolTimelineEntry { * and on rows from cores that predate output forwarding. */ result?: string; + /** + * Machine-readable result the core attached to `tool_result` as + * `structured` (today `{ kind: "web_search", query, provider, results }`). + * Lets a rich renderer skip re-parsing `result` text. Absent on rows from + * older cores, which fall back to parsing. + */ + structured?: unknown; + /** Wall time the call took, from `tool_result.elapsed_ms`. */ + elapsedMs?: number; } export interface StreamingAssistantState { @@ -1365,6 +1409,11 @@ const chatRuntimeSlice = createSlice({ success: boolean; output?: string; failure?: unknown; + args?: unknown; + elapsedMs?: number; + structured?: unknown; + displayLabel?: string; + displayDetail?: string; }> ) => { const { threadId, round, toolName, success, output, failure } = action.payload; @@ -1381,12 +1430,16 @@ const chatRuntimeSlice = createSlice({ // The core forwards the (size-capped) tool result text on `output`; accept // only non-empty payloads so a stub-less row stays `undefined`. const result = output && output.length > 0 ? output : undefined; + const settle = (entry: ToolTimelineEntry) => { + entry.status = status; + entry.failure = parsedFailure; + entry.result = result; + applyResultExtras(entry, action.payload); + }; if (toolCallId) { const entry = entries.find(e => e.id === toolCallId); if (entry) { - entry.status = status; - entry.failure = parsedFailure; - entry.result = result; + settle(entry); return; } } @@ -1400,9 +1453,7 @@ const chatRuntimeSlice = createSlice({ for (let i = 0; i < entries.length; i += 1) { const entry = entries[i]; if (entry.status === 'running' && entry.name === toolName && entry.round === round) { - entry.status = status; - entry.failure = parsedFailure; - entry.result = result; + settle(entry); return; } } diff --git a/app/src/utils/__tests__/toolTimelineFormatting.test.ts b/app/src/utils/__tests__/toolTimelineFormatting.test.ts index 5fbbdb0d2ea..51b6ece07a9 100644 --- a/app/src/utils/__tests__/toolTimelineFormatting.test.ts +++ b/app/src/utils/__tests__/toolTimelineFormatting.test.ts @@ -27,10 +27,7 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ prompt: 'Find the project brief in Notion.' }), }) ) - ).toEqual({ - title: 'Working in your Notion workspace', - detail: 'Find the project brief in Notion.', - }); + ).toEqual({ title: 'Using Notion', detail: 'Find the project brief in Notion.' }); }); it('formats spawn_subagent for integrations_agent from toolkit args', () => { @@ -47,7 +44,7 @@ describe('formatTimelineEntry', () => { }) ) ).toEqual({ - title: 'Making requests to your Gmail account', + title: 'Using Gmail', detail: 'Get my 5 most recent emails. Show subject, sender, date, and a short preview for each.', }); @@ -62,10 +59,7 @@ describe('formatTimelineEntry', () => { detail: 'Search Notion for the latest roadmap.', }) ) - ).toEqual({ - title: 'Working in your Notion workspace', - detail: 'Search Notion for the latest roadmap.', - }); + ).toEqual({ title: 'Using Notion', detail: 'Search Notion for the latest roadmap.' }); }); it('labels a direct connected-service action by its provider', () => { @@ -73,17 +67,21 @@ describe('formatTimelineEntry', () => { formatTimelineEntry( entry({ name: 'GMAIL_SEND_EMAIL', argsBuffer: JSON.stringify({ to: 'alex@example.com' }) }) ) - ).toEqual({ title: 'Making requests to your Gmail account', detail: 'Send email' }); + ).toEqual({ title: 'Using Gmail', detail: 'Send email · alex@example.com' }); expect(formatTimelineEntry(entry({ name: 'GOOGLE_CALENDAR_CREATE_EVENT' }))).toEqual({ - title: 'Updating your Google Calendar', + title: 'Using Google Calendar', detail: 'Create event', }); }); - it('keeps the generic label for upper-case names on unknown toolkits', () => { + it('never shouts an upper-case action slug, known toolkit or not', () => { expect(formatTimelineEntry(entry({ name: 'STRIPE_LIST_CHARGES' }))).toEqual({ - title: 'STRIPE LIST CHARGES', - detail: undefined, + title: 'Using Stripe', + detail: 'List charges', + }); + expect(formatTimelineEntry(entry({ name: 'ACME_DO_THING' }))).toEqual({ + title: 'Using Acme', + detail: 'Do thing', }); }); @@ -98,10 +96,7 @@ describe('formatTimelineEntry', () => { }), }) ) - ).toEqual({ - title: 'Making requests to your GitHub account', - detail: 'List my open pull requests in GitHub.', - }); + ).toEqual({ title: 'Using GitHub', detail: 'List my open pull requests in GitHub.' }); }); it('falls back to humanized generic labels for non-integration subagents', () => { @@ -113,7 +108,7 @@ describe('formatTimelineEntry', () => { it('formats composio_list_connections with user-facing copy', () => { expect(formatTimelineEntry(entry({ name: 'composio_list_connections' }))).toEqual({ - title: 'Viewing your Connections', + title: 'Checking your connections', detail: undefined, }); }); @@ -126,7 +121,7 @@ describe('formatTimelineEntry', () => { ).toEqual({ title: 'Running command', detail: 'cargo test --lib' }); }); - it('formats web_fetch with hostname in title', () => { + it('formats web_fetch with the page as detail', () => { expect( formatTimelineEntry( entry({ @@ -134,21 +129,18 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ url: 'https://docs.example.com/api/v2/users' }), }) ) - ).toEqual({ - title: 'Fetching docs.example.com', - detail: 'https://docs.example.com/api/v2/users', - }); + ).toEqual({ title: 'Reading webpage', detail: 'docs.example.com/api/v2/users' }); }); - it('formats web_search with query in title', () => { + it('formats web_search with the query as detail', () => { expect( formatTimelineEntry( entry({ name: 'web_search', argsBuffer: JSON.stringify({ query: 'rust async trait' }) }) ) - ).toEqual({ title: 'Searching: rust async trait' }); + ).toEqual({ title: 'Searching the web', detail: 'rust async trait' }); }); - it('attributes a completed web_search to the resolved provider', () => { + it('settles a completed web_search into the past tense (provider shows in the search element)', () => { expect( formatTimelineEntry( entry({ @@ -158,10 +150,10 @@ describe('formatTimelineEntry', () => { result: 'Search results for: rust async trait (via Exa)\n1. Some title\n https://x.dev', }) ) - ).toEqual({ title: 'Searched with Exa', detail: 'rust async trait' }); + ).toEqual({ title: 'Searched the web', detail: 'rust async trait' }); }); - it('reflects a different provider from the result (attribution is dynamic)', () => { + it('settles whichever provider served the search', () => { expect( formatTimelineEntry( entry({ @@ -171,7 +163,7 @@ describe('formatTimelineEntry', () => { result: 'Search results for: weather (via Brave)\n1. Forecast', }) ) - ).toEqual({ title: 'Searched with Brave', detail: 'weather' }); + ).toEqual({ title: 'Searched the web', detail: 'weather' }); }); it('keeps the running label when no result is present yet', () => { @@ -183,7 +175,7 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ query: 'rust async trait' }), }) ) - ).toEqual({ title: 'Searching: rust async trait' }); + ).toEqual({ title: 'Searching the web', detail: 'rust async trait' }); }); // `web_search_tool` is the name the core actually registers and streams for @@ -198,7 +190,7 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ query: 'rust async trait' }), }) ) - ).toEqual({ title: 'Searching: rust async trait' }); + ).toEqual({ title: 'Searching the web', detail: 'rust async trait' }); }); it('attributes a completed web_search_tool from the markdown result', () => { @@ -213,7 +205,7 @@ describe('formatTimelineEntry', () => { result: '# Search results — `rust async trait` (via Exa)\n\n## [T](https://x.dev)', }) ) - ).toEqual({ title: 'Searched with Exa', detail: 'rust async trait' }); + ).toEqual({ title: 'Searched the web', detail: 'rust async trait' }); }); it('attributes a completed web_search_tool that returned no results', () => { @@ -226,7 +218,7 @@ describe('formatTimelineEntry', () => { result: '_No results for `zzzz`_ (via Exa)', }) ) - ).toEqual({ title: 'Searched with Exa', detail: 'zzzz' }); + ).toEqual({ title: 'Searched the web', detail: 'zzzz' }); }); it('formats file_read with shortened path', () => { @@ -256,7 +248,7 @@ describe('formatTimelineEntry', () => { formatTimelineEntry( entry({ name: 'grep', argsBuffer: JSON.stringify({ pattern: 'SubagentSpawned' }) }) ) - ).toEqual({ title: 'Searching: SubagentSpawned' }); + ).toEqual({ title: 'Searching code', detail: 'SubagentSpawned' }); }); it('formats git_operations with subcommand', () => { @@ -264,7 +256,7 @@ describe('formatTimelineEntry', () => { formatTimelineEntry( entry({ name: 'git_operations', argsBuffer: JSON.stringify({ command: 'diff --stat' }) }) ) - ).toEqual({ title: 'Git diff', detail: 'diff --stat' }); + ).toEqual({ title: 'Running git', detail: 'diff --stat' }); }); it('formats glob with pattern detail', () => { @@ -272,7 +264,7 @@ describe('formatTimelineEntry', () => { formatTimelineEntry( entry({ name: 'glob', argsBuffer: JSON.stringify({ pattern: '**/*.test.ts' }) }) ) - ).toEqual({ title: 'Finding: **/*.test.ts' }); + ).toEqual({ title: 'Finding files', detail: '**/*.test.ts' }); }); it('formats list with directory path', () => { @@ -283,10 +275,10 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ path: 'crates/openhuman-core/src/tools' }), }) ) - ).toEqual({ title: 'Listing directory', detail: '…/src/tools' }); + ).toEqual({ title: 'Listing folder', detail: '…/src/tools' }); }); - it('formats browser_open with hostname', () => { + it('formats browser_open with the page as detail', () => { expect( formatTimelineEntry( entry({ @@ -294,7 +286,7 @@ describe('formatTimelineEntry', () => { argsBuffer: JSON.stringify({ url: 'https://github.com/tinyhumansai/openhuman' }), }) ) - ).toEqual({ title: 'Browsing github.com' }); + ).toEqual({ title: 'Opening page', detail: 'github.com/tinyhumansai/openhuman' }); }); }); @@ -342,16 +334,16 @@ describe('extractSearchProvider', () => { describe('formatToolName', () => { it('returns human-readable names for known tools', () => { expect(formatToolName('shell')).toBe('Running command'); - expect(formatToolName('web_fetch')).toBe('Fetching'); + expect(formatToolName('web_fetch')).toBe('Reading webpage'); expect(formatToolName('file_read')).toBe('Reading file'); expect(formatToolName('edit')).toBe('Editing file'); expect(formatToolName('grep')).toBe('Searching code'); - expect(formatToolName('git_operations')).toBe('Git operation'); - expect(formatToolName('lsp')).toBe('Code intelligence'); + expect(formatToolName('git_operations')).toBe('Running git'); + expect(formatToolName('lsp')).toBe('Analyzing code'); }); - it('falls back to humanized identifier for unknown tools', () => { - expect(formatToolName('custom_fancy_tool')).toBe('Custom Fancy Tool'); + it('falls back to a sentence-cased activity for unknown tools', () => { + expect(formatToolName('custom_fancy_tool')).toBe('Using custom fancy tool'); }); }); @@ -387,9 +379,10 @@ describe('isKnownClientTool', () => { expect(isKnownClientTool('web_search_tool')).toBe(true); }); - it('does not recognize dynamic Composio/MCP actions (server labels them)', () => { - expect(isKnownClientTool('GMAIL_SEND_EMAIL')).toBe(false); - expect(isKnownClientTool('composio_notion_create_page')).toBe(false); + it('recognizes Composio actions by their toolkit, and nothing it cannot describe', () => { + // The registry names a Composio action by its app ("Used Gmail"), so the + // core's sentence-cased slug does not override it. + expect(isKnownClientTool('GMAIL_SEND_EMAIL')).toBe(true); expect(isKnownClientTool('some_random_mcp_tool')).toBe(false); }); }); @@ -407,25 +400,25 @@ describe('summarizeToolGroup', () => { entry({ id: 'a', name: 'file_read' }), entry({ id: 'b', name: 'file_read' }), ]) - ).toBe('Read 2 files'); + ).toBe('2 steps · Read file ×2'); }); - it('joins distinct category phrases for a mixed group', () => { + it('lists the distinct steps of a mixed group, most frequent first', () => { expect( summarizeToolGroup([ entry({ id: 'a', name: 'file_write' }), entry({ id: 'b', name: 'shell' }), entry({ id: 'c', name: 'shell' }), ]) - ).toBe('Edited 1 file, ran 2 commands'); + ).toBe('3 steps · Ran command ×2, Wrote file'); }); }); describe('categorizeTool', () => { it('maps tools (incl. subagent-prefixed) to a category', () => { - expect(categorizeTool('grep')).toBe('search'); - expect(categorizeTool('subagent:web_fetch')).toBe('fetch'); - expect(categorizeTool('GMAIL_SEND_EMAIL')).toBe('other'); + expect(categorizeTool('grep')).toBe('code'); + expect(categorizeTool('subagent:web_fetch')).toBe('web'); + expect(categorizeTool('GMAIL_SEND_EMAIL')).toBe('app'); }); it('categorizes the canonical web-search name, not only its settings id', () => { @@ -436,9 +429,9 @@ describe('categorizeTool', () => { // was the one place that had not, so a real search row categorized as // `other` — wrong icon and wrong group summary in the rail, and (since // #6169) a row kept on the main transcript that belongs in the rail. - expect(categorizeTool('web_search_tool')).toBe('search'); - expect(categorizeTool('web_search')).toBe('search'); - expect(categorizeTool('subagent:web_search_tool')).toBe('search'); + expect(categorizeTool('web_search_tool')).toBe('web'); + expect(categorizeTool('web_search')).toBe('web'); + expect(categorizeTool('subagent:web_search_tool')).toBe('web'); }); }); @@ -462,7 +455,7 @@ describe('buildProcessingBlocks', () => { expect(blocks.map(b => b.kind)).toEqual(['thinking', 'narration', 'toolGroup', 'narration']); const group = blocks[2]; if (group.kind !== 'toolGroup') throw new Error('expected toolGroup'); - expect(group.summary).toBe('Read 2 files'); + expect(group.summary).toBe('2 steps · Read file ×2'); expect(group.entries).toHaveLength(2); }); diff --git a/app/src/utils/toolTimelineFormatting.ts b/app/src/utils/toolTimelineFormatting.ts index 966e225d86f..bd6c06ab79f 100644 --- a/app/src/utils/toolTimelineFormatting.ts +++ b/app/src/utils/toolTimelineFormatting.ts @@ -1,169 +1,58 @@ +/** + * Timeline-row formatting for tool calls. + * + * Every label, icon and category is resolved by the tool presentation + * registry (`features/conversations/tools/toolPresentation.ts`); this module + * adapts {@link ToolTimelineEntry} rows onto it and keeps the timeline-only + * helpers (processing blocks, sources, envelope stripping). + */ +import { + extractSearchProvider, + parseWebSearchResult, +} from '../features/conversations/tools/parseWebSearchResult'; +import { fillPlaceholders } from '../features/conversations/tools/toolPhrases'; +import { + describeToolCall, + type ToolCallPresentation, + type ToolCategory, + toolLabel, + type Translate, +} from '../features/conversations/tools/toolPresentation'; import type { ToolTimelineEntry } from '../store/chatRuntimeSlice'; import type { PersistedTranscriptItem } from '../types/turnState'; -interface ParsedToolArgs { - agent_id?: string; - prompt?: string; - toolkit?: string; - command?: string; - url?: string; - path?: string; - file_path?: string; - pattern?: string; - query?: string; - tool_name?: string; - question?: string; +export type { ToolCategory, Translate }; +export { extractSearchProvider }; + +/** Resolve a timeline row through the registry. */ +export function presentTimelineEntry(entry: ToolTimelineEntry): ToolCallPresentation { + return describeToolCall({ + name: entry.name, + args: entry.argsBuffer, + status: entry.status, + serverLabel: entry.displayName, + serverDetail: entry.detail, + toolkitHint: entry.sourceToolName, + }); } -const TOOL_DISPLAY_NAMES: Record<string, string> = { - shell: 'Running command', - node_exec: 'Running command', - npm_exec: 'Running command', - web_fetch: 'Fetching', - http_request: 'Fetching', - curl: 'Fetching', - web_search: 'Searching the web', - // The name the core actually registers and streams for the canonical search - // slot, whichever engine owns it (`crates/openhuman-core/src/search/registry.rs`). - // `web_search` above is the settings-family id, which never reaches a - // timeline row — without this entry a real search rendered as the - // humanized "Web Search Tool". - web_search_tool: 'Searching the web', - // The harness's tool-discovery bridge — NOT a web search. - // `vendor/tinyagents/crates/tinyagents-harness/src/tool/discover/bridge.rs` - // advertises two intrinsics: `tool_search` ranks the *deferred tool - // catalogue* (Composio actions, MCP tools) and `tool_call` invokes a hit by - // name. Neither reaches the network. - // - // They are `ToolSchema` values rather than `Tool` impls, so they carry no - // server `display_label` and there is nothing upstream to override — the - // label is the client's to get right. Without these entries `tool_search` - // fell through to a substring heuristic that fires on "search", and a - // Composio calendar fetch rendered as "Searched the web". - tool_search: 'Finding the right tool', - tool_call: 'Using a tool', - gitbooks_search: 'Searching docs', - file_read: 'Reading file', - file_write: 'Writing file', - edit: 'Editing file', - apply_patch: 'Applying patch', - grep: 'Searching code', - glob: 'Finding files', - list: 'Listing directory', - read_diff: 'Reading diff', - git_operations: 'Git operation', - browser: 'Browsing', - browser_open: 'Opening browser', - image_info: 'Analyzing image', - install_tool: 'Installing tool', - lsp: 'Code intelligence', - keyboard: 'Typing', - mouse: 'Clicking', - csv_export: 'Exporting CSV', - update_memory_md: 'Updating memory', - read_workspace_state: 'Reading workspace', - current_time: 'Checking time', - schedule: 'Scheduling', - detect_tools: 'Detecting tools', - tool_stats: 'Tool statistics', - vault_write_markdown: 'Writing to vault', - run_linter: 'Running linter', - run_tests: 'Running tests', - proxy_config: 'Configuring proxy', - update_check: 'Checking for updates', - update_apply: 'Applying update', - pushover: 'Sending notification', - insert_sql_record: 'Inserting record', - mcp_list_servers: 'Listing MCP servers', - mcp_list_tools: 'Listing MCP tools', - mcp_call_tool: 'Calling MCP tool', - gmail_unsubscribe: 'Unsubscribing', - gitbooks_get_page: 'Reading docs page', - audio_generate_podcast: 'Generating podcast', - audio_email_podcast: 'Emailing podcast', - audio_generate_and_email_podcast: 'Generating & emailing podcast', - composio_list_connections: 'Viewing your Connections', - agent_prepare_context: 'Preparing context', - propose_workflow: 'Proposing workflow', - // Harness work state: the session todo list and the thread goal. The pane - // renders both from these calls' results (`utils/harnessState.ts`), so the - // rows read as bookkeeping, not as work in their own right. - todo: 'Updating todo list', - goal_set: 'Setting goal', - goal_get: 'Checking goal', - goal_complete: 'Completing goal', -}; - /** - * Format a raw tool name into a short human-readable label. - * Used for subagent child tool rows and sub-mascot activity text. + * Present-tense label for a bare tool name ("Searching the web"). Used where + * only the name is known: sub-agent child rows and the mascot's activity line. */ -export function formatToolName(toolName: string | undefined): string { +export function formatToolName(toolName: string | undefined, t?: Translate): string { if (!toolName) return ''; - return TOOL_DISPLAY_NAMES[toolName] ?? humanizeIdentifier(toolName); + return toolLabel(describeToolCall({ name: toolName, status: 'running' }), t); } /** - * The fixed set of built-in / special tools this client formatter labels - * well on its own (with args-aware detail). For these, the client label is - * authoritative and a server-supplied `display_label` is ignored — the - * server label only wins for *dynamic* tools (Composio/MCP/integration - * actions) the client can't possibly know, which is where raw `snake_case` - * used to leak through. Keep in sync with {@link formatTimelineEntry} / - * {@link formatToolDetail}. - */ -const CLIENT_KNOWN_TOOLS = new Set<string>([ - ...Object.keys(TOOL_DISPLAY_NAMES), - // args-aware built-ins handled by formatToolDetail() - 'shell', - 'node_exec', - 'npm_exec', - 'web_fetch', - 'http_request', - 'curl', - 'web_search', - 'web_search_tool', - 'gitbooks_search', - 'file_read', - 'file_write', - 'vault_write_markdown', - 'edit', - 'apply_patch', - 'grep', - 'glob', - 'list', - 'git_operations', - 'browser', - 'browser_open', - 'image_info', - 'install_tool', - 'lsp', - 'run_tests', - 'run_linter', - 'read_diff', - // special-cased agent / integration rows - 'spawn_subagent', - 'integrations_agent', - 'researcher', - 'agent_prepare_context', - 'context_scout', - 'composio_list_connections', - 'orchestrator', - 'critic', - 'tools_agent', - 'code_executor', -]); - -/** - * Whether the client formatter recognizes this tool (so its label should win - * over any server-supplied one). True for built-ins, the special agent rows, - * and the `subagent:` / `delegate_` families that {@link formatTimelineEntry} - * handles explicitly. + * Whether the registry describes this tool on its own. For these the client + * label is authoritative and a server `display_label` is ignored; the server + * label wins only for dynamic tools the registry cannot know. */ export function isKnownClientTool(name: string): boolean { - return ( - name.startsWith('subagent:') || name.startsWith('delegate_') || CLIENT_KNOWN_TOOLS.has(name) - ); + const { source } = describeToolCall({ name }); + return source !== 'server' && source !== 'fallback'; } /** @@ -181,185 +70,86 @@ export function stripToolCallEnvelopes(text: string | undefined | null): string .replace(/<tool_call\b[^>]*>[\s\S]*$/i, ''); } -/** Broad activity category for a tool, used to group + icon timeline rows. */ -export type ToolCategory = 'read' | 'write' | 'search' | 'run' | 'fetch' | 'browse' | 'other'; - -const TOOL_CATEGORIES: Record<string, ToolCategory> = { - file_read: 'read', - list: 'read', - read_diff: 'read', - file_write: 'write', - vault_write_markdown: 'write', - edit: 'write', - apply_patch: 'write', - grep: 'search', - glob: 'search', - // `web_search_tool` is the runtime tool name; `web_search` is only the UI - // toggle id the core expands from (`tools/user_filter.rs:79-80`, and - // `test/e2e/specs/harness-search-tool-flow.spec.ts:10` says so outright). - // Both are mapped: the toggle id never reaches a timeline row, but leaving - // it out would break any older snapshot that recorded the alias. - web_search: 'search', - web_search_tool: 'search', - gitbooks_search: 'search', - gitbooks_get_page: 'read', - shell: 'run', - node_exec: 'run', - npm_exec: 'run', - run_tests: 'run', - run_linter: 'run', - git_operations: 'run', - web_fetch: 'fetch', - http_request: 'fetch', - curl: 'fetch', - browser: 'browse', - browser_open: 'browse', -}; - /** Categorize a (possibly `subagent:`-prefixed) tool name for grouping/icons. */ export function categorizeTool(name: string): ToolCategory { - const base = name.replace(/^subagent:/, ''); - return TOOL_CATEGORIES[base] ?? 'other'; + return describeToolCall({ name }).category; } -/** Plural-aware verb phrase per category, e.g. `read` + 2 → "Read 2 files". */ -const CATEGORY_PHRASE: Record< - ToolCategory, - { verb: string; noun: [singular: string, plural: string] } -> = { - read: { verb: 'Read', noun: ['file', 'files'] }, - write: { verb: 'Edited', noun: ['file', 'files'] }, - search: { verb: 'Ran', noun: ['search', 'searches'] }, - run: { verb: 'Ran', noun: ['command', 'commands'] }, - fetch: { verb: 'Fetched', noun: ['page', 'pages'] }, - browse: { verb: 'Browsed', noun: ['page', 'pages'] }, - other: { verb: 'Ran', noun: ['step', 'steps'] }, +const STEPS_KEY = { + one: 'conversations.tools.steps.one', + other: 'conversations.tools.steps.other', }; +const STEPS_EN = { one: '{count} step', other: '{count} steps' }; + +/** "3 steps" in the caller's locale. */ +export function formatStepCount(count: number, t?: Translate): string { + const form = count === 1 ? 'one' : 'other'; + const template = t ? t(STEPS_KEY[form], STEPS_EN[form]) : STEPS_EN[form]; + return fillPlaceholders(template, { count: String(count) }); +} /** - * Summarize a group of consecutive tool rows into a single Hermes-style - * header — "Viewed 2 files", "Ran 3 commands", or, for a mixed group, the - * distinct category phrases joined ("Edited a file, read a file"). A - * single-row group defers to that row's specific label (more informative - * than a generic count). Pure + deterministic for unit testing. + * Summarize a group of tool rows for a timeline header. + * + * One row reads as that row's own label. Several read as a step count plus + * the distinct things done, most frequent first, e.g. "6 steps · Read file + * ×3, Searched the web ×2, Ran command". Labels come from the registry, so + * the summary is translated with the rows and never invents a category + * phrase that disagrees with them. */ -export function summarizeToolGroup(entries: ToolTimelineEntry[]): string { +export function summarizeToolGroup(entries: ToolTimelineEntry[], t?: Translate): string { if (entries.length === 0) return ''; - if (entries.length === 1) { - return formatTimelineEntry(entries[0]).title; - } - // Count per category, preserving first-seen order. - const order: ToolCategory[] = []; - const counts = new Map<ToolCategory, number>(); - for (const entry of entries) { - const cat = categorizeTool(entry.name); - if (!counts.has(cat)) order.push(cat); - counts.set(cat, (counts.get(cat) ?? 0) + 1); - } - const phrases = order.map((cat, i) => { - const n = counts.get(cat) ?? 0; - const { verb, noun } = CATEGORY_PHRASE[cat]; - const word = n === 1 ? noun[0] : noun[1]; - const phrase = `${verb} ${n} ${word}`; - // Lowercase the leading verb on all but the first phrase so the joined - // sentence reads naturally ("Edited a file, ran 2 commands"). - return i === 0 ? phrase : phrase.charAt(0).toLowerCase() + phrase.slice(1); - }); - return phrases.join(', '); + if (entries.length === 1) return formatTimelineEntry(entries[0], t).title; + return summarizeToolCalls(entries.map(presentTimelineEntry), t); } -export function formatTimelineEntry(entry: ToolTimelineEntry): { title: string; detail?: string } { - const parsedArgs = parseToolArgs(entry.argsBuffer); - - if (entry.name === 'spawn_subagent' && parsedArgs?.agent_id === 'integrations_agent') { - const provider = - inferIntegrationName(parsedArgs.toolkit) ?? inferIntegrationNameFromPrompt(parsedArgs.prompt); - return { - title: provider ? integrationActivityTitle(provider) : 'Checking your connected app', - detail: parsedArgs.prompt?.trim() || entry.detail, - }; - } - - if (entry.name === 'integrations_agent' || entry.name === 'subagent:integrations_agent') { - const provider = - inferIntegrationName(entry.sourceToolName) ?? - inferIntegrationName(parsedArgs?.toolkit) ?? - inferIntegrationNameFromPrompt(entry.detail) ?? - inferIntegrationNameFromPrompt(parsedArgs?.prompt); - - return { - title: provider ? integrationActivityTitle(provider) : 'Checking your connected app', - detail: entry.detail, - }; - } - - if (entry.name === 'subagent:researcher' || entry.name === 'researcher') { - return { title: 'Researching', detail: entry.detail }; - } - if (entry.name === 'agent_prepare_context') { - return { title: 'Preparing context', detail: parsedArgs?.question?.trim() || entry.detail }; - } - if (entry.name === 'subagent:context_scout' || entry.name === 'context_scout') { - return { title: 'Scouting context', detail: entry.detail }; - } - if (entry.name === 'composio_list_connections') { - return { title: 'Viewing your Connections', detail: entry.detail }; - } - if (entry.name === 'subagent:orchestrator' || entry.name === 'orchestrator') { - return { title: 'Planning next steps', detail: entry.detail }; - } - if (entry.name === 'subagent:critic' || entry.name === 'critic') { - return { title: 'Reviewing the work', detail: entry.detail }; - } - if (entry.name === 'subagent:tools_agent' || entry.name === 'tools_agent') { - return { title: 'Using tools', detail: entry.detail }; - } - if (entry.name === 'subagent:code_executor' || entry.name === 'code_executor') { - return { title: 'Running code', detail: entry.detail }; - } - - if (entry.name.startsWith('delegate_')) { - const provider = - inferIntegrationName(parsedArgs?.toolkit) ?? - inferIntegrationNameFromPrompt(parsedArgs?.prompt) ?? - inferIntegrationName(entry.name); - - const title = provider ? integrationActivityTitle(provider) : humanizeIdentifier(entry.name); - return { title, detail: entry.detail ?? parsedArgs?.prompt }; +/** + * The multi-step summary over already-resolved presentations; shared by the + * processing panel and the chat's tool timeline header. + */ +export function summarizeToolCalls(presentations: ToolCallPresentation[], t?: Translate): string { + if (presentations.length === 0) return ''; + if (presentations.length === 1) return toolLabel(presentations[0], t); + const counts = new Map<string, number>(); + for (const presentation of presentations) { + const label = toolLabel({ ...presentation, tense: 'done' }, t); + counts.set(label, (counts.get(label) ?? 0) + 1); } + const parts = [...counts.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([label, n]) => (n > 1 ? `${label} ×${n}` : label)); + if (counts.size > 3) parts.push('…'); + return `${formatStepCount(presentations.length, t)} · ${parts.join(', ')}`; +} - // A connected-service action called directly (`GMAIL_SEND_EMAIL`, - // `SLACK_SEND_MESSAGE`): the orchestrator finds these through - // `tool_search` and calls them itself, so this is the row a user sees - // for "send that email". Label it by the service, with the action as - // the detail, rather than a raw humanised slug. - const directAction = inferIntegrationActionName(entry.name); - if (directAction) { +/** + * Title and detail for one timeline row. The title is tense-aware ("Reading + * file" while running, "Read file" once settled); the detail is the row's + * target, or for a delegation the full prompt the agent was given. + */ +export function formatTimelineEntry( + entry: ToolTimelineEntry, + t?: Translate +): { title: string; detail?: string } { + const presentation = presentTimelineEntry(entry); + const title = toolLabel(presentation, t); + if (presentation.category === 'agent' || presentation.source === 'agent') { + // A delegation's detail is the whole brief the agent was given, not a + // capped chip: the rail shows it under the row. return { - title: integrationActivityTitle(directAction.provider), - detail: entry.detail ?? directAction.action, + title, + detail: entry.detail ?? promptFromArgsBuffer(entry.argsBuffer) ?? presentation.chip, }; } - - // ── Tool-specific formatting with args-derived detail ────────────── - // Pass the completed result text so args-aware formatters can surface - // details only known post-execution (e.g. the resolved search provider). - const toolDetail = formatToolDetail(entry.name, parsedArgs, entry.result); - if (toolDetail) { - return { title: toolDetail.title, detail: toolDetail.detail ?? entry.detail }; - } - - return { - title: entry.displayName ?? humanizeIdentifier(entry.name), - detail: entry.detail ?? parsedArgs?.prompt, - }; + return { title, detail: presentation.chip ?? entry.detail }; } /** * A render block for the "View processing" panel — either a prose block * (the agent's narration or hidden reasoning) or a group of consecutive - * tool rows under a Hermes-style summary. {@link buildProcessingBlocks} - * derives an ordered list of these from the interleaved transcript. + * tool rows under a summary. {@link buildProcessingBlocks} derives an + * ordered list of these from the interleaved transcript. */ type ProcessingBlock = | { kind: 'narration'; key: string; text: string } @@ -368,9 +158,9 @@ type ProcessingBlock = /** * Turn the ordered transcript (narration / thinking / tool-call pointers) - * plus the tool timeline into the interleaved Hermes render model: prose - * flows inline, and runs of consecutive tool calls collapse into one group - * with a summary header. Tool pointers are resolved against `entries` by id; + * plus the tool timeline into the interleaved render model: prose flows + * inline, and runs of consecutive tool calls collapse into one group with a + * summary header. Tool pointers are resolved against `entries` by id; * unknown ids are skipped. Pure + deterministic for unit testing. * * When `transcript` is empty (legacy snapshot / pre-streaming row), returns a @@ -378,13 +168,14 @@ type ProcessingBlock = */ export function buildProcessingBlocks( transcript: PersistedTranscriptItem[], - entries: ToolTimelineEntry[] + entries: ToolTimelineEntry[], + t?: Translate ): ProcessingBlock[] { const byId = new Map(entries.map(e => [e.id, e])); if (transcript.length === 0) { return entries.length > 0 - ? [{ kind: 'toolGroup', key: 'all', summary: summarizeToolGroup(entries), entries }] + ? [{ kind: 'toolGroup', key: 'all', summary: summarizeToolGroup(entries, t), entries }] : []; } @@ -397,7 +188,7 @@ export function buildProcessingBlocks( blocks.push({ kind: 'toolGroup', key: `tg-${group[0].id}`, - summary: summarizeToolGroup(group), + summary: summarizeToolGroup(group, t), entries: group, }); group = []; @@ -433,54 +224,67 @@ export function buildProcessingBlocks( } export function promptFromArgsBuffer(argsBuffer?: string): string | undefined { - return parseToolArgs(argsBuffer)?.prompt?.trim() || undefined; + const prompt = parseArgsObject(argsBuffer)?.prompt; + return typeof prompt === 'string' ? prompt.trim() || undefined : undefined; } -/** A web source an agent fetched/browsed during a run. */ +/** A web source an agent fetched, browsed or found during a run. */ export interface AgentSource { - /** Stable id (the originating timeline entry id). */ + /** Stable id (the originating timeline entry id, plus a hit index for searches). */ id: string; - /** Display title — the URL hostname. */ + /** Display title — the page title for a search hit, else the URL hostname. */ title: string; /** Full URL. */ url: string; } /** Tools whose `url` arg represents a real web source the agent visited. */ -const URL_SOURCE_TOOLS = new Set(['web_fetch', 'http_request', 'curl', 'browser', 'browser_open']); +const URL_SOURCE_TOOLS = new Set([ + 'web_fetch', + 'http_request', + 'curl', + 'browser', + 'browser_open', + 'tinyfish_fetch', + 'gitbooks_get_page', +]); /** - * Extract the distinct web sources an agent run touched, for the - * "Agent Process Source" panel. Derived from real `url` args on - * fetch/browse timeline entries — never fabricated. Deduplicated by URL, - * preserving first-seen order. + * Extract the distinct web sources an agent run touched, for the sources + * list under an answer. Two kinds, both from real data, never fabricated: + * the `url` argument of fetch/browse calls, and the hits a completed web + * search returned. Deduplicated by URL, first-seen order. */ export function extractAgentSources(entries: ToolTimelineEntry[]): AgentSource[] { const seen = new Set<string>(); const sources: AgentSource[] = []; + const add = (source: AgentSource) => { + // `url` is model- or provider-supplied — prompt-injection-influenceable + // and not guaranteed to be a real web address. Only http(s) sources may + // reach an `<a href>`, so a `javascript:` / `data:` / `file:` value never + // becomes clickable. + if (!source.url || seen.has(source.url) || !isHttpUrl(source.url)) return; + seen.add(source.url); + sources.push(source); + }; for (const entry of entries) { - const baseName = entry.name.replace(/^subagent:/, ''); - if (!URL_SOURCE_TOOLS.has(baseName)) continue; - const url = parseToolArgs(entry.argsBuffer)?.url?.trim(); - // `url` is the raw tool-call argument the model emitted — it is - // prompt-injection-influenceable and not guaranteed to be a real web - // address. Only surface http(s) sources as clickable links so a - // `javascript:` / `data:` / `file:` value can never reach an `<a href>`. - if (!url || seen.has(url) || !isHttpUrl(url)) continue; - seen.add(url); - sources.push({ id: entry.id, title: hostnameFromUrl(url) ?? url, url }); + const presentation = presentTimelineEntry(entry); + if (presentation.body === 'webSearch' && entry.status === 'success') { + const parsed = parseWebSearchResult(entry.result, entry.structured); + parsed?.results.forEach((hit, index) => + add({ id: `${entry.id}#${index}`, title: hit.title, url: hit.url }) + ); + continue; + } + if (!URL_SOURCE_TOOLS.has(presentation.baseName)) continue; + const url = parseArgsObject(entry.argsBuffer)?.url; + if (typeof url !== 'string') continue; + const trimmed = url.trim(); + add({ id: entry.id, title: hostnameFromUrl(trimmed) ?? trimmed, url: trimmed }); } return sources; } -const MAX_DETAIL_LEN = 120; - -function truncateDetail(value: string): string { - const cleaned = value.trim().replace(/\s+/g, ' '); - if (cleaned.length <= MAX_DETAIL_LEN) return cleaned; - return `${cleaned.slice(0, MAX_DETAIL_LEN - 1)}…`; -} - function hostnameFromUrl(url: string): string | undefined { try { return new URL(url).hostname; @@ -499,285 +303,14 @@ function isHttpUrl(url: string): boolean { } } -function shortenPath(filePath: string): string { - const parts = filePath.split('/'); - if (parts.length <= 3) return filePath; - return `…/${parts.slice(-2).join('/')}`; -} - -/** Upper bound on a provider label, so a malformed marker can't blow up a row. */ -const MAX_SEARCH_PROVIDER_LENGTH = 32; - -/** - * Extract the resolved search provider from a completed web-search result. - * Every search engine tags its output with a `(via <Provider>)` marker on the - * heading line (managed resolves to "Exa" by default, or to whatever the - * backend reports; BYOK engines tag "Brave"/"Querit"/"Seltz"/"Tavily"). Reading it back - * keeps the timeline attribution dynamic: it is driven by what actually ran, - * never by a hardcoded provider name (#5136). - * - * Only the first line is inspected, and only its *trailing* marker, so neither - * a `(via …)` string inside a result excerpt nor one inside the echoed query - * (`Search results for: login (via OAuth) (via Exa)`) can be mistaken for the - * provider. Returns `undefined` while the call is still running (no result - * yet) or if no marker is present. - */ -export function extractSearchProvider(result: string | undefined): string | undefined { - if (!result) return undefined; - const headingLine = result.split('\n', 1)[0]; - const provider = headingLine?.match(/\(via ([^)]+)\)\s*$/i)?.[1]?.trim(); - if (!provider || provider.length > MAX_SEARCH_PROVIDER_LENGTH) return undefined; - return provider; -} - -function formatToolDetail( - name: string, - args: ParsedToolArgs | null, - result?: string -): { title: string; detail?: string } | null { - switch (name) { - case 'shell': - case 'node_exec': - case 'npm_exec': { - const cmd = args?.command?.trim(); - return { title: 'Running command', detail: cmd ? truncateDetail(cmd) : undefined }; - } - - case 'web_fetch': - case 'http_request': - case 'curl': { - const url = args?.url?.trim(); - const host = url ? hostnameFromUrl(url) : undefined; - return { - title: host ? `Fetching ${host}` : 'Fetching', - detail: url ? truncateDetail(url) : undefined, - }; - } - - // `web_search_tool` is the name the core streams; `web_search` is kept for - // the settings-family id and older persisted rows. - case 'web_search': - case 'web_search_tool': { - const query = args?.query?.trim(); - // Once the call completes, attribute the search to the provider that - // actually served it ("Searched with Exa"); the query moves to the - // detail line so it stays visible. - const provider = extractSearchProvider(result); - if (provider) { - return { - title: `Searched with ${provider}`, - detail: query ? truncateDetail(query) : undefined, - }; - } - return { title: query ? `Searching: ${truncateDetail(query)}` : 'Searching the web' }; - } - - case 'gitbooks_search': { - const query = args?.query?.trim(); - return { title: query ? `Searching docs: ${truncateDetail(query)}` : 'Searching docs' }; - } - - case 'file_read': { - const p = args?.path?.trim() ?? args?.file_path?.trim(); - return { title: 'Reading file', detail: p ? shortenPath(p) : undefined }; - } - - case 'file_write': - case 'vault_write_markdown': { - const p = args?.path?.trim() ?? args?.file_path?.trim(); - return { title: 'Writing file', detail: p ? shortenPath(p) : undefined }; - } - - case 'edit': - case 'apply_patch': { - const p = args?.path?.trim() ?? args?.file_path?.trim(); - return { title: 'Editing file', detail: p ? shortenPath(p) : undefined }; - } - - case 'grep': { - const pat = args?.pattern?.trim(); - return { title: pat ? `Searching: ${truncateDetail(pat)}` : 'Searching code' }; - } - - case 'glob': { - const pat = args?.pattern?.trim(); - return { title: pat ? `Finding: ${truncateDetail(pat)}` : 'Finding files' }; - } - - case 'list': { - const p = args?.path?.trim(); - return { title: 'Listing directory', detail: p ? shortenPath(p) : undefined }; - } - - case 'git_operations': { - const cmd = args?.command?.trim(); - if (cmd) { - const verb = cmd.split(/\s+/)[0]; - return { title: `Git ${verb}`, detail: truncateDetail(cmd) }; - } - return { title: 'Git operation' }; - } - - case 'browser': - case 'browser_open': { - const url = args?.url?.trim(); - const host = url ? hostnameFromUrl(url) : undefined; - return { title: host ? `Browsing ${host}` : 'Browsing' }; - } - - case 'image_info': - return { title: 'Analyzing image' }; - - case 'install_tool': { - const tn = args?.tool_name?.trim(); - return { title: tn ? `Installing ${tn}` : 'Installing tool' }; - } - - case 'lsp': - return { title: 'Code intelligence' }; - - case 'run_tests': - return { title: 'Running tests' }; - - case 'run_linter': - return { title: 'Running linter' }; - - case 'read_diff': - return { title: 'Reading diff' }; - - default: - return null; - } -} - -/** - * Recognise the small set of known integration toolkit slugs. Used to - * gate `inferIntegrationName` so unknown `delegate_<x>` names (e.g. - * `delegate_summarize`, `delegate_router`) don't get fake-humanised - * into bogus "integration" labels in the tool timeline. - */ -// Composio's own slugs have NO separator inside a multi-word toolkit: -// `GOOGLECALENDAR_EVENTS_LIST`, not `GOOGLE_CALENDAR_EVENTS_LIST`. Only the -// underscored spellings were listed here, so `GMAIL_*` and `DISCORD_*` -// resolved while every `GOOGLECALENDAR_*` action fell through to the raw -// humanizer and rendered "GOOGLECALENDAR EVENTS LIST". Both spellings are -// kept: the underscored ones are how `delegate_<toolkit>` names arrive. -const KNOWN_TOOLKIT_RE = - /^(gmail|notion|github|slack|discord|linear|jira|google_calendar|googlecalendar|google_drive|googledrive|calendar)$/i; - -function inferIntegrationName(input?: string): string | undefined { - if (!input) return undefined; - - const delegateMatch = input.match(/^delegate_(.+)$/); - if (delegateMatch && KNOWN_TOOLKIT_RE.test(delegateMatch[1])) { - return normalizeIntegrationName(delegateMatch[1]); - } - - if (KNOWN_TOOLKIT_RE.test(input)) { - return normalizeIntegrationName(input); - } - - return undefined; -} - -/** - * Split a Composio action slug (`GMAIL_SEND_EMAIL`) into its known provider - * and a readable action ("Send email"). `undefined` for anything that is not - * an upper-case `<TOOLKIT>_<ACTION>` name on a known toolkit, so ordinary - * tools and unknown toolkits keep their generic label. - */ -export function inferIntegrationActionName( - name: string -): { provider: string; action: string } | undefined { - if (!/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(name)) return undefined; - // Try the longest toolkit prefix first (`GOOGLE_CALENDAR_...`), then the - // shortest (`GMAIL_...`). - const parts = name.split('_'); - for (let i = Math.min(parts.length - 1, 2); i >= 1; i -= 1) { - const toolkit = parts.slice(0, i).join('_'); - if (KNOWN_TOOLKIT_RE.test(toolkit)) { - const action = parts.slice(i).join(' ').toLowerCase(); - return { - provider: normalizeIntegrationName(toolkit), - action: action.charAt(0).toUpperCase() + action.slice(1), - }; - } - } - return undefined; -} - -function integrationActivityTitle(provider: string): string { - switch (provider) { - case 'GitHub': - case 'Gmail': - case 'Linear': - case 'Jira': - return `Making requests to your ${provider} account`; - case 'Notion': - return 'Working in your Notion workspace'; - case 'Slack': - case 'Discord': - return `Working in your ${provider} workspace`; - case 'Google Calendar': - return 'Updating your Google Calendar'; - case 'Google Drive': - return 'Working in your Google Drive'; - default: - return `Checking your ${provider}`; - } -} - -function inferIntegrationNameFromPrompt(prompt?: string): string | undefined { - if (!prompt) return undefined; - const known = [ - 'Notion', - 'Gmail', - 'GitHub', - 'Slack', - 'Discord', - 'Linear', - 'Jira', - 'Google Calendar', - 'Google Drive', - ]; - - const lower = prompt.toLowerCase(); - return known.find(name => lower.includes(name.toLowerCase())); -} - -function parseToolArgs(argsBuffer?: string): ParsedToolArgs | null { +function parseArgsObject(argsBuffer?: string): Record<string, unknown> | null { if (!argsBuffer) return null; try { - const parsed = JSON.parse(argsBuffer) as ParsedToolArgs; - return parsed && typeof parsed === 'object' ? parsed : null; + const parsed: unknown = JSON.parse(argsBuffer); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? (parsed as Record<string, unknown>) + : null; } catch { return null; } } - -function normalizeIntegrationName(value: string): string { - switch (value.toLowerCase()) { - case 'github': - return 'GitHub'; - case 'gmail': - return 'Gmail'; - case 'google_calendar': - case 'googlecalendar': - case 'calendar': - return 'Google Calendar'; - case 'google_drive': - case 'googledrive': - return 'Google Drive'; - default: - return humanizeIdentifier(value); - } -} - -function humanizeIdentifier(value: string | undefined | null): string { - if (!value) return ''; - return value - .replace(/^subagent:/, '') - .replace(/^delegate_/, '') - .replace(/_/g, ' ') - .replace(/\b\w/g, char => char.toUpperCase()); -} diff --git a/app/test/playwright/specs/tool-call-presentation.spec.ts b/app/test/playwright/specs/tool-call-presentation.spec.ts new file mode 100644 index 00000000000..c36d8f37eb2 --- /dev/null +++ b/app/test/playwright/specs/tool-call-presentation.spec.ts @@ -0,0 +1,175 @@ +/** + * Tool-call presentation, end to end. + * + * Drives a real core against the mock backend: the mock LLM calls the + * managed web search and a file read, the core executes both, and the chat + * renders them through assistant-ui's tool-timeline, tool-call and + * web-search elements. Pins what the mislabelling bugs broke: the search reads + * "Searched the web" (not a raw name), its hits render as the web-search + * element, and a settled step reads in the past tense. + */ +import { expect, type Page, test } from '@playwright/test'; + +import { + bootAuthenticatedPage, + dismissWalkthroughIfPresent, + waitForAppReady, +} from '../helpers/core-rpc'; + +const MOCK_ADMIN_BASE = `http://127.0.0.1:${process.env.E2E_MOCK_PORT || '18473'}`; +const USER_ID = 'pw-tool-call-presentation'; + +async function resetMock(): Promise<void> { + await fetch(`${MOCK_ADMIN_BASE}/__admin/reset`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); +} + +async function setMockBehavior(key: string, value: string): Promise<void> { + await fetch(`${MOCK_ADMIN_BASE}/__admin/behavior`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key, value }), + }); +} + +async function openChat(page: Page): Promise<void> { + await bootAuthenticatedPage(page, USER_ID, '/chat'); + await page.goto('/#/chat'); + await waitForAppReady(page); + await dismissWalkthroughIfPresent(page); + await expect(page.getByTestId('chat-message-input')).toBeVisible(); +} + +async function selectedThreadId(page: Page): Promise<string | null> { + return page.evaluate(() => { + const store = ( + window as unknown as { + __OPENHUMAN_STORE__?: { + getState?: () => { thread?: { selectedThreadId?: string | null } }; + }; + } + ).__OPENHUMAN_STORE__; + return store?.getState?.().thread?.selectedThreadId ?? null; + }); +} + +async function createNewThread(page: Page): Promise<string> { + const before = await selectedThreadId(page); + await dismissWalkthroughIfPresent(page); + const sidebarButton = page.getByTestId('new-thread-sidebar-button'); + if (await sidebarButton.isVisible().catch(() => false)) { + await sidebarButton.click({ force: true }); + } else { + await page.getByTestId('new-thread-button').click({ force: true }); + } + const changed = await expect + .poll( + async () => { + const current = await selectedThreadId(page); + return current && current !== before ? current : null; + }, + { timeout: 10_000 } + ) + .not.toBeNull() + .then( + () => true, + () => false + ); + const id = await selectedThreadId(page); + if (changed && id) return id; + if (id) return id; + if (before) return before; + throw new Error('selectedThreadId was not populated'); +} + +async function waitForSocketConnected(page: Page): Promise<void> { + await expect + .poll( + async () => + page.evaluate(() => { + const store = ( + window as unknown as { + __OPENHUMAN_STORE__?: { + getState?: () => { socket?: { byUser?: Record<string, { status?: string }> } }; + }; + } + ).__OPENHUMAN_STORE__; + const byUser = store?.getState?.().socket?.byUser ?? {}; + return Object.values(byUser).some(entry => entry?.status === 'connected'); + }), + { timeout: 30_000 } + ) + .toBe(true); +} + +async function sendMessage(page: Page, prompt: string): Promise<void> { + await waitForSocketConnected(page); + await dismissWalkthroughIfPresent(page); + await page.getByTestId('chat-message-input').fill(prompt); + await dismissWalkthroughIfPresent(page); + await expect(page.getByTestId('send-message-button')).toBeEnabled(); + await page.getByTestId('send-message-button').click(); +} + +test.describe('Tool-call presentation', () => { + test.beforeEach(async ({ page }) => { + await resetMock(); + await openChat(page); + await createNewThread(page); + }); + + test('renders a web search and a file read as labelled timeline steps', async ({ page }) => { + const CANARY = 'canary-tool-presentation-7f3e'; + const forced = [ + { + content: '', + toolCalls: [ + { + id: 'call_web_search_1', + name: 'web_search_tool', + arguments: JSON.stringify({ query: 'rust async traits' }), + }, + { + id: 'call_file_read_1', + name: 'file_read', + arguments: JSON.stringify({ path: 'e2e/definitely-missing/README.md' }), + }, + ], + }, + { content: `Here is what I found. ${CANARY}` }, + ]; + await setMockBehavior('llmForcedResponses', JSON.stringify(forced)); + await setMockBehavior('llmStreamChunkDelayMs', '10'); + + await sendMessage(page, 'search the web for rust async traits and read the README'); + await expect(page.getByText(CANARY).last()).toBeVisible({ timeout: 60_000 }); + + const timeline = page.getByTestId('tool-timeline').last(); + await expect(timeline).toBeVisible(); + // Settled summary, not "2 tool calls". + await expect(timeline).toContainText('2 steps'); + + const calls = page.getByTestId('assistant-ui-tool-call'); + const search = calls.filter({ hasText: 'Searched the web' }); + await expect(search).toHaveCount(1); + await expect(search.getByText('rust async traits').first()).toBeVisible(); + // The hits render through the web-search element as links. + const results = page.getByTestId('web-search-results'); + await expect(results).toBeVisible(); + await expect(results.getByTestId('web-search-hit').first()).toBeVisible(); + + // The file read settled (it fails on a missing path) and reads in the + // past tense, never the raw tool name. + const read = calls.filter({ hasText: 'Read file' }); + await expect(read).toHaveCount(1); + await expect(page.getByText('file_read', { exact: true })).toHaveCount(0); + await expect(page.getByText('web_search_tool', { exact: true })).toHaveCount(0); + + if (process.env.PW_TOOL_SCREENSHOT) { + await timeline.screenshot({ path: process.env.PW_TOOL_SCREENSHOT }); + } + }); +}); diff --git a/crates/openhuman-core/src/agent/messages.rs b/crates/openhuman-core/src/agent/messages.rs index 87daccc16b3..ea3d937981b 100644 --- a/crates/openhuman-core/src/agent/messages.rs +++ b/crates/openhuman-core/src/agent/messages.rs @@ -14,6 +14,12 @@ const REPLAYED_METADATA_KEY: &str = "openhuman_replayed"; const WRAPPED_VALUE_KEY: &str = "openhuman_wrapped_value"; const WRAPPED_FLAG: &str = "wrapped"; +/// Durable `extra_metadata` key on a text-dialect `[Tool results]` user row: +/// the call ids whose results in that row failed. The per-result analogue of +/// the `tool_failure` a native `tool` row carries; written by the session codec +/// and read by the thread transcript projection. +pub(crate) const TOOL_RESULT_FAILURES_METADATA_KEY: &str = "openhuman_tool_failures"; + fn would_wrap(message: &ChatMessage) -> bool { matches!(&message.extra_metadata, Some(value) if !value.is_object()) } diff --git a/crates/openhuman-core/src/agent/progress.rs b/crates/openhuman-core/src/agent/progress.rs index 014f0c760ba..c77c421dda1 100644 --- a/crates/openhuman-core/src/agent/progress.rs +++ b/crates/openhuman-core/src/agent/progress.rs @@ -74,6 +74,21 @@ pub enum AgentProgress { /// the chat "View processing" timeline renders. `None` on success and /// on legacy snapshots. See `crate::tools::status`. failure: Option<crate::tools::status::ClassifiedFailure>, + /// Server-computed human label recomputed from the tool's OWN + /// [`tinytools::Tool::display_label`] using the real call arguments + /// (the matching `ToolCallStarted.display_label` was computed with no + /// arguments, since the harness start event carries none). Forwarded + /// on the wire as `tool_display_label` so a completed row can pick up + /// a label that only became knowable once the arguments existed. + display_label: Option<String>, + /// Server-computed contextual detail (e.g. "steven@gmail.com"), + /// recomputed the same way from `Tool::display_detail`. + display_detail: Option<String>, + /// Structured, tool-specific result payload copied from + /// [`tinytools::ToolResult::metadata`] when it is a JSON object + /// carrying a `"kind"` discriminator (e.g. `{"kind":"web_search",...}`). + /// `None` for tools that don't populate metadata of that shape. + structured: Option<serde_json::Value>, }, /// A sub-agent was spawned during tool execution. @@ -249,6 +264,14 @@ pub enum AgentProgress { /// a failed sub-agent row carries the same "why + what to do next" copy /// instead of discarding the already-computed classification (#4459). failure: Option<crate::tools::status::ClassifiedFailure>, + /// Mirrors [`Self::ToolCallCompleted::display_label`], recomputed from + /// the child tool's own `Tool::display_label` using the real call + /// arguments. + display_label: Option<String>, + /// Mirrors [`Self::ToolCallCompleted::display_detail`]. + display_detail: Option<String>, + /// Mirrors [`Self::ToolCallCompleted::structured`]. + structured: Option<serde_json::Value>, }, /// A chunk of a sub-agent's visible assistant text arrived from the diff --git a/crates/openhuman-core/src/agent/progress_tracing/journal_projection.rs b/crates/openhuman-core/src/agent/progress_tracing/journal_projection.rs index a631afb453c..55c2a0cfa95 100644 --- a/crates/openhuman-core/src/agent/progress_tracing/journal_projection.rs +++ b/crates/openhuman-core/src/agent/progress_tracing/journal_projection.rs @@ -217,6 +217,9 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: *latency_ms, iteration: scope.iteration, failure: None, + display_label: Some("Searching tools".to_string()), + display_detail: None, + structured: None, }, ], None => vec![ @@ -238,6 +241,9 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: *latency_ms, iteration: state.iteration, failure: None, + display_label: Some("Searching tools".to_string()), + display_detail: None, + structured: None, }, ], } @@ -300,6 +306,12 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: duration_ms.unwrap_or(0), iteration: scope.iteration, failure, + // The journal has no live tool registry to recompute a + // real label/detail from, and no `ToolResult.metadata` to + // replay structured payloads from. + display_label: None, + display_detail: None, + structured: None, }], None => vec![AgentProgress::ToolCallCompleted { call_id: call_id.as_str().to_string(), @@ -311,6 +323,9 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: duration_ms.unwrap_or(0), iteration: state.iteration, failure, + display_label: None, + display_detail: None, + structured: None, }], } } @@ -345,8 +360,8 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V tool_name: requested_name.clone(), arguments: arguments.clone(), iteration: scope.iteration, - display_label: Some(label), - display_detail: detail, + display_label: Some(label.clone()), + display_detail: detail.clone(), }, AgentProgress::SubagentToolCallCompleted { agent_id: scope.agent_id.clone(), @@ -360,6 +375,9 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: 0, iteration: scope.iteration, failure, + display_label: Some(label.clone()), + display_detail: detail.clone(), + structured: None, }, ], None => vec![ @@ -368,8 +386,8 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V tool_name: requested_name.clone(), arguments: arguments.clone(), iteration: state.iteration, - display_label: Some(label), - display_detail: detail, + display_label: Some(label.clone()), + display_detail: detail.clone(), }, AgentProgress::ToolCallCompleted { call_id: call_id.as_str().to_string(), @@ -381,6 +399,9 @@ fn observation_to_progress(obs: &AgentObservation, state: &mut ReplayState) -> V elapsed_ms: 0, iteration: state.iteration, failure, + display_label: Some(label), + display_detail: detail, + structured: None, }, ], } diff --git a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_attribution_tests.rs b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_attribution_tests.rs index 3166d6c8581..6163db9d7be 100644 --- a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_attribution_tests.rs +++ b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_attribution_tests.rs @@ -89,6 +89,9 @@ fn tool_io_is_captured_when_capture_content_is_on() { elapsed_ms: 4, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }, 4, ); @@ -138,6 +141,9 @@ fn tool_io_is_never_recorded_when_capture_content_is_off() { elapsed_ms: 4, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }, 4, ), @@ -413,6 +419,9 @@ fn failed_tool_records_classified_cause_only_when_capture_on() { next_action: "Try again".to_string(), recoverable: true, }), + display_label: None, + display_detail: None, + structured: None, }; // Capture ON → plain-language cause lands as error.message. @@ -581,6 +590,9 @@ fn parent_tool_completion_backfills_arguments_and_records_output() { elapsed_ms: 40, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }, 45, ), diff --git a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_span_tree_tests.rs b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_span_tree_tests.rs index 6e5e7e01231..429be231bab 100644 --- a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_span_tree_tests.rs +++ b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_span_tree_tests.rs @@ -219,6 +219,9 @@ fn subagent_lifecycle_nests_under_the_turn() { elapsed_ms: 40, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }, 30, ), diff --git a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_tests.rs b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_tests.rs index 3a1eb54eaad..b8b54508248 100644 --- a/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_tests.rs +++ b/crates/openhuman-core/src/agent/progress_tracing/progress_tracing_tests.rs @@ -57,6 +57,9 @@ fn tool_completed( elapsed_ms: elapsed, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, } } diff --git a/crates/openhuman-core/src/agent/session_host/codec.rs b/crates/openhuman-core/src/agent/session_host/codec.rs index bd553be830b..34eae1f95dd 100644 --- a/crates/openhuman-core/src/agent/session_host/codec.rs +++ b/crates/openhuman-core/src/agent/session_host/codec.rs @@ -6,14 +6,18 @@ use crate::agent::{ message_convert, - messages::{chat_message_from_transcript, transcript_message_from_chat}, + messages::{ + chat_message_from_transcript, transcript_message_from_chat, + TOOL_RESULT_FAILURES_METADATA_KEY, + }, tinyagents::host::OpenHumanRunContext, }; use tinyagents_runtime::{RuntimeError, TranscriptCodec, TranscriptTurnOptions}; use tinyagents_session::transcript::{ - MessageUsage, SessionTranscript, ToolFailure, TranscriptMessage, TurnUsage, + MessageUsage, SessionTranscript, ToolFailure, TranscriptMessage, TranscriptToolCall, TurnUsage, }; use tinyinference_llm::message::Message; +use tinytools_agent::dialect::parse_replayed_results; /// Converts OpenHuman's durable transcript rows at the TinyAgents boundary. #[derive(Default)] @@ -51,6 +55,7 @@ impl TranscriptCodec<OpenHumanRunContext> for OpenHumanTranscriptCodec { // keeps non-prefix messages. New messages alone receive this turn's // request correlation id. let mut consumed = vec![false; previous.len().min(prior.len())]; + let mut fresh = vec![false; rows.len()]; for (next_index, next_message) in next.iter().enumerate() { let matched = previous.iter().enumerate().take(consumed.len()).find_map( |(previous_index, previous_message)| { @@ -63,6 +68,7 @@ impl TranscriptCodec<OpenHumanRunContext> for OpenHumanTranscriptCodec { consumed[previous_index] = true; } else { rows[next_index].request_id = options.request_id.clone(); + fresh[next_index] = true; } } // The generic inference `Message::Tool` intentionally carries only a @@ -70,11 +76,13 @@ impl TranscriptCodec<OpenHumanRunContext> for OpenHumanTranscriptCodec { // sidecar preserves the execution failure bit until this persistence // boundary, so resumed transcript rows retain the same failure status // the live tool timeline observed. - let failures = options + let sidecar = options .context .session_sidecar .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(); + let failures = sidecar .tool_outcomes .iter() .filter(|outcome| !outcome.success) @@ -88,6 +96,12 @@ impl TranscriptCodec<OpenHumanRunContext> for OpenHumanTranscriptCodec { }); } } + attach_text_dialect_rounds( + &mut rows, + &fresh, + &sidecar.tool_outcomes, + sidecar.resolved_route.as_ref(), + ); Ok(rows) } @@ -173,9 +187,139 @@ impl TranscriptCodec<OpenHumanRunContext> for OpenHumanTranscriptCodec { // as an interim step followed by duplicate, never-settled tool // rows (which also mis-paired later FIFO results). Each call is // already recorded, once, in the envelope of the assistant row - // that issued it. + // that issued it (or in a provenance-only usage record for text + // dialects). tool_calls: Vec::new(), iteration: sidecar.model_calls.min(u32::MAX as usize) as u32, })) } } + +/// Give each text-dialect tool round's calls to the assistant row that issued +/// them, and record which of its results failed. +/// +/// A native round is persisted as a `{content, tool_calls}` envelope followed +/// by `tool` rows, so its calls and failure bits already sit on the right rows. +/// A text dialect (`xml`, `pformat`, code) persists its replay form instead: the +/// issuing assistant row holds only prose, and every result of the round is +/// folded into one `[Tool results]` user row. Neither shape can say which calls +/// were made or which of them failed, so this reads the round's call ids back +/// out of that results row and takes names, arguments and outcomes from the +/// turn sidecar: +/// +/// - the issuing row (the fresh assistant row directly before the results row) +/// gets a provenance-only [`TurnUsage`] — zero spend, since the turn's spend +/// is recorded once on its final row — whose `tool_calls` are this round's; +/// - the results row gets the ids of its failed results under +/// [`TOOL_RESULT_FAILURES_METADATA_KEY`], the per-result analogue of a native +/// `tool` row's `tool_failure`. +/// +/// Rows carried over from a previous turn are left untouched. +fn attach_text_dialect_rounds( + rows: &mut [TranscriptMessage], + fresh: &[bool], + outcomes: &[crate::agent::tinyagents::ToolCallOutcome], + route: Option<&tinyinference_llm::model::ResolvedModelRoute>, +) { + let mut iteration = 0u32; + for index in 0..rows.len() { + if !fresh[index] { + continue; + } + if rows[index].role == "assistant" { + iteration = iteration.saturating_add(1); + continue; + } + if rows[index].role != "user" { + continue; + } + let Some(results) = parse_replayed_results(&rows[index].content) else { + continue; + }; + let outcome_for = |id: &str| outcomes.iter().find(|outcome| outcome.call_id == id); + + let failed: Vec<serde_json::Value> = results + .iter() + .filter(|result| outcome_for(&result.tool_call_id).is_some_and(|o| !o.success)) + .map(|result| serde_json::Value::String(result.tool_call_id.clone())) + .collect(); + if !failed.is_empty() { + match rows[index] + .extra_metadata + .get_or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())) + { + serde_json::Value::Object(map) => { + map.insert( + TOOL_RESULT_FAILURES_METADATA_KEY.to_string(), + serde_json::Value::Array(failed), + ); + } + _ => log::warn!( + "[session_host][codec] text-dialect results row has non-object metadata; \ + failure status not recorded" + ), + } + } + + let Some(issuer) = index.checked_sub(1) else { + continue; + }; + if !fresh[issuer] || rows[issuer].role != "assistant" { + continue; + } + let calls: Vec<TranscriptToolCall> = results + .iter() + .filter_map(|result| outcome_for(&result.tool_call_id)) + .map(|outcome| TranscriptToolCall { + id: outcome.call_id.clone(), + name: outcome.name.clone(), + arguments: outcome.arguments.to_string(), + extra_content: None, + }) + .collect(); + log::debug!( + "[session_host][codec] text-dialect round iteration={iteration} results={} \ + attached_calls={} failed={}", + results.len(), + calls.len(), + rows[index] + .extra_metadata + .as_ref() + .and_then(|meta| meta.get(TOOL_RESULT_FAILURES_METADATA_KEY)) + .and_then(serde_json::Value::as_array) + .map_or(0, Vec::len) + ); + if calls.is_empty() { + continue; + } + if let Some(usage) = rows[issuer].turn_usage.as_mut() { + // The final assistant row already owns the turn's spend. Keep it + // intact and add this text-dialect round's provenance without + // duplicating a call an earlier adapter has recorded. + usage.tool_calls.extend(calls.into_iter().filter(|call| { + !usage + .tool_calls + .iter() + .any(|existing| existing.id == call.id) + })); + } else { + rows[issuer].turn_usage = Some(TurnUsage { + provider: route + .map(|route| route.provider.clone()) + .unwrap_or_default(), + model: route.map(|route| route.model.clone()).unwrap_or_default(), + usage: MessageUsage { + input: 0, + output: 0, + cached_input: 0, + context_window: 0, + cost_usd: 0.0, + }, + ts: chrono::Utc::now().to_rfc3339(), + reasoning_content: None, + tool_calls: calls, + iteration, + }); + } + } +} diff --git a/crates/openhuman-core/src/agent/session_host/tool_progress.rs b/crates/openhuman-core/src/agent/session_host/tool_progress.rs index c64a402b423..158b80a42e7 100644 --- a/crates/openhuman-core/src/agent/session_host/tool_progress.rs +++ b/crates/openhuman-core/src/agent/session_host/tool_progress.rs @@ -183,6 +183,12 @@ impl ProgressReporter for TurnProgress { elapsed_ms, iteration, failure: None, + // The legacy reporter path has no live tool registry or + // captured `ToolResult` to recompute a label or read + // structured metadata from. + display_label: None, + display_detail: None, + structured: None, }, ); } diff --git a/crates/openhuman-core/src/agent/tinyagents/host/progress_sink.rs b/crates/openhuman-core/src/agent/tinyagents/host/progress_sink.rs index c1cf3a6a810..0593c45673e 100644 --- a/crates/openhuman-core/src/agent/tinyagents/host/progress_sink.rs +++ b/crates/openhuman-core/src/agent/tinyagents/host/progress_sink.rs @@ -508,6 +508,14 @@ impl ProgressSink for OpenHumanProgressSink { elapsed_ms, iteration: opened.iteration, failure, + // Same registry gap as the `ToolCallStarted` arm above — + // TODO(phase4): resolve labels from the tool registry. + display_label: None, + display_detail: None, + // The coarse `ProgressEvent` stream carries no + // `ToolResult`, so there is no metadata to copy structured + // payloads from on this path. + structured: None, }) .await; } diff --git a/crates/openhuman-core/src/agent/tinyagents/middleware/tool_outcome_capture.rs b/crates/openhuman-core/src/agent/tinyagents/middleware/tool_outcome_capture.rs index 443d052a60c..c7a2b3894ed 100644 --- a/crates/openhuman-core/src/agent/tinyagents/middleware/tool_outcome_capture.rs +++ b/crates/openhuman-core/src/agent/tinyagents/middleware/tool_outcome_capture.rs @@ -131,6 +131,16 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> let timed_out = combined.contains("timed out"); Some(crate::tools::status::classify(&combined, timed_out)) }; + // Host-only structured payload (e.g. `{"kind":"web_search", ...}`) a + // tool attached via `ToolResult::metadata` for a richer UI + // presentation than plain text allows. Only forwarded when it is a + // JSON object carrying a `"kind"` discriminator, so an arbitrary + // metadata shape a tool sets for its own bookkeeping doesn't leak onto + // the wire as if it were a presentation contract. + let structured = result + .metadata + .clone() + .filter(|v| v.is_object() && v.get("kind").is_some()); if let Ok(mut map) = self.failure_map.lock() { // Keep duration + rendered output size as a compatibility fallback // for old/deserialized completion events; TinyAgents 1.6 supplies @@ -144,6 +154,7 @@ impl Middleware<(), crate::agent::tinyagents::host::OpenHumanRunContext> crate::agent::tinyagents::middleware::tool_result_text(result) .chars() .count(), + structured, ), ); } diff --git a/crates/openhuman-core/src/agent/tinyagents/observability/cap_pauser.rs b/crates/openhuman-core/src/agent/tinyagents/observability/cap_pauser.rs index d5dfeb368b5..6bd6db2726f 100644 --- a/crates/openhuman-core/src/agent/tinyagents/observability/cap_pauser.rs +++ b/crates/openhuman-core/src/agent/tinyagents/observability/cap_pauser.rs @@ -39,17 +39,21 @@ pub(crate) type IterationCursor = Arc<AtomicU32>; /// `tool_name` contract without the forwarder emitting those fragments itself. pub(crate) type ToolNameMap = Arc<Mutex<std::collections::HashMap<String, String>>>; -/// Shared `call_id → (success, classified failure, elapsed_ms, output_chars)` -/// side-channel. The crate's `AgentEvent::ToolCompleted` carries only `call_id` -/// + `tool_name` (no success/error, duration, or output size), so +/// Shared `call_id → (success, classified failure, elapsed_ms, output_chars, +/// structured metadata)` side-channel. The crate's `AgentEvent::ToolCompleted` +/// carries only `call_id` + `tool_name` (no success/error, duration, output +/// size, or `ToolResult.metadata`), so /// /// `ToolOutcomeCaptureMiddleware::after_tool` — which does see the `ToolResult` -/// (including the executor-measured `elapsed_ms` and the rendered content) — -/// classifies each outcome and writes it here; the bridge reads it when -/// projecting the live `ToolCallCompleted` event, so a failed tool surfaces real -/// `success: false` + a user-facing `failure`, and a completed tool surfaces its -/// real duration + output size instead of `0`/`0` (#4467, item 4). Absent entry -/// (event projected before the middleware ran) falls back to `(true, None, 0, 0)`. +/// (including the executor-measured `elapsed_ms`, the rendered content, and +/// its host-only `metadata`) — classifies each outcome and writes it here; the +/// bridge reads it when projecting the live `ToolCallCompleted` event, so a +/// failed tool surfaces real `success: false` + a user-facing `failure`, a +/// completed tool surfaces its real duration + output size instead of `0`/`0` +/// (#4467, item 4), and a tool that populated `ToolResult.metadata` with a +/// `{"kind": ...}` object (e.g. web search) surfaces it as +/// `ToolCallCompleted::structured`. Absent entry (event projected before the +/// middleware ran) falls back to `(true, None, 0, 0, None)`. pub(crate) type ToolFailureMap = Arc< Mutex< std::collections::HashMap< @@ -59,6 +63,7 @@ pub(crate) type ToolFailureMap = Arc< Option<crate::tools::status::ClassifiedFailure>, u64, usize, + Option<serde_json::Value>, ), >, >, diff --git a/crates/openhuman-core/src/agent/tinyagents/observability/event_bridge.rs b/crates/openhuman-core/src/agent/tinyagents/observability/event_bridge.rs index 0d275ce31ce..cabe8a25b4c 100644 --- a/crates/openhuman-core/src/agent/tinyagents/observability/event_bridge.rs +++ b/crates/openhuman-core/src/agent/tinyagents/observability/event_bridge.rs @@ -11,6 +11,7 @@ use tinyinference_llm::usage::Usage; use crate::agent::progress::AgentProgress; use crate::inference::provider::UsageInfo; +use tinytools::humanize_tool_name; use super::cap_pauser::{ IterationCursor, ProviderUsageCarry, SubagentScope, ToolFailureMap, ToolNameMap, @@ -87,6 +88,14 @@ pub(crate) struct OpenhumanEventBridge { /// `ToolStarted` and taken on `ToolCompleted` so the projected completion /// event carries a real `elapsed_ms` (the crate event has no timing). pub(super) tool_started_at: Mutex<std::collections::HashMap<String, std::time::Instant>>, + /// The turn's registered tool sets, retained (cheap `Arc` clones — never + /// the tools themselves) so the bridge can resolve a live `&dyn Tool` by + /// name and call its own [`tinytools::Tool::display_label`] / + /// [`tinytools::Tool::display_detail`] instead of only ever guessing from + /// the bare tool name (issue: tool-call presentation). Empty for a bridge + /// built without a turn's tool sets (e.g. a bare unit-test bridge), in + /// which case every lookup falls back to [`humanize_tool_name`]. + pub(super) tool_sets: Vec<Arc<Vec<Box<dyn tinytools::Tool>>>>, pub(super) state: Mutex<BridgeState>, /// Ordered overflow buffer for progress events that hit backpressure /// (channel `Full`). Once ANY event spills here, `draining` stays set and @@ -123,12 +132,17 @@ impl OpenhumanEventBridge { Arc::default(), Arc::default(), Arc::default(), + Vec::new(), ) } /// Build a bridge, optionally child-scoped, sharing `cursor` (iteration /// attribution) and `tool_names` (tool-call name lookup for the streamed - /// argument fragments) with the model adapter. + /// argument fragments) with the model adapter. `tool_sets` is the turn's + /// registered tool sets (cheap `Arc` clones), used to resolve a live + /// `&dyn Tool` for `display_label`/`display_detail` — pass `Vec::new()` + /// when none are available (e.g. tests). + #[allow(clippy::too_many_arguments)] pub(crate) fn with_scope( on_progress: Option<Sender<AgentProgress>>, model: impl Into<String>, @@ -139,6 +153,7 @@ impl OpenhumanEventBridge { tool_names: ToolNameMap, failure_map: ToolFailureMap, usage_carry: ProviderUsageCarry, + tool_sets: Vec<Arc<Vec<Box<dyn tinytools::Tool>>>>, ) -> Arc<Self> { Arc::new(Self { on_progress, @@ -153,11 +168,52 @@ impl OpenhumanEventBridge { recorded_iterations: Mutex::new(std::collections::HashSet::new()), resolved_calls: Mutex::new(std::collections::HashMap::new()), tool_started_at: Mutex::new(std::collections::HashMap::new()), + tool_sets, state: Mutex::new(BridgeState::default()), overflow: Arc::default(), }) } + /// Resolve `tool_name` against the turn's registered tool sets and + /// compute the presentation pair from the tool's OWN + /// [`tinytools::Tool::display_label`] / [`tinytools::Tool::display_detail`] + /// using `args` (the real call arguments when known, `Null` at call-start + /// before they've arrived). Unknown tools (not found in any set — the + /// unknown-tool-call path never registers one) fall back to a humanized + /// name with no detail, matching the pre-existing behavior. + pub(super) fn resolve_display( + &self, + tool_name: &str, + args: &serde_json::Value, + ) -> (Option<String>, Option<String>) { + match self + .tool_sets + .iter() + .flat_map(|set| set.iter()) + .find(|t| t.name() == tool_name) + { + Some(tool) => { + let label = tool.display_label(args); + let detail = tool.display_detail(args); + tracing::trace!( + tool_name, + label = ?label, + detail = ?detail, + "[tool-presentation] resolved display label/detail from registered tool" + ); + (label, detail) + } + None => { + tracing::debug!( + tool_name, + "[tool-presentation] tool not found in turn's registered sets — \ + falling back to humanized name" + ); + (Some(humanize_tool_name(tool_name)), None) + } + } + } + /// Cumulative `(input_tokens, output_tokens, charged_usd)` observed so far. #[cfg(test)] pub(super) fn totals(&self) -> (u64, u64, f64) { diff --git a/crates/openhuman-core/src/agent/tinyagents/observability/event_projection.rs b/crates/openhuman-core/src/agent/tinyagents/observability/event_projection.rs index e2f0ca2077e..bbce65f365b 100644 --- a/crates/openhuman-core/src/agent/tinyagents/observability/event_projection.rs +++ b/crates/openhuman-core/src/agent/tinyagents/observability/event_projection.rs @@ -321,7 +321,7 @@ impl EventListener for OpenhumanEventBridge { tool_name: requested_name.clone(), arguments: arguments.clone(), iteration, - display_label: Some(label), + display_label: Some(label.clone()), display_detail: Some("tool not available".to_string()), }); self.send(AgentProgress::ToolCallCompleted { @@ -334,6 +334,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms: 0, iteration, failure, + display_label: Some(label), + display_detail: Some("tool not available".to_string()), + structured: None, }); } Some(s) => { @@ -344,7 +347,7 @@ impl EventListener for OpenhumanEventBridge { tool_name: requested_name.clone(), arguments: arguments.clone(), iteration, - display_label: Some(label), + display_label: Some(label.clone()), display_detail: Some("tool not available".to_string()), }); self.send(AgentProgress::SubagentToolCallCompleted { @@ -359,6 +362,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms: 0, iteration, failure, + display_label: Some(label), + display_detail: Some("tool not available".to_string()), + structured: None, }); } } @@ -445,6 +451,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms: *latency_ms, iteration, failure: None, + display_label: Some("Searching tools".to_string()), + display_detail: None, + structured: None, }); } Some(s) => { @@ -470,6 +479,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms: *latency_ms, iteration, failure: None, + display_label: Some("Searching tools".to_string()), + display_detail: None, + structured: None, }); } } @@ -489,14 +501,25 @@ impl EventListener for OpenhumanEventBridge { .lock() .unwrap_or_else(|p| p.into_inner()) .insert(call_id.as_str().to_string(), std::time::Instant::now()); + // The harness start event carries no call input (`ToolStarted` + // has only `call_id`/`tool_name`), so the label/detail are + // computed against empty args here — a tool whose label + // doesn't depend on its arguments (the common case: a policy + // label, or a name-derived default) already reads correctly; + // one whose detail DOES depend on args (e.g. a search query) + // is recomputed with the real arguments on `ToolCallCompleted` + // below and forwarded on the wire as + // `tool_display_label`/`tool_display_detail` there too. + let (display_label, display_detail) = + self.resolve_display(tool_name, &serde_json::Value::Null); match &self.scope { None => self.send(AgentProgress::ToolCallStarted { call_id: call_id.as_str().to_string(), tool_name: tool_name.clone(), arguments: serde_json::Value::Null, iteration, - display_label: Some(humanize_tool_name(tool_name)), - display_detail: None, + display_label, + display_detail, }), Some(s) => self.send(AgentProgress::SubagentToolCallStarted { agent_id: s.agent_id.clone(), @@ -505,8 +528,8 @@ impl EventListener for OpenhumanEventBridge { tool_name: tool_name.clone(), arguments: serde_json::Value::Null, iteration, - display_label: Some(humanize_tool_name(tool_name)), - display_detail: None, + display_label, + display_detail, }), } } @@ -546,7 +569,7 @@ impl EventListener for OpenhumanEventBridge { .unwrap_or(0); let elapsed_ms = outcome .as_ref() - .map(|(_, _, e, _)| *e) + .map(|(_, _, e, ..)| *e) .filter(|e| *e > 0) .unwrap_or(stamped_elapsed); // Tool result text, captured by the harness when @@ -559,13 +582,33 @@ impl EventListener for OpenhumanEventBridge { }; let output_chars = outcome .as_ref() - .map(|(_, _, _, c)| *c) + .map(|(_, _, _, c, _)| *c) .filter(|c| *c > 0) .unwrap_or_else(|| output_text.chars().count()); + // Structured, tool-specific result payload the middleware + // copied from `ToolResult.metadata` (e.g. web search results). + let structured = outcome.as_ref().and_then(|(.., s)| s.clone()); // Carry the classified failure onto whichever completion event // this projects — main-agent OR sub-agent (#4459). Previously // the sub-agent branch dropped it on the floor. - let failure = outcome.and_then(|(_, f, _, _)| f); + let failure = outcome.and_then(|(_, f, ..)| f); + // Recompute the label/detail with the REAL call arguments + // (unlike `ToolCallStarted`, this event's `input` is the + // actual arguments the harness captured), so a tool whose + // detail depends on its args — a search query, a target + // email — surfaces it here even when the started event + // couldn't. + let args_for_display = input.clone().unwrap_or(serde_json::Value::Null); + let (display_label, display_detail) = + self.resolve_display(tool_name, &args_for_display); + tracing::debug!( + call_id = call_id.as_str(), + tool_name = tool_name.as_str(), + success, + elapsed_ms, + has_structured = structured.is_some(), + "[tool-presentation] projecting ToolCallCompleted with resolved label/detail" + ); match &self.scope { None => self.send(AgentProgress::ToolCallCompleted { call_id: call_id.as_str().to_string(), @@ -577,6 +620,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms, iteration, failure, + display_label, + display_detail, + structured, }), Some(s) => self.send(AgentProgress::SubagentToolCallCompleted { agent_id: s.agent_id.clone(), @@ -590,6 +636,9 @@ impl EventListener for OpenhumanEventBridge { elapsed_ms, iteration, failure, + display_label, + display_detail, + structured, }), } } diff --git a/crates/openhuman-core/src/agent/tinyagents/observability_tests.rs b/crates/openhuman-core/src/agent/tinyagents/observability_tests.rs index 9e3f8eeb478..31aef3de149 100644 --- a/crates/openhuman-core/src/agent/tinyagents/observability_tests.rs +++ b/crates/openhuman-core/src/agent/tinyagents/observability_tests.rs @@ -1,6 +1,47 @@ use super::*; use tinyagents_harness::events::EventSink; +/// A tool whose `display_label`/`display_detail` depend on the call +/// arguments — the shape a dynamic Composio/MCP/integration tool takes (e.g. +/// [`crate::integrations::composio::action_tool::ComposioActionTool`]'s +/// "Gmail send email"). Used to prove the bridge calls the tool's OWN +/// presentation methods instead of always deriving a label from the bare +/// tool name. +struct FakeLabeledTool; + +#[async_trait::async_trait] +impl tinytools::Tool for FakeLabeledTool { + fn name(&self) -> &str { + "fake_send_email" + } + + fn description(&self) -> &str { + "sends an email (test double)" + } + + fn parameters_schema(&self) -> serde_json::Value { + serde_json::json!({"type": "object"}) + } + + async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<tinytools::ToolResult> { + Ok(tinytools::ToolResult::success("sent")) + } + + fn display_label(&self, _args: &serde_json::Value) -> Option<String> { + Some("Sending email".to_string()) + } + + fn display_detail(&self, args: &serde_json::Value) -> Option<String> { + args.get("to").and_then(|v| v.as_str()).map(str::to_string) + } +} + +fn fake_tool_sets() -> Vec<Arc<Vec<Box<dyn tinytools::Tool>>>> { + vec![Arc::new(vec![ + Box::new(FakeLabeledTool) as Box<dyn tinytools::Tool> + ])] +} + #[tokio::test] async fn bridge_forwards_tool_and_cost_progress() { let (tx, mut rx) = tokio::sync::mpsc::channel(64); @@ -66,6 +107,7 @@ async fn model_completed_projects_generation_with_content_and_provider() { Arc::default(), Arc::default(), Arc::default(), + Vec::new(), ); let sink = EventSink::new(); sink.subscribe(bridge.clone()); @@ -138,6 +180,7 @@ async fn subagent_model_completed_carries_task_attribution() { Arc::default(), Arc::default(), Arc::default(), + Vec::new(), ); let sink = EventSink::new(); sink.subscribe(bridge.clone()); @@ -331,3 +374,83 @@ async fn duplicate_usage_for_same_model_call_is_recorded_once() { // `ToolStarted` arm above — it no longer special-cases a sentinel). The test // referenced the deleted constant (a stale reference reintroduced by a merge) // and asserted behaviour that no longer exists. + +/// #6XXX (tool-call presentation): `ToolCallStarted`/`ToolCallCompleted` must +/// carry the tool's OWN `display_label`/`display_detail` when the bridge was +/// built with the turn's tool sets, not a name-derived guess — proven with a +/// fake tool whose label is a fixed phrase and whose detail comes from a +/// `"to"` argument only known once the call completes. +#[tokio::test] +async fn tool_call_events_use_the_tool_s_own_display_label_and_detail() { + let (tx, mut rx) = tokio::sync::mpsc::channel(64); + let bridge = OpenhumanEventBridge::with_scope( + Some(tx), + "mock-model", + "managed", + 10, + None, + Arc::default(), + Arc::default(), + Arc::default(), + Arc::default(), + fake_tool_sets(), + ); + let sink = EventSink::new(); + sink.subscribe(bridge.clone()); + + sink.emit(AgentEvent::ModelStarted { + call_id: "c1".into(), + model: "mock-model".to_string(), + }); + sink.emit(AgentEvent::ToolStarted { + call_id: "c1".into(), + tool_name: "fake_send_email".to_string(), + }); + sink.emit(AgentEvent::ToolCompleted { + call_id: "c1".into(), + tool_name: "fake_send_email".to_string(), + started_at_ms: None, + input: Some(serde_json::json!({"to": "steven@example.com"})), + output: Some(serde_json::Value::String("sent".to_string())), + duration_ms: Some(5), + output_bytes: Some(4), + error: None, + metadata: None, + }); + + let mut started_label = None; + let mut completed = None; + while let Ok(p) = rx.try_recv() { + match p { + AgentProgress::ToolCallStarted { + display_label, + display_detail, + .. + } => started_label = Some((display_label, display_detail)), + AgentProgress::ToolCallCompleted { + display_label, + display_detail, + .. + } => completed = Some((display_label, display_detail)), + _ => {} + } + } + + let (started_label, started_detail) = started_label.expect("ToolCallStarted projected"); + assert_eq!( + started_label, + Some("Sending email".to_string()), + "the started label comes from the tool's own display_label, not a humanized name" + ); + // No arguments exist yet at call-start, so the arg-derived detail is + // absent — this is recovered on the completed event below. + assert_eq!(started_detail, None); + + let (completed_label, completed_detail) = completed.expect("ToolCallCompleted projected"); + assert_eq!(completed_label, Some("Sending email".to_string())); + assert_eq!( + completed_detail, + Some("steven@example.com".to_string()), + "the completed detail is recomputed from the real call arguments" + ); +} diff --git a/crates/openhuman-core/src/agent/tinyagents/turn_runner.rs b/crates/openhuman-core/src/agent/tinyagents/turn_runner.rs index b0c222c87ff..ac7215dd3b8 100644 --- a/crates/openhuman-core/src/agent/tinyagents/turn_runner.rs +++ b/crates/openhuman-core/src/agent/tinyagents/turn_runner.rs @@ -236,6 +236,11 @@ async fn run_turn_via_tinyagents_inner( // the exact same `Arc`-shared instances, so retain only the cheap Arc clone // for a hosted invocation (never clone the tools themselves). let hosted_tool_sets = hosted_root.as_ref().map(|_| tool_sets.clone()); + // Retained for the event bridge (cheap `Arc` clones — never the tools + // themselves) so it can resolve a live `&dyn Tool` by name and call the + // tool's OWN `display_label`/`display_detail` instead of only ever + // guessing from the bare name (issue: tool-call presentation). + let bridge_tool_sets = tool_sets.clone(); // The turn's crate `ChatModel` set (`turn_models`) and the provider telemetry // id are built by the caller via `build_turn_models` — the seam entry is // crate-native and no longer names `Provider` (issue #4249, Phase 5). The @@ -455,6 +460,7 @@ async fn run_turn_via_tinyagents_inner( tool_names.clone(), failure_map.clone(), provider_usage_carry.clone(), + bridge_tool_sets, ); events.subscribe(bridge.clone()); bridge diff --git a/crates/openhuman-core/src/channels/proactive.rs b/crates/openhuman-core/src/channels/proactive.rs index 6dc7de171d0..1b9f7441bec 100644 --- a/crates/openhuman-core/src/channels/proactive.rs +++ b/crates/openhuman-core/src/channels/proactive.rs @@ -219,6 +219,8 @@ impl EventHandler<DomainEvent> for ProactiveMessageSubscriber { subagent: None, tool_display_label: None, tool_display_detail: None, + elapsed_ms: None, + structured: None, usage: None, // Proactive delivery is emitted outside the seq-stamping progress // bridge; leave `seq` unset (older clients ignore it). diff --git a/crates/openhuman-core/src/core/socketio.rs b/crates/openhuman-core/src/core/socketio.rs index bab189d3e13..cc2913217d9 100644 --- a/crates/openhuman-core/src/core/socketio.rs +++ b/crates/openhuman-core/src/core/socketio.rs @@ -323,6 +323,24 @@ pub struct WebChannelEvent { /// shown after [`Self::tool_display_label`]. #[serde(skip_serializing_if = "Option::is_none")] pub tool_display_detail: Option<String>, + /// Milliseconds the tool call took to execute. Present on `tool_result` / + /// `subagent_tool_result`, mirroring `AgentProgress::ToolCallCompleted`'s + /// `elapsed_ms` / `SubagentToolCallCompleted`'s `elapsed_ms` — carried + /// as a plain top-level field (in addition to `subagent.elapsed_ms` for + /// the sub-agent case) so a frontend that only reads flat fields still + /// gets real timing instead of guessing from wall-clock deltas. + #[serde(skip_serializing_if = "Option::is_none")] + pub elapsed_ms: Option<u64>, + /// Structured, tool-specific result payload copied from a tool's + /// `ToolResult::metadata` when it is a JSON object carrying a `"kind"` + /// discriminator, e.g. + /// `{"kind":"web_search","query":"...","provider":"...","results":[...]}`. + /// Present on `tool_result` / `subagent_tool_result` only for tools that + /// populate metadata of that shape (currently the web-search tools); the + /// model-facing `output` text is unaffected and stays byte-identical to + /// what the model itself saw. + #[serde(skip_serializing_if = "Option::is_none")] + pub structured: Option<serde_json::Value>, /// Holistic token/cost/context usage for a completed turn (parent + /// sub-agents), carried on `chat_done`. Lets the UI footer show session /// tokens, USD cost, and real context-window utilisation, with a diff --git a/crates/openhuman-core/src/platform/socket/medulla/envelope_tests.rs b/crates/openhuman-core/src/platform/socket/medulla/envelope_tests.rs index 6371340f661..3f26378d9d7 100644 --- a/crates/openhuman-core/src/platform/socket/medulla/envelope_tests.rs +++ b/crates/openhuman-core/src/platform/socket/medulla/envelope_tests.rs @@ -132,6 +132,9 @@ fn tool_call_and_result_map_to_their_kinds() { elapsed_ms: 5, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }; match progress_to_event_kind(&completed) { Some(HarnessEventKind::ToolResult(tr)) => { diff --git a/crates/openhuman-core/src/search/tools/brave.rs b/crates/openhuman-core/src/search/tools/brave.rs index 9cb5b9ed9bb..23408c99796 100644 --- a/crates/openhuman-core/src/search/tools/brave.rs +++ b/crates/openhuman-core/src/search/tools/brave.rs @@ -233,6 +233,28 @@ impl Tool for BraveWebSearchTool { if options.prefer_markdown { out.markdown_formatted = Some(render_web_markdown(&results, &query, count)); } + // Host-only structured payload for the chat UI's tool-call + // presentation — never rendered to the model, so `render_web_plain`'s + // text above (and the cache key that depends on it) is unaffected. + let structured_results: Vec<crate::search::tools::WebSearchResultRef<'_>> = results + .iter() + .map(|r| crate::search::tools::WebSearchResultRef { + title: if r.title.trim().is_empty() { + "Untitled" + } else { + r.title.trim() + }, + url: r.url.as_str(), + published: r.age.as_deref(), + excerpt: Some(r.description.as_str()).filter(|d| !d.trim().is_empty()), + }) + .collect(); + out.metadata = Some(crate::search::tools::web_search_metadata( + &query, + "Brave", + &structured_results, + count, + )); Ok(out) } } @@ -406,7 +428,31 @@ impl Tool for BraveNewsSearchTool { )); } } - Ok(ToolResult::success(lines.join("\n"))) + let mut out = ToolResult::success(lines.join("\n")); + // Host-only structured payload — never rendered to the model, so the + // plain text above (and the cache key that depends on it) is + // unaffected. + let structured_results: Vec<crate::search::tools::WebSearchResultRef<'_>> = parsed + .results + .iter() + .map(|r| crate::search::tools::WebSearchResultRef { + title: if r.title.trim().is_empty() { + "Untitled" + } else { + r.title.trim() + }, + url: r.url.as_str(), + published: r.age.as_deref(), + excerpt: Some(r.description.as_str()).filter(|d| !d.trim().is_empty()), + }) + .collect(); + out.metadata = Some(crate::search::tools::web_search_metadata( + &query, + "Brave", + &structured_results, + count, + )); + Ok(out) } } diff --git a/crates/openhuman-core/src/search/tools/exa.rs b/crates/openhuman-core/src/search/tools/exa.rs index f52fa97b4ce..0918264a6f4 100644 --- a/crates/openhuman-core/src/search/tools/exa.rs +++ b/crates/openhuman-core/src/search/tools/exa.rs @@ -1,10 +1,8 @@ //! Exa neural search integration -- direct API (BYOK, not backend-proxied). //! //! **Scope**: Agent + CLI/RPC. -//! //! **Endpoints**: `POST https://api.exa.ai/search`, //! `POST https://api.exa.ai/findSimilar`, `POST https://api.exa.ai/contents`. -//! //! **Auth**: `x-api-key: <api key>`. //! //! When the user selects `exa` as their search engine and has saved their own @@ -508,7 +506,25 @@ impl Tool for ExaSearchTool { let limit = self.client.requested_results(&args); let body = self.build_body(&args, &query); let results = self.client.post_documents("search", body).await?; - Ok(self.client.to_result(&results, &query, limit, &options)) + let mut result = self.client.to_result(&results, &query, limit, &options); + let excerpts: Vec<Option<String>> = results.iter().map(ExaResultItem::excerpt).collect(); + let structured: Vec<super::WebSearchResultRef<'_>> = results + .iter() + .zip(&excerpts) + .map(|(r, e)| super::WebSearchResultRef { + title: r.display_title(), + url: &r.url, + published: r.published_date.as_deref(), + excerpt: e.as_deref(), + }) + .collect(); + result.metadata = Some(super::web_search_metadata( + &query, + "Exa", + &structured, + limit, + )); + Ok(result) } } diff --git a/crates/openhuman-core/src/search/tools/mod.rs b/crates/openhuman-core/src/search/tools/mod.rs index e90588d8d25..ecd49e60c3a 100644 --- a/crates/openhuman-core/src/search/tools/mod.rs +++ b/crates/openhuman-core/src/search/tools/mod.rs @@ -39,3 +39,67 @@ pub use web_search::WebSearchTool; // Crate-internal: the `tools.web_search` RPC reuses the same provider // resolution so both managed-search surfaces attribute a call identically. pub(crate) use web_search::resolve_managed_provider; + +/// Maximum characters kept from a web-search result's excerpt when it is +/// copied into [`ToolResult::metadata`][tinytools::ToolResult] for the UI +/// (issue: tool-call presentation). Deliberately smaller than the ~500-char +/// budget the model-facing text renders with — this metadata is for a compact +/// result card, not the full context the model reads. +pub(crate) const WEB_SEARCH_METADATA_EXCERPT_CHARS: usize = 300; + +/// One search result, borrowed from whatever provider-specific struct the +/// caller already has, for building the structured +/// `{"kind":"web_search",...}` metadata every model-facing web-search tool +/// attaches to its [`tinytools::ToolResult::metadata`] (issue: tool-call +/// presentation). Kept separate from the model-facing rendering so a change +/// here can never perturb the byte-identical prompt text the provider's cache +/// keys on. +pub(crate) struct WebSearchResultRef<'a> { + pub title: &'a str, + pub url: &'a str, + pub published: Option<&'a str>, + pub excerpt: Option<&'a str>, +} + +/// Build the structured web-search metadata payload: +/// `{"kind":"web_search","query":...,"provider":...,"results":[{"title":..., +/// "url":...,"published":...?,"excerpt":...?}]}`. `max_results` caps how many +/// of `results` are copied in, matching whatever cap the tool's own +/// model-facing rendering already applies so the structured payload never +/// claims more results exist than the model was shown. +/// +/// This is metadata (host-only, never rendered to the model) — see +/// [`tinytools::ToolResult::metadata`]'s own docs on that boundary. +pub(crate) fn web_search_metadata( + query: &str, + provider: &str, + results: &[WebSearchResultRef<'_>], + max_results: usize, +) -> serde_json::Value { + let results_json: Vec<serde_json::Value> = results + .iter() + .take(max_results) + .map(|r| { + let mut obj = serde_json::json!({ + "title": r.title, + "url": r.url, + }); + if let Some(published) = r.published.map(str::trim).filter(|s| !s.is_empty()) { + obj["published"] = serde_json::json!(published); + } + if let Some(excerpt) = r.excerpt.map(str::trim).filter(|s| !s.is_empty()) { + obj["excerpt"] = serde_json::json!(crate::util::truncate_with_ellipsis( + excerpt, + WEB_SEARCH_METADATA_EXCERPT_CHARS + )); + } + obj + }) + .collect(); + serde_json::json!({ + "kind": "web_search", + "query": query, + "provider": provider, + "results": results_json, + }) +} diff --git a/crates/openhuman-core/src/search/tools/querit.rs b/crates/openhuman-core/src/search/tools/querit.rs index acf2d110d7a..83bb7b96b6a 100644 --- a/crates/openhuman-core/src/search/tools/querit.rs +++ b/crates/openhuman-core/src/search/tools/querit.rs @@ -515,6 +515,37 @@ impl Tool for QueritSearchTool { result.markdown_formatted = Some(self.render_results_markdown(&search_resp.results.result, query)); } + // Host-only structured payload for the chat UI's tool-call + // presentation — never rendered to the model, so `render_results_plain`'s + // text above (and the cache key that depends on it) is unaffected. + let snippets: Vec<Option<String>> = search_resp + .results + .result + .iter() + .map(QueritResultItem::snippet_text) + .collect(); + let structured_results: Vec<crate::search::tools::WebSearchResultRef<'_>> = search_resp + .results + .result + .iter() + .zip(snippets.iter()) + .map(|(item, snippet)| crate::search::tools::WebSearchResultRef { + title: item + .title + .as_deref() + .filter(|t| !t.trim().is_empty()) + .unwrap_or("Untitled"), + url: item.url.as_str(), + published: item.page_age.as_deref(), + excerpt: snippet.as_deref(), + }) + .collect(); + result.metadata = Some(crate::search::tools::web_search_metadata( + query, + "Querit", + &structured_results, + self.max_results, + )); Ok(result) } } diff --git a/crates/openhuman-core/src/search/tools/tavily/search_tool.rs b/crates/openhuman-core/src/search/tools/tavily/search_tool.rs index 070d4dc3cec..2b474216d08 100644 --- a/crates/openhuman-core/src/search/tools/tavily/search_tool.rs +++ b/crates/openhuman-core/src/search/tools/tavily/search_tool.rs @@ -212,6 +212,30 @@ impl Tool for TavilySearchTool { if options.prefer_markdown { result.markdown_formatted = Some(markdown); } + // Host-only structured payload for the chat UI's tool-call + // presentation — never rendered to the model, so the plain/markdown + // text above (and the cache key that depends on it) is unaffected. + let structured_results: Vec<crate::search::tools::WebSearchResultRef<'_>> = parsed + .results + .iter() + .map(|r| crate::search::tools::WebSearchResultRef { + title: r + .title + .as_deref() + .map(str::trim) + .filter(|t| !t.is_empty()) + .unwrap_or("Untitled"), + url: r.url.as_str(), + published: None, + excerpt: r.content.as_deref(), + }) + .collect(); + result.metadata = Some(crate::search::tools::web_search_metadata( + &query, + "Tavily", + &structured_results, + limit, + )); Ok(result) } } diff --git a/crates/openhuman-core/src/search/tools/web_search.rs b/crates/openhuman-core/src/search/tools/web_search.rs index 25a6742ef0e..4e0fb81c637 100644 --- a/crates/openhuman-core/src/search/tools/web_search.rs +++ b/crates/openhuman-core/src/search/tools/web_search.rs @@ -9,7 +9,9 @@ //! to `MANAGED_DEFAULT_PROVIDER`, for UI display. `with_direct_search` can //! swap in a `SeltzSearchTool` that bypasses the proxy; only tests use it. -use super::{SearchResponse, SearchResultItem, SeltzSearchTool}; +use super::{ + web_search_metadata, SearchResponse, SearchResultItem, SeltzSearchTool, WebSearchResultRef, +}; use crate::config::Config; use crate::integrations::IntegrationClient; use async_trait::async_trait; @@ -412,6 +414,26 @@ impl Tool for WebSearchTool { result.markdown_formatted = Some(self.render_results_markdown(&resp.results, &query, provider)); } + // Host-only structured payload for the chat UI's tool-call + // presentation (issue: tool-call presentation) — never rendered to + // the model, so `parse_parallel_results`'s text above (and the cache + // key that depends on it) is unaffected. + let structured_results: Vec<WebSearchResultRef<'_>> = resp + .results + .iter() + .map(|r| WebSearchResultRef { + title: &r.title, + url: &r.url, + published: r.publish_date.as_deref(), + excerpt: r.excerpts.first().map(String::as_str), + }) + .collect(); + result.metadata = Some(web_search_metadata( + &query, + provider, + &structured_results, + self.max_results, + )); Ok(result) } } diff --git a/crates/openhuman-core/src/threads/ops/usage.rs b/crates/openhuman-core/src/threads/ops/usage.rs index ca2554b4184..1ad3802e58b 100644 --- a/crates/openhuman-core/src/threads/ops/usage.rs +++ b/crates/openhuman-core/src/threads/ops/usage.rs @@ -92,6 +92,17 @@ pub(super) fn transcript_spend(transcript: &SessionTranscript) -> TranscriptSpen let Some(usage) = message.turn_usage.as_ref() else { continue; }; + // A text-dialect tool round's issuing row carries a provenance-only + // record (its calls, zero spend); the turn's spend is on its final row. + // It is not a turn that spent, and must not become the "last" one. + if !usage.tool_calls.is_empty() + && usage.usage.input == 0 + && usage.usage.output == 0 + && usage.usage.cached_input == 0 + && usage.usage.cost_usd == 0.0 + { + continue; + } spend.input_tokens = spend.input_tokens.saturating_add(usage.usage.input); spend.output_tokens = spend.output_tokens.saturating_add(usage.usage.output); spend.cached_input_tokens = spend diff --git a/crates/openhuman-core/src/threads/ops/usage_tests.rs b/crates/openhuman-core/src/threads/ops/usage_tests.rs index 3da5c9cc05b..a281b96d9c3 100644 --- a/crates/openhuman-core/src/threads/ops/usage_tests.rs +++ b/crates/openhuman-core/src/threads/ops/usage_tests.rs @@ -306,3 +306,52 @@ fn reports_no_usage_for_an_unknown_or_spendless_thread() { assert_eq!(spend.root.turns, 0, "but it recorded no spend"); assert_eq!(spend.root.input_tokens, 0); } + +/// A text-dialect tool round's issuing row carries a provenance-only record — +/// its calls, zero spend — beside the turn's real record on the final row. +/// It is not a turn that spent and must not take over the last-turn view. +#[test] +fn provenance_only_tool_round_records_are_not_counted_as_turns() { + let tmp = tempfile::tempdir().expect("tempdir"); + let thread = "thread-text-dialect"; + let path = tmp + .path() + .join("session_raw") + .join("1790000001_orchestrator_text.jsonl"); + std::fs::create_dir_all(path.parent().unwrap()).expect("create session_raw"); + + let mut issuing = TranscriptMessage::assistant(""); + issuing.turn_usage = Some(TurnUsage { + tool_calls: vec![tinyagents_session::transcript::TranscriptToolCall { + id: "call-1".into(), + name: "web_search_tool".into(), + arguments: "{}".into(), + extra_content: None, + }], + ..turn_usage(0, 0, 0) + }); + let rows = vec![ + TranscriptMessage::new("user", "q"), + issuing, + TranscriptMessage::new( + "user", + "[Tool results]\n<tool_result id=\"call-1\">\nok\n</tool_result>\n", + ), + TranscriptMessage::assistant("a"), + ]; + append_transcript_turn( + &path, + &[], + &rows, + &meta("orchestrator", "root", Some(thread)), + Some(&turn_usage(5_000, 50, 1_000)), + Some("req-0"), + ) + .expect("append turn"); + + let spend = thread_spend(tmp.path(), thread); + + assert_eq!(spend.root.turns, 1); + assert_eq!(spend.root.input_tokens, 5_000); + assert_eq!(spend.root.last_input_tokens, 5_000); +} diff --git a/crates/openhuman-core/src/threads/transcript_view/mod.rs b/crates/openhuman-core/src/threads/transcript_view/mod.rs index d94ae0ad7e4..3ac807f9376 100644 --- a/crates/openhuman-core/src/threads/transcript_view/mod.rs +++ b/crates/openhuman-core/src/threads/transcript_view/mod.rs @@ -105,6 +105,9 @@ fn parse_cursor(cursor: Option<&str>) -> usize { #[cfg(test)] #[path = "transcript_view_tests.rs"] mod tests; +#[cfg(test)] +#[path = "transcript_view_tool_round_tests.rs"] +mod tool_round_tests; #[cfg(test)] #[path = "transcript_ordering_tests.rs"] diff --git a/crates/openhuman-core/src/threads/transcript_view/project.rs b/crates/openhuman-core/src/threads/transcript_view/project.rs index 3aa9924a3dc..aaf1601e30e 100644 --- a/crates/openhuman-core/src/threads/transcript_view/project.rs +++ b/crates/openhuman-core/src/threads/transcript_view/project.rs @@ -10,6 +10,9 @@ use std::collections::{HashSet, VecDeque}; use std::path::{Path, PathBuf}; use tinyagents_session::transcript::{self, CompactionMarker, DisplayMessage, DisplayRecord}; +use tinytools_agent::dialect::{parse_replayed_results, ToolResultEntry}; + +use crate::agent::messages::TOOL_RESULT_FAILURES_METADATA_KEY; use super::resolve; use super::subagents; @@ -209,6 +212,12 @@ impl Projector { log::debug!("{LOG_PREFIX} sanitize: dropped system line from projection"); } "user" => { + // A text dialect folds a round's results into one user turn; + // it is tool output, never the user's words. + if let Some(results) = parse_replayed_results(&msg.message.content) { + project_text_tool_results(msg, results, &mut self.items, &mut self.pending); + return; + } // A legacy turn without request ids still restarts the step // count at its prompt. self.step = 0; @@ -310,6 +319,19 @@ impl Projector { for (call_id, name, arguments) in tool_calls { let args = parse_tool_args(&arguments); + // Repair legacy rows which put aggregate calls on the final + // answer after their result had already projected as an orphan. + if let Some(DisplayItem::ToolCall { + name: settled_name, + args: settled_args, + .. + }) = settled_orphan_mut(&mut self.items, &call_id) + { + log::debug!("{LOG_PREFIX} call {call_id} recorded after its result — merged"); + *settled_name = name; + *settled_args = args; + continue; + } if !call_id.is_empty() { self.seen_call_ids.insert(call_id.clone()); } @@ -438,6 +460,83 @@ fn unwrap_tool_result(raw: &str) -> (String, Option<String>) { ) } +/// Pair each result of a text-dialect `[Tool results]` row with its pending +/// call. Failure status comes from the ids the session codec recorded on the +/// row ([`TOOL_RESULT_FAILURES_METADATA_KEY`]); a result with no pending call +/// surfaces as an orphan row, as for a native `tool` line. +fn project_text_tool_results( + msg: &DisplayMessage, + results: Vec<ToolResultEntry>, + items: &mut Vec<DisplayItem>, + pending: &mut VecDeque<(String, usize)>, +) { + let failed: Vec<&str> = msg + .message + .extra_metadata + .as_ref() + .and_then(|meta| meta.get(TOOL_RESULT_FAILURES_METADATA_KEY)) + .and_then(serde_json::Value::as_array) + .map(|ids| ids.iter().filter_map(serde_json::Value::as_str).collect()) + .unwrap_or_default(); + log::debug!( + "{LOG_PREFIX} text-dialect results row results={} failed={} pending={}", + results.len(), + failed.len(), + pending.len() + ); + for result in results { + let (status, failure) = if failed.contains(&result.tool_call_id.as_str()) { + ( + ToolCallStatus::Error, + Some(ToolCallFailure { detail: None }), + ) + } else { + (ToolCallStatus::Success, None) + }; + if let Some(idx) = take_pending_by_id(pending, &result.tool_call_id) { + if let Some(DisplayItem::ToolCall { + result: slot, + status: status_slot, + failure: failure_slot, + .. + }) = items.get_mut(idx) + { + *slot = Some(result.content); + *status_slot = status; + *failure_slot = failure; + continue; + } + } + items.push(DisplayItem::ToolCall { + call_id: result.tool_call_id, + name: "tool".to_string(), + args: None, + result: Some(result.content), + status, + failure, + }); + } +} + +/// The already-settled orphan row for `call_id` in the current turn — a result +/// that projected before any call named it. +fn settled_orphan_mut<'a>( + items: &'a mut [DisplayItem], + call_id: &str, +) -> Option<&'a mut DisplayItem> { + let turn_start = items + .iter() + .rposition(|item| matches!(item, DisplayItem::TurnBoundary { .. })) + .map_or(0, |idx| idx + 1); + items[turn_start..].iter_mut().find(|item| { + matches!( + item, + DisplayItem::ToolCall { call_id: id, name, result: Some(_), .. } + if id == call_id && name == "tool" + ) + }) +} + /// Remove and return the pending entry whose call id matches `id`, if any. fn take_pending_by_id(pending: &mut VecDeque<(String, usize)>, id: &str) -> Option<usize> { let pos = pending.iter().position(|(cid, _)| cid == id)?; diff --git a/crates/openhuman-core/src/threads/transcript_view/transcript_view_tool_round_tests.rs b/crates/openhuman-core/src/threads/transcript_view/transcript_view_tool_round_tests.rs new file mode 100644 index 00000000000..1c2376f3bd2 --- /dev/null +++ b/crates/openhuman-core/src/threads/transcript_view/transcript_view_tool_round_tests.rs @@ -0,0 +1,300 @@ +//! Tool-round projection tests: text-dialect rounds persisted through the real +//! session codec and writer, and transcripts written before calls rode their +//! issuing row. + +use super::project::{project_records, project_thread}; +use super::types::{DisplayItem, ToolCallStatus}; +use crate::agent::messages::ChatMessage; +use tempfile::TempDir; +use tinyagents_session::transcript::{self, read_transcript_display}; + +/// Write a raw JSONL transcript (meta header + `body` lines) for `thread_id`. +fn write_raw(workspace: &std::path::Path, stem: &str, thread_id: &str, body: &[&str]) { + let path = transcript::resolve_keyed_transcript_path(workspace, stem).expect("resolve"); + let mut buf = format!( + r#"{{"_meta":{{"version":1,"agent":"orchestrator","dispatcher":"xml","created":"2026-09-24T00:00:00Z","updated":"2026-09-24T00:00:10Z","turn_count":1,"input_tokens":0,"output_tokens":0,"cached_input_tokens":0,"charged_amount_usd":0.0,"thread_id":"{thread_id}"}}}}"# + ); + buf.push('\n'); + for line in body { + buf.push_str(line); + buf.push('\n'); + } + std::fs::write(&path, buf).expect("write raw transcript"); +} + +/// A text-dialect (`xml`/`python`/`pformat`) tool turn, persisted through the +/// real runtime codec and writer, must attach each call to the assistant row +/// that issued it and pair it with its `[Tool results]` entry — so the derived +/// transcript reports settled calls as success/error, not "running". +/// +/// Regression: the codec put every tool outcome of the turn on the turn-level +/// usage record, which the writer attaches to the turn's *final* assistant row. +/// The calls then landed after their own results, the results rendered as a +/// user message, and every reloaded call projected as `running` (the UI showed +/// them as cancelled). +#[test] +fn text_dialect_tool_turn_projects_calls_on_their_issuing_row_as_settled() { + use crate::agent::messages::{ConversationMessage, ToolResultMessage}; + use crate::agent::session_host::OpenHumanTranscriptCodec; + use crate::agent::tinyagents::host::OpenHumanRunContext; + use crate::inference::provider::ToolCall; + use tinyagents_runtime::{ResumeMode, TranscriptCodec, TranscriptTurnOptions}; + use tinyinference_llm::message::Message; + + let dir = TempDir::new().unwrap(); + + // What the session driver persists for a text dialect: the conversation + // rendered through the dialect's replay form. + let conversation = vec![ + ConversationMessage::AssistantToolCalls { + text: None, + tool_calls: vec![ + ToolCall { + id: "call_web_search_1".into(), + name: "web_search_tool".into(), + arguments: r#"{"query":"rust async traits"}"#.into(), + extra_content: None, + }, + ToolCall { + id: "call_file_read_1".into(), + name: "file_read".into(), + arguments: r#"{"path":"README.md"}"#.into(), + extra_content: None, + }, + ], + reasoning_content: None, + extra_metadata: None, + }, + ConversationMessage::ToolResults(vec![ + ToolResultMessage { + tool_call_id: "call_web_search_1".into(), + content: "Search results for: rust async traits".into(), + }, + ToolResultMessage { + tool_call_id: "call_file_read_1".into(), + content: "unknown tool `file_read`".into(), + }, + ]), + ConversationMessage::Chat(ChatMessage::assistant("Here is what I found.")), + ]; + let rendered = crate::agent::message_convert::provider_messages_from_conversation( + &tinytools_agent::dialect::XmlDialect, + &conversation, + ); + let mut next = vec![Message::user("search the web and read the README")]; + next.extend(crate::agent::message_convert::history_to_messages( + &rendered, + )); + + let context = OpenHumanRunContext::new(); + { + let mut sidecar = context.session_sidecar.lock().unwrap(); + sidecar.model_calls = 2; + sidecar.input_tokens = 40; + sidecar.output_tokens = 12; + sidecar.resolved_route = Some(tinyinference_llm::model::ResolvedModelRoute { + provider: "e2e".into(), + model: "e2e-mock-model".into(), + route: "e2e".into(), + }); + for (id, name, arguments, success, content) in [ + ( + "call_web_search_1", + "web_search_tool", + serde_json::json!({"query": "rust async traits"}), + true, + "Search results for: rust async traits", + ), + ( + "call_file_read_1", + "file_read", + serde_json::json!({"path": "README.md"}), + false, + "unknown tool `file_read`", + ), + ] { + sidecar + .tool_outcomes + .push(crate::agent::tinyagents::ToolCallOutcome { + call_id: id.into(), + name: name.into(), + arguments, + success, + content: content.into(), + duration_ms: 1, + }); + } + } + let options = TranscriptTurnOptions { + request_id: Some("req-xml".into()), + thread_id: Some("thr_xml".into()), + stream: false, + resume: ResumeMode::Never, + context, + }; + let rows = OpenHumanTranscriptCodec + .reconcile(&[], &[], &next, &options) + .unwrap(); + let usage = OpenHumanTranscriptCodec.turn_usage(&options).unwrap(); + + let meta = transcript::TranscriptMeta { + session_id: None, + parent_session_id: None, + agent_name: "orchestrator".into(), + agent_id: Some("orchestrator".into()), + agent_type: Some("root".into()), + dispatcher: "xml".into(), + provider: None, + model: None, + created: "2026-09-24T00:00:00Z".into(), + updated: "2026-09-24T00:00:00Z".into(), + turn_count: 1, + input_tokens: 0, + output_tokens: 0, + cached_input_tokens: 0, + charged_amount_usd: 0.0, + thread_id: Some("thr_xml".into()), + task_id: None, + }; + let path = transcript::resolve_keyed_transcript_path(dir.path(), "xml_orchestrator").unwrap(); + transcript::append_transcript_turn(&path, &[], &rows, &meta, usage.as_ref(), Some("req-xml")) + .unwrap(); + + // The durable rows: the calls ride the issuing row, not the final answer. + let persisted = transcript::read_transcript(&path).unwrap(); + let assistants: Vec<_> = persisted + .messages + .iter() + .filter(|m| m.role == "assistant") + .collect(); + assert_eq!(assistants.len(), 2); + let issued: Vec<String> = assistants[0] + .turn_usage + .as_ref() + .map(|tu| tu.tool_calls.iter().map(|c| c.id.clone()).collect()) + .unwrap_or_default(); + assert_eq!( + issued, + vec![ + "call_web_search_1".to_string(), + "call_file_read_1".to_string() + ], + "the issuing assistant row carries its calls" + ); + assert!( + assistants[1] + .turn_usage + .as_ref() + .is_some_and(|tu| tu.tool_calls.is_empty() && tu.usage.input == 40), + "the final answer carries the turn's usage but none of its calls" + ); + + // The projection: calls settled with their results, no raw results bubble. + let display = read_transcript_display(&path).unwrap(); + let items = project_records(&display.records); + let calls: Vec<_> = items + .iter() + .filter_map(|item| match item { + DisplayItem::ToolCall { + call_id, + name, + result, + status, + .. + } => Some((call_id.clone(), name.clone(), result.clone(), *status)), + _ => None, + }) + .collect(); + assert_eq!( + calls.len(), + 2, + "one item per call, no duplicates: {items:?}" + ); + assert_eq!(calls[0].0, "call_web_search_1"); + assert_eq!(calls[0].1, "web_search_tool"); + assert_eq!( + calls[0].2.as_deref(), + Some("Search results for: rust async traits") + ); + assert_eq!(calls[0].3, ToolCallStatus::Success); + assert_eq!(calls[1].0, "call_file_read_1"); + assert_eq!(calls[1].1, "file_read"); + assert_eq!(calls[1].2.as_deref(), Some("unknown tool `file_read`")); + assert_eq!(calls[1].3, ToolCallStatus::Error); + assert!( + !items.iter().any(|item| matches!( + item, + DisplayItem::UserMessage { content, .. } if content.starts_with("[Tool results]") + )), + "a tool-results turn is not a user message: {items:?}" + ); + let first_call = items + .iter() + .position(|i| matches!(i, DisplayItem::ToolCall { .. })) + .unwrap(); + let final_answer = items + .iter() + .position(|i| { + matches!(i, DisplayItem::AssistantMessage { content, .. } if content == "Here is what I found.") + }) + .unwrap(); + assert!( + first_call < final_answer, + "calls precede the answer they fed" + ); +} + +/// Transcripts already written while the codec filed a turn's calls on its +/// final row (calls recorded *after* their own results) still project each call +/// once, settled, with its real name. +#[test] +fn calls_recorded_after_their_results_project_as_settled() { + let dir = TempDir::new().unwrap(); + let thread = "thr_late_calls"; + write_raw( + dir.path(), + "late_orchestrator", + thread, + &[ + r#"{"role":"user","content":"search and read","request_id":"R"}"#, + r#"{"role":"assistant","content":"","request_id":"R"}"#, + r#"{"role":"user","content":"[Tool results]\n<tool_result id=\"call_web_search_1\">\nhits\n</tool_result>\n<tool_result id=\"call_file_read_1\">\nunknown tool\n</tool_result>\n","request_id":"R"}"#, + r#"{"role":"assistant","content":"Here is what I found.","provider":"e2e","model":"m","usage":{"input":0,"output":0,"cached_input":0,"context_window":0,"cost_usd":0.0},"ts":"2026-09-24T00:49:35Z","iteration":2,"tool_calls":[{"id":"call_web_search_1","name":"web_search_tool","arguments":"{\"query\":\"q\"}"},{"id":"call_file_read_1","name":"file_read","arguments":"{\"path\":\"p\"}"}],"request_id":"R"}"#, + ], + ); + + let items = project_thread(dir.path(), thread) + .expect("transcript") + .items; + let calls: Vec<_> = items + .iter() + .filter_map(|item| match item { + DisplayItem::ToolCall { + call_id, + name, + args, + status, + .. + } => Some((call_id.clone(), name.clone(), args.is_some(), *status)), + _ => None, + }) + .collect(); + assert_eq!( + calls, + vec![ + ( + "call_web_search_1".to_string(), + "web_search_tool".to_string(), + true, + ToolCallStatus::Success + ), + ( + "call_file_read_1".to_string(), + "file_read".to_string(), + true, + ToolCallStatus::Success + ), + ], + "{items:?}" + ); +} diff --git a/crates/openhuman-core/src/threads/turn_state/mirror_finish_and_subagent_args_tests.rs b/crates/openhuman-core/src/threads/turn_state/mirror_finish_and_subagent_args_tests.rs index 9aadcbb18ad..d74c3d68904 100644 --- a/crates/openhuman-core/src/threads/turn_state/mirror_finish_and_subagent_args_tests.rs +++ b/crates/openhuman-core/src/threads/turn_state/mirror_finish_and_subagent_args_tests.rs @@ -61,6 +61,9 @@ fn subagent_transcript_persists_interleaved_prose_and_tools() { elapsed_ms: 12, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let activity = m.snapshot().tool_timeline[0] @@ -421,6 +424,9 @@ fn tinyagents_path_backfills_arguments_from_the_completion_event() { elapsed_ms: 12, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let activity = m.snapshot().tool_timeline[0] @@ -473,6 +479,9 @@ fn completion_arguments_do_not_overwrite_arguments_captured_at_start() { elapsed_ms: 12, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let activity = m.snapshot().tool_timeline[0] diff --git a/crates/openhuman-core/src/threads/turn_state/mirror_observe_tests.rs b/crates/openhuman-core/src/threads/turn_state/mirror_observe_tests.rs index 3e917c5b50e..83780b9482a 100644 --- a/crates/openhuman-core/src/threads/turn_state/mirror_observe_tests.rs +++ b/crates/openhuman-core/src/threads/turn_state/mirror_observe_tests.rs @@ -121,6 +121,9 @@ fn tool_call_start_and_complete_track_timeline() { elapsed_ms: 50, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let s = m.snapshot(); assert_eq!(s.tool_timeline[0].status, ToolTimelineStatus::Success); @@ -150,6 +153,9 @@ fn tool_call_completed_persists_capped_output() { elapsed_ms: 50, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let s = m.snapshot(); assert_eq!(s.tool_timeline[0].output.as_deref(), Some("hello world")); @@ -175,6 +181,9 @@ fn tool_call_completed_persists_capped_output() { elapsed_ms: 50, iteration: 2, failure: None, + display_label: None, + display_detail: None, + structured: None, }); let s = m.snapshot(); let persisted = s.tool_timeline[1].output.as_deref().unwrap(); @@ -406,6 +415,9 @@ fn tool_call_started_reuses_args_delta_placeholder_for_same_call_id() { elapsed_ms: 5, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }); assert_eq!(m.snapshot().tool_timeline.len(), 1); assert_eq!( diff --git a/crates/openhuman-core/src/tools/ops_tests.rs b/crates/openhuman-core/src/tools/ops_tests.rs index 7feb5510eb8..5d874ba3bef 100644 --- a/crates/openhuman-core/src/tools/ops_tests.rs +++ b/crates/openhuman-core/src/tools/ops_tests.rs @@ -436,6 +436,8 @@ const ALWAYS_PRESENT_MEMORY_TOOLS: &[&str] = &["update_memory_md", "memory_store #[path = "ops_tests_capability_gating_tests.rs"] mod capability_gating_tests; +#[path = "ops_tests_catalog_fixture_tests.rs"] +mod catalog_fixture_tests; #[path = "ops_tests_default_registry_tests.rs"] mod default_registry_tests; #[path = "ops_tests_domain_family_tests.rs"] diff --git a/crates/openhuman-core/src/tools/ops_tests_catalog_fixture_tests.rs b/crates/openhuman-core/src/tools/ops_tests_catalog_fixture_tests.rs new file mode 100644 index 00000000000..e93e0099725 --- /dev/null +++ b/crates/openhuman-core/src/tools/ops_tests_catalog_fixture_tests.rs @@ -0,0 +1,148 @@ +//! Drift guard between the core's registered tool catalog and the frontend's +//! own copy of that name list (issue: tool-call presentation). +//! +//! `app/src/features/conversations/tools/` renders a fallback label/icon for +//! any tool name it recognizes even before the server-computed +//! `display_label`/`display_detail` arrive (e.g. on a cold reconnect that +//! replays a persisted timeline). That fallback table is only ever as +//! accurate as the day someone last updated it by hand, so this test builds +//! the REAL registered catalog on every core test run and fails loudly the +//! moment it disagrees with the frontend's copy, naming exactly what was +//! added or removed and how to regenerate. +use super::*; +use std::path::PathBuf; + +/// Path to the frontend's copy of the tool-name list, relative to this +/// crate's manifest directory (`crates/openhuman-core`). +fn fixture_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../app/src/features/conversations/tools/__fixtures__/coreToolNames.json") +} + +/// The full model-facing tool catalog this build can register, sorted and +/// deduplicated. +/// +/// Built from [`all_tools`] (which thinly wraps [`all_tools_with_runtime`] +/// with the native runtime adapter) under a config that widens every toggle +/// this test controls — the browser tool enabled, in addition to whatever +/// `Config::default()` already turns on — so the registered set is as close +/// to maximal as a config alone can make it. What this canNOT widen: +/// +/// * **Composio per-connection action tools** (`ComposioActionTool`, dynamic +/// slugs like `GMAIL_SEND_EMAIL`) are never part of this static list. +/// `all_composio_agent_tools` only ever registers its five fixed dispatcher +/// tools (`composio_list_toolkits`, `composio_list_connections`, +/// `composio_authorize`, `composio_connect`, `composio_list_tools`, +/// `composio_execute`) and gates even those on a signed-in session, which +/// this test's config does not have — so this build contributes none of +/// them, static or dynamic, and the fixture should never carry a +/// `COMPOSIO_*`/upper-snake action slug. +/// * **BYOK search engines** (Exa, Tavily, Querit, Brave, ...) and other +/// API-key-gated tools that require a live key in config are absent here; +/// only the managed `web_search_tool` (or whichever tool the enabled +/// feature set + config resolves to) is registered. +/// * **Cargo feature gates**: this test runs under this crate's default +/// features (`cargo test -p openhuman`), matching the contributor build +/// `AGENTS.md` documents as authoritative for the test lane. A tool +/// compiled out under a non-default feature set (see +/// `scripts/ci/product-features.txt` for the shipped product's gates) +/// will not appear here even though it exists in the source tree; this is +/// intentional; add a comment at the call site (not in the fixture) when +/// a name conditionally disappears under a feature combination CI covers. +/// +/// On top of the domain registry this adds the two harness-intrinsic bridge +/// tool names, `tool_search` and `tool_call` +/// (`tinyagents_harness::tool::discover::{TOOL_SEARCH_NAME, TOOL_CALL_NAME}`): +/// neither is ever a registered [`tinytools::Tool`] — the agent loop answers +/// both itself once a turn has deferred tools (see that module's doc comment) +/// — but both are model-visible tool names the frontend's tool-call +/// presentation must recognize exactly like any other. +fn full_tool_catalog_names() -> Vec<String> { + let tmp = TempDir::new().unwrap(); + let security = Arc::new(SecurityPolicy::default()); + let mut cfg = test_config(&tmp); + cfg.browser.enabled = true; + let browser = cfg.browser.clone(); + let http = cfg.http_request.clone(); + + let tools = all_tools( + Arc::new(cfg.clone()), + &security, + AuditLogger::disabled(), + &browser, + &http, + tmp.path(), + &HashMap::new(), + &cfg, + ); + + let mut names: Vec<String> = tools.iter().map(|t| t.name().to_string()).collect(); + names.push(tinyagents_harness::tool::discover::TOOL_SEARCH_NAME.to_string()); + names.push(tinyagents_harness::tool::discover::TOOL_CALL_NAME.to_string()); + names.sort(); + names.dedup(); + // Defensive: a Composio per-connection action tool would be an + // upper-snake slug (e.g. `GMAIL_SEND_EMAIL`) and must never reach this + // static fixture — see the doc comment above for why none should be + // registered here in the first place. + for name in &names { + assert!( + !(name.chars().any(|c| c.is_ascii_uppercase()) && name.contains('_')), + "catalog contains what looks like a dynamic Composio action slug \ + ({name}); those must be excluded from the static fixture" + ); + } + names +} + +const REGENERATE_COMMAND: &str = "UPDATE_TOOL_CATALOG=1 cargo test -p openhuman --lib \ + tools::ops::tests::catalog_fixture_tests::tool_catalog_matches_frontend_fixture"; + +/// Regenerates the fixture when `UPDATE_TOOL_CATALOG=1`, otherwise fails with +/// exactly what was added/removed relative to it. +#[test] +fn tool_catalog_matches_frontend_fixture() { + let names = full_tool_catalog_names(); + let path = fixture_path(); + + if std::env::var("UPDATE_TOOL_CATALOG").as_deref() == Ok("1") { + let json = serde_json::to_string_pretty(&names).expect("serialize tool catalog"); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).expect("create fixture directory"); + } + std::fs::write(&path, format!("{json}\n")).expect("write tool catalog fixture"); + eprintln!( + "[tool-catalog] rewrote {} with {} names", + path.display(), + names.len() + ); + return; + } + + let existing = std::fs::read_to_string(&path).unwrap_or_else(|e| { + panic!( + "missing tool-catalog fixture at {}: {e}\nGenerate it with:\n {REGENERATE_COMMAND}", + path.display() + ) + }); + let mut expected: Vec<String> = serde_json::from_str(&existing).unwrap_or_else(|e| { + panic!( + "fixture at {} is not a JSON array of strings: {e}", + path.display() + ) + }); + expected.sort(); + expected.dedup(); + + if names != expected { + let added: Vec<&String> = names.iter().filter(|n| !expected.contains(n)).collect(); + let removed: Vec<&String> = expected.iter().filter(|n| !names.contains(n)).collect(); + panic!( + "core tool catalog drifted from the frontend fixture at {}.\n\ + added: {added:?}\n\ + removed: {removed:?}\n\n\ + Regenerate with:\n {REGENERATE_COMMAND}", + path.display() + ); + } +} diff --git a/crates/openhuman-core/src/web_chat/presentation.rs b/crates/openhuman-core/src/web_chat/presentation.rs index 41aef9be92f..fe746e10a5a 100644 --- a/crates/openhuman-core/src/web_chat/presentation.rs +++ b/crates/openhuman-core/src/web_chat/presentation.rs @@ -183,6 +183,8 @@ pub(crate) async fn deliver_response( subagent: None, tool_display_label: None, tool_display_detail: None, + elapsed_ms: None, + structured: None, citations: if i == 0 && !citations.is_empty() { Some(serde_json::json!(citations)) } else { @@ -224,6 +226,8 @@ pub(crate) async fn deliver_response( subagent: None, tool_display_label: None, tool_display_detail: None, + elapsed_ms: None, + structured: None, citations: if citations.is_empty() { None } else { @@ -302,6 +306,8 @@ fn publish_chat_done( subagent: None, tool_display_label: None, tool_display_detail: None, + elapsed_ms: None, + structured: None, citations: if citations.is_empty() { None } else { diff --git a/crates/openhuman-core/src/web_chat/progress_bridge.rs b/crates/openhuman-core/src/web_chat/progress_bridge.rs index f2fcf9ddb34..49b6e644f7b 100644 --- a/crates/openhuman-core/src/web_chat/progress_bridge.rs +++ b/crates/openhuman-core/src/web_chat/progress_bridge.rs @@ -650,10 +650,13 @@ pub(crate) fn spawn_progress_bridge( success, output_chars, output, + arguments, elapsed_ms, iteration, failure, - .. + display_label, + display_detail, + structured, } => { // Serialize the classified failure (if any) for the UI + ledger. let failure_json = failure.as_ref().and_then(|f| serde_json::to_value(f).ok()); @@ -673,6 +676,17 @@ pub(crate) fn spawn_progress_bridge( }), }, ); + log::debug!( + "[web_channel][bridge] tool_result round={} tool={} call_id={} \ + success={} elapsed_ms={} has_structured={} request_id={}", + iteration, + tool_name, + call_id, + success, + elapsed_ms, + structured.is_some(), + request_id + ); publish_seq_stamped( &mut emit_seq, WebChannelEvent { @@ -687,10 +701,23 @@ pub(crate) fn spawn_progress_bridge( // `subagent_tool_result` path. Frontends that only // need size/timing read the ledger telemetry instead. output: Some(cap_wire_output(output)), + // The call arguments the harness captured at + // completion (`ToolCallStarted.arguments` is + // always `Null` on this path). Omitted when the + // harness ran with payload capture off. + args: arguments.filter(|v| !v.is_null()), success: Some(success), round: Some(iteration), tool_call_id: Some(call_id), failure: failure_json, + elapsed_ms: Some(elapsed_ms), + structured, + // Recomputed from the real arguments (unlike the + // started event's args-free computation), so a + // completed row can pick up a detail that only + // became knowable once the arguments existed. + tool_display_label: display_label, + tool_display_detail: display_detail, ..Default::default() }, ); @@ -1130,10 +1157,13 @@ pub(crate) fn spawn_progress_bridge( success, output_chars, output, + arguments, elapsed_ms, iteration, failure, - .. + display_label, + display_detail, + structured, } => { // Serialize the classified failure (if any) so a failed // sub-agent tool row carries its "why + next" copy on the @@ -1173,6 +1203,11 @@ pub(crate) fn spawn_progress_bridge( // bounded size for the wire (#4007); `output_chars` + // `elapsed_ms` still ride along in `subagent` below. output: Some(cap_wire_output(output)), + args: arguments.filter(|v| !v.is_null()), + elapsed_ms: Some(elapsed_ms), + structured, + tool_display_label: display_label, + tool_display_detail: display_detail, failure: failure_json, subagent: Some(SubagentProgressDetail { child_iteration: Some(iteration), diff --git a/crates/openhuman-core/src/web_chat/progress_bridge_tests.rs b/crates/openhuman-core/src/web_chat/progress_bridge_tests.rs index 252e6dc8cfe..0c216a04b08 100644 --- a/crates/openhuman-core/src/web_chat/progress_bridge_tests.rs +++ b/crates/openhuman-core/src/web_chat/progress_bridge_tests.rs @@ -126,6 +126,9 @@ async fn tool_call_completed_forwards_real_output_on_tool_result() { elapsed_ms: 42, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }) .await .expect("send progress"); @@ -367,6 +370,9 @@ async fn stamps_monotonic_seq_on_emitted_events() { elapsed_ms: 5, iteration: 1, failure: None, + display_label: None, + display_detail: None, + structured: None, }) .await .unwrap(); diff --git a/gitbooks/developing/architecture/frontend.md b/gitbooks/developing/architecture/frontend.md index 4794fbd85ee..b21c11cf04c 100644 --- a/gitbooks/developing/architecture/frontend.md +++ b/gitbooks/developing/architecture/frontend.md @@ -387,6 +387,43 @@ iteration, the delegation prompt excerpt, or final status. The thread timeline remains the authoritative detailed view; sub-mascots are only the glanceable orchestration layer around the main mascot. +### Tool-call presentation + +Every surface that names a tool call (chat cards, the processing panel, the +status line, the mascot) resolves it through one registry, +`app/src/features/conversations/tools/toolPresentation.ts` +(`describeToolCall`). It returns the icon, a translated phrase in two tenses +("Reading file" while running, "Read file" once settled), the target chip, and +which rich body the call expands into. The data lives in `toolSpecs.ts` (exact +names, collapsed tools that switch on an argument, prefix families, named +agents) and `toolPhrases.ts` (phrases, served as +`conversations.tools.<id>.active|done`). Composio action slugs +(`GMAIL_SEND_EMAIL`) resolve through the toolkit catalog in +`components/composio/toolkitMeta.tsx` to "Used Gmail · Send email" with the +app's logo. The server's `tool_display_label` is used only for tools the +registry cannot describe. + +Rendering uses assistant-ui's elements, vendored under +`app/src/components/assistant-ui/elements/` (tool-call, tool-timeline, +web-search, terminal-block, code-diff, web-preview) with the `tw-shimmer` +utility. `ChatToolGroup` wraps a run of calls in the tool timeline; +`AssistantUiToolCallCard` renders each call. The adapters in +`tools/ToolBodies.tsx` only map tool data onto those elements. + +The core's `tool_result` socket event carries `args`, `elapsed_ms`, the +recomputed `tool_display_label` / `tool_display_detail`, and `structured` +(the tool's `ToolResult.metadata`; web searches send +`{ kind: "web_search", query, provider, results: [...] }`). +`parseWebSearchResult.ts` prefers that payload and falls back to parsing the +text rendering for older turns. + +`tools/__fixtures__/coreToolNames.json` lists every tool the core registers. +The Rust test `tools/ops_tests_catalog_fixture_tests.rs` keeps it in sync +(`UPDATE_TOOL_CATALOG=1` regenerates it) and +`toolPresentation.catalog.test.ts` fails if any listed tool falls through to +the generic fallback, so a new core tool cannot reach the chat unlabelled. +`/dev/tools` (dev builds only) renders every state and the whole catalog. + --- ## Pages & Routing diff --git a/vendor/tinyagents b/vendor/tinyagents index fcf7e884c40..1763e8b26b4 160000 --- a/vendor/tinyagents +++ b/vendor/tinyagents @@ -1 +1 @@ -Subproject commit fcf7e884c407479721e03f08aa78a5166e44dd52 +Subproject commit 1763e8b26b4981c3bd13bb62a6c805ad8fbbcce3