diff --git a/packages/app/src/components/session/index.ts b/packages/app/src/components/session/index.ts index c4fdb2ff3834..243f8cf8740b 100644 --- a/packages/app/src/components/session/index.ts +++ b/packages/app/src/components/session/index.ts @@ -1,6 +1,8 @@ export { SessionHeader } from "./session-header" export { SessionContextTab } from "./session-context-tab" +export { SessionAgentsTab } from "./session-agents-tab" export { SortableTab, FileVisual } from "./session-sortable-tab" export { SortableTabV2 } from "./session-sortable-tab-v2" +export { SessionSubAgentsUsage } from "./session-sub-agents-usage" export { SortableTerminalTab } from "./session-sortable-terminal-tab" export { NewSessionView } from "./session-new-view" diff --git a/packages/app/src/components/session/session-agents-tab.test.ts b/packages/app/src/components/session/session-agents-tab.test.ts new file mode 100644 index 000000000000..b4325e545883 --- /dev/null +++ b/packages/app/src/components/session/session-agents-tab.test.ts @@ -0,0 +1,138 @@ +import { beforeAll, describe, expect, mock, test } from "bun:test" +import type { ToolPart, ToolState } from "@opencode-ai/sdk/v2/client" + +// Pure-function coverage for the terminal-state reader ported from the CLI footer +// (`packages/opencode/src/cli/cmd/run/subagent-data.ts:295-309`). Nothing is rendered here, but the +// module under test is a `.tsx` whose transitive `@solidjs/router` import evaluates client-only APIs +// at module scope, so it is stubbed the same way `src/components/file-tree.test.ts` does. +let text: typeof import("./session-agents-tab").text +let taskMetadata: typeof import("./session-agents-tab").taskMetadata +let taskSessionID: typeof import("./session-agents-tab").taskSessionID +let taskStatus: typeof import("./session-agents-tab").taskStatus + +beforeAll(async () => { + await mock.module("@solidjs/router", () => ({ + useNavigate: () => () => undefined, + useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], + })) + const mod = await import("./session-agents-tab") + text = mod.text + taskMetadata = mod.taskMetadata + taskSessionID = mod.taskSessionID + taskStatus = mod.taskStatus +}) + +const input = {} +const time = { start: 1, end: 2 } + +function part(state: ToolState, metadata?: Record): ToolPart { + return { + id: "prt_test", + sessionID: "ses_parent", + messageID: "msg_parent", + type: "tool", + callID: "call_test", + tool: "task", + state, + ...(metadata ? { metadata } : {}), + } +} + +function pending(): ToolState { + return { status: "pending", input, raw: "" } +} + +function failed(error: string, metadata?: Record): ToolState { + return { status: "error", input, error, time, ...(metadata ? { metadata } : {}) } +} + +function completed(output: string): ToolState { + return { status: "completed", input, output, title: "", metadata: {}, time } +} + +describe("taskStatus", () => { + const cases: [string, string, ToolPart][] = [ + ["completed wire status", "completed", part(completed(""))], + ["completed with ordinary output", "completed", part(completed("Task completed in 4s."))], + // The `task` tool the user actually runs finalizes an abort as a successful result and signals it only + // in the output's first line - see `.omo/evidence/subagents-tab-cleanup/F3-manual-qa.md` section 5. + ["completed with the long-running abort line", "cancelled", part(completed("Task aborted.\nRan for 12s."))], + ["completed with the bare abort line", "cancelled", part(completed("Aborted"))], + ["completed mentioning abort mid-line", "completed", part(completed("Successfully handled an abort case"))], + ["completed with abort after the first line", "completed", part(completed("Task completed in 4s.\nAborted"))], + ["error refined by state.metadata.interrupted", "cancelled", part(failed("boom", { interrupted: true }))], + ["error refined by part.metadata.interrupted", "cancelled", part(failed("boom"), { interrupted: true })], + ["error carrying the abort sentinel", "cancelled", part(failed("Tool execution aborted"))], + ["error carrying the abort sentinel padded", "cancelled", part(failed(" Tool execution aborted "))], + ["error with any other message", "error", part(failed("ENOENT"))], + ["error with a non-boolean interrupted marker", "error", part(failed("boom", { interrupted: "true" }))], + ["error with an empty message", "error", part(failed(""))], + ["running wire status", "running", part({ status: "running", input, time: { start: 1 } })], + ["pending wire status", "running", part(pending())], + ] + + test.each(cases)("%s maps to %p", (_name, expected, fixture) => { + expect(taskStatus(fixture)).toBe(expected) + }) + + test("an unmapped wire status falls back to running instead of throwing", () => { + const unmapped = part({ status: "queued", input, time } as unknown as ToolState) + expect(() => taskStatus(unmapped)).not.toThrow() + expect(taskStatus(unmapped)).toBe("running") + }) + + test("a state with no metadata carrier at all does not throw", () => { + expect(taskStatus(part(pending()))).toBe("running") + }) +}) + +describe("taskMetadata", () => { + test("prefers the state carrier over the part carrier", () => { + expect(taskMetadata(part(failed("boom", { sessionId: "ses_state" }), { sessionId: "ses_part" }), "sessionId")).toBe( + "ses_state", + ) + }) + + test("falls back to the part carrier when the state has no metadata", () => { + expect(taskMetadata(part(pending(), { sessionId: "ses_part" }), "sessionId")).toBe("ses_part") + }) + + test("is undefined when neither carrier holds the key", () => { + expect(taskMetadata(part(pending()), "sessionId")).toBeUndefined() + }) +}) + +describe("taskSessionID", () => { + test("reads the lowercase-d spelling", () => { + expect(taskSessionID(part(pending(), { sessionId: "ses_child" }))).toBe("ses_child") + }) + + test("reads the uppercase-D spelling", () => { + expect(taskSessionID(part(pending(), { sessionID: "ses_child" }))).toBe("ses_child") + }) + + test("ignores a blank id so a pending part never joins on an empty key", () => { + expect(taskSessionID(part(pending(), { sessionId: " " }))).toBeUndefined() + expect(taskSessionID(part(pending()))).toBeUndefined() + }) +}) + +describe("text", () => { + const cases: [unknown, string | undefined][] = [ + ["ses_child", "ses_child"], + [" padded ", "padded"], + ["", undefined], + [" ", undefined], + [undefined, undefined], + [null, undefined], + [42, undefined], + [true, undefined], + [{}, undefined], + ] + + test.each(cases)("text(%p) is %p", (value, expected) => { + expect(text(value)).toBe(expected) + }) +}) diff --git a/packages/app/src/components/session/session-agents-tab.tsx b/packages/app/src/components/session/session-agents-tab.tsx new file mode 100644 index 000000000000..b9c38a14d502 --- /dev/null +++ b/packages/app/src/components/session/session-agents-tab.tsx @@ -0,0 +1,236 @@ +import { createMemo, For, Show, createSignal } from "solid-js" +import { useNavigate } from "@solidjs/router" +import { useSync } from "@/context/sync" +import { useLanguage } from "@/context/language" +import { useSubAgents } from "@/context/sub-agents" +import { useSessionLayout } from "@/pages/session/session-layout" +import { sessionHref, requireServerKey } from "@/utils/session-route" +import { Icon } from "@opencode-ai/ui/icon" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2" +import type { SessionStatus, ToolPart } from "@opencode-ai/sdk/v2/client" +import { createSessionContextFormatter } from "./session-context-format" + +const COLLAPSE_THRESHOLD = 5 + +type DerivedStatus = SessionStatus["type"] | "cancelled" | "completed" | "error" + +function AgentStatusIcon(props: { status: DerivedStatus }) { + return ( + + } + > + + + ) +} + +function statusLabel(status: DerivedStatus, t: (key: string) => string): string { + if (status === "busy") return t("session.agents.status.busy") + if (status === "retry") return t("session.agents.status.retry") + if (status === "cancelled") return t("session.agents.status.cancelled") + if (status === "completed") return t("session.agents.status.completed") + if (status === "error") return t("session.agents.status.error") + return t("session.agents.status.idle") +} + +function formatTime(ts: number, locale: string): string { + return new Date(ts).toLocaleString(locale, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }) +} + +// Ported from the CLI footer's subagent reader, the authoritative source for a subagent's terminal +// state: packages/opencode/src/cli/cmd/run/subagent-data.ts:125-132,291-331. +export type TaskStatus = "running" | "completed" | "cancelled" | "error" + +export function text(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + return value.trim() || undefined +} + +// `part.state.metadata` and `part.metadata` are separate carriers, and a pending state has neither. +export function taskMetadata(part: ToolPart, key: string) { + return ("metadata" in part.state ? part.state.metadata?.[key] : undefined) ?? part.metadata?.[key] +} + +export function taskSessionID(part: ToolPart) { + return text(taskMetadata(part, "sessionId")) ?? text(taskMetadata(part, "sessionID")) +} + +export function taskStatus(part: ToolPart): TaskStatus { + if (part.state.status === "completed") { + // A `task` tool that swallows the abort finalizes a normal successful result, so the cancellation + // survives only as the first line of the free-text output. + const firstLine = text(part.state.output)?.split("\n")[0]?.trim() ?? "" + if (firstLine === "Aborted" || firstLine.startsWith("Task aborted")) return "cancelled" + return "completed" + } + + if (part.state.status === "error") { + // Cancellation is never a wire status, only a refinement of `error`. + const interrupted = taskMetadata(part, "interrupted") === true + if (interrupted || text(part.state.error) === "Tool execution aborted") return "cancelled" + return "error" + } + + // `pending` and `running` both fall through here - there is no separate pending bucket. + return "running" +} + +export function SessionAgentsTab() { + const sync = useSync() + const language = useLanguage() + const navigate = useNavigate() + const { params } = useSessionLayout() + const { children, totalCost } = useSubAgents() + const [expanded, setExpanded] = createSignal(false) + + const formatter = createMemo(() => createSessionContextFormatter(language.intl())) + + const visibleChildren = createMemo(() => { + const list = children() + if (expanded() || list.length <= COLLAPSE_THRESHOLD) return list + return list.slice(0, COLLAPSE_THRESHOLD) + }) + + const hasMore = createMemo(() => children().length > COLLAPSE_THRESHOLD && !expanded()) + + // The parent session's task tool parts are the only authoritative source for a child's terminal + // state, and they are already live in the store, so this joins without issuing any request. + const taskParts = createMemo(() => { + const parentID = params.id + if (!parentID) return new Map() + const parts = (sync().data.message[parentID] ?? []).flatMap((message) => sync().data.part[message.id] ?? []) + return new Map( + parts + .filter((part): part is ToolPart => part.type === "tool" && part.tool === "task") + // A pending task part carries no metadata yet, so it has no child sessionID to join on. + .flatMap((part) => { + const childID = taskSessionID(part) + return childID ? [[childID, part] as const] : [] + }), + ) + }) + + const deriveStatus = (sessionID: string): DerivedStatus => { + const task = taskParts().get(sessionID) + const derived = task ? taskStatus(task) : undefined + if (derived && derived !== "running") return derived + const status = sync().data.session_status[sessionID] + if (status?.type === "busy") return "busy" + if (status?.type === "retry") return "retry" + return "idle" + } + + const navigateToSession = (sessionID: string) => { + const serverKey = params.serverKey + if (!serverKey) return + navigate(sessionHref(requireServerKey(serverKey), sessionID)) + } + + return ( + +
+ 0} + fallback={
{language.t("session.agents.empty")}
} + > +
+
{language.t("session.agents.costTotal")}
+
{formatter().cost(totalCost())}
+
+ +
+
+ {language.t("session.agents.costSubagents")} ({children().length}) +
+ +
+ + {(session) => { + const status = createMemo(() => deriveStatus(session.id)) + const tokenTotal = createMemo(() => { + const tokens = session.tokens + return tokens ? tokens.input + tokens.output + tokens.reasoning + tokens.cache.read : 0 + }) + const tokenLabel = createMemo(() => formatter().tokens(tokenTotal())) + return ( + + ) + }} + +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/app/src/components/session/session-context-format.test.ts b/packages/app/src/components/session/session-context-format.test.ts new file mode 100644 index 000000000000..e6cb97f1bc99 --- /dev/null +++ b/packages/app/src/components/session/session-context-format.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test" +import { createSessionContextFormatter } from "./session-context-format" + +// The eight fixture values the extraction (todo 9) was verified against. Expected strings are +// hand-written, never re-derived from Intl or from the formatter's own thresholds. +const fixtures = [0, 0.5, 999, 1000, 9999, 10000, 999999, 1000000] + +describe("createSessionContextFormatter", () => { + describe("cost", () => { + const cases: [number, string][] = [ + [0, "$0.00"], + [0.5, "$0.50"], + [999, "$999.00"], + [1000, "$1,000.00"], + [9999, "$9,999.00"], + [10000, "$10,000.00"], + [999999, "$999,999.00"], + [1000000, "$1,000,000.00"], + ] + + test("covers every fixture value", () => { + expect(cases.map(([value]) => value)).toEqual(fixtures) + }) + + test.each(cases)("cost(%p) is %p in en-US", (value, expected) => { + expect(createSessionContextFormatter("en-US").cost(value)).toBe(expected) + }) + + test("is locale sensitive rather than hardcoded to en-US", () => { + const formatted = createSessionContextFormatter("de-DE").cost(1000) + // de-DE swaps the grouping and decimal separators and moves the symbol to the end. + expect(formatted).toContain("1.000,00") + expect(formatted).toContain("$") + expect(formatted).not.toBe(createSessionContextFormatter("en-US").cost(1000)) + }) + + test("reuses one Intl.NumberFormat across calls on the same formatter", () => { + const formatter = createSessionContextFormatter("en-US") + expect(formatter.cost(1000)).toBe(formatter.cost(1000)) + }) + }) + + describe("tokens", () => { + const cases: [number, string][] = [ + [0, "0"], + [0.5, "0.5"], + [999, "999"], + [1000, "1.0k"], + [9999, "10.0k"], + [10000, "10k"], + [999999, "1000k"], + [1000000, "1.0M"], + ] + + test("covers every fixture value", () => { + expect(cases.map(([value]) => value)).toEqual(fixtures) + }) + + test.each(cases)("tokens(%p) is %p", (value, expected) => { + expect(createSessionContextFormatter("en-US").tokens(value)).toBe(expected) + }) + + test("is not locale sensitive", () => { + for (const value of fixtures) { + expect(createSessionContextFormatter("de-DE").tokens(value)).toBe( + createSessionContextFormatter("en-US").tokens(value), + ) + } + }) + }) +}) diff --git a/packages/app/src/components/session/session-context-format.ts b/packages/app/src/components/session/session-context-format.ts index e7c536d58411..9c082cb2dd47 100644 --- a/packages/app/src/components/session/session-context-format.ts +++ b/packages/app/src/components/session/session-context-format.ts @@ -1,6 +1,8 @@ import { DateTime } from "luxon" export function createSessionContextFormatter(locale: string) { + const currency = new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }) + return { number(value: number | null | undefined) { if (value === undefined) return "—" @@ -16,5 +18,13 @@ export function createSessionContextFormatter(locale: string) { if (!value) return "—" return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED) }, + cost(value: number) { + return currency.format(value) + }, + tokens(total: number) { + if (total < 1000) return String(total) + if (total < 1_000_000) return `${(total / 1000).toFixed(total < 10_000 ? 1 : 0)}k` + return `${(total / 1_000_000).toFixed(1)}M` + }, } } diff --git a/packages/app/src/components/session/session-sub-agents-usage.tsx b/packages/app/src/components/session/session-sub-agents-usage.tsx new file mode 100644 index 000000000000..b8844cc1e6bb --- /dev/null +++ b/packages/app/src/components/session/session-sub-agents-usage.tsx @@ -0,0 +1,61 @@ +import { Match, Switch, createMemo, type ComponentProps } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { useLanguage } from "@/context/language" +import { useSubAgents } from "@/context/sub-agents" +import { createSessionContextFormatter } from "./session-context-format" + +interface SessionSubAgentsUsageProps { + variant?: "indicator" | "button" + placement?: ComponentProps["placement"] +} + +export function SessionSubAgentsUsage(props: SessionSubAgentsUsageProps) { + const { children, agentsBusy, totalCost } = useSubAgents() + const language = useLanguage() + + const formatter = createMemo(() => createSessionContextFormatter(language.intl())) + + const tooltipValue = () => ( +
+
+ {language.t("session.agents.costTotal")} + {formatter().cost(totalCost())} +
+
+ {language.t("session.agents.count")} + {children().length} +
+
+ ) + + const indicator = () => ( +
+ + +
+ ) + + return ( + + + {indicator()} + + + + + + ) +} diff --git a/packages/app/src/context/layout-tabs.ts b/packages/app/src/context/layout-tabs.ts index ea4c675ff4ec..83ff0a7ca8e5 100644 --- a/packages/app/src/context/layout-tabs.ts +++ b/packages/app/src/context/layout-tabs.ts @@ -49,7 +49,7 @@ export function openSessionTab(current: SessionTabState, tab: string): SessionTa } } - if (tab === "context") { + if (tab === "context" || tab === "agents") { return { tabs: { all: [tab, ...current.tabs.all.filter((item) => item !== tab)], active: tab }, preview, diff --git a/packages/app/src/context/sub-agents.test.ts b/packages/app/src/context/sub-agents.test.ts new file mode 100644 index 000000000000..72eab082792c --- /dev/null +++ b/packages/app/src/context/sub-agents.test.ts @@ -0,0 +1,170 @@ +import { beforeAll, describe, expect, mock, test } from "bun:test" +import { plugin } from "bun" +import { createRoot } from "solid-js" +import type { JSX } from "solid-js" +import type { Session } from "@opencode-ai/sdk/v2/client" + +// `packages/app/tsconfig.json` sets `"jsx": "preserve"`, so Bun's transpiler falls back to the +// classic `React.createElement` factory and `sub-agents.tsx` throws `ReferenceError: React is not +// defined` the moment its provider returns. Binding that factory to Solid's own `createComponent` +// inside the module's scope makes `SubAgentsContext.Provider` behave exactly as compiled JSX would, +// without writing to globalThis and without a Babel toolchain. `bun run test:unit` passes no extra +// `--preload`, so the loader has to register itself here. +plugin({ + name: "sub-agents-jsx-factory", + setup(build) { + build.onLoad({ filter: /[\\/]sub-agents\.tsx$/ }, async (args) => ({ + contents: + `import { createComponent } from "solid-js"\n` + + `const React = { createElement: (type, props, ...children) => createComponent(type, children.length === 0 ? props : { ...props, children: children.length === 1 ? children[0] : children }) }\n` + + (await Bun.file(args.path).text()), + loader: "tsx", + })) + }, +}) + +const PARENT = "ses_parent" +const OTHER_PARENT = "ses_other_parent" + +let SubAgentsProvider: typeof import("./sub-agents").SubAgentsProvider +let useSubAgents: typeof import("./sub-agents").useSubAgents + +let liveSessions: Session[] = [] +let backfillSessions: Session[] = [] + +const session = (id: string, parentID: string | undefined, title = id) => + ({ + id, + parentID, + title, + cost: 0, + time: { created: 1, updated: 1 }, + }) as Session + +beforeAll(async () => { + // Bun's module mocks are process-wide and outlive this file, so a stub that replaces a module + // wholesale breaks whichever later test file imports its other exports. Every module that other + // tests reach for therefore keeps its real exports and overrides only the one hook + // `sub-agents.tsx` consumes. None of them pulls in `sub-agents.tsx`, so loading them first cannot + // beat the stubs into place. + const sync = await import("@/context/sync") + const sdk = await import("@/context/sdk") + const serverSDK = await import("@/context/server-sdk") + const solidQuery = await import("@tanstack/solid-query") + + await mock.module("@/context/sync", () => ({ + ...sync, + useSync: () => () => ({ + data: { session: liveSessions, session_status: {} }, + session: { get: (id: string) => liveSessions.find((s) => s.id === id) }, + }), + })) + // Only `directory` is read, as part of the backfill query key. The query function never runs + // because `useQuery` is stubbed below. + await mock.module("@/context/sdk", () => ({ ...sdk, useSDK: () => () => ({ directory: "/tmp" }) })) + await mock.module("@/context/server-sdk", () => ({ ...serverSDK, useServerSDK: () => () => ({ scope: "test" }) })) + // Replaced wholesale on purpose: importing the real module evaluates `@solidjs/router`, which + // throws "Client-only API called on the server side" under the Solid build `bun run test:unit` + // resolves. Nothing else in the suite imports it. + await mock.module("@/pages/session/session-layout", () => ({ useSessionLayout: () => ({ params: { id: PARENT } }) })) + // Same strategy as `global-sync/child-store.test.ts`: stubbing `useQuery` keeps the one-shot + // backfill snapshot synchronous, so the union under test is the only moving part. + await mock.module("@tanstack/solid-query", () => ({ + ...solidQuery, + useQuery: () => ({ + get data() { + return backfillSessions + }, + }), + })) + + const subAgents = await import("./sub-agents") + SubAgentsProvider = subAgents.SubAgentsProvider + useSubAgents = subAgents.useSubAgents +}) + +function withSubAgents( + input: { live: Session[]; backfill: Session[] }, + read: (agents: ReturnType) => T, +) { + liveSessions = input.live + backfillSessions = input.backfill + + return createRoot((dispose) => { + let captured: { value: T } | undefined + // Solid resolves a function child lazily, under the owner the provider just pushed its context + // onto - the non-JSX spelling of ``. + // `JSX.Element` does not model that function form, hence the cast. + const consume = () => { + captured = { value: read(useSubAgents()) } + return null + } + + SubAgentsProvider({ children: consume as unknown as JSX.Element }) + dispose() + if (!captured) throw new Error("provider children were never resolved") + return captured.value + }) +} + +const childIDs = (input: { live: Session[]; backfill: Session[] }) => + withSubAgents(input, (agents) => agents.children().map((s) => s.id)) + +describe("SubAgentsProvider children", () => { + test("returns the live store children when the backfill is empty", () => { + expect( + childIDs({ + live: [session("ses_child_b", PARENT), session("ses_child_a", PARENT)], + backfill: [], + }), + ).toEqual(["ses_child_a", "ses_child_b"]) + }) + + test("returns the backfill children when the live store holds none for this parent", () => { + expect( + childIDs({ + live: [session(PARENT, undefined)], + backfill: [session("ses_child_b", PARENT), session("ses_child_a", PARENT)], + }), + ).toEqual(["ses_child_a", "ses_child_b"]) + }) + + test("keeps one entry for an id held by both sources, and the live store copy wins", () => { + const input = { + live: [session("ses_child_b", PARENT, "live-title")], + backfill: [session("ses_child_a", PARENT, "backfill-only"), session("ses_child_b", PARENT, "stale-title")], + } + + expect(childIDs(input)).toEqual(["ses_child_a", "ses_child_b"]) + expect(withSubAgents(input, (agents) => agents.children().map((s) => s.title))).toEqual([ + "backfill-only", + "live-title", + ]) + }) + + test("excludes a session that belongs to a different parent", () => { + expect( + childIDs({ + live: [session("ses_child_a", PARENT), session("ses_other_child", OTHER_PARENT)], + backfill: [], + }), + ).toEqual(["ses_child_a"]) + }) + + test("returns an empty list when neither source has children", () => { + expect(childIDs({ live: [], backfill: [] })).toEqual([]) + }) + + test("orders both sources lexicographically by id, and repeated reads agree", () => { + const input = { + live: [session("ses_2", PARENT), session("ses_10", PARENT)], + backfill: [session("ses_20", PARENT), session("ses_1", PARENT)], + } + const expected = ["ses_1", "ses_10", "ses_2", "ses_20"] + + expect( + withSubAgents(input, (agents) => [agents.children().map((s) => s.id), agents.children().map((s) => s.id)]), + ).toEqual([expected, expected]) + expect(childIDs(input)).toEqual(expected) + }) +}) diff --git a/packages/app/src/context/sub-agents.tsx b/packages/app/src/context/sub-agents.tsx new file mode 100644 index 000000000000..bbe6b6881e10 --- /dev/null +++ b/packages/app/src/context/sub-agents.tsx @@ -0,0 +1,77 @@ +import { createContext, useContext, createMemo } from "solid-js" +import type { JSX } from "solid-js" +import { useQuery } from "@tanstack/solid-query" +import type { Session } from "@opencode-ai/sdk/v2/client" +import { useSync } from "@/context/sync" +import { useSDK } from "@/context/sdk" +import { useServerSDK } from "@/context/server-sdk" +import { useSessionLayout } from "@/pages/session/session-layout" + +interface SubAgentsContextValue { + children: () => Session[] + agentsBusy: () => boolean + totalCost: () => number +} + +const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) + +const SubAgentsContext = createContext() + +export function SubAgentsProvider(props: { children: JSX.Element }) { + const sync = useSync() + const { params } = useSessionLayout() + const sdk = useSDK() + const serverSDK = useServerSDK() + + // One-shot backfill for subagents created before the live store's paginated fetch window + // included them. Never invalidated - the live store below owns freshness. + const backfillQuery = useQuery(() => ({ + queryKey: [serverSDK().scope, sdk().directory, "childSessions", params.id] as const, + queryFn: async () => { + const id = params.id + if (!id) return [] + const resp = await sdk().client.session.list() + const all = resp.data ?? [] + return all.filter((s) => s.parentID === id) + }, + enabled: () => !!params.id, + staleTime: Infinity, + })) + + const children = createMemo(() => { + const parentID = params.id + if (!parentID) return [] + const merged = new Map((backfillQuery.data ?? []).map((s) => [s.id, s])) + // The live store is fresher than the backfill snapshot, so it wins on conflict. + sync() + .data.session.filter((s) => s.parentID === parentID) + .forEach((s) => merged.set(s.id, s)) + return [...merged.values()].sort((a, b) => cmp(a.id, b.id)) + }) + + const totalCost = createMemo(() => { + const parent = params.id ? sync().session.get(params.id) : undefined + const parentCost = parent?.cost ?? 0 + const childCost = children().reduce((sum, s) => sum + (s.cost ?? 0), 0) + return parentCost + childCost + }) + + const agentsBusy = createMemo(() => { + const statuses = sync().data.session_status + return children().some((s) => statuses[s.id]?.type === "busy") + }) + + const value: SubAgentsContextValue = { + children, + agentsBusy, + totalCost, + } + + return {props.children} +} + +export function useSubAgents(): SubAgentsContextValue { + const context = useContext(SubAgentsContext) + if (!context) throw new Error("useSubAgents must be used within a SubAgentsProvider") + return context +} diff --git a/packages/app/src/i18n/am.ts b/packages/app/src/i18n/am.ts index bda48350e532..badf33f6fa0b 100644 --- a/packages/app/src/i18n/am.ts +++ b/packages/app/src/i18n/am.ts @@ -691,6 +691,19 @@ export const dict = { "session.tab.session": "ክፍለ ጊዜ", "session.tab.review": "ግምገማ", "session.tab.context": "አውድ", + "session.tab.agents": "ንዑስ ወኪሎች", + "session.agents.empty": "ምንም ንዑስ ወኪል እየሰራ አይደለም", + "session.agents.costTotal": "ጠቅላላ ወጪ", + "session.agents.costSubagents": "ንዑስ ወኪሎች", + "session.agents.count": "ወኪሎች", + "session.agents.tokens": "ቶከኖች", + "session.agents.showAll": "ሁሉንም አሳይ ({{count}})", + "session.agents.status.idle": "ባዶ", + "session.agents.status.busy": "በመሥራት ላይ", + "session.agents.status.retry": "እንደገና በመሞከር ላይ", + "session.agents.status.cancelled": "ተሰርዟል", + "session.agents.status.completed": "ተጠናቋል", + "session.agents.status.error": "አልተሳካም", "session.tab.unknown": "ያልታወቀ ክፍለ ጊዜ", "session.panel.reviewAndFiles": "ግምገማ እና ፋይሎች", "session.error.notFound": "ይህ ክፍለ ጊዜ ሊገኝ አይችልም", diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 3f55b3084088..ca8c02402a93 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -690,6 +690,19 @@ export const dict = { "session.tab.session": "جلسة", "session.tab.review": "مراجعة", "session.tab.context": "سياق", + "session.tab.agents": "الوكلاء", + "session.agents.empty": "لا توجد وكلاء فرعيين قيد التشغيل", + "session.agents.costTotal": "التكلفة الإجمالية", + "session.agents.costSubagents": "الوكلاء الفرعيون", + "session.agents.count": "الوكلاء", + "session.agents.tokens": "رموز", + "session.agents.showAll": "عرض الكل ({{count}})", + "session.agents.status.idle": "خامل", + "session.agents.status.busy": "قيد التشغيل", + "session.agents.status.retry": "إعادة المحاولة", + "session.agents.status.cancelled": "ملغية", + "session.agents.status.completed": "مكتمل", + "session.agents.status.error": "فشل", "session.tab.unknown": "جلسة غير معروفة", "session.panel.reviewAndFiles": "المراجعة والملفات", "session.error.notFound": "لا يمكن العثور على هذه الجلسة", @@ -1103,6 +1116,9 @@ export const dict = { "session.header.reveal.fileExplorer": "إظهار في مستكشف الملفات", "session.header.reveal.containingFolder": "فتح المجلد الحاوي", "session.header.open.fileManager": "مدير الملفات", + "session.header.reveal.finder": "إظهار في Finder", + "session.header.reveal.fileExplorer": "إظهار في مستكشف الملفات", + "session.header.reveal.containingFolder": "فتح المجلد المحتوي", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/az.ts b/packages/app/src/i18n/az.ts index d818ae57fd6c..533a81f8a1ff 100644 --- a/packages/app/src/i18n/az.ts +++ b/packages/app/src/i18n/az.ts @@ -713,6 +713,19 @@ export const dict = { "session.tab.session": "Sessiya", "session.tab.review": "İcmal", "session.tab.context": "Kontekst", + "session.tab.agents": "Subagentlər", + "session.agents.empty": "Heç bir subagent işləmir", + "session.agents.costTotal": "Ümumi xərc", + "session.agents.costSubagents": "Subagentlər", + "session.agents.count": "Agentlər", + "session.agents.tokens": "token", + "session.agents.showAll": "Hamısını göstər ({{count}})", + "session.agents.status.idle": "Boş", + "session.agents.status.busy": "İşləyir", + "session.agents.status.retry": "Yenidən cəhd", + "session.agents.status.cancelled": "Ləğv edildi", + "session.agents.status.completed": "Tamamlandı", + "session.agents.status.error": "Uğursuz", "session.tab.unknown": "Naməlum sessiya", "session.panel.reviewAndFiles": "İcmal və fayllar", "session.error.notFound": "Bu sessiyanı tapmaq mümkün deyil", diff --git a/packages/app/src/i18n/bg.ts b/packages/app/src/i18n/bg.ts index 7f63d1fa4b3e..bdbd486ed0e4 100644 --- a/packages/app/src/i18n/bg.ts +++ b/packages/app/src/i18n/bg.ts @@ -709,6 +709,19 @@ export const dict = { "session.tab.session": "Сесия", "session.tab.review": "Преглед", "session.tab.context": "Контекст", + "session.tab.agents": "Подагенти", + "session.agents.empty": "Няма активни подабгенти", + "session.agents.costTotal": "Обща цена", + "session.agents.costSubagents": "Подагенти", + "session.agents.count": "Агенти", + "session.agents.tokens": "токена", + "session.agents.showAll": "Покажи всички ({{count}})", + "session.agents.status.idle": "Неактивен", + "session.agents.status.busy": "Изпълнява се", + "session.agents.status.retry": "Повторен опит", + "session.agents.status.cancelled": "Отменен", + "session.agents.status.completed": "Завършен", + "session.agents.status.error": "Неуспешен", "session.tab.unknown": "Неизвестна сесия", "session.panel.reviewAndFiles": "Преглед и файлове", "session.error.notFound": "Тази сесия не може да бъде намерена", diff --git a/packages/app/src/i18n/bn.ts b/packages/app/src/i18n/bn.ts index 5f0698ab64f1..83783217bd9d 100644 --- a/packages/app/src/i18n/bn.ts +++ b/packages/app/src/i18n/bn.ts @@ -703,6 +703,19 @@ export const dict: Record = { "session.tab.session": "সেশন", "session.tab.review": "পর্যালোচনা", "session.tab.context": "প্রসঙ্গ", + "session.tab.agents": "সাবএজেন্ট", + "session.agents.empty": "কোনো সাবএজেন্ট চলছে না", + "session.agents.costTotal": "মোট খরচ", + "session.agents.costSubagents": "সাবএজেন্ট", + "session.agents.count": "এজেন্ট", + "session.agents.tokens": "টোকেন", + "session.agents.showAll": "সব দেখান ({{count}})", + "session.agents.status.idle": "নিষ্ক্রিয়", + "session.agents.status.busy": "চলছে", + "session.agents.status.retry": "পুনরায় চেষ্টা", + "session.agents.status.cancelled": "বাতিল", + "session.agents.status.completed": "সম্পন্ন", + "session.agents.status.error": "ব্যর্থ", "session.tab.unknown": "অজানা সেশন", "session.panel.reviewAndFiles": "পর্যালোচনা এবং ফাইল", "session.error.notFound": "এই সেশন খুঁজে পাওয়া যাবে না", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 09da845ccb53..35ea2630adc0 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -697,6 +697,19 @@ export const dict = { "session.tab.session": "Sessão", "session.tab.review": "Revisão", "session.tab.context": "Contexto", + "session.tab.agents": "Subagentes", + "session.agents.empty": "Nenhum subagente em execução", + "session.agents.costTotal": "Custo total", + "session.agents.costSubagents": "Subagentes", + "session.agents.count": "Agentes", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Mostrar tudo ({{count}})", + "session.agents.status.idle": "Ocioso", + "session.agents.status.busy": "Executando", + "session.agents.status.retry": "Tentando novamente", + "session.agents.status.cancelled": "Cancelado", + "session.agents.status.completed": "Concluído", + "session.agents.status.error": "Falhou", "session.tab.unknown": "Sessão desconhecida", "session.panel.reviewAndFiles": "Revisão e arquivos", "session.error.notFound": "Esta sessão não foi encontrada", @@ -1111,6 +1124,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Revelar no Explorador de Arquivos", "session.header.reveal.containingFolder": "Abrir pasta que contém este item", "session.header.open.fileManager": "Gerenciador de Arquivos", + "session.header.reveal.finder": "Revelar no Finder", + "session.header.reveal.fileExplorer": "Revelar no Explorador de Arquivos", + "session.header.reveal.containingFolder": "Abrir pasta que contém", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index feb41c651a87..70fa5677d709 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -750,6 +750,19 @@ export const dict = { "session.tab.session": "Sesija", "session.tab.review": "Pregled", "session.tab.context": "Kontekst", + "session.tab.agents": "Podagenti", + "session.agents.empty": "Nema pokrenutih podagenata", + "session.agents.costTotal": "Ukupni trošak", + "session.agents.costSubagents": "Podagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "tokena", + "session.agents.showAll": "Prikaži sve ({{count}})", + "session.agents.status.idle": "Neaktivno", + "session.agents.status.busy": "Pokrenuto", + "session.agents.status.retry": "Ponavljanje", + "session.agents.status.cancelled": "Otkazano", + "session.agents.status.completed": "Završeno", + "session.agents.status.error": "Neuspjelo", "session.tab.unknown": "Nepoznata sesija", "session.panel.reviewAndFiles": "Pregled i datoteke", "session.error.notFound": "Ova sesija nije pronađena", @@ -1187,6 +1200,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Prikaži u File Exploreru", "session.header.reveal.containingFolder": "Otvori fasciklu koja sadrži ovu stavku", "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Prikaži u Finderu", + "session.header.reveal.fileExplorer": "Prikaži u File Exploreru", + "session.header.reveal.containingFolder": "Otvori folder koji ga sadrži", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ca.ts b/packages/app/src/i18n/ca.ts index 21ad325607a6..dc5136497d62 100644 --- a/packages/app/src/i18n/ca.ts +++ b/packages/app/src/i18n/ca.ts @@ -709,6 +709,19 @@ export const dict = { "session.tab.session": "Sessió", "session.tab.review": "Revisió", "session.tab.context": "Context", + "session.tab.agents": "Subagents", + "session.agents.empty": "Cap subagent en execució", + "session.agents.costTotal": "Cost total", + "session.agents.costSubagents": "Subagents", + "session.agents.count": "Agents", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Mostra-ho tot ({{count}})", + "session.agents.status.idle": "Inactiu", + "session.agents.status.busy": "En execució", + "session.agents.status.retry": "Reintentant", + "session.agents.status.cancelled": "Cancel·lat", + "session.agents.status.completed": "Completat", + "session.agents.status.error": "Error", "session.tab.unknown": "Sessió desconeguda", "session.panel.reviewAndFiles": "Revisió i arxius", "session.error.notFound": "No es pot trobar aquesta sessió", diff --git a/packages/app/src/i18n/cs.ts b/packages/app/src/i18n/cs.ts index d79d4eb5a7f8..0e09a8ca355c 100644 --- a/packages/app/src/i18n/cs.ts +++ b/packages/app/src/i18n/cs.ts @@ -704,6 +704,19 @@ export const dict = { "session.tab.session": "Relace", "session.tab.review": "Recenze", "session.tab.context": "Kontext", + "session.tab.agents": "Podagenti", + "session.agents.empty": "Žádní podagenti neběží", + "session.agents.costTotal": "Celkové náklady", + "session.agents.costSubagents": "Podagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "tokenů", + "session.agents.showAll": "Zobrazit vše ({{count}})", + "session.agents.status.idle": "Nečinný", + "session.agents.status.busy": "Běží", + "session.agents.status.retry": "Opakování", + "session.agents.status.cancelled": "Zrušeno", + "session.agents.status.completed": "Dokončeno", + "session.agents.status.error": "Selhalo", "session.tab.unknown": "Neznámá relace", "session.panel.reviewAndFiles": "Recenze a soubory", "session.error.notFound": "Tuto relaci nelze najít", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 87737b094240..19b268d1b8cd 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -630,6 +630,19 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Gennemgang", "session.tab.context": "Kontekst", + "session.tab.agents": "Underagenter", + "session.agents.empty": "Ingen underkører i gang", + "session.agents.costTotal": "Samlet omkostning", + "session.agents.costSubagents": "Underkører", + "session.agents.count": "Agenter", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Vis alle ({{count}})", + "session.agents.status.idle": "Inaktiv", + "session.agents.status.busy": "Kører", + "session.agents.status.retry": "Forsøger igen", + "session.agents.status.cancelled": "Annulleret", + "session.agents.status.completed": "Fuldført", + "session.agents.status.error": "Mislykkedes", "session.tab.unknown": "Ukendt session", "session.panel.reviewAndFiles": "Gennemgang og filer", "session.error.notFound": "Denne session kan ikke findes", @@ -1061,6 +1074,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Vis i Stifinder", "session.header.reveal.containingFolder": "Åbn den overordnede mappe", "session.header.open.fileManager": "Filhåndtering", + "session.header.reveal.finder": "Vis i Finder", + "session.header.reveal.fileExplorer": "Vis i Stifinder", + "session.header.reveal.containingFolder": "Åbn indeholdende mappe", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 00849598c5c5..d8ab566a18a0 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -589,6 +589,19 @@ export const dict = { "session.tab.session": "Sitzung", "session.tab.review": "Überprüfung", "session.tab.context": "Kontext", + "session.tab.agents": "Sub-Agents", + "session.agents.empty": "Keine Sub-Agents aktiv", + "session.agents.costTotal": "Gesamtkosten", + "session.agents.costSubagents": "Sub-Agents", + "session.agents.count": "Agents", + "session.agents.tokens": "Token", + "session.agents.showAll": "Alle anzeigen ({{count}})", + "session.agents.status.idle": "Inaktiv", + "session.agents.status.busy": "Läuft", + "session.agents.status.retry": "Erneuter Versuch", + "session.agents.status.cancelled": "Abgebrochen", + "session.agents.status.completed": "Abgeschlossen", + "session.agents.status.error": "Fehlgeschlagen", "session.tab.unknown": "Unbekannte Sitzung", "session.panel.reviewAndFiles": "Überprüfung und Dateien", "session.error.notFound": "Diese Sitzung wurde nicht gefunden", @@ -1005,6 +1018,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Im Datei-Explorer anzeigen", "session.header.reveal.containingFolder": "Übergeordneten Ordner öffnen", "session.header.open.fileManager": "Dateimanager", + "session.header.reveal.finder": "Im Finder anzeigen", + "session.header.reveal.fileExplorer": "Im Datei-Explorer anzeigen", + "session.header.reveal.containingFolder": "Übergeordneten Ordner öffnen", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/dv.ts b/packages/app/src/i18n/dv.ts index 27b5fea86999..4f5b3d97d055 100644 --- a/packages/app/src/i18n/dv.ts +++ b/packages/app/src/i18n/dv.ts @@ -715,6 +715,19 @@ export const dict = { "session.tab.session": "ސެޝަން", "session.tab.review": "ރިވިއު", "session.tab.context": "ކޮންޓެކްސްޓް", + "session.tab.agents": "ސަބްއެޖެންޓުން", + "session.agents.empty": "ކުރިއަށް އޮތް ސަބްއެޖެންޓެއް ނެތް", + "session.agents.costTotal": "ޖުމްލަ އަގު", + "session.agents.costSubagents": "ސަބްއެޖެންޓުން", + "session.agents.count": "އޭޖެންޓުން", + "session.agents.tokens": "ޓޯކަންތައް", + "session.agents.showAll": "ހުރިހާ އެއްޗެއް ދައްކާ ({{count}})", + "session.agents.status.idle": "ހުސްވެފައި", + "session.agents.status.busy": "ކުރިއަށް ދަނީ", + "session.agents.status.retry": "އަލުން މަސައްކަތް ކުރަނީ", + "session.agents.status.cancelled": "ކެންސަލްކޮށްފި", + "session.agents.status.completed": "ނިމިއްޖެ", + "session.agents.status.error": "ނާކާމިޔާބު", "session.tab.unknown": "ނޭނގޭ ސެޝަނެއް", "session.panel.reviewAndFiles": "ރިވިއުކޮށް ފައިލްކުރުން", "session.error.notFound": "މި ސެޝަން ހޯދިފައެއް ނުވެއެވެ", @@ -755,14 +768,17 @@ export const dict = { "session.question.restore": "ސުވާލު އަލުން އާލާކުރުން", "session.question.pending.one": "{{count}} ކުރިއަށް އޮތް ސުވާލު", "session.question.pending.other": "{{count}} ކުރިއަށް އޮތް ސުވާލުތައް", + "session.question.pending.many": "{{count}} ކުރިއަށް އޮތް ސުވާލުތައް", "session.followupDock.summary.one": "{{count}} ކިއު ކޮށްފައިވާ މެސެޖެކެވެ", "session.followupDock.summary.other": "{{count}} ކިއު ކުރެވިފައިވާ މެސެޖުތައް", + "session.followupDock.summary.many": "{{count}} ކިއު ކުރެވިފައިވާ މެސެޖުތައް", "session.followupDock.sendNow": "މިހާރު ފޮނުއްވާ", "session.followupDock.edit": "ބަދަލު ގެނައުން", "session.followupDock.collapse": "ކިއު ޖަހާފައިވާ މެސެޖުތައް ކޮލަޕްސް ކޮށްލާށެވެ", "session.followupDock.expand": "ކިއު ކުރެވިފައިވާ މެސެޖުތައް ފުޅާކުރުން", "session.revertDock.summary.one": "{{count}} ރޯލް ބެކް މެސެޖެއް", "session.revertDock.summary.other": "{{count}} ރޯލްބެކް މެސެޖުތައް", + "session.revertDock.summary.many": "{{count}} ރޯލްބެކް މެސެޖުތައް", "session.revertDock.collapse": "ކޮލަޕްސް ރޯލް ބެކް މެސެޖުތަކެވެ", "session.revertDock.expand": "ރޯލް ބެކް މެސެޖުތައް ފުޅާކުރުން", "session.revertDock.restore": "މެސެޖު ރިސްޓޯރ ކުރާށެވެ", diff --git a/packages/app/src/i18n/dz.ts b/packages/app/src/i18n/dz.ts index 3b2ad46ca304..04503f5f2038 100644 --- a/packages/app/src/i18n/dz.ts +++ b/packages/app/src/i18n/dz.ts @@ -715,6 +715,19 @@ export const dict: Record = { "session.tab.session": "ལཱ་ཡུན།", "session.tab.review": "བསྐྱར་ཞིབ", "session.tab.context": "སྐབས་དོན།", + "session.tab.agents": "སབ་ཨེ་ཇེནཊི།", + "session.agents.empty": "སབ་ཨེ་ཇེནཊི་གང་ཡང་མི་རྒྱུགས།", + "session.agents.costTotal": "བསྡོམས་གོང་ཚད།", + "session.agents.costSubagents": "སབ་ཨེ་ཇེནཊི།", + "session.agents.count": "ཨེ་ཇེནཊི།", + "session.agents.tokens": "ཊོ་ཁེན།", + "session.agents.showAll": "ཆ་མཇུག་གང་རུང་སྟོན། ({{count}})", + "session.agents.status.idle": "ལས་མེད།", + "session.agents.status.busy": "ལས་ཀ་བྱེད་བཞིན།", + "session.agents.status.retry": "བསྐྱར་སྦྱོར།", + "session.agents.status.cancelled": "རྒྱབ་འཐེན་འབད།", + "session.agents.status.completed": "མཇུག་རྫོགས།", + "session.agents.status.error": "ནོར་འཁྲུལ།", "session.tab.unknown": "མ་ཤེས་པའི་ལཱ་ཡུན།", "session.panel.reviewAndFiles": "བསྐྱར་ཞིབ་དང་ཡིག་སྣོད།", "session.error.notFound": "ལཱ་ཡུན་འདི་འཚོལ་མ་ཐོབ།", diff --git a/packages/app/src/i18n/el.ts b/packages/app/src/i18n/el.ts index 8f91b46af010..0807dea4eb3d 100644 --- a/packages/app/src/i18n/el.ts +++ b/packages/app/src/i18n/el.ts @@ -710,6 +710,19 @@ export const dict = { "session.tab.session": "Συνεδρία", "session.tab.review": "Επισκόπηση", "session.tab.context": "Πλαίσιο", + "session.tab.agents": "Υπο-πράκτορες", + "session.agents.empty": "Κανένας υπο-πράκτορας σε εκτέλεση", + "session.agents.costTotal": "Συνολικό κόστος", + "session.agents.costSubagents": "Υπο-πράκτορες", + "session.agents.count": "Πράκτορες", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Εμφάνιση όλων ({{count}})", + "session.agents.status.idle": "Αδρανής", + "session.agents.status.busy": "Εκτελείται", + "session.agents.status.retry": "Επανάληψη", + "session.agents.status.cancelled": "Ακυρώθηκε", + "session.agents.status.completed": "Ολοκληρώθηκε", + "session.agents.status.error": "Απέτυχε", "session.tab.unknown": "Άγνωστη συνεδρία", "session.panel.reviewAndFiles": "Επισκόπηση και αρχεία", "session.error.notFound": "Δεν μπορεί να βρεθεί αυτή η συνεδρία", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 62d4dc1ae6e9..57299efdbfa2 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -668,6 +668,19 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Review", "session.tab.context": "Context", + "session.tab.agents": "Subagents", + "session.agents.empty": "No subagents running", + "session.agents.costTotal": "Total cost", + "session.agents.costSubagents": "Subagents", + "session.agents.count": "Agents", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Show all ({{count}})", + "session.agents.status.idle": "Idle", + "session.agents.status.busy": "Running", + "session.agents.status.retry": "Retrying", + "session.agents.status.cancelled": "Cancelled", + "session.agents.status.completed": "Completed", + "session.agents.status.error": "Failed", "session.tab.unknown": "Unknown Session", "session.panel.reviewAndFiles": "Review and files", "session.error.notFound": "This session cannot be found", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 9e9ff2c36750..6484c7867d56 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -753,6 +753,19 @@ export const dict = { "session.tab.session": "Sesión", "session.tab.review": "Revisión", "session.tab.context": "Contexto", + "session.tab.agents": "Sub-agentes", + "session.agents.empty": "No hay sub-agentes ejecutándose", + "session.agents.costTotal": "Coste total", + "session.agents.costSubagents": "Sub-agentes", + "session.agents.count": "Agentes", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Mostrar todo ({{count}})", + "session.agents.status.idle": "Inactivo", + "session.agents.status.busy": "Ejecutando", + "session.agents.status.retry": "Reintentando", + "session.agents.status.cancelled": "Cancelado", + "session.agents.status.completed": "Completado", + "session.agents.status.error": "Fallido", "session.tab.unknown": "Sesión desconocida", "session.panel.reviewAndFiles": "Revisión y archivos", "session.error.notFound": "No se encuentra esta sesión", @@ -1195,6 +1208,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Mostrar en el Explorador de archivos", "session.header.reveal.containingFolder": "Abrir carpeta contenedora", "session.header.open.fileManager": "Gestor de archivos", + "session.header.reveal.finder": "Mostrar en Finder", + "session.header.reveal.fileExplorer": "Mostrar en el Explorador de archivos", + "session.header.reveal.containingFolder": "Abrir carpeta contenedora", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/et.ts b/packages/app/src/i18n/et.ts index d78296675e3d..a3937d06e50b 100644 --- a/packages/app/src/i18n/et.ts +++ b/packages/app/src/i18n/et.ts @@ -699,6 +699,19 @@ export const dict = { "session.tab.session": "Seanss", "session.tab.review": "Ülevaade", "session.tab.context": "Kontekst", + "session.tab.agents": "Alam-agendid", + "session.agents.empty": "Ühtegi alam-agenti ei tööta", + "session.agents.costTotal": "Kogukulu", + "session.agents.costSubagents": "Alam-agendid", + "session.agents.count": "Agendid", + "session.agents.tokens": "tokenit", + "session.agents.showAll": "Näita kõiki ({{count}})", + "session.agents.status.idle": "Jõude", + "session.agents.status.busy": "Töötab", + "session.agents.status.retry": "Proovib uuesti", + "session.agents.status.cancelled": "Tühistatud", + "session.agents.status.completed": "Lõpetatud", + "session.agents.status.error": "Ebaõnnestus", "session.tab.unknown": "Tundmatu seanss", "session.panel.reviewAndFiles": "Ülevaatus ja failid", "session.error.notFound": "Seda seanssi ei leitud", diff --git a/packages/app/src/i18n/fa.ts b/packages/app/src/i18n/fa.ts index 0d3d721b5324..04ed6f1775ce 100644 --- a/packages/app/src/i18n/fa.ts +++ b/packages/app/src/i18n/fa.ts @@ -703,6 +703,19 @@ export const dict = { "session.tab.session": "جلسه", "session.tab.review": "بررسی کنید", "session.tab.context": "زمینه", + "session.tab.agents": "زیرعامل‌ها", + "session.agents.empty": "هیچ زیرعاملی در حال اجرا نیست", + "session.agents.costTotal": "هزینه کل", + "session.agents.costSubagents": "زیرعامل‌ها", + "session.agents.count": "عامل‌ها", + "session.agents.tokens": "توکن", + "session.agents.showAll": "نمایش همه ({{count}})", + "session.agents.status.idle": "بیکار", + "session.agents.status.busy": "در حال اجرا", + "session.agents.status.retry": "تلاش مجدد", + "session.agents.status.cancelled": "لغو شد", + "session.agents.status.completed": "تکمیل شد", + "session.agents.status.error": "ناموفق", "session.tab.unknown": "جلسه ناشناخته", "session.panel.reviewAndFiles": "بررسی و فایل ها", "session.error.notFound": "این جلسه یافت نمی شود", diff --git a/packages/app/src/i18n/fi.ts b/packages/app/src/i18n/fi.ts index aa9c92574cff..a1aca8a9b89a 100644 --- a/packages/app/src/i18n/fi.ts +++ b/packages/app/src/i18n/fi.ts @@ -601,6 +601,19 @@ export const dict = { "session.tab.session": "Istunto", "session.tab.review": "Tarkistus", "session.tab.context": "Konteksti", + "session.tab.agents": "Aliagentit", + "session.agents.empty": "Ei aliagentteja käynnissä", + "session.agents.costTotal": "Kokonaishinta", + "session.agents.costSubagents": "Aliagentit", + "session.agents.count": "Agentit", + "session.agents.tokens": "tokenia", + "session.agents.showAll": "Näytä kaikki ({{count}})", + "session.agents.status.idle": "Jouten", + "session.agents.status.busy": "Käynnissä", + "session.agents.status.retry": "Yritetään uudelleen", + "session.agents.status.cancelled": "Peruutettu", + "session.agents.status.completed": "Valmis", + "session.agents.status.error": "Epäonnistui", "session.tab.unknown": "Tuntematon istunto", "session.panel.reviewAndFiles": "Tarkistus ja tiedostot", "session.error.notFound": "Tätä istuntoa ei löydy", diff --git a/packages/app/src/i18n/fo.ts b/packages/app/src/i18n/fo.ts index 4329a35b4247..1b74b5fa30ba 100644 --- a/packages/app/src/i18n/fo.ts +++ b/packages/app/src/i18n/fo.ts @@ -704,6 +704,19 @@ export const dict = { "session.tab.session": "Seta", "session.tab.review": "Ummæli", "session.tab.context": "Samanhangur", + "session.tab.agents": "Undirumboð", + "session.agents.empty": "Eingin undirumboð koyrir", + "session.agents.costTotal": "Samlaður kostnaður", + "session.agents.costSubagents": "Undirumboð", + "session.agents.count": "Umboð", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Vís alt ({{count}})", + "session.agents.status.idle": "Óvirkið", + "session.agents.status.busy": "Koyrir", + "session.agents.status.retry": "Royndar aftur", + "session.agents.status.cancelled": "Avlýst", + "session.agents.status.completed": "Liðugt", + "session.agents.status.error": "Miseydnaðist", "session.tab.unknown": "Ókend seta", "session.panel.reviewAndFiles": "Ummæli og skráir", "session.error.notFound": "Henda setan er ikki at finna", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 3c89860a4490..dbec077e2a9f 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -707,6 +707,19 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Revue", "session.tab.context": "Contexte", + "session.tab.agents": "Sous-agents", + "session.agents.empty": "Aucun sous-agent en cours", + "session.agents.costTotal": "Coût total", + "session.agents.costSubagents": "Sous-agents", + "session.agents.count": "Agents", + "session.agents.tokens": "jetons", + "session.agents.showAll": "Tout afficher ({{count}})", + "session.agents.status.idle": "Inactif", + "session.agents.status.busy": "En cours", + "session.agents.status.retry": "Nouvelle tentative", + "session.agents.status.cancelled": "Annulé", + "session.agents.status.completed": "Terminé", + "session.agents.status.error": "Échec", "session.tab.unknown": "Session inconnue", "session.panel.reviewAndFiles": "Revue et fichiers", "session.error.notFound": "Cette session est introuvable", @@ -1121,6 +1134,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Afficher dans l’Explorateur de fichiers", "session.header.reveal.containingFolder": "Ouvrir le dossier contenant", "session.header.open.fileManager": "Gestionnaire de fichiers", + "session.header.reveal.finder": "Afficher dans le Finder", + "session.header.reveal.fileExplorer": "Afficher dans l'Explorateur de fichiers", + "session.header.reveal.containingFolder": "Ouvrir le dossier contenant", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/hi.ts b/packages/app/src/i18n/hi.ts index 0aaf8e14dc6d..3b7efb805ef0 100644 --- a/packages/app/src/i18n/hi.ts +++ b/packages/app/src/i18n/hi.ts @@ -713,6 +713,19 @@ export const dict = { "session.tab.session": "सेशन", "session.tab.review": "समीक्षा", "session.tab.context": "कॉन्टेक्स्ट", + "session.tab.agents": "सबएजेंट", + "session.agents.empty": "कोई सबएजेंट नहीं चल रहा", + "session.agents.costTotal": "कुल लागत", + "session.agents.costSubagents": "सबएजेंट", + "session.agents.count": "एजेंट", + "session.agents.tokens": "टोकन", + "session.agents.showAll": "सभी दिखाएं ({{count}})", + "session.agents.status.idle": "निष्क्रिय", + "session.agents.status.busy": "चल रहा है", + "session.agents.status.retry": "पुनः प्रयास", + "session.agents.status.cancelled": "रद्द", + "session.agents.status.completed": "पूर्ण", + "session.agents.status.error": "विफल", "session.tab.unknown": "अज्ञात सेशन", "session.panel.reviewAndFiles": "समीक्षा और फ़ाइलें", "session.error.notFound": "यह सेशन नहीं मिल सका", diff --git a/packages/app/src/i18n/hr.ts b/packages/app/src/i18n/hr.ts index 2130c88ada2d..7a32e08e0250 100644 --- a/packages/app/src/i18n/hr.ts +++ b/packages/app/src/i18n/hr.ts @@ -711,6 +711,19 @@ export const dict = { "session.tab.session": "Sesija", "session.tab.review": "Pregled", "session.tab.context": "Kontekst", + "session.tab.agents": "Podagenti", + "session.agents.empty": "Nijedan podagent nije pokrenut", + "session.agents.costTotal": "Ukupni trošak", + "session.agents.costSubagents": "Podagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "tokena", + "session.agents.showAll": "Prikaži sve ({{count}})", + "session.agents.status.idle": "Neaktivan", + "session.agents.status.busy": "U tijeku", + "session.agents.status.retry": "Pokušava ponovno", + "session.agents.status.cancelled": "Otkazano", + "session.agents.status.completed": "Dovršeno", + "session.agents.status.error": "Neuspješno", "session.tab.unknown": "Nepoznata sesija", "session.panel.reviewAndFiles": "Pregled i datoteke", "session.error.notFound": "Ovu sesiju nije moguće pronaći", diff --git a/packages/app/src/i18n/hu.ts b/packages/app/src/i18n/hu.ts index 664abf8ee4c0..fc086e044f2f 100644 --- a/packages/app/src/i18n/hu.ts +++ b/packages/app/src/i18n/hu.ts @@ -709,6 +709,19 @@ export const dict = { "session.tab.session": "Munkamenet", "session.tab.review": "Tekintse át", "session.tab.context": "Kontextus", + "session.tab.agents": "Alügynökök", + "session.agents.empty": "Nincs futó alügynök", + "session.agents.costTotal": "Összköltség", + "session.agents.costSubagents": "Alügynökök", + "session.agents.count": "Ügynökök", + "session.agents.tokens": "token", + "session.agents.showAll": "Összes megjelenítése ({{count}})", + "session.agents.status.idle": "Tétlen", + "session.agents.status.busy": "Fut", + "session.agents.status.retry": "Újrapróbálás", + "session.agents.status.cancelled": "Megszakítva", + "session.agents.status.completed": "Befejezve", + "session.agents.status.error": "Sikertelen", "session.tab.unknown": "Ismeretlen munkamenet", "session.panel.reviewAndFiles": "Áttekintés és fájlok", "session.error.notFound": "Ez a munkamenet nem található", diff --git a/packages/app/src/i18n/hy.ts b/packages/app/src/i18n/hy.ts index 6eac43b5e6dd..556295f673f6 100644 --- a/packages/app/src/i18n/hy.ts +++ b/packages/app/src/i18n/hy.ts @@ -708,6 +708,19 @@ export const dict = { "session.tab.session": "Նիստ", "session.tab.review": "Վերանայել", "session.tab.context": "Համատեքստ", + "session.tab.agents": "Ենթագործակալներ", + "session.agents.empty": "Ենթագործակալ չի աշխատում", + "session.agents.costTotal": "Ընդհանուր արժեք", + "session.agents.costSubagents": "Ենթագործակալներ", + "session.agents.count": "Գործակալներ", + "session.agents.tokens": "տոքեն", + "session.agents.showAll": "Ցույց տալ բոլորը ({{count}})", + "session.agents.status.idle": "Անգործ", + "session.agents.status.busy": "Աշխատում է", + "session.agents.status.retry": "Կրկին փորձում է", + "session.agents.status.cancelled": "Չեղարկված", + "session.agents.status.completed": "Ավարտված", + "session.agents.status.error": "Ձախողված", "session.tab.unknown": "Անհայտ նիստ", "session.panel.reviewAndFiles": "Վերանայել և ֆայլեր", "session.error.notFound": "Այս նիստը հնարավոր չէ գտնել", diff --git a/packages/app/src/i18n/id.ts b/packages/app/src/i18n/id.ts index 96dc3c7592e4..44c3f7128d3d 100644 --- a/packages/app/src/i18n/id.ts +++ b/packages/app/src/i18n/id.ts @@ -768,6 +768,19 @@ export const dict = { "session.tab.session": "Sesi", "session.tab.review": "Tinjauan", "session.tab.context": "Konteks", + "session.tab.agents": "Subagen", + "session.agents.empty": "Tidak ada subagen berjalan", + "session.agents.costTotal": "Total biaya", + "session.agents.costSubagents": "Subagen", + "session.agents.count": "Agen", + "session.agents.tokens": "token", + "session.agents.showAll": "Tampilkan semua ({{count}})", + "session.agents.status.idle": "Menganggur", + "session.agents.status.busy": "Berjalan", + "session.agents.status.retry": "Mencoba lagi", + "session.agents.status.cancelled": "Dibatalkan", + "session.agents.status.completed": "Selesai", + "session.agents.status.error": "Gagal", "session.tab.unknown": "Sesi Tidak Dikenal", "session.panel.reviewAndFiles": "Tinjauan dan berkas", "session.error.notFound": "Sesi ini tidak dapat ditemukan", diff --git a/packages/app/src/i18n/is.ts b/packages/app/src/i18n/is.ts index a45ed9a40cb0..da66891a1870 100644 --- a/packages/app/src/i18n/is.ts +++ b/packages/app/src/i18n/is.ts @@ -706,6 +706,19 @@ export const dict = { "session.tab.session": "Lota", "session.tab.review": "Upprifjun", "session.tab.context": "Samhengi", + "session.tab.agents": "Undirfulltrúar", + "session.agents.empty": "Enginn undirfulltrúi keyrir", + "session.agents.costTotal": "Heildarkostnaður", + "session.agents.costSubagents": "Undirfulltrúar", + "session.agents.count": "Fulltrúar", + "session.agents.tokens": "tákn", + "session.agents.showAll": "Sýna allt ({{count}})", + "session.agents.status.idle": "Aðgerðalaus", + "session.agents.status.busy": "Keyrir", + "session.agents.status.retry": "Reynir aftur", + "session.agents.status.cancelled": "Hætt við", + "session.agents.status.completed": "Lokið", + "session.agents.status.error": "Mistókst", "session.tab.unknown": "Óþekkt lota", "session.panel.reviewAndFiles": "Yfirferð og skrár", "session.error.notFound": "Þessi lota finnst ekki", diff --git a/packages/app/src/i18n/it.ts b/packages/app/src/i18n/it.ts index a093e11485a3..88ab2ea40b6f 100644 --- a/packages/app/src/i18n/it.ts +++ b/packages/app/src/i18n/it.ts @@ -619,6 +619,19 @@ export const dict = { "session.tab.session": "Sessione", "session.tab.review": "Revisione", "session.tab.context": "Contesto", + "session.tab.agents": "Subagenti", + "session.agents.empty": "Nessun subagente in esecuzione", + "session.agents.costTotal": "Costo totale", + "session.agents.costSubagents": "Subagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "token", + "session.agents.showAll": "Mostra tutti ({{count}})", + "session.agents.status.idle": "Inattivo", + "session.agents.status.busy": "In esecuzione", + "session.agents.status.retry": "Nuovo tentativo", + "session.agents.status.cancelled": "Annullato", + "session.agents.status.completed": "Completato", + "session.agents.status.error": "Errore", "session.tab.unknown": "Sessione sconosciuta", "session.panel.reviewAndFiles": "Revisione e file", "session.error.notFound": "Impossibile trovare questa sessione", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index b96e8b544b2b..cdc929cb9e85 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -693,6 +693,19 @@ export const dict = { "session.tab.session": "セッション", "session.tab.review": "レビュー", "session.tab.context": "コンテキスト", + "session.tab.agents": "サブエージェント", + "session.agents.empty": "実行中のサブエージェントはありません", + "session.agents.costTotal": "合計コスト", + "session.agents.costSubagents": "サブエージェント", + "session.agents.count": "エージェント", + "session.agents.tokens": "トークン", + "session.agents.showAll": "すべて表示 ({{count}})", + "session.agents.status.idle": "アイドル", + "session.agents.status.busy": "実行中", + "session.agents.status.retry": "再試行", + "session.agents.status.cancelled": "キャンセル済み", + "session.agents.status.completed": "完了", + "session.agents.status.error": "失敗", "session.tab.unknown": "不明なセッション", "session.panel.reviewAndFiles": "レビューとファイル", "session.error.notFound": "このセッションが見つかりません", @@ -1093,6 +1106,9 @@ export const dict = { "session.header.reveal.fileExplorer": "エクスプローラーで表示", "session.header.reveal.containingFolder": "親フォルダーを開く", "session.header.open.fileManager": "ファイルマネージャー", + "session.header.reveal.finder": "Finder で表示", + "session.header.reveal.fileExplorer": "エクスプローラーで表示", + "session.header.reveal.containingFolder": "格納フォルダーを開く", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ka.ts b/packages/app/src/i18n/ka.ts index be052930a2ec..6b50883f1fcd 100644 --- a/packages/app/src/i18n/ka.ts +++ b/packages/app/src/i18n/ka.ts @@ -704,6 +704,19 @@ export const dict = { "session.tab.session": "სესია", "session.tab.review": "მიმოხილვა", "session.tab.context": "კონტექსტი", + "session.tab.agents": "ქვეაგენტები", + "session.agents.empty": "არცერთი ქვეაგენტი არ მუშაობს", + "session.agents.costTotal": "საერთო ღირებულება", + "session.agents.costSubagents": "ქვეაგენტები", + "session.agents.count": "აგენტები", + "session.agents.tokens": "ტოკენი", + "session.agents.showAll": "ყველას ჩვენება ({{count}})", + "session.agents.status.idle": "უმოქმედო", + "session.agents.status.busy": "მუშაობს", + "session.agents.status.retry": "ხელახლა ცდა", + "session.agents.status.cancelled": "გაუქმებული", + "session.agents.status.completed": "დასრულებული", + "session.agents.status.error": "წარუმატებელი", "session.tab.unknown": "უცნობი სესია", "session.panel.reviewAndFiles": "მიმოხილვა და ფაილები", "session.error.notFound": "ეს სესია ვერ მოიძებნა", diff --git a/packages/app/src/i18n/km.ts b/packages/app/src/i18n/km.ts index 7f1b61fe3667..9981f262388a 100644 --- a/packages/app/src/i18n/km.ts +++ b/packages/app/src/i18n/km.ts @@ -701,6 +701,19 @@ export const dict = { "session.tab.session": "សម័យ", "session.tab.review": "ពិនិត្យ", "session.tab.context": "បរិបទ", + "session.tab.agents": "ភ្នាក់ងាររង", + "session.agents.empty": "គ្មានភ្នាក់ងាររងណាមួយកំពុងដំណើរការ", + "session.agents.costTotal": "ថ្លៃដើមសរុប", + "session.agents.costSubagents": "ភ្នាក់ងាររង", + "session.agents.count": "ភ្នាក់ងារ", + "session.agents.tokens": "ថូខឹន", + "session.agents.showAll": "បង្ហាញទាំងអស់ ({{count}})", + "session.agents.status.idle": "ទំនេរ", + "session.agents.status.busy": "កំពុងដំណើរការ", + "session.agents.status.retry": "ព្យាយាមម្តងទៀត", + "session.agents.status.cancelled": "បានលុបចោល", + "session.agents.status.completed": "បានបញ្ចប់", + "session.agents.status.error": "បរាជ័យ", "session.tab.unknown": "សម័យមិនស្គាល់", "session.panel.reviewAndFiles": "ពិនិត្យ និងឯកសារ", "session.error.notFound": "សម័យនេះមិនអាចរកឃើញទេ។", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index b96fb4c59471..f9662243a46a 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -476,6 +476,19 @@ export const dict = { "session.tab.session": "세션", "session.tab.review": "검토", "session.tab.context": "컨텍스트", + "session.tab.agents": "서브에이전트", + "session.agents.empty": "실행 중인 서브에이전트 없음", + "session.agents.costTotal": "총 비용", + "session.agents.costSubagents": "서브에이전트", + "session.agents.count": "에이전트", + "session.agents.tokens": "토큰", + "session.agents.showAll": "모두 표시 ({{count}})", + "session.agents.status.idle": "대기 중", + "session.agents.status.busy": "실행 중", + "session.agents.status.retry": "재시도", + "session.agents.status.cancelled": "취소됨", + "session.agents.status.completed": "완료", + "session.agents.status.error": "실패", "session.panel.reviewAndFiles": "검토 및 파일", "session.review.filesChanged": "{{count}}개 파일 변경됨", "session.review.change.one": "변경", @@ -818,6 +831,9 @@ export const dict = { "session.header.reveal.fileExplorer": "파일 탐색기에서 보기", "session.header.reveal.containingFolder": "이 항목이 있는 폴더 열기", "session.header.open.fileManager": "파일 관리자", + "session.header.reveal.finder": "Finder에서 보기", + "session.header.reveal.fileExplorer": "파일 탐색기에서 보기", + "session.header.reveal.containingFolder": "상위 폴더 열기", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/lo.ts b/packages/app/src/i18n/lo.ts index acdc26df08aa..845a1ae3f363 100644 --- a/packages/app/src/i18n/lo.ts +++ b/packages/app/src/i18n/lo.ts @@ -699,6 +699,19 @@ export const dict = { "session.tab.session": "ເຊດຊັນ", "session.tab.review": "ການທົບທວນຄືນ", "session.tab.context": "ບໍລິບົດ", + "session.tab.agents": "ຕົວແທນຍ່ອຍ", + "session.agents.empty": "ບໍ່ມີຕົວແທນຍ່ອຍໃດກຳລັງແລ່ນ", + "session.agents.costTotal": "ຄ່າໃຊ້ຈ່າຍລວມ", + "session.agents.costSubagents": "ຕົວແທນຍ່ອຍ", + "session.agents.count": "ຕົວແທນ", + "session.agents.tokens": "ໂທເຄັນ", + "session.agents.showAll": "ສະແດງທັງໝົດ ({{count}})", + "session.agents.status.idle": "ວ່າງ", + "session.agents.status.busy": "ກຳລັງແລ່ນ", + "session.agents.status.retry": "ລອງໃໝ່", + "session.agents.status.cancelled": "ຍົກເລີກແລ້ວ", + "session.agents.status.completed": "ສຳເລັດແລ້ວ", + "session.agents.status.error": "ລົ້ມເຫລວ", "session.tab.unknown": "ເຊດຊັນທີ່ບໍ່ຮູ້ຈັກ", "session.panel.reviewAndFiles": "ການທົບທວນຄືນແລະໄຟລ໌", "session.error.notFound": "ບໍ່ພົບເຊດຊັນນີ້", diff --git a/packages/app/src/i18n/lt.ts b/packages/app/src/i18n/lt.ts index ba161eb31879..d11992598292 100644 --- a/packages/app/src/i18n/lt.ts +++ b/packages/app/src/i18n/lt.ts @@ -710,6 +710,19 @@ export const dict = { "session.tab.session": "Seansas", "session.tab.review": "Apžvalga", "session.tab.context": "Kontekstas", + "session.tab.agents": "Subagentai", + "session.agents.empty": "Nevyksta jokių subagentų", + "session.agents.costTotal": "Bendra kaina", + "session.agents.costSubagents": "Subagentai", + "session.agents.count": "Agentai", + "session.agents.tokens": "žetonų", + "session.agents.showAll": "Rodyti viską ({{count}})", + "session.agents.status.idle": "Neaktyvus", + "session.agents.status.busy": "Vykdoma", + "session.agents.status.retry": "Bandoma iš naujo", + "session.agents.status.cancelled": "Atšaukta", + "session.agents.status.completed": "Baigta", + "session.agents.status.error": "Nepavyko", "session.tab.unknown": "Nežinomas seansas", "session.panel.reviewAndFiles": "Apžvalga ir failai", "session.error.notFound": "Šios sesijos rasti nepavyko", diff --git a/packages/app/src/i18n/lv.ts b/packages/app/src/i18n/lv.ts index df4c5b2dcd91..154d7f9de7b0 100644 --- a/packages/app/src/i18n/lv.ts +++ b/packages/app/src/i18n/lv.ts @@ -705,6 +705,19 @@ export const dict = { "session.tab.session": "Sesija", "session.tab.review": "Pārskats", "session.tab.context": "Konteksts", + "session.tab.agents": "Apakšaģenti", + "session.agents.empty": "Neviens apakšaģents nedarbojas", + "session.agents.costTotal": "Kopējās izmaksas", + "session.agents.costSubagents": "Apakšaģenti", + "session.agents.count": "Aģenti", + "session.agents.tokens": "žetoni", + "session.agents.showAll": "Rādīt visu ({{count}})", + "session.agents.status.idle": "Neaktīvs", + "session.agents.status.busy": "Darbojas", + "session.agents.status.retry": "Mēģina vēlreiz", + "session.agents.status.cancelled": "Atcelts", + "session.agents.status.completed": "Pabeigts", + "session.agents.status.error": "Neizdevās", "session.tab.unknown": "Nezināma sesija", "session.panel.reviewAndFiles": "Pārskats un faili", "session.error.notFound": "Šī sesija nav atrodama", diff --git a/packages/app/src/i18n/mk.ts b/packages/app/src/i18n/mk.ts index 01e60baa408b..7309cabd2515 100644 --- a/packages/app/src/i18n/mk.ts +++ b/packages/app/src/i18n/mk.ts @@ -707,6 +707,19 @@ export const dict = { "session.tab.session": "Сесија", "session.tab.review": "Преглед", "session.tab.context": "Контекст", + "session.tab.agents": "Подагенти", + "session.agents.empty": "Нема активни подабгенти", + "session.agents.costTotal": "Вкупен трошок", + "session.agents.costSubagents": "Подагенти", + "session.agents.count": "Агенти", + "session.agents.tokens": "токени", + "session.agents.showAll": "Прикажи сè ({{count}})", + "session.agents.status.idle": "Неактивен", + "session.agents.status.busy": "Во тек", + "session.agents.status.retry": "Повторен обид", + "session.agents.status.cancelled": "Откажано", + "session.agents.status.completed": "Завршено", + "session.agents.status.error": "Неуспешно", "session.tab.unknown": "Непозната сесија", "session.panel.reviewAndFiles": "Преглед и датотеки", "session.error.notFound": "Оваа сесија не може да се најде", diff --git a/packages/app/src/i18n/mn.ts b/packages/app/src/i18n/mn.ts index 41c4fa085d1a..5600ff48fb74 100644 --- a/packages/app/src/i18n/mn.ts +++ b/packages/app/src/i18n/mn.ts @@ -709,6 +709,19 @@ export const dict = { "session.tab.session": "Сесс", "session.tab.review": "Хяналт", "session.tab.context": "Контекст", + "session.tab.agents": "Дэд агентууд", + "session.agents.empty": "Ямар ч дэд агент ажиллахгүй байна", + "session.agents.costTotal": "Нийт зардал", + "session.agents.costSubagents": "Дэд агентууд", + "session.agents.count": "Агентууд", + "session.agents.tokens": "токен", + "session.agents.showAll": "Бүгдийг харуулах ({{count}})", + "session.agents.status.idle": "Сул", + "session.agents.status.busy": "Ажиллаж байна", + "session.agents.status.retry": "Дахин оролдох", + "session.agents.status.cancelled": "Цуцлагдсан", + "session.agents.status.completed": "Дууссан", + "session.agents.status.error": "Амжилтгүй", "session.tab.unknown": "Үл мэдэгдэх сесс", "session.panel.reviewAndFiles": "Хяналт ба файлууд", "session.error.notFound": "Энэ сесс олдохгүй байна", diff --git a/packages/app/src/i18n/ms.ts b/packages/app/src/i18n/ms.ts index 2c3783d43b7d..035087f9428c 100644 --- a/packages/app/src/i18n/ms.ts +++ b/packages/app/src/i18n/ms.ts @@ -702,6 +702,19 @@ export const dict = { "session.tab.session": "Sesi", "session.tab.review": "Semak", "session.tab.context": "Konteks", + "session.tab.agents": "Sub-ejen", + "session.agents.empty": "Tiada sub-ejen berjalan", + "session.agents.costTotal": "Jumlah kos", + "session.agents.costSubagents": "Sub-ejen", + "session.agents.count": "Ejen", + "session.agents.tokens": "token", + "session.agents.showAll": "Tunjuk semua ({{count}})", + "session.agents.status.idle": "Melandang", + "session.agents.status.busy": "Berjalan", + "session.agents.status.retry": "Mencuba semula", + "session.agents.status.cancelled": "Dibatalkan", + "session.agents.status.completed": "Selesai", + "session.agents.status.error": "Gagal", "session.tab.unknown": "Sesi Tidak Diketahui", "session.panel.reviewAndFiles": "Semak dan fail", "session.error.notFound": "Sesi ini tidak dapat ditemui", diff --git a/packages/app/src/i18n/my.ts b/packages/app/src/i18n/my.ts index 6046f337ff06..8591f3c2ff3f 100644 --- a/packages/app/src/i18n/my.ts +++ b/packages/app/src/i18n/my.ts @@ -711,6 +711,19 @@ export const dict = { "session.tab.session": "အပိုင်း", "session.tab.review": "သုံးသပ်ချက်", "session.tab.context": "ဆက်စပ်အကြောင်းအရာ", + "session.tab.agents": "ခွဲအေးဂျင့်များ", + "session.agents.empty": "မည်သည့် ခွဲအေးဂျင့်မှ မလည်ပတ်နေပါ", + "session.agents.costTotal": "စုစုပေါင်းကုန်ကျစရိတ်", + "session.agents.costSubagents": "ခွဲအေးဂျင့်များ", + "session.agents.count": "အေးဂျင့်များ", + "session.agents.tokens": "တိုကင်များ", + "session.agents.showAll": "အားလုံးပြရန် ({{count}})", + "session.agents.status.idle": "အားလပ်နေသည်", + "session.agents.status.busy": "လည်ပတ်နေသည်", + "session.agents.status.retry": "ထပ်ကြိုးစားနေသည်", + "session.agents.status.cancelled": "ပယ်ဖျက်ပြီး", + "session.agents.status.completed": "ပြီးမြောက်ပြီး", + "session.agents.status.error": "မအောင်မြင်ပါ", "session.tab.unknown": "အမည်မသိ ဆက်ရှင်", "session.panel.reviewAndFiles": "ပြန်လည်သုံးသပ်ခြင်းနှင့် ဖိုင်များ", "session.error.notFound": "ဤစက်ရှင်ကို ရှာမတွေ့ပါ။", diff --git a/packages/app/src/i18n/ne.ts b/packages/app/src/i18n/ne.ts index cc60bff9f400..a158b9eb9eef 100644 --- a/packages/app/src/i18n/ne.ts +++ b/packages/app/src/i18n/ne.ts @@ -704,6 +704,19 @@ export const dict: Record = { "session.tab.session": "सत्र", "session.tab.review": "समीक्षा गर्नुहोस्", "session.tab.context": "सन्दर्भ", + "session.tab.agents": "उप-एजेन्टहरू", + "session.agents.empty": "कुनै उप-एजेन्ट चलिरहेको छैन", + "session.agents.costTotal": "कुल लागत", + "session.agents.costSubagents": "उप-एजेन्टहरू", + "session.agents.count": "एजेन्टहरू", + "session.agents.tokens": "टोकनहरू", + "session.agents.showAll": "सबै देखाउनुहोस् ({{count}})", + "session.agents.status.idle": "निष्क्रिय", + "session.agents.status.busy": "चलिरहेको", + "session.agents.status.retry": "पुनः प्रयास", + "session.agents.status.cancelled": "रद्द गरियो", + "session.agents.status.completed": "सम्पन्न", + "session.agents.status.error": "असफल", "session.tab.unknown": "अज्ञात सत्र", "session.panel.reviewAndFiles": "समीक्षा र फाइलहरू", "session.error.notFound": "यो सत्र फेला पार्न सकिँदैन", diff --git a/packages/app/src/i18n/nl.ts b/packages/app/src/i18n/nl.ts index 6fb1dc09f24b..e540acf747e3 100644 --- a/packages/app/src/i18n/nl.ts +++ b/packages/app/src/i18n/nl.ts @@ -711,6 +711,19 @@ export const dict = { "session.tab.session": "Sessie", "session.tab.review": "Beoordeling", "session.tab.context": "Context", + "session.tab.agents": "Subagents", + "session.agents.empty": "Geen subagents actief", + "session.agents.costTotal": "Totale kosten", + "session.agents.costSubagents": "Subagents", + "session.agents.count": "Agents", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Alles weergeven ({{count}})", + "session.agents.status.idle": "Inactief", + "session.agents.status.busy": "Actief", + "session.agents.status.retry": "Opnieuw proberen", + "session.agents.status.cancelled": "Geannuleerd", + "session.agents.status.completed": "Voltooid", + "session.agents.status.error": "Mislukt", "session.tab.unknown": "Onbekende sessie", "session.panel.reviewAndFiles": "Beoordeling en bestanden", "session.error.notFound": "Deze sessie kan niet worden gevonden", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 11958029dc38..bdb0a83b0e9e 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -635,6 +635,19 @@ export const dict = { "session.tab.session": "Sesjon", "session.tab.review": "Gjennomgang", "session.tab.context": "Kontekst", + "session.tab.agents": "Underagenter", + "session.agents.empty": "Ingen underagenturer kjører", + "session.agents.costTotal": "Totalkostnad", + "session.agents.costSubagents": "Underagenturer", + "session.agents.count": "Agenter", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Vis alle ({{count}})", + "session.agents.status.idle": "Inaktiv", + "session.agents.status.busy": "Kjører", + "session.agents.status.retry": "Prøver igjen", + "session.agents.status.cancelled": "Kansellert", + "session.agents.status.completed": "Fullført", + "session.agents.status.error": "Mislyktes", "session.panel.reviewAndFiles": "Gjennomgang og filer", "session.review.filesChanged": "{{count}} filer endret", "session.review.change.one": "Endring", @@ -1014,6 +1027,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Vis i Filutforsker", "session.header.reveal.containingFolder": "Åpne mappen som inneholder dette elementet", "session.header.open.fileManager": "Filbehandler", + "session.header.reveal.finder": "Vis i Finder", + "session.header.reveal.fileExplorer": "Vis i Filutforsker", + "session.header.reveal.containingFolder": "Åpne mappen som inneholder", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/pa.ts b/packages/app/src/i18n/pa.ts index 6298581c0aab..e26f94c9d675 100644 --- a/packages/app/src/i18n/pa.ts +++ b/packages/app/src/i18n/pa.ts @@ -713,6 +713,19 @@ export const dict = { "session.tab.session": "سیشن", "session.tab.review": "جائزہ", "session.tab.context": "تناظر", + "session.tab.agents": "ਸਬਏਜੰਟ", + "session.agents.empty": "ਕੋਈ ਸਬਏਜੰਟ ਨਹੀਂ ਚੱਲ ਰਿਹਾ", + "session.agents.costTotal": "ਕੁੱਲ ਲਾਗਤ", + "session.agents.costSubagents": "ਸਬਏਜੰਟ", + "session.agents.count": "ਏਜੰਟ", + "session.agents.tokens": "ਟੋਕਨ", + "session.agents.showAll": "ਸਭ ਦਿਖਾਓ ({{count}})", + "session.agents.status.idle": "ਵਿਹਲਾ", + "session.agents.status.busy": "ਚੱਲ ਰਿਹਾ ਹੈ", + "session.agents.status.retry": "ਮੁੜ ਕੋਸ਼ਿਸ਼", + "session.agents.status.cancelled": "ਰੱਦ ਕੀਤਾ", + "session.agents.status.completed": "ਮੁਕੰਮਲ", + "session.agents.status.error": "ਅਸਫਲ", "session.tab.unknown": "نامعلوم سیشن", "session.panel.reviewAndFiles": "جائزہ تے فائلاں", "session.error.notFound": "ایہہ سیشن نئیں لبیا جا سکدا", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 9b3c927498d8..177331455ef3 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -697,6 +697,19 @@ export const dict = { "session.tab.session": "Sesja", "session.tab.review": "Przegląd", "session.tab.context": "Kontekst", + "session.tab.agents": "Podagenty", + "session.agents.empty": "Brak uruchomionych podagentów", + "session.agents.costTotal": "Łączny koszt", + "session.agents.costSubagents": "Podagenty", + "session.agents.count": "Agenty", + "session.agents.tokens": "tokenów", + "session.agents.showAll": "Pokaż wszystkie ({{count}})", + "session.agents.status.idle": "Bezczynny", + "session.agents.status.busy": "Uruchomiono", + "session.agents.status.retry": "Ponawianie", + "session.agents.status.cancelled": "Anulowano", + "session.agents.status.completed": "Zakończono", + "session.agents.status.error": "Niepowodzenie", "session.tab.unknown": "Nieznana sesja", "session.panel.reviewAndFiles": "Przegląd i pliki", "session.error.notFound": "Nie można znaleźć tej sesji", @@ -1113,6 +1126,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Pokaż w Eksploratorze plików", "session.header.reveal.containingFolder": "Otwórz folder zawierający", "session.header.open.fileManager": "Menedżer plików", + "session.header.reveal.finder": "Pokaż w Finderze", + "session.header.reveal.fileExplorer": "Pokaż w Eksploratorze plików", + "session.header.reveal.containingFolder": "Otwórz folder nadrzędny", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ro.ts b/packages/app/src/i18n/ro.ts index c84f9a4ee76e..20f30ce98338 100644 --- a/packages/app/src/i18n/ro.ts +++ b/packages/app/src/i18n/ro.ts @@ -704,6 +704,19 @@ export const dict = { "session.tab.session": "Sesiune", "session.tab.review": "Revizuire", "session.tab.context": "Context", + "session.tab.agents": "Subagenți", + "session.agents.empty": "Niciun subagent în execuție", + "session.agents.costTotal": "Cost total", + "session.agents.costSubagents": "Subagenți", + "session.agents.count": "Agenți", + "session.agents.tokens": "tokenuri", + "session.agents.showAll": "Arată tot ({{count}})", + "session.agents.status.idle": "Inactiv", + "session.agents.status.busy": "În execuție", + "session.agents.status.retry": "Reîncercare", + "session.agents.status.cancelled": "Anulat", + "session.agents.status.completed": "Finalizat", + "session.agents.status.error": "Eșuat", "session.tab.unknown": "Sesiune necunoscută", "session.panel.reviewAndFiles": "Revizuire și fișiere", "session.error.notFound": "Această sesiune nu poate fi găsită", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index cf4fa8d8f4ca..f65e6ea8a1f2 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -750,6 +750,19 @@ export const dict = { "session.tab.session": "Сессия", "session.tab.review": "Обзор", "session.tab.context": "Контекст", + "session.tab.agents": "Подагенты", + "session.agents.empty": "Нет запущенных подагентов", + "session.agents.costTotal": "Общая стоимость", + "session.agents.costSubagents": "Подагенты", + "session.agents.count": "Агенты", + "session.agents.tokens": "токенов", + "session.agents.showAll": "Показать все ({{count}})", + "session.agents.status.idle": "Ожидание", + "session.agents.status.busy": "Выполняется", + "session.agents.status.retry": "Повторная попытка", + "session.agents.status.cancelled": "Отменено", + "session.agents.status.completed": "Завершено", + "session.agents.status.error": "Ошибка", "session.tab.unknown": "Неизвестная сессия", "session.panel.reviewAndFiles": "Обзор и файлы", "session.error.notFound": "Эта сессия не найдена", @@ -1192,6 +1205,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Показать в Проводнике", "session.header.reveal.containingFolder": "Открыть папку расположения", "session.header.open.fileManager": "Файловый менеджер", + "session.header.reveal.finder": "Показать в Finder", + "session.header.reveal.fileExplorer": "Показать в Проводнике", + "session.header.reveal.containingFolder": "Открыть содержащую папку", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/session-agents-usage.test.ts b/packages/app/src/i18n/session-agents-usage.test.ts new file mode 100644 index 000000000000..051e286947cb --- /dev/null +++ b/packages/app/src/i18n/session-agents-usage.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "bun:test" +import path from "node:path" +import { dict } from "./en" + +// Every consumer of these keys is JSX, and the dictionaries themselves are `.ts`, so scanning only +// `.tsx` reads real call sites without matching the declarations it is checking them against. +const root = path.join(import.meta.dir, "..") + +// Matches `language.t("...")` and the bare `t("...")` form `statusLabel` receives as a parameter. +const call = /\bt\(\s*(["'])(session\.tab\.agents|session\.agents\.[A-Za-z0-9.]+)\1/g + +const declared = Object.keys(dict) + .filter((key) => key === "session.tab.agents" || key.startsWith("session.agents.")) + .sort() + +const scan = await (async () => { + const consumers = new Map() + const files: string[] = [] + for await (const file of new Bun.Glob("**/*.tsx").scan({ cwd: root })) { + files.push(file) + for (const match of (await Bun.file(path.join(root, file)).text()).matchAll(call)) { + consumers.set(match[2], [...(consumers.get(match[2]) ?? []), file]) + } + } + return { consumers, files } +})() + +describe("session.agents i18n usage", () => { + test("the scan actually walked the source tree", () => { + expect(scan.files.length).toBeGreaterThan(0) + expect(declared.length).toBeGreaterThan(0) + expect(scan.consumers.size).toBeGreaterThan(0) + }) + + test("every key used in a component is declared in en.ts", () => { + const undeclared = [...scan.consumers] + .filter(([key]) => !Object.hasOwn(dict, key)) + .map(([key, files]) => `${key} (used in ${files.join(", ")})`) + expect(undeclared).toEqual([]) + }) + + test("every key declared in en.ts has at least one consumer", () => { + expect(declared.filter((key) => !scan.consumers.has(key))).toEqual([]) + }) + + test("declared and used sets are exactly one-to-one", () => { + expect([...scan.consumers.keys()].sort()).toEqual(declared) + }) +}) diff --git a/packages/app/src/i18n/si.ts b/packages/app/src/i18n/si.ts index 00ca188b1581..350219a56130 100644 --- a/packages/app/src/i18n/si.ts +++ b/packages/app/src/i18n/si.ts @@ -701,6 +701,19 @@ export const dict: Record = { "session.tab.session": "සැසිය", "session.tab.review": "සමාලෝචනය කරන්න", "session.tab.context": "සන්දර්භය", + "session.tab.agents": "උප නියෝජිතයෝ", + "session.agents.empty": "কোন ඔහු නියෝජිතයෙක් ක්‍රියාත්මක නොවේ", + "session.agents.costTotal": "මුළු පිරිවැය", + "session.agents.costSubagents": "උප නියෝජිතයෝ", + "session.agents.count": "නියෝජිතයෝ", + "session.agents.tokens": "ටෝකන", + "session.agents.showAll": "සියල්ල පෙන්වන්න ({{count}})", + "session.agents.status.idle": "අක්‍රිය", + "session.agents.status.busy": "ක්‍රියාත්මක වෙමින්", + "session.agents.status.retry": "නැවත උත්සාහ කරමින්", + "session.agents.status.cancelled": "අවලංගු කරන ලදී", + "session.agents.status.completed": "සම්පූර්ණ කරන ලදී", + "session.agents.status.error": "අසාර්ථක විය", "session.tab.unknown": "නොදන්නා සැසිය", "session.panel.reviewAndFiles": "සමාලෝචනය සහ ගොනු", "session.error.notFound": "මෙම සැසිය සොයාගත නොහැක", diff --git a/packages/app/src/i18n/sk.ts b/packages/app/src/i18n/sk.ts index 9992e06cc4af..22fe2aa090a5 100644 --- a/packages/app/src/i18n/sk.ts +++ b/packages/app/src/i18n/sk.ts @@ -702,6 +702,19 @@ export const dict = { "session.tab.session": "Relácia", "session.tab.review": "Revízia", "session.tab.context": "Kontext", + "session.tab.agents": "Podagenti", + "session.agents.empty": "Žiadni podagenti nebežia", + "session.agents.costTotal": "Celkové náklady", + "session.agents.costSubagents": "Podagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "tokenov", + "session.agents.showAll": "Zobraziť všetko ({{count}})", + "session.agents.status.idle": "Nečinný", + "session.agents.status.busy": "Beží", + "session.agents.status.retry": "Opakuje sa", + "session.agents.status.cancelled": "Zrušené", + "session.agents.status.completed": "Dokončené", + "session.agents.status.error": "Zlyhalo", "session.tab.unknown": "Neznáma relácia", "session.panel.reviewAndFiles": "Revízia a súbory", "session.error.notFound": "Táto relácia sa nenašla", diff --git a/packages/app/src/i18n/sl.ts b/packages/app/src/i18n/sl.ts index 3c23fcd7f708..d272a31a727d 100644 --- a/packages/app/src/i18n/sl.ts +++ b/packages/app/src/i18n/sl.ts @@ -704,6 +704,19 @@ export const dict = { "session.tab.session": "Seja", "session.tab.review": "Pregled", "session.tab.context": "Kontekst", + "session.tab.agents": "Podagenti", + "session.agents.empty": "Noben podagent ne teče", + "session.agents.costTotal": "Skupni strošek", + "session.agents.costSubagents": "Podagenti", + "session.agents.count": "Agenti", + "session.agents.tokens": "žetonov", + "session.agents.showAll": "Prikaži vse ({{count}})", + "session.agents.status.idle": "Nedejaven", + "session.agents.status.busy": "Deluje", + "session.agents.status.retry": "Poskuša znova", + "session.agents.status.cancelled": "Preklicano", + "session.agents.status.completed": "Končano", + "session.agents.status.error": "Spodletelo", "session.tab.unknown": "Neznana seja", "session.panel.reviewAndFiles": "Pregled in datoteke", "session.error.notFound": "Te seje ni mogoče najti", diff --git a/packages/app/src/i18n/sq.ts b/packages/app/src/i18n/sq.ts index ca94793ab0e6..8dd67f57c132 100644 --- a/packages/app/src/i18n/sq.ts +++ b/packages/app/src/i18n/sq.ts @@ -706,6 +706,19 @@ export const dict = { "session.tab.session": "Sesioni", "session.tab.review": "Rishikimi", "session.tab.context": "Konteksti", + "session.tab.agents": "Nën-agjentë", + "session.agents.empty": "Asnjë nën-agjent nuk po ekzekutohet", + "session.agents.costTotal": "Kosto totale", + "session.agents.costSubagents": "Nën-agjentë", + "session.agents.count": "Agjentë", + "session.agents.tokens": "tokenë", + "session.agents.showAll": "Shfaq të gjitha ({{count}})", + "session.agents.status.idle": "I papunë", + "session.agents.status.busy": "Në ekzekutim", + "session.agents.status.retry": "Riprovo", + "session.agents.status.cancelled": "Anuluar", + "session.agents.status.completed": "Përfunduar", + "session.agents.status.error": "Dështoi", "session.tab.unknown": "Sesion i panjohur", "session.panel.reviewAndFiles": "Rishikimi dhe dosjet", "session.error.notFound": "Ky sesion nuk mund të gjendet", diff --git a/packages/app/src/i18n/sr.ts b/packages/app/src/i18n/sr.ts index f059160087d8..7c8c0e48d184 100644 --- a/packages/app/src/i18n/sr.ts +++ b/packages/app/src/i18n/sr.ts @@ -705,6 +705,19 @@ export const dict = { "session.tab.session": "Сесија", "session.tab.review": "Преглед", "session.tab.context": "Контекст", + "session.tab.agents": "Подагенти", + "session.agents.empty": "Ниједан подабагент није покренут", + "session.agents.costTotal": "Укупан трошак", + "session.agents.costSubagents": "Подагенти", + "session.agents.count": "Агенти", + "session.agents.tokens": "токена", + "session.agents.showAll": "Прикажи све ({{count}})", + "session.agents.status.idle": "Неактиван", + "session.agents.status.busy": "У току", + "session.agents.status.retry": "Покушава поново", + "session.agents.status.cancelled": "Отказано", + "session.agents.status.completed": "Завршено", + "session.agents.status.error": "Неуспешно", "session.tab.unknown": "Непозната сесија", "session.panel.reviewAndFiles": "Преглед и датотеке", "session.error.notFound": "Ова сесија није пронађена", diff --git a/packages/app/src/i18n/sv.ts b/packages/app/src/i18n/sv.ts index 39b363c6b657..5dd278cfab0c 100644 --- a/packages/app/src/i18n/sv.ts +++ b/packages/app/src/i18n/sv.ts @@ -708,6 +708,19 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Granskning", "session.tab.context": "Kontext", + "session.tab.agents": "Subagenter", + "session.agents.empty": "Inga subagenter körs", + "session.agents.costTotal": "Total kostnad", + "session.agents.costSubagents": "Subagenter", + "session.agents.count": "Agenter", + "session.agents.tokens": "tokens", + "session.agents.showAll": "Visa alla ({{count}})", + "session.agents.status.idle": "Inaktiv", + "session.agents.status.busy": "Körs", + "session.agents.status.retry": "Försöker igen", + "session.agents.status.cancelled": "Avbruten", + "session.agents.status.completed": "Slutförd", + "session.agents.status.error": "Misslyckades", "session.tab.unknown": "Okänd session", "session.panel.reviewAndFiles": "Granskning och filer", "session.error.notFound": "Denna session kan inte hittas", diff --git a/packages/app/src/i18n/tg.ts b/packages/app/src/i18n/tg.ts index ffa08409f684..287551fd34b6 100644 --- a/packages/app/src/i18n/tg.ts +++ b/packages/app/src/i18n/tg.ts @@ -706,6 +706,19 @@ export const dict = { "session.tab.session": "Сессия", "session.tab.review": "Барраси", "session.tab.context": "Контекст", + "session.tab.agents": "Зерагентҳо", + "session.agents.empty": "Ҳеҷ як зерагент кор намекунад", + "session.agents.costTotal": "Арзиши умумӣ", + "session.agents.costSubagents": "Зерагентҳо", + "session.agents.count": "Агентҳо", + "session.agents.tokens": "токенҳо", + "session.agents.showAll": "Ҳамаро нишон додан ({{count}})", + "session.agents.status.idle": "Ғайрифаъол", + "session.agents.status.busy": "Дар ҷараён", + "session.agents.status.retry": "Аз нав кӯшиш", + "session.agents.status.cancelled": "Лағв шуд", + "session.agents.status.completed": "Анҷом ёфт", + "session.agents.status.error": "Нокомёб", "session.tab.unknown": "Сеанси номаълум", "session.panel.reviewAndFiles": "Барраси ва файлҳо", "session.error.notFound": "Ин сессияро ёфтан мумкин нест", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 1a0e9db72b1b..9042ec60ee51 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -742,6 +742,19 @@ export const dict = { "session.tab.session": "เซสชัน", "session.tab.review": "ตรวจสอบ", "session.tab.context": "บริบท", + "session.tab.agents": "ตัวแทนย่อย", + "session.agents.empty": "ไม่มีตัวแทนย่อยที่กำลังทำงาน", + "session.agents.costTotal": "ต้นทุนรวม", + "session.agents.costSubagents": "ตัวแทนย่อย", + "session.agents.count": "ตัวแทน", + "session.agents.tokens": "โทเค็น", + "session.agents.showAll": "แสดงทั้งหมด ({{count}})", + "session.agents.status.idle": "ว่าง", + "session.agents.status.busy": "กำลังทำงาน", + "session.agents.status.retry": "ลองใหม่", + "session.agents.status.cancelled": "ถูกยกเลิก", + "session.agents.status.completed": "เสร็จสิ้น", + "session.agents.status.error": "ล้มเหลว", "session.tab.unknown": "เซสชันที่ไม่รู้จัก", "session.panel.reviewAndFiles": "ตรวจสอบและไฟล์", "session.error.notFound": "ไม่พบเซสชันนี้", @@ -1168,6 +1181,9 @@ export const dict = { "session.header.reveal.fileExplorer": "แสดงใน File Explorer", "session.header.reveal.containingFolder": "เปิดโฟลเดอร์ที่มีรายการนี้", "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "แสดงใน Finder", + "session.header.reveal.fileExplorer": "แสดงใน File Explorer", + "session.header.reveal.containingFolder": "เปิดโฟลเดอร์ที่บรรจุอยู่", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/tk.ts b/packages/app/src/i18n/tk.ts index 2b144c6a26c7..dc7f94d1840c 100644 --- a/packages/app/src/i18n/tk.ts +++ b/packages/app/src/i18n/tk.ts @@ -703,6 +703,19 @@ export const dict = { "session.tab.session": "Sessiýa", "session.tab.review": "Syn", "session.tab.context": "Kontekst", + "session.tab.agents": "Kiçi agentler", + "session.agents.empty": "Hiç bir kiçi agent işlemeýär", + "session.agents.costTotal": "Jemi çykdajy", + "session.agents.costSubagents": "Kiçi agentler", + "session.agents.count": "Agentler", + "session.agents.tokens": "token", + "session.agents.showAll": "Hemmesini görkez ({{count}})", + "session.agents.status.idle": "Boş", + "session.agents.status.busy": "Işleýär", + "session.agents.status.retry": "Gaýtadan synanyşýk", + "session.agents.status.cancelled": "Ýatyryldy", + "session.agents.status.completed": "Tamamlandy", + "session.agents.status.error": "Şowsuz", "session.tab.unknown": "Näbelli sessiýa", "session.panel.reviewAndFiles": "Gözden geçiriň we faýllary", "session.error.notFound": "Bu sessiýany tapyp bolmaýar", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 85623036e429..814411b4e8a9 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -754,6 +754,19 @@ export const dict = { "session.tab.session": "Oturum", "session.tab.review": "İnceleme", "session.tab.context": "Bağlam", + "session.tab.agents": "Alt ajanlar", + "session.agents.empty": "Çalışan alt ajan yok", + "session.agents.costTotal": "Toplam maliyet", + "session.agents.costSubagents": "Alt ajanlar", + "session.agents.count": "Ajanlar", + "session.agents.tokens": "token", + "session.agents.showAll": "Tümünü göster ({{count}})", + "session.agents.status.idle": "Boşta", + "session.agents.status.busy": "Çalışıyor", + "session.agents.status.retry": "Yeniden deneniyor", + "session.agents.status.cancelled": "İptal edildi", + "session.agents.status.completed": "Tamamlandı", + "session.agents.status.error": "Başarısız", "session.tab.unknown": "Bilinmeyen Oturum", "session.panel.reviewAndFiles": "İnceleme ve dosyalar", "session.error.notFound": "Bu oturum bulunamıyor", @@ -1191,6 +1204,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Dosya Gezgini", "session.header.open.fileManager": "Dosya Yöneticisi", + "session.header.reveal.finder": "Finder'da göster", + "session.header.reveal.fileExplorer": "Dosya Gezgini'nde göster", + "session.header.reveal.containingFolder": "İçeren klasörü aç", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 54732a87b7eb..d0da07d35857 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -773,6 +773,19 @@ export const dict = { "session.tab.session": "Сесія", "session.tab.review": "Огляд", "session.tab.context": "Контекст", + "session.tab.agents": "Підагенти", + "session.agents.empty": "Немає запущених підагентів", + "session.agents.costTotal": "Загальна вартість", + "session.agents.costSubagents": "Підагенти", + "session.agents.count": "Агенти", + "session.agents.tokens": "токенів", + "session.agents.showAll": "Показати всі ({{count}})", + "session.agents.status.idle": "Очікування", + "session.agents.status.busy": "Виконується", + "session.agents.status.retry": "Повторна спроба", + "session.agents.status.cancelled": "Скасовано", + "session.agents.status.completed": "Завершено", + "session.agents.status.error": "Помилка", "session.tab.unknown": "Невідома сесія", "session.panel.reviewAndFiles": "Огляд і файли", "session.error.notFound": "Цю сесію не знайдено", @@ -860,6 +873,9 @@ export const dict = { "session.header.reveal.fileExplorer": "Показати у Файловому провіднику", "session.header.reveal.containingFolder": "Відкрити папку розташування", "session.header.open.fileManager": "Файловий менеджер", + "session.header.reveal.finder": "Показати у Finder", + "session.header.reveal.fileExplorer": "Показати у Провіднику файлів", + "session.header.reveal.containingFolder": "Відкрити теку, що містить файл", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/ur.ts b/packages/app/src/i18n/ur.ts index 2ab69e7aec71..9aec86d75318 100644 --- a/packages/app/src/i18n/ur.ts +++ b/packages/app/src/i18n/ur.ts @@ -714,6 +714,19 @@ export const dict = { "session.tab.session": "سیشن", "session.tab.review": "جائزہ لیں", "session.tab.context": "سیاق و سباق", + "session.tab.agents": "سب ایجنٹس", + "session.agents.empty": "کوئی سب ایجنٹ نہیں چل رہا", + "session.agents.costTotal": "کل لاگت", + "session.agents.costSubagents": "سب ایجنٹس", + "session.agents.count": "ایجنٹس", + "session.agents.tokens": "ٹوکن", + "session.agents.showAll": "سب دکھائیں ({{count}})", + "session.agents.status.idle": "غیر فعال", + "session.agents.status.busy": "جاری ہے", + "session.agents.status.retry": "دوبارہ کوشش", + "session.agents.status.cancelled": "منسوخ", + "session.agents.status.completed": "مکمل", + "session.agents.status.error": "ناکام", "session.tab.unknown": "نامعلوم سیشن", "session.panel.reviewAndFiles": "جائزہ اور فائلیں۔", "session.error.notFound": "یہ سیشن نہیں مل سکتا", diff --git a/packages/app/src/i18n/uz.ts b/packages/app/src/i18n/uz.ts index 8cb7097fc07a..0ce982516ce5 100644 --- a/packages/app/src/i18n/uz.ts +++ b/packages/app/src/i18n/uz.ts @@ -707,6 +707,19 @@ export const dict = { "session.tab.session": "Sessiya", "session.tab.review": "Koʻrib chiqish", "session.tab.context": "Kontekst", + "session.tab.agents": "Subagentlar", + "session.agents.empty": "Hech qanday subagent ishlamayapti", + "session.agents.costTotal": "Jami xarajat", + "session.agents.costSubagents": "Subagentlar", + "session.agents.count": "Agentlar", + "session.agents.tokens": "tokenlar", + "session.agents.showAll": "Barchasini ko‘rsatish ({{count}})", + "session.agents.status.idle": "Bo‘sh", + "session.agents.status.busy": "Ishlamoqda", + "session.agents.status.retry": "Qayta urinish", + "session.agents.status.cancelled": "Bekor qilindi", + "session.agents.status.completed": "Yakunlandi", + "session.agents.status.error": "Muvaffaqiyatsiz", "session.tab.unknown": "Noma'lum sessiya", "session.panel.reviewAndFiles": "Ko'rib chiqish va fayllar", "session.error.notFound": "Bu sessiya topilmadi", diff --git a/packages/app/src/i18n/vi.ts b/packages/app/src/i18n/vi.ts index 06fda4ce707c..419ab19d9a7b 100644 --- a/packages/app/src/i18n/vi.ts +++ b/packages/app/src/i18n/vi.ts @@ -713,6 +713,19 @@ export const dict = { "session.tab.session": "Phiên", "session.tab.review": "Xem lại", "session.tab.context": "Ngữ cảnh", + "session.tab.agents": "Subagent", + "session.agents.empty": "Không có subagent đang chạy", + "session.agents.costTotal": "Tổng chi phí", + "session.agents.costSubagents": "Subagent", + "session.agents.count": "Agent", + "session.agents.tokens": "token", + "session.agents.showAll": "Hiển thị tất cả ({{count}})", + "session.agents.status.idle": "Nhàn rỗi", + "session.agents.status.busy": "Đang chạy", + "session.agents.status.retry": "Đang thử lại", + "session.agents.status.cancelled": "Đã hủy", + "session.agents.status.completed": "Hoàn thành", + "session.agents.status.error": "Thất bại", "session.tab.unknown": "Phiên không xác định", "session.panel.reviewAndFiles": "Xem lại và tệp", "session.error.notFound": "Không thể tìm thấy phiên này", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index b0ba0453479b..2055b8b5c6cc 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -743,6 +743,19 @@ export const dict = { "session.tab.session": "会话", "session.tab.review": "审查", "session.tab.context": "上下文", + "session.tab.agents": "子代理", + "session.agents.empty": "没有运行中的子代理", + "session.agents.costTotal": "总费用", + "session.agents.costSubagents": "子代理", + "session.agents.count": "代理", + "session.agents.tokens": "Token", + "session.agents.showAll": "显示全部 ({{count}})", + "session.agents.status.idle": "空闲", + "session.agents.status.busy": "运行中", + "session.agents.status.retry": "重试中", + "session.agents.status.cancelled": "已取消", + "session.agents.status.completed": "已完成", + "session.agents.status.error": "失败", "session.tab.unknown": "未知会话", "session.panel.reviewAndFiles": "审查和文件", "session.error.notFound": "找不到此会话", @@ -1159,6 +1172,9 @@ export const dict = { "session.header.reveal.fileExplorer": "在文件资源管理器中显示", "session.header.reveal.containingFolder": "打开所在文件夹", "session.header.open.fileManager": "文件管理器", + "session.header.reveal.finder": "在访达中显示", + "session.header.reveal.fileExplorer": "在文件资源管理器中显示", + "session.header.reveal.containingFolder": "打开所在文件夹", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 6cc3d555289d..58cdcfda058f 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -738,6 +738,19 @@ export const dict = { "session.tab.session": "工作階段", "session.tab.review": "檢閱", "session.tab.context": "上下文", + "session.tab.agents": "子代理", + "session.agents.empty": "沒有執行中的子代理", + "session.agents.costTotal": "總費用", + "session.agents.costSubagents": "子代理", + "session.agents.count": "代理", + "session.agents.tokens": "Token", + "session.agents.showAll": "顯示全部 ({{count}})", + "session.agents.status.idle": "閒置", + "session.agents.status.busy": "執行中", + "session.agents.status.retry": "重試中", + "session.agents.status.cancelled": "已取消", + "session.agents.status.completed": "已完成", + "session.agents.status.error": "失敗", "session.tab.unknown": "未知的工作階段", "session.panel.reviewAndFiles": "檢閱與檔案", "session.error.notFound": "找不到此工作階段", @@ -1155,6 +1168,9 @@ export const dict = { "session.header.reveal.fileExplorer": "在檔案總管中顯示", "session.header.reveal.containingFolder": "開啟所在的檔案夾", "session.header.open.fileManager": "檔案管理員", + "session.header.reveal.finder": "在 Finder 中顯示", + "session.header.reveal.fileExplorer": "在檔案總管中顯示", + "session.header.reveal.containingFolder": "開啟所在資料夾", "session.header.open.app.vscode": "VS Code", "session.header.open.app.cursor": "Cursor", "session.header.open.app.zed": "Zed", diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index c43f6bc6a365..372543b98b91 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -33,6 +33,7 @@ export const createSessionTabs = (input: TabsInput) => { const hasReview = input.hasReview ?? (() => false) const fileBrowser = input.fileBrowser ?? (() => false) const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context")) + const agentsOpen = createMemo(() => input.tabs().active() === "agents" || input.tabs().all().includes("agents")) const openFileOpen = createMemo( () => fileBrowser() && @@ -45,7 +46,7 @@ export const createSessionTabs = (input: TabsInput) => { .tabs() .all() .flatMap((tab) => { - if (tab === "context" || tab === "review") return [] + if (tab === "context" || tab === "review" || tab === "agents") return [] if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return [] const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab if (seen.has(value)) return [] @@ -61,7 +62,7 @@ export const createSessionTabs = (input: TabsInput) => { }) const activeTab = createMemo(() => { const active = input.tabs().active() - if (active === "context") return active + if (active === "context" || active === "agents") return active if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active if (active === "review" && review()) return active if (active && input.pathFromTab(active)) return input.normalizeTab(active) @@ -79,7 +80,7 @@ export const createSessionTabs = (input: TabsInput) => { }) const closableTab = createMemo(() => { const active = activeTab() - if (active === "context") return active + if (active === "context" || active === "agents") return active if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active if (!openedTabs().includes(active)) return return active @@ -87,6 +88,7 @@ export const createSessionTabs = (input: TabsInput) => { return { contextOpen, + agentsOpen, openFileOpen, panelTabs, openedTabs, diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 6f074fa20304..9267532990b5 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -27,7 +27,6 @@ import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import type { FileDiffInfo } from "@opencode-ai/client/promise" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" - import FileTree from "@/components/file-tree" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" import { SessionContextUsage } from "@/components/session-context-usage" @@ -35,8 +34,16 @@ import { SessionContextUsage } from "@/components/session-context-usage" const reviewTabID = "session-side-panel-review-tab" const reviewTabPanelID = "session-side-panel-review-tabpanel" const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel" -import { SessionContextTab, SortableTab, SortableTabV2, FileVisual } from "@/components/session" +import { + SessionContextTab, + SessionAgentsTab, + SortableTab, + SortableTabV2, + FileVisual, + SessionSubAgentsUsage, +} from "@/components/session" import { OpenInAppV2 } from "@/components/session/open-in-app-v2" +import { SubAgentsProvider } from "@/context/sub-agents" import { useCommand } from "@/context/command" import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" @@ -91,7 +98,6 @@ export function SessionSidePanel(props: { const sdk = useSDK() const { sessionKey, tabs, view, params } = useSessionLayout() const projectDirectory = createMemo(() => sdk().directory) - const isDesktop = createMediaQuery("(min-width: 768px)") const shown = settings.visibility.fileTree @@ -182,6 +188,7 @@ export function SessionSidePanel(props: { fileBrowser: () => !!props.fileBrowserState, }) const contextOpen = tabState.contextOpen + const agentsOpen = tabState.agentsOpen const openFileOpen = tabState.openFileOpen const panelTabs = tabState.panelTabs const openedTabs = tabState.openedTabs @@ -220,6 +227,7 @@ export function SessionSidePanel(props: { const path = file.pathFromTab(next) if (path) void file.load(path) openReviewPanel() + void tabs().open(next) tabs().setActive(next) } const browserTab = createMemo(() => { @@ -238,14 +246,13 @@ export function SessionSidePanel(props: { }) const fileBrowserVisible = createMemo(() => { const active = activeTab() - return active !== "review" && active !== "context" && active !== "empty" + return active !== "review" && active !== "context" && active !== "agents" && active !== "empty" }) const openFileKeybind = createMemo(() => command.keybindParts("file.open")) const closeTabKeybind = createMemo(() => command.keybindParts("tab.close")) const [store, setStore] = createStore({ activeDraggable: undefined as string | undefined, }) - const handleDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return @@ -341,35 +348,296 @@ export function SessionSidePanel(props: { > + + +
+ { + const stop = createFileTabListSync({ el, contextOpen }) + onCleanup(stop) + }} + > + + +
+
{language.t("session.tab.review")}
+ +
{props.reviewCount()}
+
+
+
+
+ + + tabs().close("context")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("context")} + > +
+ +
{language.t("session.tab.context")}
+
+
+
+ + + tabs().close("agents")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("agents")} + > +
+ +
{language.t("session.tab.agents")}
+
+
+
+ + + {(tab) => ( + + } + > + + tabs().close(SESSION_OPEN_FILE_TAB)} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} + > +
+ + {language.t("command.file.open")} +
+
+
+ )} +
+
+
+ + { + void import("@/components/dialog-select-file").then((x) => { + dialog.show(() => ( + + )) + }) + }} + aria-label={language.t("command.file.open")} + /> + +
+
+
+ + +
+ {props.reviewPanel()} +
+
+ + + +
+
+ +
+ {language.t("session.files.selectToOpen")} +
+
+
+
+
+ + + +
+ +
+
+
+ + +
+ +
+
+ + + {(tab) => } + +
+
+ + + {(tab) => { + const path = file.pathFromTab(tab) + return ( +
+ + {(p) => } + +
+ ) + }} +
+
+ + } + > + + event.target instanceof Element && + (!!event.target.closest('[data-slot="tabs-trigger-close-button"]') || + !!event.target.closest(".session-review-v2-open-in-app-slot")), + }), + ]} + modifiers={[ + RestrictToHorizontalAxis, + RestrictToElement.configure({ element: () => tabList ?? null }), + ]} + plugins={(defaults) => [ + ...defaults.filter((plugin) => plugin !== Accessibility), + AutoScroller.configure({ acceleration: 8, threshold: { x: 0.05, y: 0 } }), + Feedback.configure({ dropAnimation: null }), + ]} + onDragEnd={(event) => { + const source = event.operation.source + if (event.canceled || !isSortable(source) || source.initialIndex === source.index) return + tabs().move(source.id.toString(), source.index) + }} + > + -
+
{ + tabList = el const stop = createFileTabListSync({ el, contextOpen }) onCleanup(stop) }} > + + {(toggle) => ( +
+ {toggle()(activeTab() === SESSION_OPEN_FILE_TAB)} +
+ )} +
-
-
{language.t("session.tab.review")}
- -
{props.reviewCount()}
-
-
+ {props.hasReview() + ? language.t("session.review.filesChanged", { count: props.reviewCount() }) + : language.t("session.tab.review")}
+ {language.t("common.closeTab")} + 0}> + + + + } placement="bottom" gutter={10} > @@ -380,7 +648,7 @@ export function SessionSidePanel(props: { onClick={() => tabs().close("context")} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => tabs().close("context")} @@ -391,77 +659,125 @@ export function SessionSidePanel(props: {
- - - {(tab) => ( - + + + {language.t("common.closeTab")} + 0}> + + + } + placement="bottom" + gutter={10} > - - tabs().close(SESSION_OPEN_FILE_TAB)} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} - > -
- - {language.t("command.file.open")} -
-
-
- )} -
-
+ tabs().close("agents")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("agents")} + > +
+ +
{language.t("session.tab.agents")}
+
+ + + + {(tab) => ( + tabs().all().indexOf(tab)} + temporary={temporaryTab() === tab} + onTabClose={tabs().close} + onTabDoubleClick={temporaryTab() === tab ? openTab : undefined} + /> + } + > + + {language.t("common.closeTab")} + 0}> + + + + } + placement="bottom" + gutter={10} + > + tabs().close(SESSION_OPEN_FILE_TAB)} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} + > +
+ + {language.t("command.file.open")} +
+
+
+ )} +
- + {language.t("command.file.open")} + 0}> + + + + } + placement="bottom" class="flex items-center" > - { - void import("@/components/dialog-select-file").then((x) => { - dialog.show(() => ) - }) - }} + } + variant="ghost-muted" + size="large" + onClick={() => openFileBrowser()} aria-label={language.t("command.file.open")} /> - +
+
event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > + +
@@ -498,258 +814,45 @@ export function SessionSidePanel(props: { - - {(tab) => } - -
- - - {(tab) => { - const path = file.pathFromTab(tab) - return ( -
- - {(p) => } - -
- ) - }} -
-
- - } - > - - event.target instanceof Element && - (!!event.target.closest('[data-slot="tabs-trigger-close-button"]') || - !!event.target.closest(".session-review-v2-open-in-app-slot")), - }), - ]} - modifiers={[ - RestrictToHorizontalAxis, - RestrictToElement.configure({ element: () => tabList ?? null }), - ]} - plugins={(defaults) => [ - ...defaults.filter((plugin) => plugin !== Accessibility), - AutoScroller.configure({ acceleration: 8, threshold: { x: 0.05, y: 0 } }), - Feedback.configure({ dropAnimation: null }), - ]} - onDragEnd={(event) => { - const source = event.operation.source - if (event.canceled || !isSortable(source) || source.initialIndex === source.index) return - tabs().move(source.id.toString(), source.index) - }} - > - -
- { - tabList = el - const stop = createFileTabListSync({ el, contextOpen }) - onCleanup(stop) - }} + - - {(toggle) => ( -
- {toggle()(activeTab() === SESSION_OPEN_FILE_TAB)} -
- )} -
- - - {props.hasReview() - ? language.t("session.review.filesChanged", { count: props.reviewCount() }) - : language.t("session.tab.review")} - - - - - {language.t("common.closeTab")} - 0}> - - - - } - placement="bottom" - gutter={10} - > - tabs().close("context")} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close("context")} - > -
- -
{language.t("session.tab.context")}
-
-
-
- - {(tab) => ( - tabs().all().indexOf(tab)} - temporary={temporaryTab() === tab} - onTabClose={tabs().close} - onTabDoubleClick={temporaryTab() === tab ? openTab : undefined} - /> - } - > - - {language.t("common.closeTab")} - 0}> - - - - } - placement="bottom" - gutter={10} - > - tabs().close(SESSION_OPEN_FILE_TAB)} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)} - > -
- - {language.t("command.file.open")} -
-
-
- )} -
- - {language.t("command.file.open")} - 0}> - - - - } - placement="bottom" - class="flex items-center" - > - } - variant="ghost-muted" - size="large" - onClick={() => openFileBrowser()} - aria-label={language.t("command.file.open")} - /> - -
-
-
event.stopPropagation()} - onClick={(event) => event.stopPropagation()} - > - -
-
- - -
- {props.reviewPanel()} -
-
- - - -
-
- -
- {language.t("session.files.selectToOpen")} -
-
+
-
- - -
- + +
+ previewTab(file.tab(path))} + onSelectPermanent={(path) => openTab(file.tab(path))} + filterRef={(element) => (fileFilter = element)} + />
- -
- - -
- previewTab(file.tab(path))} - onSelectPermanent={(path) => openTab(file.tab(path))} - filterRef={(element) => (fileFilter = element)} - /> -
-
- + + +
diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 18a153d29971..07292ea28f74 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -33,6 +33,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { Dialog } from "@opencode-ai/ui/dialog" @@ -63,6 +64,8 @@ import { SessionContextUsage } from "@/components/session-context-usage" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" import { useSessionKey } from "@/pages/session/session-layout" +import { useLayout } from "@/context/layout" +import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionArchive } from "@/pages/session/session-archive" import { useServerSDK } from "@/context/server-sdk" import { usePlatform } from "@/context/platform" @@ -235,6 +238,17 @@ function TimelineDiffView(props: { diff: SummaryDiff }) { ) } +function openSessionAgents(args: { + view: ReturnType["view"]> + layout: ReturnType + tabs: ReturnType["tabs"]> +}) { + args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "agents-button") + if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") + void args.tabs.open("agents") + args.tabs.setActive("agents") +} + export function MessageTimeline(props: { actions?: UserActions scroll: { overflow: boolean; bottom: boolean; jump: boolean } @@ -272,6 +286,28 @@ export function MessageTimeline(props: { const initialMeasurements = cached?.measurements const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom() const platform = usePlatform() + const layout = useLayout() + const { tabs: sessionTabs, view } = useSessionLayout() + const agentsVisible = createMemo(() => view().reviewPanel.opened() && sessionTabs().active() === "agents") + const hasOtherAgentTabs = createMemo(() => + sessionTabs() + .all() + .some((tab) => tab !== "agents" && tab !== "review"), + ) + const openAgents = () => { + if (!params.id) return + const sessionView = view() + if (agentsVisible()) { + sessionTabs().close("agents") + if (sessionView.reviewPanel.source() === "agents-button" && !hasOtherAgentTabs()) sessionView.reviewPanel.close() + return + } + openSessionAgents({ + view: sessionView, + layout, + tabs: sessionTabs(), + }) + } const [listRoot, setListRoot] = createSignal() const sessionID = createMemo(() => params.id) @@ -280,6 +316,13 @@ export function MessageTimeline(props: { if (!id) return idle return sync().data.session_status[id] ?? idle }) + const hasBusyChildren = createMemo(() => { + const id = sessionID() + if (!id) return false + const sessions = sync().data.session ?? [] + const statuses = sync().data.session_status + return sessions.some((s) => s.parentID === id && statuses[s.id]?.type === "busy") + }) const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) const projectedMessages = createMemo(() => { const id = sessionID() @@ -1476,6 +1519,26 @@ export function MessageTimeline(props: { placement="bottom" buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"} /> + + +
+ } + onClick={openAgents} + aria-label={language.t("session.tab.agents")} + /> + +
+
+
, + // No task parts, so `deriveStatus` falls through to the live `session_status` map. + message: {} as Record, + part: {} as Record, +}) + +const host = document.createElement("div") +let dispose = () => {} + +beforeAll(async () => { + await mock.module("@solidjs/router", () => ({ useNavigate: () => () => {} })) + await mock.module("@opencode-ai/ui/icon", () => ({ Icon: () => null })) + await mock.module("@opencode-ai/ui/scroll-view", () => ({ + ScrollView: (props: { children: JSX.Element }) => props.children, + })) + await mock.module("@opencode-ai/session-ui/v2/session-progress-indicator-v2", () => ({ + SessionProgressIndicatorV2: () => null, + })) + await mock.module("@/pages/session/session-layout", () => ({ + useSessionLayout: () => ({ params: { id: PARENT, serverKey: "srv" } }), + })) + // `t` echoes the key so assertions read as the i18n contract instead of English copy. + await mock.module("@/context/language", () => ({ + useLanguage: () => ({ t: (key: string) => key, intl: () => "en-US" }), + })) + await mock.module("@/context/server-sdk", () => ({ useServerSDK: () => () => ({ scope: "test" }) })) + await mock.module("@/context/sdk", () => ({ + useSDK: () => () => ({ + directory: "/tmp", + client: { session: { list: async () => ({ data: [] }) } }, + }), + })) + // `useSync` returns an accessor, hence the double arrow. `data` is the store proxy itself, so + // reads inside the component subscribe exactly as they do against the real global sync store. + await mock.module("@/context/sync", () => ({ + useSync: () => () => ({ + data: state, + session: { get: (id: string) => state.session.find((s) => s.id === id) }, + }), + })) + + const { render } = await import("solid-js/web") + const { QueryClient, QueryClientProvider } = await import("@tanstack/solid-query") + const { SubAgentsProvider } = await import("@/context/sub-agents") + const { SessionAgentsTab } = await import("@/components/session/session-agents-tab") + + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + document.body.appendChild(host) + dispose = render( + () => ( + + + + + + ), + host, + ) + + const deadline = Date.now() + 5000 + while (host.querySelectorAll("button").length < 1 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } +}) + +afterAll(() => { + dispose() + host.remove() +}) + +test("status label updates in place when only session_status mutates", () => { + const row = host.querySelectorAll("button")[0] + expect(row.textContent).toContain("session.agents.status.busy") + + setState("session_status", CHILD, { type: "idle" }) + + expect(row.textContent).toContain("session.agents.status.idle") + expect(row.textContent).not.toContain("session.agents.status.busy") + // Identical element reference: the row was updated in place, not remounted. + expect(host.querySelectorAll("button")[0]).toBe(row) +}) + +test("token total updates in place when only session.tokens mutates", () => { + const row = host.querySelectorAll("button")[0] + expect(row.textContent).toContain("100 session.agents.tokens") + + setState("session", 0, "tokens", "input", 2000) + + expect(row.textContent).toContain("2.0k session.agents.tokens") + expect(row.textContent).not.toContain("100 session.agents.tokens") + expect(host.querySelectorAll("button")[0]).toBe(row) +}) + +test("cost of exactly 0 still renders the $0.00 span instead of hiding it", () => { + const row = host.querySelectorAll("button")[0] + expect(row.textContent).toContain("$0.00") +}) + +// NON-VACUITY (why these tests cannot pass against the pre-fix component) +// +// The argument is structural, not a revert-and-rerun: the fixture is seeded so that a frozen +// first-render snapshot is red by construction. +// +// The pre-fix row callback held `const st = deriveStatus(session.id)` and `const totalT = ...` -- +// plain values, evaluated once, because in Solid a `` row callback body runs once per row. +// Both tests assert the seeded value FIRST (`busy`, `100 tokens`), which pins the snapshot to the +// pre-mutation state, and only then mutate. A frozen snapshot therefore still reads `busy` / +// `100 tokens` at the second assertion, and both tests fail. Only the `createMemo` accessors the +// fix introduced can satisfy both assertions in the same test. +// +// The element-identity assertions rule out the opposite escape hatch: passing by remounting the +// row. A remount would produce a different `