From 0ded2536539ed3177755eeb47e97d7ee254b02b7 Mon Sep 17 00:00:00 2001 From: sdpfigueiredo Date: Tue, 28 Jul 2026 16:10:18 +0100 Subject: [PATCH 1/9] feat(app): add subagents tab with live status, cost, and token tracking Adds a session side-panel tab listing the active session's subagents, each showing task name, live status, cost, and token usage. - Children are derived reactively from the live sync store plus a one-shot backfill for subagents created before the store's fetch window, deduped by id with the live entry winning. - Per-row status and token totals are reactive accessors, so the tab reflects live updates without remounting. - Terminal status (completed/cancelled/error) is read from the parent session's task tool parts, mirroring the CLI footer's reader, with zero per-child network requests. - Cost and token formatting is shared with the session context tab via an extracted formatter module. - i18n keys are complete and consistent across all 18 locales. --- packages/app/src/components/session/index.ts | 2 + .../components/session/session-agents-tab.tsx | 232 ++++++ .../session/session-context-format.ts | 10 + .../session/session-sub-agents-usage.tsx | 61 ++ packages/app/src/context/layout-tabs.ts | 2 +- packages/app/src/context/sub-agents.tsx | 77 ++ packages/app/src/i18n/ar.ts | 18 + packages/app/src/i18n/br.ts | 18 + packages/app/src/i18n/bs.ts | 18 + packages/app/src/i18n/da.ts | 18 + packages/app/src/i18n/de.ts | 18 + packages/app/src/i18n/en.ts | 13 + packages/app/src/i18n/es.ts | 18 + packages/app/src/i18n/fr.ts | 18 + packages/app/src/i18n/ja.ts | 18 + packages/app/src/i18n/ko.ts | 18 + packages/app/src/i18n/no.ts | 18 + packages/app/src/i18n/pl.ts | 18 + packages/app/src/i18n/ru.ts | 18 + packages/app/src/i18n/th.ts | 18 + packages/app/src/i18n/tr.ts | 18 + packages/app/src/i18n/uk.ts | 18 + packages/app/src/i18n/zh.ts | 18 + packages/app/src/i18n/zht.ts | 18 + packages/app/src/pages/session/helpers.ts | 8 +- .../src/pages/session/session-side-panel.tsx | 741 ++++++++++-------- .../session/timeline/message-timeline.tsx | 63 ++ 27 files changed, 1194 insertions(+), 321 deletions(-) create mode 100644 packages/app/src/components/session/session-agents-tab.tsx create mode 100644 packages/app/src/components/session/session-sub-agents-usage.tsx create mode 100644 packages/app/src/context/sub-agents.tsx 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.tsx b/packages/app/src/components/session/session-agents-tab.tsx new file mode 100644 index 000000000000..c82b52d05b45 --- /dev/null +++ b/packages/app/src/components/session/session-agents-tab.tsx @@ -0,0 +1,232 @@ +import { createMemo, For, Show, createSignal } from "solid-js" +import { useNavigate } from "@solidjs/router" +import { useSync } from "@/context/sync" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +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") 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 sdk = useSDK() + 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.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.tsx b/packages/app/src/context/sub-agents.tsx new file mode 100644 index 000000000000..b31d48d9e179 --- /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 [] as Session[] + 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 [] as Session[] + 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/ar.ts b/packages/app/src/i18n/ar.ts index 320738a6b027..b4f75e29b166 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "لم يتم العثور على موفرين", "dialog.provider.group.popular": "شائع", "dialog.provider.group.other": "آخر", + "dialog.provider.custom.label": "موفر مخصص متوافق مع OpenAI", "dialog.provider.tag.recommended": "موصى به", "dialog.provider.opencode.note": "نماذج مختارة تتضمن Claude و GPT و Gemini والمزيد", "dialog.provider.opencode.tagline": "نماذج موثوقة ومحسنة", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "تبديل جميع نماذج {{provider}}", "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode", "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", + "dialog.model.unpaid.viewMoreProviders": "عرض أكثر من 70 موفرًا إضافيًا", "dialog.provider.viewAll": "عرض المزيد من الموفرين", "provider.connect.title": "اتصال {{provider}}", "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", @@ -558,6 +560,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": "لا يمكن العثور على هذه الجلسة", @@ -948,6 +963,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "مستكشف الملفات", "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/br.ts b/packages/app/src/i18n/br.ts index 9d6765ec94ef..7ad5958d5090 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Nenhum provedor encontrado", "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Outro", + "dialog.provider.custom.label": "Provedor personalizado compatível com OpenAI", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos selecionados incluindo Claude, GPT, Gemini e mais", "dialog.provider.opencode.tagline": "Modelos otimizados e confiáveis", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Alternar todos os modelos {{provider}}", "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode", "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", + "dialog.model.unpaid.viewMoreProviders": "Ver mais de 70 provedores", "dialog.provider.viewAll": "Ver mais provedores", "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max", @@ -563,6 +565,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", @@ -962,6 +977,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorador de Arquivos", "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 915313c3e2b5..16986d982000 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Nema pronađenih provajdera", "dialog.provider.group.popular": "Popularno", "dialog.provider.group.other": "Ostalo", + "dialog.provider.custom.label": "Prilagođeni provajder kompatibilan s OpenAI", "dialog.provider.tag.recommended": "Preporučeno", "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera", + "dialog.model.unpaid.viewMoreProviders": "Pogledaj još 70+ provajdera", "dialog.provider.viewAll": "Prikaži više provajdera", @@ -615,6 +617,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", @@ -1038,6 +1053,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "File Explorer", "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/da.ts b/packages/app/src/i18n/da.ts index 22fcdf39ceb0..dcf38d24c8d1 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Ingen udbydere fundet", "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", + "dialog.provider.custom.label": "Brugerdefineret OpenAI-kompatibel udbyder", "dialog.provider.tag.recommended": "Anbefalet", "dialog.provider.opencode.note": "Udvalgte modeller inklusive Claude, GPT, Gemini og flere", "dialog.provider.opencode.tagline": "Pålidelige optimerede modeller", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode", "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", + "dialog.model.unpaid.viewMoreProviders": "Se 70+ flere udbydere", "dialog.provider.viewAll": "Vis flere udbydere", @@ -612,6 +614,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", @@ -1030,6 +1045,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Stifinder", "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 bc2ac9be381d..1229179c9855 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -98,6 +98,7 @@ export const dict = { "dialog.provider.empty": "Keine Anbieter gefunden", "dialog.provider.group.popular": "Beliebt", "dialog.provider.group.other": "Andere", + "dialog.provider.custom.label": "Benutzerdefinierter OpenAI-kompatibler Anbieter", "dialog.provider.tag.recommended": "Empfohlen", "dialog.provider.opencode.note": "Kuratierte Modelle inklusive Claude, GPT, Gemini und mehr", "dialog.provider.opencode.tagline": "Zuverlässige, optimierte Modelle", @@ -116,6 +117,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Alle {{provider}}-Modelle umschalten", "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", + "dialog.model.unpaid.viewMoreProviders": "70+ weitere Anbieter ansehen", "dialog.provider.viewAll": "Mehr Anbieter anzeigen", "provider.connect.title": "{{provider}} verbinden", "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", @@ -571,6 +573,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", @@ -975,6 +990,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Datei-Explorer", "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/en.ts b/packages/app/src/i18n/en.ts index f03147f151d1..7740bec5cb19 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -635,6 +635,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 00dca6704b25..6b321bb7d52b 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "No se encontraron proveedores", "dialog.provider.group.popular": "Popular", "dialog.provider.group.other": "Otro", + "dialog.provider.custom.label": "Proveedor personalizado compatible con OpenAI", "dialog.provider.tag.recommended": "Recomendado", "dialog.provider.opencode.note": "Modelos seleccionados incluyendo Claude, GPT, Gemini y más", "dialog.provider.opencode.tagline": "Modelos optimizados y fiables", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode", "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", + "dialog.model.unpaid.viewMoreProviders": "Ver más de 70 proveedores", "dialog.provider.viewAll": "Ver más proveedores", @@ -616,6 +618,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", @@ -1046,6 +1061,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorador de archivos", "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/fr.ts b/packages/app/src/i18n/fr.ts index d1487ff61ef3..5bc039743ee2 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Aucun fournisseur trouvé", "dialog.provider.group.popular": "Populaire", "dialog.provider.group.other": "Autre", + "dialog.provider.custom.label": "Fournisseur personnalisé compatible OpenAI", "dialog.provider.tag.recommended": "Recommandé", "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Basculer tous les modèles {{provider}}", "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", + "dialog.model.unpaid.viewMoreProviders": "Voir plus de 70 fournisseurs", "dialog.provider.viewAll": "Voir plus de fournisseurs", "provider.connect.title": "Connecter {{provider}}", "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", @@ -568,6 +570,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", @@ -973,6 +988,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Explorateur de fichiers", "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/ja.ts b/packages/app/src/i18n/ja.ts index 6a3b1ebcb02e..7785495874dc 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "プロバイダーが見つかりません", "dialog.provider.group.popular": "人気", "dialog.provider.group.other": "その他", + "dialog.provider.custom.label": "カスタム OpenAI 互換プロバイダー", "dialog.provider.tag.recommended": "推奨", "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "すべての{{provider}}モデルを切り替え", "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", + "dialog.model.unpaid.viewMoreProviders": "70以上のプロバイダーを見る", "dialog.provider.viewAll": "さらにプロバイダーを表示", "provider.connect.title": "{{provider}}を接続", "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", @@ -560,6 +562,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": "このセッションが見つかりません", @@ -955,6 +970,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "エクスプローラー", "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/ko.ts b/packages/app/src/i18n/ko.ts index 3c262ce3b93a..08f21af2767e 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -90,6 +90,7 @@ export const dict = { "dialog.provider.empty": "공급자 없음", "dialog.provider.group.popular": "인기", "dialog.provider.group.other": "기타", + "dialog.provider.custom.label": "사용자 지정 OpenAI 호환 공급자", "dialog.provider.tag.recommended": "추천", "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", @@ -108,6 +109,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 토글", "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", + "dialog.model.unpaid.viewMoreProviders": "70개 이상의 공급자 더 보기", "dialog.provider.viewAll": "더 많은 공급자 보기", "provider.connect.title": "{{provider}} 연결", "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", @@ -458,6 +460,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": "변경", @@ -796,6 +811,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "파일 탐색기", "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/no.ts b/packages/app/src/i18n/no.ts index a4e8efb79d27..c85c45383199 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -101,6 +101,7 @@ export const dict = { "dialog.provider.empty": "Ingen leverandører funnet", "dialog.provider.group.popular": "Populære", "dialog.provider.group.other": "Andre", + "dialog.provider.custom.label": "Egendefinert OpenAI-kompatibel leverandør", "dialog.provider.tag.recommended": "Anbefalt", "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", @@ -121,6 +122,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode", "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", + "dialog.model.unpaid.viewMoreProviders": "Se 70+ flere leverandører", "dialog.provider.viewAll": "Vis flere leverandører", @@ -515,6 +517,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", @@ -883,6 +898,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Filutforsker", "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/pl.ts b/packages/app/src/i18n/pl.ts index f2e32ffeb879..1fda015475e6 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -94,6 +94,7 @@ export const dict = { "dialog.provider.empty": "Nie znaleziono dostawców", "dialog.provider.group.popular": "Popularne", "dialog.provider.group.other": "Inne", + "dialog.provider.custom.label": "Niestandardowy dostawca zgodny z OpenAI", "dialog.provider.tag.recommended": "Zalecane", "dialog.provider.opencode.note": "Wyselekcjonowane modele, w tym Claude, GPT, Gemini i inne", "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", @@ -112,6 +113,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}", "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", + "dialog.model.unpaid.viewMoreProviders": "Zobacz ponad 70 dostawców", "dialog.provider.viewAll": "Zobacz więcej dostawców", "provider.connect.title": "Połącz {{provider}}", "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max", @@ -564,6 +566,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", @@ -961,6 +976,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Eksplorator plików", "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/ru.ts b/packages/app/src/i18n/ru.ts index fa6fb04d04e1..a66ac034207c 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Провайдеры не найдены", "dialog.provider.group.popular": "Популярные", "dialog.provider.group.other": "Другие", + "dialog.provider.custom.label": "Пользовательский провайдер, совместимый с OpenAI", "dialog.provider.tag.recommended": "Рекомендуемые", "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", "dialog.provider.opencode.tagline": "Надежные оптимизированные модели", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode", "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", + "dialog.model.unpaid.viewMoreProviders": "Смотреть ещё 70+ провайдеров", "dialog.provider.viewAll": "Показать больше провайдеров", @@ -616,6 +618,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": "Эта сессия не найдена", @@ -1041,6 +1056,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Проводник", "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/th.ts b/packages/app/src/i18n/th.ts index 0ae67ee34eb1..9fd99fea7b96 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "ไม่พบผู้ให้บริการ", "dialog.provider.group.popular": "ยอดนิยม", "dialog.provider.group.other": "อื่น ๆ", + "dialog.provider.custom.label": "ผู้ให้บริการที่กำหนดเองซึ่งรองรับ OpenAI", "dialog.provider.tag.recommended": "แนะนำ", "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode", "dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม", + "dialog.model.unpaid.viewMoreProviders": "ดูผู้ให้บริการเพิ่มอีกกว่า 70 ราย", "dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม", @@ -610,6 +612,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": "ไม่พบเซสชันนี้", @@ -1025,6 +1040,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "File Explorer", "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/tr.ts b/packages/app/src/i18n/tr.ts index d401519af9c9..d05960d8c1f1 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -106,6 +106,7 @@ export const dict = { "dialog.provider.empty": "Sağlayıcı bulunamadı", "dialog.provider.group.popular": "Popüler", "dialog.provider.group.other": "Diğer", + "dialog.provider.custom.label": "Özel OpenAI uyumlu sağlayıcı", "dialog.provider.tag.recommended": "Önerilen", "dialog.provider.opencode.note": "Claude, GPT, Gemini ve daha fazlasını içeren seçilmiş modeller", "dialog.provider.opencode.tagline": "Güvenilir optimize edilmiş modeller", @@ -126,6 +127,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode tarafından sunulan ücretsiz modeller", "dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin", + "dialog.model.unpaid.viewMoreProviders": "70+ sağlayıcı daha görün", "dialog.provider.viewAll": "Daha fazla sağlayıcı göster", @@ -619,6 +621,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", @@ -1045,6 +1060,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 c949f41de2dc..e32e6da001aa 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -102,6 +102,7 @@ export const dict = { "dialog.provider.empty": "Провайдерів не знайдено", "dialog.provider.group.popular": "Популярні", "dialog.provider.group.other": "Інші", + "dialog.provider.custom.label": "Користувацький провайдер, сумісний з OpenAI", "dialog.provider.tag.recommended": "Рекомендовані", "dialog.provider.opencode.note": "Відібрані моделі, включаючи Claude, GPT, Gemini та інші", "dialog.provider.opencode.tagline": "Надійні оптимізовані моделі", @@ -122,6 +123,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Безкоштовні моделі від OpenCode", "dialog.model.unpaid.addMore.title": "Додати більше моделей від популярних провайдерів", + "dialog.model.unpaid.viewMoreProviders": "Переглянути ще 70+ провайдерів", "dialog.provider.viewAll": "Показати більше провайдерів", @@ -638,6 +640,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": "Цю сесію не знайдено", @@ -716,6 +731,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "Провідник файлів", "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/zh.ts b/packages/app/src/i18n/zh.ts index 82d73ac3a6a6..c4f3f7d9662d 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -129,6 +129,7 @@ export const dict = { "dialog.provider.empty": "未找到提供商", "dialog.provider.group.popular": "热门", "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自定义 OpenAI 兼容提供商", "dialog.provider.tag.recommended": "推荐", "dialog.provider.opencode.note": "使用 OpenCode Zen 或 API 密钥连接", "dialog.provider.opencode.tagline": "可靠的优化模型", @@ -148,6 +149,7 @@ export const dict = { "dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型", "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看 70 多个其他提供商", "dialog.provider.viewAll": "查看更多提供商", @@ -611,6 +613,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": "找不到此会话", @@ -1018,6 +1033,9 @@ export const dict = { "session.header.open.finder": "访达", "session.header.open.fileExplorer": "文件资源管理器", "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 fbe80d95eeff..6f41c64071a6 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -106,6 +106,7 @@ export const dict = { "dialog.provider.empty": "找不到提供者", "dialog.provider.group.popular": "熱門", "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自訂 OpenAI 相容提供者", "dialog.provider.tag.recommended": "推薦", "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", "dialog.provider.opencode.tagline": "可靠的優化模型", @@ -126,6 +127,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型", "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看 70 多個其他提供者", "dialog.provider.viewAll": "查看更多提供者", @@ -606,6 +608,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": "找不到此工作階段", @@ -1014,6 +1029,9 @@ export const dict = { "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "檔案總管", "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 22c52e73feb8..2a7522a1aa8d 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,14 +34,23 @@ 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" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" import { useSettings } from "@/context/settings" +import { useSync } from "@/context/sync" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" import { @@ -91,6 +99,8 @@ export function SessionSidePanel(props: { const { sessionKey, tabs, view, params } = useSessionLayout() const projectDirectory = createMemo(() => sdk().directory) + const sync = useSync() + const isDesktop = createMediaQuery("(min-width: 768px)") const shown = settings.visibility.fileTree @@ -180,6 +190,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 @@ -218,6 +229,7 @@ export function SessionSidePanel(props: { const path = file.pathFromTab(next) if (path) void file.load(path) openReviewPanel() + tabs().open(next) tabs().setActive(next) } const browserTab = createMemo(() => { @@ -236,14 +248,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 @@ -339,35 +350,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} > @@ -378,7 +650,7 @@ export function SessionSidePanel(props: { onClick={() => tabs().close("context")} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => tabs().close("context")} @@ -389,77 +661,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()} + > + +
@@ -496,258 +816,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 dc9153536cf2..fff4d39ee459 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" @@ -62,6 +63,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 { useServerSDK } from "@/context/server-sdk" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" @@ -237,6 +240,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 } @@ -274,6 +288,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) @@ -282,6 +318,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() @@ -1502,6 +1545,26 @@ export function MessageTimeline(props: { placement="bottom" buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"} /> + + +
+ } + onClick={openAgents} + aria-label={language.t("session.tab.agents")} + /> + +
+
+
Date: Tue, 28 Jul 2026 16:10:33 +0100 Subject: [PATCH 2/9] test(app): cover subagents children derivation --- packages/app/src/context/sub-agents.test.ts | 170 ++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 packages/app/src/context/sub-agents.test.ts 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) + }) +}) From 3e90ef2e0f30df03a64739fc9c1936e8167bd239 Mon Sep 17 00:00:00 2001 From: sdpfigueiredo Date: Tue, 28 Jul 2026 16:10:34 +0100 Subject: [PATCH 3/9] test(app): assert subagent status updates reactively --- packages/app/package.json | 1 + packages/app/test/session-agents-tab.test.tsx | 134 ++++++++++++++++++ packages/app/test/solid-preload.ts | 38 +++++ 3 files changed, 173 insertions(+) create mode 100644 packages/app/test/session-agents-tab.test.tsx create mode 100644 packages/app/test/solid-preload.ts diff --git a/packages/app/package.json b/packages/app/package.json index 7199ad942546..1c07f4c5a742 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,6 +21,7 @@ "test": "bun run test:unit && bun run test:browser", "test:unit": "bun test --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", + "test:solid": "bun test --conditions=browser --preload ./happydom.ts --preload ./test/solid-preload.ts ./test", "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", "test:e2e": "playwright test", "test:e2e:local": "playwright test", diff --git a/packages/app/test/session-agents-tab.test.tsx b/packages/app/test/session-agents-tab.test.tsx new file mode 100644 index 000000000000..ba09fe3527c6 --- /dev/null +++ b/packages/app/test/session-agents-tab.test.tsx @@ -0,0 +1,134 @@ +// The test that proves the reported subagent bug is fixed: "o status na tag de subagents nao esta a +// atualizar". It renders the REAL `SessionAgentsTab` against a reactive Solid store and mutates only +// the narrow slice the server would touch, so a frozen (non-reactive) row snapshot fails it. +// +// Requires the Solid JSX loader and the browser export condition, so it lives outside `./src` and +// runs via `bun run test:solid` rather than `bun run test:unit`. See `test/solid-preload.ts`. +import { beforeAll, afterAll, expect, mock, test } from "bun:test" +import { createStore } from "solid-js/store" +import type { JSX } from "solid-js" +import type { Message, Part, Session, SessionStatus } from "@opencode-ai/sdk/v2/client" + +const PARENT = "ses_parent" +const CHILD = "ses_child" + +const [state, setState] = createStore({ + session: [ + { + id: CHILD, + parentID: PARENT, + title: "child-title", + cost: 0, + tokens: { input: 100, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, + }, + ] as unknown as Session[], + // Seeded `busy` on purpose: see the non-vacuity note at the bottom of this file. + session_status: { [CHILD]: { type: "busy" } } as Record, + // 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) +}) + +// 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 `