Revert "Revert translation breaking behavior (#1342)" (#1348)

This reverts commit 1dd639b033.
This commit is contained in:
Omar McAdam
2026-04-04 13:11:37 -07:00
committed by GitHub
parent 1dd639b033
commit e40b050c3b
27 changed files with 6831 additions and 1859 deletions

View File

@@ -168,7 +168,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
statusPoll = window.setInterval(async () => {
if (Date.now() - startedAt >= MCP_AUTH_TIMEOUT_MS) {
stopStatusPolling();
setError("Request timed out.");
setError(translate("mcp.auth.request_timed_out"));
return;
}
@@ -694,7 +694,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
<CheckCircle2 size={24} class="text-green-11" />
</div>
<div>
<p class="text-sm font-medium text-gray-12">Already Connected</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.already_connected")}</p>
<p class="text-xs text-gray-11">
{translate("mcp.auth.already_connected_description", { server: serverName() })}
</p>
@@ -804,7 +804,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
</div>
<div class="rounded-xl border border-gray-6/70 bg-gray-2/40 px-3 py-2 flex items-center gap-3">
<div class="flex-1 min-w-0">
<div class="text-[10px] uppercase tracking-wide text-gray-8">Authorization link</div>
<div class="text-[10px] uppercase tracking-wide text-gray-8">{translate("mcp.auth.authorization_link")}</div>
<div class="text-[11px] text-gray-11 font-mono truncate">
{authorizationUrl()}
</div>
@@ -814,7 +814,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
class="text-xs"
onClick={handleCopyAuthorizationUrl}
>
{authUrlCopied() ? "Copied" : "Copy link"}
{authUrlCopied() ? translate("mcp.auth.copied") : translate("mcp.auth.copy_link")}
</Button>
</div>
<TextInput
@@ -851,7 +851,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
1
</div>
<div>
<p class="text-sm font-medium text-gray-12">Opening your browser</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step1_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step1_description", { server: serverName() })}
</p>
@@ -863,7 +863,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
2
</div>
<div>
<p class="text-sm font-medium text-gray-12">Authorize OpenWork</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step2_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step2_description")}
</p>
@@ -875,7 +875,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
3
</div>
<div>
<p class="text-sm font-medium text-gray-12">Return here when you're done</p>
<p class="text-sm font-medium text-gray-12">{translate("mcp.auth.step3_title")}</p>
<p class="text-xs text-gray-10 mt-1">
{translate("mcp.auth.step3_description")}
</p>

View File

@@ -4,6 +4,7 @@ import type { QuestionInfo } from "@opencode-ai/sdk/v2/client";
import { Check, ChevronRight, HelpCircle } from "lucide-solid";
import Button from "./button";
import { t } from "../../i18n";
export type QuestionModalProps = {
open: boolean;
@@ -138,10 +139,10 @@ export default function QuestionModal(props: QuestionModalProps) {
</div>
<div>
<h3 class="text-lg font-semibold text-gray-12">
{currentQuestion()!.header || "Question"}
{currentQuestion()!.header || t("common.question")}
</h3>
<div class="text-xs text-gray-11 font-medium">
Question {currentIndex() + 1} of {props.questions.length}
{t("question_modal.question_counter", undefined, { current: currentIndex() + 1, total: props.questions.length })}
</div>
</div>
</div>
@@ -186,14 +187,14 @@ export default function QuestionModal(props: QuestionModalProps) {
<Show when={currentQuestion()!.custom}>
<div class="mt-4 pt-4 border-t border-dls-border">
<label class="block text-xs font-semibold text-dls-secondary mb-2 uppercase tracking-wide">
Or type a custom answer
{t("question_modal.custom_answer_label")}
</label>
<input
type="text"
value={customInput()}
onInput={(e) => setCustomInput(e.currentTarget.value)}
class="w-full px-4 py-3 rounded-xl bg-dls-surface border border-dls-border focus:border-dls-accent focus:ring-4 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:outline-none text-sm text-dls-text placeholder:text-dls-secondary transition-shadow"
placeholder="Type your answer here..."
placeholder={t("question_modal.custom_answer_placeholder")}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.isComposing || e.keyCode === 229) return;
@@ -209,15 +210,15 @@ export default function QuestionModal(props: QuestionModalProps) {
<div class="p-6 border-t border-dls-border bg-dls-hover flex justify-between items-center">
<div class="text-xs text-dls-secondary flex items-center gap-2">
<span class="px-1.5 py-0.5 rounded border border-dls-border bg-dls-active font-mono"></span>
<span>navigate</span>
<span>{t("common.navigate")}</span>
<span class="px-1.5 py-0.5 rounded border border-gray-6 bg-gray-3 font-mono ml-2"></span>
<span>select</span>
<span>{t("common.select")}</span>
</div>
<div class="flex gap-2">
<Show when={currentQuestion()?.multiple || currentQuestion()?.custom}>
<Button onClick={handleNext} disabled={!canProceed() || props.busy} class="!px-6">
{isLastQuestion() ? "Submit" : "Next"}
{isLastQuestion() ? t("common.submit") : t("common.next")}
<Show when={!isLastQuestion()}>
<ChevronRight size={16} class="ml-1 -mr-1 opacity-60" />
</Show>

View File

@@ -7,6 +7,7 @@ import ComposerNotice, { type ComposerNotice as ComposerNoticeData } from "./com
import type { ComposerAttachment, ComposerDraft, ComposerPart, PromptMode, SlashCommandOption } from "../../types";
import { perfNow, recordPerfLog } from "../../lib/perf-log";
import { t } from "../../../i18n";
type MentionOption = {
id: string;
@@ -516,7 +517,7 @@ export default function Composer(props: ComposerProps) {
span.dataset.pasteId = part.id;
span.dataset.pasteLabel = part.label;
span.dataset.pasteLines = String(part.lines);
span.title = "Click to expand pasted text";
span.title = t("composer.expand_pasted");
span.className =
"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold bg-gray-3 text-gray-10 border border-gray-6 cursor-pointer hover:bg-gray-4 hover:text-gray-11";
return span;
@@ -1058,7 +1059,7 @@ export default function Composer(props: ComposerProps) {
const addAttachments = async (files: File[]) => {
if (attachmentsDisabled()) {
props.onNotice({
title: props.attachmentsDisabledReason ?? "Attachments are unavailable.",
title: props.attachmentsDisabledReason ?? t("composer.attachments_unavailable"),
tone: "warning",
});
return;
@@ -1078,7 +1079,7 @@ export default function Composer(props: ComposerProps) {
for (const file of supportedFiles) {
if (file.size > MAX_ATTACHMENT_BYTES) {
props.onNotice({
title: `${file.name} exceeds the 8MB limit.`,
title: t("composer.file_exceeds_limit", undefined, { name: file.name }),
tone: "warning",
});
continue;
@@ -1089,7 +1090,7 @@ export default function Composer(props: ComposerProps) {
const estimatedJsonBytes = estimateInlineAttachmentBytes(processed);
if (estimatedJsonBytes > MAX_ATTACHMENT_BYTES) {
props.onNotice({
title: `${file.name} is too large after encoding. Try a smaller image.`,
title: t("composer.file_too_large_encoding", undefined, { name: file.name }),
tone: "warning",
});
continue;
@@ -1105,7 +1106,7 @@ export default function Composer(props: ComposerProps) {
});
} catch (error) {
props.onNotice({
title: error instanceof Error ? error.message : "Failed to read attachment",
title: error instanceof Error ? error.message : t("composer.failed_read_attachment"),
tone: "error",
});
}
@@ -1221,29 +1222,29 @@ export default function Composer(props: ComposerProps) {
props.onNotice({
title:
links.length === 1
? `Uploaded ${links[0].name} to the shared folder and inserted a link.`
: `Uploaded ${links.length} files to the shared folder and inserted links.`,
? t("composer.uploaded_single_file", undefined, { name: links[0].name })
: t("composer.uploaded_multiple_files", undefined, { count: links.length }),
tone: "success",
});
return;
}
}
props.onNotice({
title: "Couldn't upload to the shared folder. Inserted local links instead.",
title: t("composer.upload_failed_local_links"),
tone: "warning",
});
}
const text = formatLinks(fallbackLinks());
if (!text) {
props.onNotice({ title: "Unsupported attachment type.", tone: "warning" });
props.onNotice({ title: t("composer.unsupported_attachment_type"), tone: "warning" });
return;
}
insertPlainTextAtSelection(text);
updateMentionQuery();
updateSlashQuery();
emitDraftChange();
props.onNotice({ title: "Inserted links for unsupported files.", tone: "info" });
props.onNotice({ title: t("composer.inserted_links_unsupported"), tone: "info" });
};
const handlePaste = (event: ClipboardEvent) => {
@@ -1277,10 +1278,9 @@ export default function Composer(props: ComposerProps) {
const hasAbsoluteWindows = /(^|\s)[a-zA-Z]:\\/.test(trimmedForCheck);
if (hasFileUrl || hasAbsolutePosix || hasAbsoluteWindows) {
props.onNotice({
title:
"This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Shared folder in the sidebar.",
title: t("composer.remote_worker_paste_warning"),
tone: "warning",
actionLabel: props.onUploadInboxFiles ? "Upload to shared folder" : undefined,
actionLabel: props.onUploadInboxFiles ? t("composer.upload_to_shared_folder") : undefined,
onAction: props.onUploadInboxFiles ? () => inboxFileInputRef?.click() : undefined,
});
}
@@ -1572,7 +1572,7 @@ export default function Composer(props: ComposerProps) {
<div class="max-h-64 overflow-y-auto bg-dls-surface p-2" onMouseDown={(event: MouseEvent) => event.preventDefault()}>
<Show
when={mentionVisible().length}
fallback={<div class="px-3 py-2 text-xs text-gray-10">No matches found.</div>}
fallback={<div class="px-3 py-2 text-xs text-gray-10">{t("composer.no_matches")}</div>}
>
<For each={mentionVisible()}>
{(option: MentionOption) => {
@@ -1635,7 +1635,7 @@ export default function Composer(props: ComposerProps) {
when={slashFiltered().length}
fallback={
<div class="px-3 py-2 text-xs text-gray-10">
{slashLoading() ? "Loading commands..." : "No commands found."}
{slashLoading() ? t("composer.loading_commands") : t("composer.no_commands")}
</div>
}
>
@@ -1662,7 +1662,7 @@ export default function Composer(props: ComposerProps) {
</div>
<Show when={cmd.source && cmd.source !== "command"}>
<span class="text-[10px] uppercase tracking-wider text-gray-10 shrink-0">
{cmd.source === "skill" ? "Skill" : cmd.source === "mcp" ? "MCP" : ""}
{cmd.source === "skill" ? t("composer.skill_source") : cmd.source === "mcp" ? "MCP" : ""}
</span>
</Show>
</button>
@@ -1697,7 +1697,7 @@ export default function Composer(props: ComposerProps) {
<div class="max-w-[160px]">
<div class="truncate text-gray-11">{attachment.name}</div>
<div class="text-[10px] text-gray-10">
{attachment.kind === "image" ? "Image" : attachment.mimeType || "File"}
{attachment.kind === "image" ? t("composer.image_kind") : attachment.mimeType || t("composer.file_kind")}
</div>
</div>
<button
@@ -1721,7 +1721,7 @@ export default function Composer(props: ComposerProps) {
<div class="relative">
<Show when={!hasDraftContent()}>
<div class="absolute left-0 top-0 text-gray-9 text-[15px] leading-relaxed pointer-events-none">
Describe your task...
{t("composer.placeholder")}
</div>
</Show>
<div
@@ -1775,8 +1775,8 @@ export default function Composer(props: ComposerProps) {
disabled={attachmentsDisabled()}
title={
attachmentsDisabled()
? props.attachmentsDisabledReason ?? "Attachments are unavailable."
: "Attach files"
? props.attachmentsDisabledReason ?? t("composer.attachments_unavailable")
: t("composer.attach_files")
}
>
<Paperclip size={16} />
@@ -1794,10 +1794,10 @@ export default function Composer(props: ComposerProps) {
? "bg-gray-4 text-gray-10"
: "bg-dls-accent text-white hover:bg-[var(--dls-accent-hover)]"
}`}
title="Run task"
title={t("composer.run_task")}
>
<ArrowUp size={15} />
<span>Run task</span>
<span>{t("composer.run_task")}</span>
</button>
}
>
@@ -1805,10 +1805,10 @@ export default function Composer(props: ComposerProps) {
type="button"
onClick={() => props.onStop()}
class="inline-flex items-center gap-2 rounded-full bg-gray-12 px-4 py-2 text-[13px] font-medium text-gray-1 transition-colors hover:bg-gray-11"
title="Stop"
title={t("composer.stop")}
>
<Square size={12} fill="currentColor" />
<span>Stop</span>
<span>{t("composer.stop")}</span>
</button>
</Show>
</div>
@@ -1829,7 +1829,7 @@ export default function Composer(props: ComposerProps) {
onClick={props.onToggleAgentPicker}
disabled={props.busy}
aria-expanded={props.agentPickerOpen}
title="Agent"
title={t("composer.agent_label")}
>
<span class="max-w-[140px] truncate">{props.agentLabel}</span>
<ChevronDown size={13} />
@@ -1838,14 +1838,14 @@ export default function Composer(props: ComposerProps) {
<Show when={props.agentPickerOpen}>
<div class="absolute left-0 bottom-full z-40 mb-2 w-64 overflow-hidden rounded-[18px] border border-dls-border bg-dls-surface shadow-[var(--dls-shell-shadow)]">
<div class="border-b border-dls-border px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-gray-10">
Agent
{t("composer.agent_label")}
</div>
<div class="p-2 space-y-1 max-h-64 overflow-y-auto" onMouseDown={(event: MouseEvent) => event.preventDefault()}>
<Show
when={!props.agentPickerBusy}
fallback={
<div class="px-3 py-2 text-xs text-gray-10">Loading agents...</div>
<div class="px-3 py-2 text-xs text-gray-10">{t("composer.loading_agents")}</div>
}
>
<Show when={!props.agentPickerError}>
@@ -1860,7 +1860,7 @@ export default function Composer(props: ComposerProps) {
props.onSelectAgent(null);
}}
>
<span>Default agent</span>
<span>{t("composer.default_agent")}</span>
<Show when={!props.selectedAgent}>
<Check size={14} class="text-gray-10" />
</Show>
@@ -1931,7 +1931,7 @@ export default function Composer(props: ComposerProps) {
<Show when={variantMenuOpen()}>
<div class="absolute left-0 bottom-full z-40 mb-2 w-48 overflow-hidden rounded-[18px] border border-dls-border bg-dls-surface shadow-[var(--dls-shell-shadow)]">
<div class="border-b border-dls-border px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-gray-10">
Behavior
{t("composer.behavior_label")}
</div>
<div class="p-2 space-y-1">
<For each={props.modelBehaviorOptions}>

View File

@@ -3,6 +3,7 @@ import { ChevronDown, Circle, File, Folder, Package } from "lucide-solid";
import { useConnections } from "../../connections/provider";
import { SUGGESTED_PLUGINS } from "../../constants";
import { t } from "../../../i18n";
import type { McpStatus, SkillCard } from "../../types";
import { stripPluginVersion } from "../../utils/plugins";
@@ -106,19 +107,19 @@ const getSmartFileName = (files: string[], file: string): string => {
};
const mcpStatusLabel = (status?: McpStatus, disabled?: boolean) => {
if (disabled) return "Disabled";
if (!status) return "Disconnected";
if (disabled) return t("context_panel.mcp_disabled");
if (!status) return t("context_panel.mcp_disconnected");
switch (status.status) {
case "connected":
return "Connected";
return t("context_panel.mcp_connected");
case "needs_auth":
return "Needs auth";
return t("context_panel.mcp_needs_auth");
case "needs_client_registration":
return "Register client";
return t("context_panel.mcp_register_client");
case "failed":
return "Failed";
return t("context_panel.mcp_failed");
default:
return "Disconnected";
return t("context_panel.mcp_disconnected");
}
};
@@ -151,7 +152,7 @@ export default function ContextPanel(props: ContextPanelProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("context")}
>
<span>Context</span>
<span>{t("context_panel.context")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.context ? "rotate-180" : ""}`.trim()}
@@ -161,12 +162,12 @@ export default function ContextPanel(props: ContextPanelProps) {
<div class="px-4 pb-4 pt-1 space-y-5">
<div>
<div class="flex items-center justify-between text-[11px] uppercase tracking-wider text-gray-9 font-semibold mb-2">
<span>Working files</span>
<span>{t("context_panel.working_files")}</span>
</div>
<div class="space-y-2">
<Show
when={props.workingFiles.length}
fallback={<div class="text-xs text-gray-9">None yet.</div>}
fallback={<div class="text-xs text-gray-9">{t("context_panel.none_yet")}</div>}
>
<For each={props.workingFiles}>
{(file) => {
@@ -182,7 +183,7 @@ export default function ContextPanel(props: ContextPanelProps) {
: "cursor-default opacity-70"
}`.trim()}
onClick={() => props.onFileClick?.(file)}
title={canOpen() ? `Open ${displayPath()}` : displayPath()}
title={canOpen() ? t("context_panel.open_file", undefined, { path: displayPath() }) : displayPath()}
disabled={!canOpen()}
>
<File size={12} class="text-gray-9" />
@@ -203,7 +204,7 @@ export default function ContextPanel(props: ContextPanelProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("plugins")}
>
<span>Plugins</span>
<span>{t("context_panel.plugins")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.plugins ? "rotate-180" : ""}`.trim()}
@@ -216,7 +217,7 @@ export default function ContextPanel(props: ContextPanelProps) {
when={props.activePlugins.length}
fallback={
<div class="text-xs text-gray-9">
{props.activePluginStatus ?? "No plugins loaded."}
{props.activePluginStatus ?? t("context_panel.no_plugins")}
</div>
}
>
@@ -253,7 +254,7 @@ export default function ContextPanel(props: ContextPanelProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("mcp")}
>
<span>MCP</span>
<span>{t("context_panel.mcp")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.mcp ? "rotate-180" : ""}`.trim()}
@@ -266,7 +267,7 @@ export default function ContextPanel(props: ContextPanelProps) {
when={connections.mcpServers().length}
fallback={
<div class="text-xs text-gray-9">
{connections.mcpStatus() ?? "No MCP servers loaded."}
{connections.mcpStatus() ?? t("context_panel.no_mcp")}
</div>
}
>
@@ -303,7 +304,7 @@ export default function ContextPanel(props: ContextPanelProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("skills")}
>
<span>Skills</span>
<span>{t("context_panel.skills")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.skills ? "rotate-180" : ""}`.trim()}
@@ -316,7 +317,7 @@ export default function ContextPanel(props: ContextPanelProps) {
when={props.skills.length}
fallback={
<div class="text-xs text-gray-9">
{props.skillsStatus ?? "No skills loaded."}
{props.skillsStatus ?? t("context_panel.no_skills")}
</div>
}
>
@@ -352,7 +353,7 @@ export default function ContextPanel(props: ContextPanelProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("authorizedFolders")}
>
<span>Authorized folders</span>
<span>{t("context_panel.authorized_folders")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${
@@ -365,7 +366,7 @@ export default function ContextPanel(props: ContextPanelProps) {
<div class="space-y-2">
<Show
when={props.authorizedDirs.length}
fallback={<div class="text-xs text-gray-9">None yet.</div>}
fallback={<div class="text-xs text-gray-9">{t("context_panel.none_yet")}</div>}
>
<For each={props.authorizedDirs.slice(0, 3)}>
{(folder) => (

View File

@@ -2,6 +2,7 @@ import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
import { Download, RefreshCw, UploadCloud } from "lucide-solid";
import { getOpenWorkDeployment } from "../../lib/openwork-deployment";
import { t } from "../../../i18n";
import type { OpenworkInboxItem, OpenworkServerClient } from "../../lib/openwork-server";
import WebUnavailableSurface from "../web-unavailable-surface";
import { formatBytes, formatRelativeTime } from "../../utils";
@@ -48,7 +49,7 @@ export default function InboxPanel(props: InboxPanelProps) {
});
const connected = createMemo(() => Boolean(props.client && (props.workspaceId ?? "").trim()));
const helperText = "Share files with this worker from the app.";
const helperText = () => t("inbox_panel.helper_text");
const visibleItems = createMemo(() => (items() ?? []).slice(0, maxPreview()));
const hiddenCount = createMemo(() => Math.max(0, (items() ?? []).length - visibleItems().length));
@@ -72,7 +73,7 @@ export default function InboxPanel(props: InboxPanelProps) {
setItems(result.items ?? []);
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to load shared folder";
err instanceof Error ? err.message : t("inbox_panel.load_failed");
setError(message);
setItems([]);
} finally {
@@ -84,7 +85,7 @@ export default function InboxPanel(props: InboxPanelProps) {
const client = props.client;
const workspaceId = (props.workspaceId ?? "").trim();
if (!client || !workspaceId) {
toast("Connect to a worker to upload files to the shared folder.");
toast(t("inbox_panel.upload_needs_worker"));
return;
}
if (!files.length) return;
@@ -93,15 +94,15 @@ export default function InboxPanel(props: InboxPanelProps) {
setError(null);
try {
const label = files.length === 1 ? files[0]?.name ?? "file" : `${files.length} files`;
toast(`Uploading ${label}...`);
toast(t("inbox_panel.uploading_label", undefined, { label }));
for (const file of files) {
await client.uploadInbox(workspaceId, file);
}
toast("Uploaded to the shared folder.");
toast(t("inbox_panel.upload_success"));
await refresh();
} catch (err) {
const message =
err instanceof Error ? err.message : "Shared folder upload failed";
err instanceof Error ? err.message : t("inbox_panel.upload_failed");
setError(message);
toast(message);
} finally {
@@ -113,9 +114,9 @@ export default function InboxPanel(props: InboxPanelProps) {
const path = toInboxWorkspacePath(item);
try {
await navigator.clipboard.writeText(path);
toast(`Copied: ${path}`);
toast(t("inbox_panel.copied_path", undefined, { path }));
} catch {
toast("Copy failed. Your browser may block clipboard access.");
toast(t("inbox_panel.copy_failed"));
}
};
@@ -123,12 +124,12 @@ export default function InboxPanel(props: InboxPanelProps) {
const client = props.client;
const workspaceId = (props.workspaceId ?? "").trim();
if (!client || !workspaceId) {
toast("Connect to a worker to download shared files.");
toast(t("inbox_panel.connect_to_download"));
return;
}
const id = String(item.id ?? "").trim();
if (!id) {
toast("Missing shared file id.");
toast(t("inbox_panel.missing_file_id"));
return;
}
@@ -144,7 +145,7 @@ export default function InboxPanel(props: InboxPanelProps) {
a.remove();
URL.revokeObjectURL(url);
} catch (err) {
const message = err instanceof Error ? err.message : "Download failed";
const message = err instanceof Error ? err.message : t("inbox_panel.download_failed");
toast(message);
}
};
@@ -160,7 +161,7 @@ export default function InboxPanel(props: InboxPanelProps) {
<WebUnavailableSurface unavailable={webDeployment()} compact>
<div id={props.id}>
<div class="flex items-center justify-between px-2 mb-3">
<span class="text-[11px] font-semibold uppercase tracking-wider text-gray-10">Shared folder</span>
<span class="text-[11px] font-semibold uppercase tracking-wider text-gray-10">{t("inbox_panel.shared_folder")}</span>
<div class="flex items-center gap-2">
<Show when={(items() ?? []).length > 0}>
<span class="text-[11px] font-medium bg-gray-4/60 text-gray-10 px-1.5 rounded">
@@ -171,8 +172,8 @@ export default function InboxPanel(props: InboxPanelProps) {
type="button"
class="rounded-md p-1 text-gray-9 hover:text-gray-11 hover:bg-gray-3 transition-colors"
onClick={() => void refresh()}
title="Refresh shared folder"
aria-label="Refresh shared folder"
title={t("inbox_panel.refresh_tooltip")}
aria-label={t("inbox_panel.refresh_tooltip")}
disabled={!connected() || loading()}
>
<RefreshCw size={14} class={loading() ? "animate-spin" : ""} />
@@ -214,14 +215,14 @@ export default function InboxPanel(props: InboxPanelProps) {
if (files.length) void uploadFiles(files);
}}
disabled={uploading()}
title={connected() ? "Drop files here to upload" : "Connect to a worker to upload"}
title={connected() ? t("inbox_panel.drop_to_upload") : t("inbox_panel.connect_to_upload")}
>
<div class="flex flex-col items-center justify-center text-center">
<UploadCloud size={18} class="text-gray-9 mb-2" />
<span class="text-[13px] font-medium text-gray-11">
{uploading() ? "Uploading..." : "Drop files or click to upload"}
{uploading() ? t("inbox_panel.uploading") : t("inbox_panel.upload_prompt")}
</span>
<span class="mt-0.5 text-[11px] text-gray-9">{helperText}</span>
<span class="mt-0.5 text-[11px] text-gray-9">{helperText()}</span>
</div>
</button>
@@ -234,8 +235,8 @@ export default function InboxPanel(props: InboxPanelProps) {
when={visibleItems().length > 0}
fallback={
<div class="text-xs text-gray-10 px-1 py-1">
<Show when={connected()} fallback={"Connect to see shared files."}>
No shared files yet.
<Show when={connected()} fallback={t("inbox_panel.connect_to_see")}>
{t("inbox_panel.no_files")}
</Show>
</div>
}
@@ -275,8 +276,8 @@ export default function InboxPanel(props: InboxPanelProps) {
type="button"
class="shrink-0 rounded-md p-1 text-gray-9 opacity-0 group-hover:opacity-100 hover:text-gray-11 hover:bg-gray-3"
onClick={() => void downloadItem(item)}
title="Download"
aria-label="Download"
title={t("inbox_panel.download")}
aria-label={t("inbox_panel.download")}
disabled={!connected()}
>
<Download size={14} />
@@ -288,7 +289,7 @@ export default function InboxPanel(props: InboxPanelProps) {
</Show>
<Show when={hiddenCount() > 0}>
<div class="text-[11px] text-gray-10 px-1 py-1">Showing first {maxPreview()}.</div>
<div class="text-[11px] text-gray-10 px-1 py-1">{t("inbox_panel.showing_first", undefined, { count: maxPreview() })}</div>
</Show>
</div>
</div>

View File

@@ -31,6 +31,7 @@ import {
} from "../../utils";
import PartView from "../part-view";
import { perfNow, recordPerfLog } from "../../lib/perf-log";
import { t } from "../../../i18n";
export type MessageListProps = {
messages: MessageWithParts[];
@@ -228,57 +229,57 @@ function toolHeadline(part: Part) {
const description = pick("description");
if (description) return compactText(description);
const command = pick("command", "cmd");
return command ? compactText(`Run ${command}`, 48) : "Run command";
return command ? compactText(t("message_list.tool_run_command", undefined, { command }), 48) : t("message_list.tool_run_command_fallback");
}
if (tool === "read") {
const file = target("filePath", "path", "file");
return file ? `Reviewed ${file}` : "Reviewed file";
return file ? t("message_list.tool_reviewed_file", undefined, { file }) : t("message_list.tool_reviewed_file_fallback");
}
if (tool === "edit") {
const file = target("filePath", "path", "file");
return file ? `Updated ${file}` : "Updated file";
return file ? t("message_list.tool_updated_file", undefined, { file }) : t("message_list.tool_updated_file_fallback");
}
if (tool === "write" || tool === "apply_patch") {
const file = target("filePath", "path", "file");
return file ? `Update ${file}` : "Update file";
return file ? t("message_list.tool_update_file", undefined, { file }) : t("message_list.tool_update_file_fallback");
}
if (tool === "grep" || tool === "glob" || tool === "search") {
const pattern = pick("pattern", "query");
return pattern ? `Searched ${compactText(pattern, 36)}` : "Searched code";
return pattern ? t("message_list.tool_searched_pattern", undefined, { pattern: compactText(pattern, 36) }) : t("message_list.tool_searched_code_fallback");
}
if (tool === "list" || tool === "list_files") {
const path = target("path");
return path ? `Reviewed ${path}` : "Reviewed files";
return path ? t("message_list.tool_reviewed_path", undefined, { path }) : t("message_list.tool_reviewed_files_fallback");
}
if (tool === "task") {
const description = pick("description");
if (description) return compactText(description);
const agent = pick("subagent_type");
return agent ? `Delegate ${agent}` : "Delegate task";
return agent ? t("message_list.tool_delegate_agent", undefined, { agent }) : t("message_list.tool_delegate_task_fallback");
}
if (tool === "todowrite") {
return "Update todo list";
return t("message_list.tool_update_todo");
}
if (tool === "todoread") {
return "Read todo list";
return t("message_list.tool_read_todo");
}
if (tool === "webfetch") {
const url = pick("url");
return url ? `Checked ${compactText(url, 36)}` : "Checked web page";
return url ? t("message_list.tool_checked_url", undefined, { url: compactText(url, 36) }) : t("message_list.tool_checked_web_fallback");
}
if (tool === "skill") {
const name = pick("name");
return name ? `Load skill ${name}` : "Load skill";
return name ? t("message_list.tool_load_skill_named", undefined, { name }) : t("message_list.tool_load_skill_fallback");
}
const fallback = tool
@@ -695,17 +696,17 @@ export default function MessageList(props: MessageListProps) {
const title = session()?.title?.trim();
if (title) return title;
if (task().description) return task().description!;
if (task().agentType) return `${task().agentType} task`;
return "Subagent session";
if (task().agentType) return t("message_list.subagent_type_task", undefined, { agentType: task().agentType! });
return t("message_list.subagent_session_fallback");
});
const statusLabel = createMemo(() => {
if (loading()) return "Loading transcript";
if (streaming()) return "Running";
if (loading()) return t("message_list.subagent_loading_transcript");
if (streaming()) return t("message_list.subagent_running");
if (childMessages().length > 0) {
const count = childMessages().length;
return `${count} message${count === 1 ? "" : "s"}`;
return t("message_list.subagent_message_count", undefined, { count, plural: count === 1 ? "" : "s" });
}
return "Waiting for transcript";
return t("message_list.subagent_waiting_transcript");
});
createEffect(() => {
@@ -745,7 +746,7 @@ export default function MessageList(props: MessageListProps) {
props.openSessionById?.(id);
}}
>
Open session
{t("message_list.open_session")}
</button>
</Show>
</div>
@@ -753,7 +754,7 @@ export default function MessageList(props: MessageListProps) {
<div class="mt-3 rounded-[18px] border border-dls-border/70 bg-dls-surface px-3 py-3">
<Show
when={childMessages().length > 0}
fallback={<div class="text-[12px] leading-5 text-gray-9">Waiting for the subagent transcript to arrive.</div>}
fallback={<div class="text-[12px] leading-5 text-gray-9">{t("message.waiting_subagent")}</div>}
>
<MessageList
messages={childMessages()}
@@ -812,7 +813,7 @@ export default function MessageList(props: MessageListProps) {
const title = summary().title?.trim() ?? "";
if (title) return title;
const fromTool = toolHeadline(rowProps.part);
return fromTool || "Updates progress";
return fromTool || t("message_list.step_updates_progress");
});
const reasoningText = createMemo(() => {
if (rowProps.part.type !== "reasoning") return "";
@@ -854,13 +855,13 @@ export default function MessageList(props: MessageListProps) {
<div class="mt-3 ml-[22px] space-y-3">
<Show when={hasStructuredValue(toolInput())}>
<div>
<div class="mb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-gray-8">Request</div>
<div class="mb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-gray-8">{t("message.tool_request_label")}</div>
<pre class="overflow-x-auto rounded-[16px] border border-dls-border/70 bg-dls-surface px-4 py-3 text-[12px] leading-6 text-gray-10">{formatStructuredValue(toolInput())}</pre>
</div>
</Show>
<Show when={hasStructuredValue(toolOutput())}>
<div>
<div class="mb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-gray-8">Result</div>
<div class="mb-1 text-[11px] font-medium uppercase tracking-[0.12em] text-gray-8">{t("message.tool_result_label")}</div>
<pre class="overflow-x-auto rounded-[16px] border border-dls-border/70 bg-dls-surface px-4 py-3 text-[12px] leading-6 text-gray-10">{formatStructuredValue(toolOutput())}</pre>
</div>
</Show>
@@ -1142,7 +1143,7 @@ export default function MessageList(props: MessageListProps) {
<div class="absolute bottom-2 right-2 flex justify-end opacity-100 pointer-events-auto md:opacity-0 md:pointer-events-none md:group-hover:opacity-100 md:group-hover:pointer-events-auto md:group-focus-within:opacity-100 md:group-focus-within:pointer-events-auto transition-opacity select-none">
<button
class="text-dls-secondary hover:text-dls-text p-1 rounded hover:bg-dls-hover transition-colors"
title="Copy message"
title={t("message_list.copy_message")}
onClick={() => {
const text = block.renderableParts
.map((part) => partToText(part))

View File

@@ -1,5 +1,6 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
import { Check, ChevronDown, GripVertical, Loader2, Plus, RefreshCcw, Settings, Square, Trash2 } from "lucide-solid";
import { t } from "../../../i18n";
import type { TodoItem, WorkspaceConnectionState } from "../../types";
import type { WorkspaceInfo } from "../../lib/tauri";
@@ -266,21 +267,21 @@ export default function SessionSidebar(props: SidebarProps) {
disabled={props.newTaskDisabled}
>
<Plus size={16} />
New task
{t("session.new_task")}
</button>
</div>
<div class="flex-1 overflow-y-auto px-4 py-4 space-y-6">
<div>
<div class="flex items-center justify-between px-2 mb-2">
<div class="text-xs text-gray-10 font-semibold uppercase tracking-wider">Workspaces</div>
<div class="text-xs text-gray-10 font-semibold uppercase tracking-wider">{t("dashboard.workspaces")}</div>
</div>
<div class="space-y-4">
<Show
when={props.workspaceGroups.length > 0}
fallback={
<div class="px-3 py-2 rounded-lg border border-dashed border-gray-6 text-xs text-gray-9">
No workspaces in this session yet. Add one to get started.
{t("sidebar.no_workspaces")}
</div>
}
>
@@ -356,7 +357,7 @@ export default function SessionSidebar(props: SidebarProps) {
</span>
<Show when={group.workspace.workspaceType === "remote"}>
<span class="text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-3 text-gray-11">
{isSandboxWorkspace() ? "Sandbox" : "Remote"}
{isSandboxWorkspace() ? t("workspace.sandbox_badge") : t("workspace.remote_badge")}
</span>
</Show>
</div>
@@ -373,11 +374,11 @@ export default function SessionSidebar(props: SidebarProps) {
</Show>
<Show when={!isConnecting() && connectionStatus() !== "connecting"}>
<Show when={connectionStatus() === "error"}>
<span class="text-red-11 font-medium">Needs attention</span>
<span class="text-red-11 font-medium">{t("sidebar.needs_attention")}</span>
</Show>
<Show when={connectionStatus() !== "error"}>
<Show when={isActive()} fallback={<span class="text-gray-9">Switch</span>}>
<span class="text-green-11 font-medium">Active</span>
<Show when={isActive()} fallback={<span class="text-gray-9">{t("sidebar.switch")}</span>}>
<span class="text-green-11 font-medium">{t("sidebar.active")}</span>
</Show>
</Show>
</Show>
@@ -389,7 +390,7 @@ export default function SessionSidebar(props: SidebarProps) {
type="button"
class="p-1 rounded-md text-gray-9 hover:text-gray-12 hover:bg-gray-2"
onClick={() => toggleWorkspaceCollapse(group.workspace.id)}
title={collapsed() ? "Expand" : "Collapse"}
title={collapsed() ? t("sidebar.expand") : t("sidebar.collapse")}
>
<ChevronDown
size={14}
@@ -399,7 +400,7 @@ export default function SessionSidebar(props: SidebarProps) {
<button
type="button"
class="p-1 rounded-md text-gray-9 hover:text-gray-12 hover:bg-gray-2 cursor-grab"
title="Drag to reorder"
title={t("sidebar.drag_reorder")}
draggable
onDragStart={(event) => handleDragStart(event, group.workspace.id)}
onDragEnd={handleDragEnd}
@@ -424,7 +425,7 @@ export default function SessionSidebar(props: SidebarProps) {
disabled={isActivelyConnecting()}
>
<Settings size={12} />
Edit connection
{t("sidebar.edit_connection")}
</button>
<button
type="button"
@@ -433,7 +434,7 @@ export default function SessionSidebar(props: SidebarProps) {
disabled={isActivelyConnecting()}
>
<RefreshCcw size={12} class={connectionStatus() === "connecting" ? "animate-spin" : ""} />
Test connection
{t("sidebar.test_connection")}
</button>
</Show>
<Show when={group.workspace.sandboxContainerName?.trim() && props.onStopSandbox}>
@@ -444,7 +445,7 @@ export default function SessionSidebar(props: SidebarProps) {
disabled={isActivelyConnecting()}
>
<Square size={12} />
Stop sandbox
{t("sidebar.stop_sandbox")}
</button>
</Show>
<button
@@ -454,14 +455,14 @@ export default function SessionSidebar(props: SidebarProps) {
disabled={isActivelyConnecting()}
>
<Trash2 size={12} />
Remove
{t("sidebar.remove_workspace")}
</button>
</div>
<Show
when={sessions().length > 0}
fallback={
<div class="px-3 py-2 rounded-lg border border-dashed border-gray-6 text-xs text-gray-9">
No sessions yet.
{t("sidebar.no_sessions_yet")}
</div>
}
>
@@ -519,9 +520,7 @@ export default function SessionSidebar(props: SidebarProps) {
class="w-full px-3 py-2 rounded-lg text-xs text-gray-9 hover:text-gray-12 hover:bg-gray-2 transition-colors"
onClick={() => toggleShowAllSessions(group.workspace.id)}
>
{showingAll()
? "Show fewer"
: `Show ${sessions().length - MAX_SESSIONS_PREVIEW} more`}
{showingAll() ? t("sidebar.show_fewer") : t("sidebar.show_more", undefined, { count: sessions().length - MAX_SESSIONS_PREVIEW })}
</button>
</Show>
</Show>
@@ -542,7 +541,7 @@ export default function SessionSidebar(props: SidebarProps) {
onDrop={(event) => handleDrop(event, null)}
>
<Plus size={14} />
Add workspace
{t("sidebar.add_workspace")}
</button>
</div>
</div>
@@ -555,7 +554,7 @@ export default function SessionSidebar(props: SidebarProps) {
class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium"
onClick={() => props.onToggleSection("progress")}
>
<span>Progress</span>
<span>{t("sidebar.progress")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${
@@ -614,7 +613,7 @@ export default function SessionSidebar(props: SidebarProps) {
closeContextMenu();
}}
>
New task
{t("session.new_task")}
</button>
<button
class="w-full text-left px-3 py-2 text-sm rounded-lg text-red-11 hover:bg-red-1/40 transition-colors"
@@ -624,7 +623,7 @@ export default function SessionSidebar(props: SidebarProps) {
closeContextMenu();
}}
>
Delete session
{t("sidebar.delete_session")}
</button>
</div>
</div>

View File

@@ -25,6 +25,7 @@ import {
getWorkspaceTaskLoadErrorDisplay,
isWindowsPlatform,
} from "../../utils";
import { t } from "../../../i18n";
type Props = {
workspaceSessionGroups: WorkspaceSessionGroup[];
@@ -57,7 +58,6 @@ type Props = {
};
const MAX_SESSIONS_PREVIEW = 6;
const COLLAPSED_SESSIONS_PREVIEW = MAX_SESSIONS_PREVIEW;
type SessionListItem = WorkspaceSessionGroup["sessions"][number];
type FlattenedSessionRow = { session: SessionListItem; depth: number };
@@ -160,16 +160,16 @@ const workspaceLabel = (workspace: WorkspaceInfo) =>
workspace.openworkWorkspaceName?.trim() ||
workspace.name?.trim() ||
workspace.path?.trim() ||
"Workspace";
t("workspace_list.workspace_fallback");
const workspaceKindLabel = (workspace: WorkspaceInfo) =>
workspace.workspaceType === "remote"
? workspace.sandboxBackend === "docker" ||
Boolean(workspace.sandboxRunId?.trim()) ||
Boolean(workspace.sandboxContainerName?.trim())
? "Sandbox"
: "Remote"
: "Local";
? t("workspace.sandbox_badge")
: t("workspace.remote_badge")
: t("workspace.local_badge");
const WORKSPACE_SWATCHES = ["#2563eb", "#5a67d8", "#f97316", "#10b981"];
@@ -184,9 +184,9 @@ const workspaceSwatchColor = (seed: string) => {
};
export default function WorkspaceSessionList(props: Props) {
const revealLabel = isWindowsPlatform()
? "Reveal in Explorer"
: "Reveal in Finder";
const revealLabel = () => isWindowsPlatform()
? t("workspace_list.reveal_explorer")
: t("workspace_list.reveal_finder");
const [expandedWorkspaceIds, setExpandedWorkspaceIds] = createSignal<
Set<string>
>(new Set());
@@ -238,13 +238,8 @@ export default function WorkspaceSessionList(props: Props) {
expandWorkspace(props.selectedWorkspaceId);
});
const previewCount = (workspaceId: string) => {
const base =
previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW;
return isWorkspaceExpanded(workspaceId)
? base
: Math.min(COLLAPSED_SESSIONS_PREVIEW, base);
};
const previewCount = (workspaceId: string) =>
previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW;
const previewSessions = (
workspaceId: string,
@@ -287,7 +282,7 @@ export default function WorkspaceSessionList(props: Props) {
const showMoreLabel = (workspaceId: string, totalRoots: number) => {
const remaining = Math.max(0, totalRoots - previewCount(workspaceId));
const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining);
return nextCount > 0 ? `Show ${nextCount} more` : "Show more";
return nextCount > 0 ? t("workspace_list.show_more", undefined, { count: nextCount }) : t("workspace_list.show_more_fallback");
};
createEffect(() => {
@@ -411,7 +406,7 @@ export default function WorkspaceSessionList(props: Props) {
<button
type="button"
class="-ml-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-gray-9 transition-colors hover:bg-gray-3/80 hover:text-gray-11"
aria-label={isExpanded() ? "Hide child sessions" : "Show child sessions"}
aria-label={isExpanded() ? t("workspace_list.hide_child_sessions") : t("workspace_list.show_child_sessions")}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -441,7 +436,7 @@ export default function WorkspaceSessionList(props: Props) {
<button
type="button"
class="flex h-7 w-7 items-center justify-center rounded-md text-gray-9 transition-colors hover:bg-gray-3/80 hover:text-gray-11"
aria-label="Session actions"
aria-label={t("workspace_list.session_actions")}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@@ -469,7 +464,7 @@ export default function WorkspaceSessionList(props: Props) {
props.onOpenRenameSession?.();
}}
>
Rename session
{t("workspace_list.rename_session")}
</button>
</Show>
@@ -482,7 +477,7 @@ export default function WorkspaceSessionList(props: Props) {
props.onOpenDeleteSession?.();
}}
>
Delete session
{t("workspace_list.delete_session")}
</button>
</Show>
</div>
@@ -524,9 +519,9 @@ export default function WorkspaceSessionList(props: Props) {
getWorkspaceTaskLoadErrorDisplay(workspace(), group.error);
const statusLabel = () => {
if (group.status === "error") return taskLoadError().label;
if (isConnectionActionBusy()) return "Connecting";
if (isConnectionActionBusy()) return t("workspace_list.connecting");
if (!props.developerMode) return "";
if (props.selectedWorkspaceId === workspace().id) return "Selected";
if (props.selectedWorkspaceId === workspace().id) return t("workspace.selected");
return workspaceKindLabel(workspace());
};
const statusTone = () => {
@@ -600,7 +595,7 @@ export default function WorkspaceSessionList(props: Props) {
props.onCreateTaskInWorkspace(workspace().id);
}}
disabled={props.newTaskDisabled}
aria-label="New task"
aria-label={t("session.new_task")}
>
<Plus size={14} />
</button>
@@ -616,7 +611,7 @@ export default function WorkspaceSessionList(props: Props) {
: workspace().id,
);
}}
aria-label="Workspace options"
aria-label={t("workspace_list.workspace_options")}
>
<MoreHorizontal size={14} />
</button>
@@ -627,8 +622,8 @@ export default function WorkspaceSessionList(props: Props) {
class="rounded-md p-1 text-gray-9 hover:bg-gray-3/80 hover:text-gray-11"
aria-label={
isWorkspaceExpanded(workspace().id)
? "Collapse"
: "Expand"
? t("sidebar.collapse")
: t("sidebar.expand")
}
onClick={(event) => {
event.stopPropagation();
@@ -659,7 +654,7 @@ export default function WorkspaceSessionList(props: Props) {
setWorkspaceMenuId(null);
}}
>
Edit name
{t("workspace_list.edit_name")}
</button>
<button
type="button"
@@ -669,7 +664,7 @@ export default function WorkspaceSessionList(props: Props) {
setWorkspaceMenuId(null);
}}
>
Share...
{t("workspace_list.share")}
</button>
<Show when={workspace().workspaceType === "local"}>
<button
@@ -680,7 +675,7 @@ export default function WorkspaceSessionList(props: Props) {
setWorkspaceMenuId(null);
}}
>
{revealLabel}
{revealLabel()}
</button>
</Show>
<Show when={workspace().workspaceType === "remote"}>
@@ -696,7 +691,7 @@ export default function WorkspaceSessionList(props: Props) {
}}
disabled={isConnectionActionBusy()}
>
Recover
{t("workspace_list.recover")}
</button>
</Show>
<button
@@ -710,7 +705,7 @@ export default function WorkspaceSessionList(props: Props) {
}}
disabled={isConnectionActionBusy()}
>
Test connection
{t("workspace_list.test_connection")}
</button>
<button
type="button"
@@ -721,7 +716,7 @@ export default function WorkspaceSessionList(props: Props) {
}}
disabled={isConnectionActionBusy()}
>
Edit connection
{t("workspace_list.edit_connection")}
</button>
</Show>
<button
@@ -732,155 +727,110 @@ export default function WorkspaceSessionList(props: Props) {
setWorkspaceMenuId(null);
}}
>
Remove workspace
{t("workspace_list.remove_workspace")}
</button>
</div>
</Show>
</div>
<div class="mt-3 px-1 pb-1">
<div class="relative flex flex-col gap-1 pl-2.5 before:absolute before:bottom-2 before:left-0 before:top-2 before:w-[2px] before:bg-gray-3 before:content-['']">
<Show
when={isWorkspaceExpanded(workspace().id)}
fallback={
<Show when={group.sessions.length > 0}>
<For
each={previewSessions(
workspace().id,
group.sessions,
tree,
forcedExpandedSessionIds,
)}
<Show when={isWorkspaceExpanded(workspace().id)}>
<div class="mt-3 px-1 pb-1">
<div class="relative flex flex-col gap-1 pl-2.5 before:absolute before:bottom-2 before:left-0 before:top-2 before:w-[2px] before:bg-gray-3 before:content-['']">
<Show
when={
group.status === "loading" &&
group.sessions.length === 0
}
fallback={
<Show
when={group.sessions.length > 0}
fallback={
<Show when={group.status === "error"}>
<div
class={`w-full rounded-[15px] border px-3 py-2.5 text-left text-[11px] ${
taskLoadError().tone === "offline"
? "border-amber-7/35 bg-amber-2/50 text-amber-11"
: "border-red-7/35 bg-red-1/40 text-red-11"
}`}
title={taskLoadError().title}
>
{taskLoadError().message}
</div>
</Show>
}
>
{(row) =>
renderSessionRow(
<For
each={previewSessions(
workspace().id,
row,
group.sessions,
tree,
forcedExpandedSessionIds,
)}
</For>
<Show
when={
getRootSessions(group.sessions).length >
previewCount(workspace().id)
}
>
<button
type="button"
class="w-full rounded-[15px] border border-transparent px-3 py-2.5 text-left text-[11px] text-gray-10 transition-colors hover:bg-gray-2/60 hover:text-gray-11"
onClick={() =>
showMoreSessions(
>
{(row) =>
renderSessionRow(
workspace().id,
getRootSessions(group.sessions).length,
)
row,
tree,
forcedExpandedSessionIds,
)}
</For>
<Show
when={
group.sessions.length === 0 &&
group.status === "ready"
}
>
{showMoreLabel(
workspace().id,
getRootSessions(group.sessions).length,
)}
</button>
</Show>
</Show>
}
>
<Show
when={
group.status === "loading" &&
group.sessions.length === 0
}
fallback={
<Show
when={group.sessions.length > 0}
fallback={
<Show when={group.status === "error"}>
<div
class={`w-full rounded-[15px] border px-3 py-2.5 text-left text-[11px] ${
taskLoadError().tone === "offline"
? "border-amber-7/35 bg-amber-2/50 text-amber-11"
: "border-red-7/35 bg-red-1/40 text-red-11"
}`}
title={taskLoadError().title}
<button
type="button"
class="group/empty w-full rounded-[15px] border border-transparent px-3 py-2.5 text-left text-[11px] text-gray-10 transition-colors hover:bg-gray-2/60 hover:text-gray-11"
onClick={() =>
props.onCreateTaskInWorkspace(workspace().id)
}
disabled={props.newTaskDisabled}
>
{taskLoadError().message}
</div>
<span class="group-hover/empty:hidden">
{t("workspace.no_tasks")}
</span>
<span class="hidden group-hover/empty:inline font-medium">
{t("workspace.new_task_inline")}
</span>
</button>
</Show>
}
>
<For
each={previewSessions(
workspace().id,
group.sessions,
tree,
forcedExpandedSessionIds,
)}
>
{(row) =>
renderSessionRow(
workspace().id,
row,
tree,
forcedExpandedSessionIds,
)}
</For>
<Show
when={
group.sessions.length === 0 &&
group.status === "ready"
}
>
<button
type="button"
class="group/empty w-full rounded-[15px] border border-transparent px-3 py-2.5 text-left text-[11px] text-gray-10 transition-colors hover:bg-gray-2/60 hover:text-gray-11"
onClick={() =>
props.onCreateTaskInWorkspace(workspace().id)
<Show
when={
getRootSessions(group.sessions).length >
previewCount(workspace().id)
}
disabled={props.newTaskDisabled}
>
<span class="group-hover/empty:hidden">
No tasks yet.
</span>
<span class="hidden group-hover/empty:inline font-medium">
+ New task
</span>
</button>
</Show>
<Show
when={
getRootSessions(group.sessions).length >
previewCount(workspace().id)
}
>
<button
type="button"
class="w-full rounded-[15px] border border-transparent px-3 py-2.5 text-left text-[11px] text-gray-10 transition-colors hover:bg-gray-2/60 hover:text-gray-11"
onClick={() =>
showMoreSessions(
<button
type="button"
class="w-full rounded-[15px] border border-transparent px-3 py-2.5 text-left text-[11px] text-gray-10 transition-colors hover:bg-gray-2/60 hover:text-gray-11"
onClick={() =>
showMoreSessions(
workspace().id,
getRootSessions(group.sessions).length,
)
}
>
{showMoreLabel(
workspace().id,
getRootSessions(group.sessions).length,
)
}
>
{showMoreLabel(
workspace().id,
getRootSessions(group.sessions).length,
)}
</button>
)}
</button>
</Show>
</Show>
</Show>
}
>
<div class="w-full rounded-[15px] px-3 py-2.5 text-left text-[11px] text-gray-10">
Loading tasks...
</div>
</Show>
</Show>
}
>
<div class="w-full rounded-[15px] px-3 py-2.5 text-left text-[11px] text-gray-10">
{t("workspace.loading_tasks")}
</div>
</Show>
</div>
</div>
</div>
</Show>
</div>
);
}}
@@ -895,7 +845,7 @@ export default function WorkspaceSessionList(props: Props) {
onClick={props.onOpenCreateWorkspace}
>
<Plus size={14} />
Add workspace
{t("workspace_list.add_workspace")}
</button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { Show, createMemo } from "solid-js";
import { MessageCircle, Settings } from "lucide-solid";
import { t } from "../../i18n";
import { useConnections } from "../connections/provider";
import type { OpenworkServerStatus } from "../lib/openwork-server";
@@ -47,19 +48,19 @@ export default function StatusBar(props: StatusBarProps) {
if (props.clientConnected) {
const detailBits: string[] = [];
if (providers > 0) {
detailBits.push(`${providers} provider${providers === 1 ? "" : "s"} connected`);
detailBits.push(t("status.providers_connected", undefined, { count: providers, plural: providers === 1 ? "" : "s" }));
}
if (mcp > 0) {
detailBits.push(`${mcp} MCP connected`);
detailBits.push(t("status.mcp_connected", undefined, { count: mcp }));
}
if (!detailBits.length) {
detailBits.push("Ready for new tasks");
detailBits.push(t("status.ready_for_tasks"));
}
if (props.developerMode) {
detailBits.push("Developer mode");
detailBits.push(t("status.developer_mode"));
}
return {
label: "OpenWork Ready",
label: t("status.openwork_ready"),
detail: detailBits.join(" · "),
dotClass: "bg-green-9",
pingClass: "bg-green-9/45 animate-ping",
@@ -69,11 +70,11 @@ export default function StatusBar(props: StatusBarProps) {
if (props.openworkServerStatus === "limited") {
return {
label: "Limited Mode",
label: t("status.limited_mode"),
detail:
mcp > 0
? `${mcp} MCP connected · reconnect for full features`
: "Reconnect to restore full OpenWork features",
? t("status.limited_mcp_hint", undefined, { count: mcp })
: t("status.limited_hint"),
dotClass: "bg-amber-9",
pingClass: "bg-amber-9/35",
pulse: false,
@@ -81,8 +82,8 @@ export default function StatusBar(props: StatusBarProps) {
}
return {
label: "Disconnected",
detail: "Open settings to reconnect",
label: t("status.disconnected_label"),
detail: t("status.disconnected_hint"),
dotClass: "bg-red-9",
pingClass: "bg-red-9/35",
pulse: false,
@@ -108,19 +109,19 @@ export default function StatusBar(props: StatusBarProps) {
type="button"
class="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={props.onSendFeedback}
title="Send feedback"
aria-label="Send feedback"
title={t("status.send_feedback")}
aria-label={t("status.send_feedback")}
>
<MessageCircle class="h-4 w-4" />
<span class="text-[11px] font-medium">Feedback</span>
<span class="text-[11px] font-medium">{t("status.feedback")}</span>
</button>
<Show when={props.showSettingsButton !== false}>
<button
type="button"
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text"
onClick={props.onOpenSettings}
title={props.settingsOpen ? "Back to previous screen" : "Settings"}
aria-label={props.settingsOpen ? "Back to previous screen" : "Settings"}
title={props.settingsOpen ? t("status.back") : t("status.settings")}
aria-label={props.settingsOpen ? t("status.back") : t("status.settings")}
>
<Settings class="h-4 w-4" />
</button>

View File

@@ -5,6 +5,7 @@ import { schedulerDeleteJob, schedulerListJobs } from "../lib/tauri";
import { isTauriRuntime } from "../utils";
import { createWorkspaceContextKey } from "./workspace-context";
import type { OpenworkServerStore } from "../connections/openwork-server-store";
import { t } from "../../i18n";
export type AutomationsStore = ReturnType<typeof createAutomationsStore>;
@@ -33,10 +34,10 @@ const buildCreateAutomationPrompt = (
const schedule = input.schedule.trim();
const prompt = normalizeSentence(input.prompt);
if (!schedule) {
return { ok: false, error: "Schedule is required." };
return { ok: false, error: t("automations.schedule_required") };
}
if (!prompt) {
return { ok: false, error: "Prompt is required." };
return { ok: false, error: t("automations.prompt_required") };
}
const workdir = (input.workdir ?? "").trim();
const nameSegment = name ? ` named \"${name}\"` : "";
@@ -58,7 +59,7 @@ const buildRunAutomationPrompt = (
if (job.run?.prompt || job.prompt) {
const promptBody = (job.run?.prompt ?? job.prompt ?? "").trim();
if (!promptBody) {
return { ok: false, error: "Automation prompt is empty." };
return { ok: false, error: t("automations.prompt_empty") };
}
return {
ok: true,
@@ -136,10 +137,10 @@ export function createAutomationsStore(options: {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const status =
options.openworkServer.openworkServerStatus() === "disconnected"
? "OpenWork server unavailable. Connect to sync scheduled tasks."
? t("automations.server_unavailable")
: options.openworkServer.openworkServerStatus() === "limited"
? "OpenWork server needs a token to load scheduled tasks."
: "OpenWork server not ready.";
? t("automations.server_needs_token")
: t("automations.server_not_ready");
setScheduledJobsStatus(status);
return "unavailable";
}
@@ -155,7 +156,7 @@ export function createAutomationsStore(options: {
} catch (error) {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const message = error instanceof Error ? error.message : String(error);
setScheduledJobsStatus(message || "Failed to load scheduled tasks.");
setScheduledJobsStatus(message || t("automations.failed_to_load"));
return "error";
} finally {
setScheduledJobsBusy(false);
@@ -180,7 +181,7 @@ export function createAutomationsStore(options: {
} catch (error) {
if (scheduledJobsContextKey() !== requestContextKey) return "skipped";
const message = error instanceof Error ? error.message : String(error);
setScheduledJobsStatus(message || "Failed to load scheduled tasks.");
setScheduledJobsStatus(message || t("automations.failed_to_load"));
return "error";
} finally {
setScheduledJobsBusy(false);
@@ -192,7 +193,7 @@ export function createAutomationsStore(options: {
const client = options.openworkServer.openworkServerClient();
const workspaceId = (options.runtimeWorkspaceId() ?? "").trim();
if (!client || !workspaceId) {
throw new Error("OpenWork server unavailable. Connect to sync scheduled tasks.");
throw new Error(t("automations.server_unavailable"));
}
const response = await client.deleteScheduledJob(workspaceId, name);
setScheduledJobs((current) => current.filter((entry) => entry.slug !== response.job.slug));
@@ -200,7 +201,7 @@ export function createAutomationsStore(options: {
}
if (!isTauriRuntime()) {
throw new Error("Scheduled tasks require the desktop app.");
throw new Error(t("automations.desktop_required"));
}
const root = options.selectedWorkspaceRoot().trim();
const job = await schedulerDeleteJob(name, root || undefined);

View File

@@ -2,7 +2,7 @@ import { createMemo, createSignal, type Accessor } from "solid-js";
import type { ProviderAuthAuthorization, ProviderListResponse } from "@opencode-ai/sdk/v2/client";
import { t, currentLocale } from "../../../i18n";
import { t } from "../../../i18n";
import { unwrap, waitForHealthy } from "../../lib/opencode";
import type { Client, ProviderListItem, WorkspaceDisplay } from "../../types";
import { safeStringify } from "../../utils";
@@ -70,7 +70,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const assertNoClientError = (result: unknown) => {
const maybe = result as { error?: unknown } | null | undefined;
if (!maybe || maybe.error === undefined) return;
throw new Error(describeProviderError(maybe.error, t("app.error_request_failed", currentLocale())));
throw new Error(describeProviderError(maybe.error, t("providers.request_failed")));
};
const describeProviderError = (error: unknown, fallback: string) => {
@@ -125,9 +125,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const generic = raw && /^unknown\s+error$/i.test(raw);
const heading = (() => {
if (status === 401 || status === 403) return t("app.error_auth_failed", currentLocale());
if (status === 429) return t("app.error_rate_limit", currentLocale());
if (provider) return `Provider error (${provider})`;
if (status === 401 || status === 403) return t("providers.auth_failed");
if (status === 429) return t("providers.rate_limit_exceeded");
if (provider) return t("providers.provider_error", undefined, { provider });
return fallback;
})();
@@ -167,7 +167,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
if (!Array.isArray(provider.env) || provider.env.length === 0) continue;
const existing = merged[id] ?? [];
if (existing.some((method) => method.type === "api")) continue;
merged[id] = [...existing, { type: "api", label: "API key" }];
merged[id] = [...existing, { type: "api", label: t("providers.api_key_label") }];
}
for (const [id, providerMethods] of Object.entries(merged)) {
const provider = availableProviders.find((item) => item.id === id);
@@ -188,7 +188,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const loadProviderAuthMethods = async (workerType: "local" | "remote") => {
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const methods = unwrap(await c.provider.auth());
return buildProviderAuthMethods(
@@ -205,7 +205,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
try {
const cachedMethods = providerAuthMethods();
@@ -214,17 +214,17 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
: await loadProviderAuthMethods(providerAuthWorkerType());
const providerIds = Object.keys(authMethods).sort();
if (!providerIds.length) {
throw new Error("No providers available");
throw new Error(t("providers.no_providers_available"));
}
const resolved = providerId?.trim() ?? "";
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
const methods = authMethods[resolved];
if (!methods || !methods.length) {
throw new Error(`Unknown provider: ${resolved}`);
throw new Error(`${t("providers.unknown_provider")}: ${resolved}`);
}
const oauthIndex =
@@ -232,12 +232,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
? methodIndex
: methods.find((method) => method.type === "oauth")?.methodIndex ?? -1;
if (oauthIndex === -1) {
throw new Error(`No OAuth flow available for ${resolved}. Use an API key instead.`);
throw new Error(`${t("providers.no_oauth_prefix")} ${resolved}. ${t("providers.use_api_key_suffix")}`);
}
const selectedMethod = methods.find((method) => method.methodIndex === oauthIndex);
if (!selectedMethod || selectedMethod.type !== "oauth") {
throw new Error(`Selected auth method is not an OAuth flow for ${resolved}.`);
throw new Error(`${t("providers.not_oauth_flow_prefix")} ${resolved}.`);
}
const auth = unwrap(await c.provider.oauth.authorize({ providerID: resolved, method: oauthIndex }));
@@ -246,7 +246,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
authorization: auth,
};
} catch (error) {
const message = describeProviderError(error, "Failed to connect provider");
const message = describeProviderError(error, t("providers.connect_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -310,16 +310,16 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const resolved = providerId?.trim();
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
if (!Number.isInteger(methodIndex) || methodIndex < 0) {
throw new Error("OAuth method is required");
throw new Error(t("providers.oauth_method_required"));
}
const waitForProviderConnection = async (timeoutMs = 15_000, pollMs = 2_000) => {
@@ -354,26 +354,26 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
const updated = await refreshProviders({ dispose: true });
const connectedNow = Array.isArray(updated?.connected) && updated.connected.includes(resolved);
if (connectedNow) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
const connected = await waitForProviderConnection();
if (connected) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
return { connected: false, pending: true };
} catch (error) {
if (isPendingOauthError(error)) {
const updated = await refreshProviders({ dispose: true });
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
const connected = await waitForProviderConnection();
if (connected) {
return { connected: true, message: `Connected ${resolved}` };
return { connected: true, message: `${t("status.connected")} ${resolved}` };
}
return { connected: false, pending: true };
}
const message = describeProviderError(error, "Failed to complete OAuth");
const message = describeProviderError(error, t("providers.oauth_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -383,12 +383,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const trimmed = apiKey.trim();
if (!trimmed) {
throw new Error("API key is required");
throw new Error(t("providers.api_key_required"));
}
try {
@@ -397,9 +397,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
auth: { type: "api", key: trimmed },
});
await refreshProviders({ dispose: true });
return `Connected ${providerId}`;
return `${t("status.connected")} ${providerId}`;
} catch (error) {
const message = describeProviderError(error, "Failed to save API key");
const message = describeProviderError(error, t("providers.save_api_key_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -409,12 +409,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
setProviderAuthError(null);
const c = options.client();
if (!c) {
throw new Error(t("app.error_not_connected", currentLocale()));
throw new Error(t("providers.not_connected"));
}
const resolved = providerId.trim();
if (!resolved) {
throw new Error("Provider ID is required");
throw new Error(t("providers.provider_id_required"));
}
const provider = options.providers().find((entry) => entry.id === resolved) as
@@ -447,7 +447,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
return;
}
throw new Error("Provider auth removal is not supported by this client.");
throw new Error(t("providers.removal_unsupported"));
};
const disableProvider = async () => {
@@ -492,18 +492,18 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
}
if (!Array.isArray(updated?.connected) || !updated.connected.includes(resolved)) {
return disabled
? `Disconnected ${resolved} and disabled it in OpenCode config.`
: `Disconnected ${resolved}.`;
? `${t("providers.disconnected_prefix")} ${resolved} ${t("providers.disabled_in_config_suffix")}`
: `${t("providers.disconnected_prefix")} ${resolved}.`;
}
}
if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) {
return `Removed stored credentials for ${resolved}, but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.`;
return `Removed stored credentials for ${resolved}${t("providers.still_connected_suffix")}`;
}
removeProviderFromState(resolved);
return `Disconnected ${resolved}`;
return `${t("providers.disconnected_prefix")} ${resolved}`;
} catch (error) {
const message = describeProviderError(error, "Failed to disconnect provider");
const message = describeProviderError(error, t("providers.disconnect_failed"));
setProviderAuthError(message);
throw error instanceof Error ? error : new Error(message);
}
@@ -524,7 +524,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) {
} catch (error) {
setProviderAuthPreferredProviderId(null);
setProviderAuthReturnFocusTarget("none");
const message = describeProviderError(error, "Failed to load providers");
const message = describeProviderError(error, t("providers.load_failed"));
setProviderAuthError(message);
throw error;
} finally {

View File

@@ -4,6 +4,7 @@ import type { ScheduledJob } from "../types";
import { useAutomations } from "../automations/provider";
import { usePlatform } from "../context/platform";
import { formatRelativeTime, isTauriRuntime } from "../utils";
import { t } from "../../i18n";
import {
BookOpen,
@@ -51,7 +52,7 @@ const pillGhostClass = `${pillButtonClass} border border-dls-border bg-dls-surfa
const tagClass =
"inline-flex items-center rounded-md border border-dls-border bg-dls-hover px-2 py-1 text-[11px] text-dls-secondary";
const DEFAULT_AUTOMATION_NAME = "Daily bug scan";
const DEFAULT_AUTOMATION_NAME = () => t("scheduled.default_automation_name");
const DEFAULT_AUTOMATION_PROMPT =
"Scan recent commits and flag riskier diffs with the most important follow-ups.";
const DEFAULT_SCHEDULE_TIME = "09:00";
@@ -61,79 +62,79 @@ const DEFAULT_INTERVAL_HOURS = 6;
const automationTemplates: AutomationTemplate[] = [
{
icon: Calendar,
name: "Daily planning brief",
description: "Build a focused plan from your tasks and calendar before the day starts.",
name: t("scheduled.tpl_daily_planning_name"),
description: t("scheduled.tpl_daily_planning_desc"),
prompt:
"Review my pending tasks and calendar, then draft a practical plan for today with top priorities and one follow-up reminder.",
scheduleMode: "daily",
scheduleTime: "08:30",
scheduleDays: ["mo", "tu", "we", "th", "fr"],
badge: "Weekday morning",
badge: t("scheduled.badge_weekday_morning"),
},
{
icon: BookOpen,
name: "Inbox zero helper",
description: "Summarize unread messages and suggest concise replies for the top threads.",
name: t("scheduled.tpl_inbox_zero_name"),
description: t("scheduled.tpl_inbox_zero_desc"),
prompt:
"Summarize unread inbox messages, suggest priority order, and draft concise reply options for the top conversations.",
scheduleMode: "daily",
scheduleTime: "17:30",
scheduleDays: ["mo", "tu", "we", "th", "fr"],
badge: "End-of-day",
badge: t("scheduled.badge_end_of_day"),
},
{
icon: MessageSquare,
name: "Meeting prep notes",
description: "Generate prep bullets, context, and unblockers for tomorrow's meetings.",
name: t("scheduled.tpl_meeting_prep_name"),
description: t("scheduled.tpl_meeting_prep_desc"),
prompt:
"Prepare meeting briefs for tomorrow with context, talking points, and questions to unblock decisions.",
scheduleMode: "daily",
scheduleTime: "18:00",
scheduleDays: ["mo", "tu", "we", "th", "fr"],
badge: "Weekday evening",
badge: t("scheduled.badge_weekday_evening"),
},
{
icon: TrendingUp,
name: "Weekly wins recap",
description: "Turn the week into wins, blockers, and clear next steps to share.",
name: t("scheduled.tpl_weekly_wins_name"),
description: t("scheduled.tpl_weekly_wins_desc"),
prompt:
"Summarize the week into wins, blockers, and clear next steps I can share with the team.",
scheduleMode: "daily",
scheduleTime: "16:00",
scheduleDays: ["fr"],
badge: "Friday wrap-up",
badge: t("scheduled.badge_friday_wrapup"),
},
{
icon: Trophy,
name: "Learning digest",
description: "Collect saved links and notes into a weekly digest with actions.",
name: t("scheduled.tpl_learning_digest_name"),
description: t("scheduled.tpl_learning_digest_desc"),
prompt:
"Collect my saved links and notes, then draft a weekly learning digest with key ideas and follow-up actions.",
scheduleMode: "daily",
scheduleTime: "10:00",
scheduleDays: ["su"],
badge: "Weekend review",
badge: t("scheduled.badge_weekend_review"),
},
{
icon: Brain,
name: "Habit check-in",
description: "Run a quick accountability check-in and suggest one concrete next action.",
name: t("scheduled.tpl_habit_checkin_name"),
description: t("scheduled.tpl_habit_checkin_desc"),
prompt:
"Ask me for a quick progress check-in, capture blockers, and suggest one concrete next action.",
scheduleMode: "interval",
intervalHours: 6,
badge: "Every few hours",
badge: t("scheduled.badge_every_few_hours"),
},
];
const dayOptions = [
{ id: "mo", label: "Mo", cron: "1" },
{ id: "tu", label: "Tu", cron: "2" },
{ id: "we", label: "We", cron: "3" },
{ id: "th", label: "Th", cron: "4" },
{ id: "fr", label: "Fr", cron: "5" },
{ id: "sa", label: "Sa", cron: "6" },
{ id: "su", label: "Su", cron: "0" },
{ id: "mo", label: () => t("scheduled.day_mon"), cron: "1" },
{ id: "tu", label: () => t("scheduled.day_tue"), cron: "2" },
{ id: "we", label: () => t("scheduled.day_wed"), cron: "3" },
{ id: "th", label: () => t("scheduled.day_thu"), cron: "4" },
{ id: "fr", label: () => t("scheduled.day_fri"), cron: "5" },
{ id: "sa", label: () => t("scheduled.day_sat"), cron: "6" },
{ id: "su", label: () => t("scheduled.day_sun"), cron: "0" },
];
export type AutomationsViewProps = {
@@ -179,9 +180,9 @@ const parseCronNumbers = (value: string) => {
const humanizeCron = (cron: string) => {
const parts = cron.trim().split(/\s+/);
if (parts.length < 5) return "Custom schedule";
if (parts.length < 5) return t("scheduled.custom_schedule");
const [minuteRaw, hourRaw, dom, mon, dowRaw] = parts;
if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return "Custom schedule";
if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return t("scheduled.custom_schedule");
if (
minuteRaw === "0" &&
@@ -192,19 +193,19 @@ const humanizeCron = (cron: string) => {
) {
const interval = Number.parseInt(hourRaw.slice(2), 10);
if (Number.isFinite(interval) && interval > 0) {
return interval === 1 ? "Every hour" : `Every ${interval} hours`;
return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval });
}
}
const hour = Number.parseInt(hourRaw, 10);
const minute = Number.parseInt(minuteRaw, 10);
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return "Custom schedule";
if (dom !== "*" || mon !== "*") return "Custom schedule";
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return t("scheduled.custom_schedule");
if (dom !== "*" || mon !== "*") return t("scheduled.custom_schedule");
const timeLabel = `${pad2(hour)}:${pad2(minute)}`;
if (dowRaw === "*") {
return `Every day at ${timeLabel}`;
return t("scheduled.every_day_at", undefined, { time: timeLabel });
}
const days = parseCronNumbers(dowRaw);
@@ -213,28 +214,28 @@ const humanizeCron = (cron: string) => {
const weekdayDays = [1, 2, 3, 4, 5];
const weekendDays = [0, 6];
if (allDays.every((d) => normalized.has(d))) return `Every day at ${timeLabel}`;
if (allDays.every((d) => normalized.has(d))) return t("scheduled.every_day_at", undefined, { time: timeLabel });
if (
weekdayDays.every((d) => normalized.has(d)) &&
!weekendDays.some((d) => normalized.has(d))
) {
return `Weekdays at ${timeLabel}`;
return t("scheduled.weekdays_at", undefined, { time: timeLabel });
}
if (
weekendDays.every((d) => normalized.has(d)) &&
!weekdayDays.some((d) => normalized.has(d))
) {
return `Weekends at ${timeLabel}`;
return t("scheduled.weekends_at", undefined, { time: timeLabel });
}
const labels: Record<number, string> = {
0: "Sun",
1: "Mon",
2: "Tue",
3: "Wed",
4: "Thu",
5: "Fri",
6: "Sat",
0: t("scheduled.day_sun"),
1: t("scheduled.day_mon"),
2: t("scheduled.day_tue"),
3: t("scheduled.day_wed"),
4: t("scheduled.day_thu"),
5: t("scheduled.day_fri"),
6: t("scheduled.day_sat"),
};
const list = Array.from(normalized)
@@ -243,7 +244,7 @@ const humanizeCron = (cron: string) => {
.map((d) => labels[d] ?? String(d))
.join(", ");
return list ? `${list} at ${timeLabel}` : `At ${timeLabel}`;
return list ? t("scheduled.days_at", undefined, { days: list, time: timeLabel }) : t("scheduled.at_time", undefined, { time: timeLabel });
};
const buildCronFromDaily = (timeValue: string, days: string[]) => {
@@ -276,19 +277,20 @@ const taskSummary = (job: ScheduledJob) => {
return `${run.command}${args}`;
}
const prompt = run?.prompt ?? job.prompt;
return prompt?.trim() || "No prompt or command configured yet.";
return prompt?.trim() || t("scheduled.task_summary_no_prompt");
};
const toRelative = (value?: string | null) => {
if (!value) return "Never";
if (!value) return t("scheduled.never");
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) return "Never";
if (!Number.isFinite(parsed)) return t("scheduled.never");
return formatRelativeTime(parsed);
};
const templateScheduleLabel = (template: AutomationTemplate) => {
if (template.scheduleMode === "interval") {
return `Every ${template.intervalHours ?? DEFAULT_INTERVAL_HOURS} hours`;
const interval = template.intervalHours ?? DEFAULT_INTERVAL_HOURS;
return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval });
}
return humanizeCron(
buildCronFromDaily(
@@ -299,10 +301,10 @@ const templateScheduleLabel = (template: AutomationTemplate) => {
};
const statusLabel = (status?: string | null) => {
if (!status) return "Not run yet";
if (status === "running") return "Running";
if (status === "success") return "Healthy";
if (status === "failed") return "Needs attention";
if (!status) return t("scheduled.not_run_yet");
if (status === "running") return t("scheduled.running_status");
if (status === "success") return t("scheduled.success_status");
if (status === "failed") return t("scheduled.failed_status");
return status;
};
@@ -344,10 +346,10 @@ const TemplateCard = (props: {
</div>
<div class="flex items-center justify-between gap-3 border-t border-dls-border pt-4">
<span class={tagClass}>Template</span>
<span class={tagClass}>{t("scheduled.template_badge")}</span>
<button type="button" class={pillPrimaryClass} onClick={props.onUse} disabled={props.disabled}>
<Sparkles size={14} />
Use template
{t("scheduled.explore_more")}
</button>
</div>
</div>
@@ -387,22 +389,22 @@ const JobCard = (props: {
</Show>
</div>
<div class="mt-3 flex flex-wrap items-center gap-4 text-[12px] text-dls-secondary">
<div>Last run {toRelative(props.job.lastRunAt)}</div>
<div>Created {toRelative(props.job.createdAt)}</div>
<div>{t("scheduled.last_run_prefix")} {toRelative(props.job.lastRunAt)}</div>
<div>{t("scheduled.created_prefix")} {toRelative(props.job.createdAt)}</div>
</div>
</div>
</div>
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-dls-border pt-4">
<span class={tagClass}>Scheduled</span>
<span class={tagClass}>{t("scheduled.filter_scheduled")}</span>
<div class="flex flex-wrap gap-2">
<button type="button" class={pillSecondaryClass} onClick={props.onRun} disabled={props.busy}>
<Play size={14} />
Run in chat
{t("scheduled.run_label")}
</button>
<button type="button" class={pillGhostClass} onClick={props.onDelete} disabled={props.busy}>
<Trash2 size={14} />
Remove
{t("scheduled.delete_label")}
</button>
</div>
</div>
@@ -425,7 +427,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
const [createModalOpen, setCreateModalOpen] = createSignal(false);
const [createBusy, setCreateBusy] = createSignal(false);
const [createError, setCreateError] = createSignal<string | null>(null);
const [automationName, setAutomationName] = createSignal(DEFAULT_AUTOMATION_NAME);
const [automationName, setAutomationName] = createSignal(DEFAULT_AUTOMATION_NAME());
const [automationPrompt, setAutomationPrompt] = createSignal(DEFAULT_AUTOMATION_PROMPT);
const [scheduleMode, setScheduleMode] = createSignal<ScheduleMode>("daily");
const [scheduleTime, setScheduleTime] = createSignal(DEFAULT_SCHEDULE_TIME);
@@ -444,7 +446,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
};
const resetDraft = (template?: AutomationTemplate) => {
setAutomationName(template?.name ?? DEFAULT_AUTOMATION_NAME);
setAutomationName(template?.name ?? DEFAULT_AUTOMATION_NAME());
setAutomationPrompt(template?.prompt ?? DEFAULT_AUTOMATION_PROMPT);
setScheduleMode(template?.scheduleMode ?? "daily");
setScheduleTime(template?.scheduleTime ?? DEFAULT_SCHEDULE_TIME);
@@ -469,25 +471,25 @@ export default function AutomationsView(props: AutomationsViewProps) {
);
const sourceLabel = createMemo(() =>
automations.jobsSource() === "remote" ? "OpenWork server" : "Local scheduler",
automations.jobsSource() === "remote" ? t("scheduled.source_remote") : t("scheduled.source_local"),
);
const sourceDescription = createMemo(() =>
automations.jobsSource() === "remote"
? "Scheduled tasks that are currently synced from the connected OpenWork server."
: "Scheduled tasks that are currently registered on this device through the local scheduler.",
? t("scheduled.subtitle_remote")
: t("scheduled.subtitle_local"),
);
const supportNote = createMemo(() => {
if (automations.jobsSource() === "remote") return null;
if (!isTauriRuntime()) return "Automations require the desktop app or a connected OpenWork server.";
if (!isTauriRuntime()) return t("scheduled.desktop_required");
if (!props.schedulerInstalled || schedulerInstallRequested()) return null;
return null;
});
const lastUpdatedLabel = createMemo(() => {
lastUpdatedNow();
if (!automations.jobsUpdatedAt()) return "Not synced yet";
if (!automations.jobsUpdatedAt()) return t("scheduled.not_synced_yet");
return formatRelativeTime(automations.jobsUpdatedAt() as number);
});
@@ -548,7 +550,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
setSchedulerInstallRequested(true);
try {
await Promise.resolve(props.addPlugin("opencode-scheduler"));
showToast("Scheduler install requested.", "success");
showToast(t("scheduled.scheduler_install_requested"), "success");
} finally {
setInstallingScheduler(false);
}
@@ -590,10 +592,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
try {
await Promise.resolve(props.createSessionAndOpen(plan.prompt));
setCreateModalOpen(false);
showToast("Prepared automation in chat.", "success");
showToast(t("scheduled.prepared_automation_in_chat"), "success");
} catch (error) {
setCreateError(
error instanceof Error ? error.message : "Failed to prepare automation in chat.",
error instanceof Error ? error.message : t("scheduled.prepare_error_fallback"),
);
} finally {
setCreateBusy(false);
@@ -608,7 +610,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
return;
}
await Promise.resolve(props.createSessionAndOpen(plan.prompt));
showToast(`Prepared ${job.name} in chat.`, "success");
showToast(t("scheduled.prepared_job_in_chat", undefined, { name: job.name }), "success");
};
const confirmDelete = async () => {
@@ -619,10 +621,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
try {
await automations.remove(target.slug);
setDeleteTarget(null);
showToast(`Removed ${target.name}.`, "success");
showToast(t("scheduled.removed_job", undefined, { name: target.name }), "success");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setDeleteError(message || "Failed to delete automation.");
setDeleteError(message || t("scheduled.delete_error_fallback"));
} finally {
setDeleteBusy(false);
}
@@ -646,9 +648,9 @@ export default function AutomationsView(props: AutomationsViewProps) {
const jobsEmptyMessage = createMemo(() => {
const query = searchQuery().trim();
if (query) return `No automations match \"${query}\".`;
if (schedulerGateActive()) return "Install the scheduler or connect to an OpenWork server to start creating automations.";
return "No automations yet. Start with a template or prepare one in chat.";
if (query) return t("scheduled.no_automations_match", undefined, { query });
if (schedulerGateActive()) return t("scheduled.install_scheduler_hint");
return t("scheduled.empty_hint");
});
return (
@@ -657,21 +659,21 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0">
<Show when={props.showHeader !== false}>
<h2 class={pageTitleClass}>Automations</h2>
<h2 class={pageTitleClass}>{t("scheduled.title")}</h2>
</Show>
<p class="mt-2 max-w-2xl text-[14px] leading-relaxed text-dls-secondary">
Schedule recurring tasks for this worker, monitor what is already registered, and start from a reusable template.
{t("scheduled.page_description")}
</p>
</div>
<div class="flex flex-wrap gap-3 lg:justify-end">
<button type="button" onClick={openSchedulerDocs} class={pillSecondaryClass}>
<PlugZap size={14} />
Scheduler docs
{t("scheduled.view_scheduler_docs")}
</button>
<button type="button" onClick={refreshJobs} disabled={props.busy} class={pillSecondaryClass}>
<RefreshCw size={14} />
Refresh
{props.busy ? t("scheduled.refreshing") : t("common.refresh")}
</button>
<button
type="button"
@@ -680,7 +682,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
class={pillPrimaryClass}
>
<Plus size={14} />
New automation
{t("scheduled.new_automation")}
</button>
</div>
</div>
@@ -692,7 +694,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
type="text"
value={searchQuery()}
onInput={(event) => setSearchQuery(event.currentTarget.value)}
placeholder="Search automations or templates"
placeholder={t("scheduled.search_placeholder")}
class="w-full rounded-xl border border-dls-border bg-dls-surface py-3 pl-11 pr-4 text-[14px] text-dls-text focus:outline-none focus:ring-2 focus:ring-[rgba(var(--dls-accent-rgb),0.12)]"
/>
</div>
@@ -706,10 +708,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
class={activeFilter() === filter ? pillPrimaryClass : pillGhostClass}
>
{filter === "all"
? "All"
? t("scheduled.filter_all")
: filter === "scheduled"
? "Scheduled"
: "Templates"}
? t("scheduled.filter_scheduled")
: t("scheduled.filter_templates")}
</button>
)}
</For>
@@ -727,13 +729,13 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div>
<div class="text-[15px] font-medium tracking-[-0.2px] text-dls-text">
{props.schedulerInstalled
? "Reload OpenWork to activate automations"
: "Install the scheduler to unlock automations"}
? t("scheduled.reload_activate_title")
: t("scheduled.install_scheduler_title")}
</div>
<p class="mt-1 text-[13px] leading-relaxed text-dls-secondary">
{props.schedulerInstalled
? "OpenCode loads plugins at startup. Reload OpenWork to activate opencode-scheduler for this workspace."
: "Automations run through the opencode-scheduler plugin today. Add it to this workspace to unlock local scheduling."}
? t("scheduled.reload_activate_hint")
: t("scheduled.install_scheduler_hint")}
</p>
</div>
</div>
@@ -745,7 +747,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
class={pillSecondaryClass}
>
<Plus size={14} />
{installingScheduler() ? "Installing" : "Install scheduler"}
{installingScheduler() ? t("scheduled.installing") : t("scheduled.install_scheduler")}
</button>
<button
type="button"
@@ -754,7 +756,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
class={pillSecondaryClass}
>
<RefreshCw size={14} />
{props.reloadBusy ? "Reloading" : "Reload OpenWork"}
{props.reloadBusy ? t("scheduled.reloading") : t("scheduled.reload_openwork")}
</button>
</div>
</div>
@@ -783,11 +785,11 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="space-y-4">
<div class="flex items-end justify-between gap-3">
<div>
<h3 class={sectionTitleClass}>Your automations</h3>
<h3 class={sectionTitleClass}>{t("scheduled.your_automations")}</h3>
<p class="mt-1 text-[13px] text-dls-secondary">{sourceDescription()}</p>
</div>
<div class="text-[12px] text-dls-secondary">
{sourceLabel()} · synced {lastUpdatedLabel()}
{sourceLabel()} · {t("scheduled.last_updated_prefix")} {lastUpdatedLabel()}
</div>
</div>
@@ -822,19 +824,19 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="space-y-4">
<div class="flex items-end justify-between gap-3">
<div>
<h3 class={sectionTitleClass}>Quick start templates</h3>
<h3 class={sectionTitleClass}>{t("scheduled.quick_start_templates")}</h3>
<p class="mt-1 text-[13px] text-dls-secondary">
Start from a proven recurring workflow, then tailor the prompt before you prepare it in chat.
{t("scheduled.quick_start_templates_desc")}
</p>
</div>
<div class="text-[12px] text-dls-secondary">{filteredTemplates().length} templates</div>
<div class="text-[12px] text-dls-secondary">{t("scheduled.template_count", undefined, { count: filteredTemplates().length })}</div>
</div>
<Show
when={filteredTemplates().length}
fallback={
<div class="rounded-[20px] border border-dashed border-dls-border bg-dls-surface px-5 py-8 text-[14px] text-dls-secondary">
No templates match this search.
{t("scheduled.no_templates_match")}
</div>
}
>
@@ -860,9 +862,9 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="bg-dls-surface border border-dls-border w-full max-w-md rounded-2xl shadow-2xl overflow-hidden">
<div class="p-6 space-y-4">
<div>
<h3 class="text-lg font-semibold text-dls-text">Remove automation?</h3>
<h3 class="text-lg font-semibold text-dls-text">{t("scheduled.delete_confirm_title")}</h3>
<p class="mt-1 text-sm text-dls-secondary">
This removes the schedule and deletes the job definition from {sourceLabel().toLowerCase()}.
{t("scheduled.delete_confirm_desc", undefined, { source: sourceLabel().toLowerCase() })}
</p>
</div>
@@ -872,10 +874,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="flex justify-end gap-2">
<button type="button" class={pillGhostClass} onClick={() => setDeleteTarget(null)} disabled={deleteBusy()}>
Cancel
{t("common.cancel")}
</button>
<button type="button" class={pillPrimaryClass} onClick={() => void confirmDelete()} disabled={deleteBusy()}>
{deleteBusy() ? "Removing…" : "Remove"}
{deleteBusy() ? t("scheduled.deleting") : t("scheduled.delete_label")}
</button>
</div>
</div>
@@ -888,9 +890,9 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="w-full max-w-2xl rounded-2xl border border-dls-border bg-dls-surface shadow-2xl overflow-hidden">
<div class="px-5 py-4 border-b border-dls-border flex items-center justify-between gap-3">
<div>
<div class="text-sm font-semibold text-dls-text">Create automation</div>
<div class="text-sm font-semibold text-dls-text">{t("scheduled.create_title")}</div>
<p class="mt-1 text-xs text-dls-secondary">
The form is ready for direct writes. For now, OpenWork prepares the scheduler command in chat for you.
{t("scheduled.create_desc")}
</p>
</div>
<button
@@ -904,7 +906,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="p-5 space-y-5">
<div class="space-y-1.5">
<label class="text-[13px] font-medium text-dls-text">Name</label>
<label class="text-[13px] font-medium text-dls-text">{t("scheduled.name_label")}</label>
<input
type="text"
value={automationName()}
@@ -914,7 +916,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
</div>
<div class="space-y-1.5">
<label class="text-[13px] font-medium text-dls-text">Prompt</label>
<label class="text-[13px] font-medium text-dls-text">{t("scheduled.task_summary_prompt")}</label>
<textarea
rows={4}
value={automationPrompt()}
@@ -925,21 +927,21 @@ export default function AutomationsView(props: AutomationsViewProps) {
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<label class="text-[13px] font-medium text-dls-text">Schedule</label>
<label class="text-[13px] font-medium text-dls-text">{t("scheduled.schedule_label")}</label>
<div class="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setScheduleMode("daily")}
class={scheduleMode() === "daily" ? pillPrimaryClass : pillGhostClass}
>
Daily
{t("scheduled.daily_mode")}
</button>
<button
type="button"
onClick={() => setScheduleMode("interval")}
class={scheduleMode() === "interval" ? pillPrimaryClass : pillGhostClass}
>
Interval
{t("scheduled.interval_mode")}
</button>
</div>
</div>
@@ -948,7 +950,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
when={scheduleMode() === "daily"}
fallback={
<div class="flex flex-wrap items-center gap-3 rounded-[20px] border border-dls-border bg-dls-hover p-4">
<div class="text-[13px] text-dls-secondary">Every</div>
<div class="text-[13px] text-dls-secondary">{t("scheduled.every_prefix")}</div>
<input
type="number"
min={1}
@@ -957,7 +959,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
onInput={(event) => updateIntervalHours(event.currentTarget.value)}
class="w-20 rounded-xl border border-dls-border bg-dls-surface px-3 py-2 text-[14px] text-dls-text focus:outline-none"
/>
<div class="text-[13px] text-dls-secondary">hours</div>
<div class="text-[13px] text-dls-secondary">{t("scheduled.hours_suffix")}</div>
</div>
}
>
@@ -982,7 +984,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
onClick={() => toggleDay(day.id)}
class={scheduleDays().includes(day.id) ? pillPrimaryClass : pillGhostClass}
>
{day.label}
{day.label()}
</button>
)}
</For>
@@ -1006,10 +1008,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
</div>
<div class="px-5 py-4 border-t border-dls-border flex items-center justify-between gap-3">
<div class="text-[12px] text-dls-secondary">Worker root is inferred from the selected workspace.</div>
<div class="text-[12px] text-dls-secondary">{t("scheduled.worker_root_hint")}</div>
<div class="flex items-center gap-2">
<button type="button" class={pillGhostClass} onClick={closeCreateModal} disabled={createBusy()}>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@@ -1017,7 +1019,7 @@ export default function AutomationsView(props: AutomationsViewProps) {
onClick={() => void handleCreateAutomation()}
disabled={createBusy() || automationDisabled()}
>
{createBusy() ? "Preparing…" : "Prepare in chat"}
{createBusy() ? t("scheduled.create_button") : t("scheduled.create_button")}
</button>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-j
import { readDevLogs } from "../lib/dev-log";
import { isTauriRuntime } from "../utils";
import { readPerfLogs } from "../lib/perf-log";
import { t } from "../../i18n";
import Button from "../components/button";
import TextInput from "../components/text-input";
@@ -69,11 +70,11 @@ export default function ConfigView(props: ConfigViewProps) {
const openworkStatusLabel = createMemo(() => {
switch (props.openworkServerStatus) {
case "connected":
return "Connected";
return t("config.status_connected");
case "limited":
return "Limited";
return t("config.status_limited");
default:
return "Not connected";
return t("config.status_not_connected");
}
});
@@ -89,14 +90,14 @@ export default function ConfigView(props: ConfigViewProps) {
});
const reloadAvailabilityReason = createMemo(() => {
if (!props.clientConnected) return "Connect to this worker to reload.";
if (!props.clientConnected) return t("config.reload_connect_hint");
if (!props.canReloadWorkspace) {
return "Reloading is only available for local workers or connected OpenWork servers.";
return t("config.reload_availability_hint");
}
return null;
});
const reloadButtonLabel = createMemo(() => (props.reloadBusy ? "Reloading..." : "Reload engine"));
const reloadButtonLabel = createMemo(() => (props.reloadBusy ? t("config.reloading") : t("config.reload_engine")));
const reloadButtonTone = createMemo(() => (props.anyActiveRuns ? "danger" : "secondary"));
const reloadButtonDisabled = createMemo(() => props.reloadBusy || Boolean(reloadAvailabilityReason()));
@@ -129,8 +130,8 @@ export default function ConfigView(props: ConfigViewProps) {
() => hostInfo()?.remoteAccessEnabled === true,
);
const hostStatusLabel = createMemo(() => {
if (!hostInfo()?.running) return "Offline";
return hostRemoteAccessEnabled() ? "Remote enabled" : "Local only";
if (!hostInfo()?.running) return t("config.host_offline");
return hostRemoteAccessEnabled() ? t("config.host_remote_enabled") : t("config.host_local_only");
});
const hostStatusStyle = createMemo(() => {
if (!hostInfo()?.running) return "bg-gray-4/60 text-gray-11 border-gray-7/50";
@@ -226,29 +227,29 @@ export default function ConfigView(props: ConfigViewProps) {
return (
<section class="space-y-6">
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-2">
<div class="text-sm font-medium text-gray-12">Workspace config</div>
<div class="text-sm font-medium text-gray-12">{t("config.workspace_config_title")}</div>
<div class="text-xs text-gray-10">
These settings affect the selected workspace. Runtime-only actions apply to whichever workspace is currently connected.
{t("config.workspace_config_desc")}
</div>
<Show when={props.runtimeWorkspaceId}>
<div class="text-[11px] text-gray-7 font-mono truncate">
Workspace: {props.runtimeWorkspaceId}
{t("config.workspace_id_prefix")}{props.runtimeWorkspaceId}
</div>
</Show>
</div>
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
<div>
<div class="text-sm font-medium text-gray-12">Engine reload</div>
<div class="text-xs text-gray-10">Restart the OpenCode server for this workspace.</div>
<div class="text-sm font-medium text-gray-12">{t("config.engine_reload_title")}</div>
<div class="text-xs text-gray-10">{t("config.engine_reload_desc")}</div>
</div>
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0 space-y-1">
<div class="text-sm text-gray-12">Reload now</div>
<div class="text-xs text-gray-7">Applies config updates and reconnects your session.</div>
<div class="text-sm text-gray-12">{t("config.reload_now_title")}</div>
<div class="text-xs text-gray-7">{t("config.reload_now_desc")}</div>
<Show when={props.anyActiveRuns}>
<div class="text-[11px] text-amber-11">Reloading will stop active tasks.</div>
<div class="text-[11px] text-amber-11">{t("config.reload_active_tasks_warning")}</div>
</Show>
<Show when={props.reloadError}>
<div class="text-[11px] text-red-11">{props.reloadError}</div>
@@ -270,10 +271,10 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0 space-y-1">
<div class="text-sm text-gray-12">Auto reload (local)</div>
<div class="text-xs text-gray-7">Reload automatically after agents/skills/commands/config change (only when idle).</div>
<div class="text-sm text-gray-12">{t("config.auto_reload_title")}</div>
<div class="text-xs text-gray-7">{t("config.auto_reload_desc")}</div>
<Show when={!props.workspaceAutoReloadAvailable}>
<div class="text-[11px] text-gray-9">Available for local workspaces in the desktop app.</div>
<div class="text-[11px] text-gray-9">{t("config.auto_reload_unavailable")}</div>
</Show>
</div>
<Button
@@ -282,15 +283,15 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => props.setWorkspaceAutoReloadEnabled(!props.workspaceAutoReloadEnabled)}
disabled={props.busy || !props.workspaceAutoReloadAvailable}
>
{props.workspaceAutoReloadEnabled ? "On" : "Off"}
{props.workspaceAutoReloadEnabled ? t("common.on") : t("common.off")}
</Button>
</div>
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0 space-y-1">
<div class="text-sm text-gray-12">Resume sessions after auto reload</div>
<div class="text-sm text-gray-12">{t("config.resume_sessions_title")}</div>
<div class="text-xs text-gray-7">
If a reload was queued while tasks were running, send a resume message afterward.
{t("config.resume_sessions_desc")}
</div>
</div>
<Button
@@ -302,9 +303,9 @@ export default function ConfigView(props: ConfigViewProps) {
!props.workspaceAutoReloadAvailable ||
!props.workspaceAutoReloadEnabled
}
title={props.workspaceAutoReloadEnabled ? "" : "Enable auto reload first"}
title={props.workspaceAutoReloadEnabled ? "" : t("config.enable_auto_reload_first")}
>
{props.workspaceAutoReloadResumeEnabled ? "On" : "Off"}
{props.workspaceAutoReloadResumeEnabled ? t("common.on") : t("common.off")}
</Button>
</div>
</div>
@@ -313,8 +314,8 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-3">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-sm font-medium text-gray-12">Diagnostics bundle</div>
<div class="text-xs text-gray-10">Copy sanitized runtime state for debugging.</div>
<div class="text-sm font-medium text-gray-12">{t("config.diagnostics_title")}</div>
<div class="text-xs text-gray-10">{t("config.diagnostics_desc")}</div>
</div>
<Button
variant="secondary"
@@ -322,7 +323,7 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => void handleCopy(diagnosticsBundleJson(), "debug-bundle")}
disabled={props.busy}
>
{copyingField() === "debug-bundle" ? "Copied" : "Copy"}
{copyingField() === "debug-bundle" ? t("config.copied") : t("config.copy")}
</Button>
</div>
<pre class="text-xs text-gray-12 whitespace-pre-wrap break-words max-h-64 overflow-auto bg-gray-1/20 border border-gray-6 rounded-xl p-3">
@@ -335,9 +336,9 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-sm font-medium text-gray-12">OpenWork server sharing</div>
<div class="text-sm font-medium text-gray-12">{t("config.server_sharing_title")}</div>
<div class="text-xs text-gray-10">
Share these details with a trusted device. Keep the server on the same network for the fastest setup.
{t("config.server_sharing_desc")}
</div>
</div>
<div class={`text-xs px-2 py-1 rounded-full border ${hostStatusStyle()}`}>
@@ -348,15 +349,15 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="grid gap-3">
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0">
<div class="text-xs font-medium text-gray-11">OpenWork Server URL</div>
<div class="text-xs text-gray-7 font-mono truncate">{hostConnectUrl() || "Starting server"}</div>
<div class="text-xs font-medium text-gray-11">{t("config.server_url_label")}</div>
<div class="text-xs text-gray-7 font-mono truncate">{hostConnectUrl() || t("config.starting_server")}</div>
<Show when={hostConnectUrl()}>
<div class="text-[11px] text-gray-8 mt-1">
{!hostRemoteAccessEnabled()
? "Remote access is off. Use Share workspace to enable it before connecting from another machine."
? t("config.remote_access_off_hint")
: hostConnectUrlUsesMdns()
? ".local names are easier to remember but may not resolve on all networks."
: "Use your local IP on the same Wi-Fi for the fastest connection."}
? t("config.mdns_hint")
: t("config.local_ip_hint")}
</div>
</Show>
</div>
@@ -366,13 +367,13 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => handleCopy(hostConnectUrl(), "host-url")}
disabled={!hostConnectUrl()}
>
{copyingField() === "host-url" ? "Copied" : "Copy"}
{copyingField() === "host-url" ? t("config.copied") : t("config.copy")}
</Button>
</div>
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0">
<div class="text-xs font-medium text-gray-11">Collaborator token</div>
<div class="text-xs font-medium text-gray-11">{t("config.collaborator_token_label")}</div>
<div class="text-xs text-gray-7 font-mono truncate">
{clientTokenVisible()
? hostInfo()?.clientToken || "—"
@@ -382,8 +383,8 @@ export default function ConfigView(props: ConfigViewProps) {
</div>
<div class="text-[11px] text-gray-8 mt-1">
{hostRemoteAccessEnabled()
? "Routine remote access for phones or laptops connecting to this server."
: "Stored in advance for remote sharing, but remote access is currently disabled."}
? t("config.collaborator_token_remote_hint")
: t("config.collaborator_token_disabled_hint")}
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
@@ -393,7 +394,7 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => setClientTokenVisible((prev) => !prev)}
disabled={!hostInfo()?.clientToken}
>
{clientTokenVisible() ? "Hide" : "Show"}
{clientTokenVisible() ? t("common.hide") : t("common.show")}
</Button>
<Button
variant="outline"
@@ -401,14 +402,14 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => handleCopy(hostInfo()?.clientToken ?? "", "client-token")}
disabled={!hostInfo()?.clientToken}
>
{copyingField() === "client-token" ? "Copied" : "Copy"}
{copyingField() === "client-token" ? t("config.copied") : t("config.copy")}
</Button>
</div>
</div>
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0">
<div class="text-xs font-medium text-gray-11">Owner token</div>
<div class="text-xs font-medium text-gray-11">{t("config.owner_token_label")}</div>
<div class="text-xs text-gray-7 font-mono truncate">
{ownerTokenVisible()
? hostInfo()?.ownerToken || "—"
@@ -418,8 +419,8 @@ export default function ConfigView(props: ConfigViewProps) {
</div>
<div class="text-[11px] text-gray-8 mt-1">
{hostRemoteAccessEnabled()
? "Use this when a remote client needs to answer permission prompts or take owner-only actions."
: "Only relevant after you enable remote access for this worker."}
? t("config.owner_token_remote_hint")
: t("config.owner_token_disabled_hint")}
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
@@ -429,7 +430,7 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => setOwnerTokenVisible((prev) => !prev)}
disabled={!hostInfo()?.ownerToken}
>
{ownerTokenVisible() ? "Hide" : "Show"}
{ownerTokenVisible() ? t("common.hide") : t("common.show")}
</Button>
<Button
variant="outline"
@@ -437,14 +438,14 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => handleCopy(hostInfo()?.ownerToken ?? "", "owner-token")}
disabled={!hostInfo()?.ownerToken}
>
{copyingField() === "owner-token" ? "Copied" : "Copy"}
{copyingField() === "owner-token" ? t("config.copied") : t("config.copy")}
</Button>
</div>
</div>
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0">
<div class="text-xs font-medium text-gray-11">Host admin token</div>
<div class="text-xs font-medium text-gray-11">{t("config.host_admin_token_label")}</div>
<div class="text-xs text-gray-7 font-mono truncate">
{hostTokenVisible()
? hostInfo()?.hostToken || "—"
@@ -452,7 +453,7 @@ export default function ConfigView(props: ConfigViewProps) {
? "••••••••••••"
: "—"}
</div>
<div class="text-[11px] text-gray-8 mt-1">Internal host-only token for approvals CLI and admin APIs. Do not use this in the remote app connect flow.</div>
<div class="text-[11px] text-gray-8 mt-1">{t("config.host_admin_token_hint")}</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<Button
@@ -461,7 +462,7 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => setHostTokenVisible((prev) => !prev)}
disabled={!hostInfo()?.hostToken}
>
{hostTokenVisible() ? "Hide" : "Show"}
{hostTokenVisible() ? t("common.hide") : t("common.show")}
</Button>
<Button
variant="outline"
@@ -469,14 +470,14 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => handleCopy(hostInfo()?.hostToken ?? "", "host-token")}
disabled={!hostInfo()?.hostToken}
>
{copyingField() === "host-token" ? "Copied" : "Copy"}
{copyingField() === "host-token" ? t("config.copied") : t("config.copy")}
</Button>
</div>
</div>
</div>
<div class="text-xs text-gray-9">
For per-workspace sharing links, use <span class="font-medium">Share...</span> in the workspace menu.
{t("config.server_sharing_menu_hint")}
</div>
</div>
</Show>
@@ -484,9 +485,9 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-sm font-medium text-gray-12">OpenWork server</div>
<div class="text-sm font-medium text-gray-12">{t("config.server_section_title")}</div>
<div class="text-xs text-gray-10">
Connect to an OpenWork server. Use the URL plus a collaborator or owner token from your server admin.
{t("config.server_section_desc")}
</div>
</div>
<div class={`text-xs px-2 py-1 rounded-full border ${openworkStatusStyle()}`}>{openworkStatusLabel()}</div>
@@ -494,22 +495,22 @@ export default function ConfigView(props: ConfigViewProps) {
<div class="grid gap-3">
<TextInput
label="OpenWork server URL"
label={t("config.server_url_input_label")}
value={openworkUrl()}
onInput={(event) => setOpenworkUrl(event.currentTarget.value)}
placeholder="http://127.0.0.1:<port>"
hint="Use the URL shared by your OpenWork server. Local desktop workers reuse a persistent high port in the 48000-51000 range."
hint={t("config.server_url_hint")}
disabled={props.busy}
/>
<label class="block">
<div class="mb-1 text-xs font-medium text-gray-11">Collaborator or owner token</div>
<div class="mb-1 text-xs font-medium text-gray-11">{t("config.token_label")}</div>
<div class="flex items-center gap-2">
<input
type={openworkTokenVisible() ? "text" : "password"}
value={openworkToken()}
onInput={(event) => setOpenworkToken(event.currentTarget.value)}
placeholder="Paste your token"
placeholder={t("config.token_placeholder")}
disabled={props.busy}
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
/>
@@ -519,16 +520,16 @@ export default function ConfigView(props: ConfigViewProps) {
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
disabled={props.busy}
>
{openworkTokenVisible() ? "Hide" : "Show"}
{openworkTokenVisible() ? t("common.hide") : t("common.show")}
</Button>
</div>
<div class="mt-1 text-xs text-gray-10">Optional. Paste a collaborator token for routine access or an owner token when this client must answer permission prompts.</div>
<div class="mt-1 text-xs text-gray-10">{t("config.token_hint")}</div>
</label>
</div>
<div class="space-y-1">
<div class="text-[11px] text-gray-7 font-mono truncate">Resolved worker URL: {resolvedWorkspaceUrl() || "Not set"}</div>
<div class="text-[11px] text-gray-8 font-mono truncate">Worker ID: {resolvedWorkspaceId() || "Unavailable"}</div>
<div class="text-[11px] text-gray-7 font-mono truncate">{t("config.resolved_worker_url")}{resolvedWorkspaceUrl() || t("config.not_set")}</div>
<div class="text-[11px] text-gray-8 font-mono truncate">{t("config.worker_id")}{resolvedWorkspaceId() || t("config.unavailable")}</div>
</div>
<div class="flex flex-wrap gap-2">
@@ -544,27 +545,27 @@ export default function ConfigView(props: ConfigViewProps) {
const ok = await props.testOpenworkServerConnection(next);
setOpenworkTestState(ok ? "success" : "error");
setOpenworkTestMessage(
ok ? "Connection successful." : "Connection failed. Check the host URL and token.",
ok ? t("config.connection_successful") : t("config.connection_failed"),
);
} catch (error) {
const message = error instanceof Error ? error.message : "Connection failed.";
const message = error instanceof Error ? error.message : t("config.connection_failed_check");
setOpenworkTestState("error");
setOpenworkTestMessage(message);
}
}}
disabled={props.busy || openworkTestState() === "testing"}
>
{openworkTestState() === "testing" ? "Testing..." : "Test connection"}
{openworkTestState() === "testing" ? t("config.testing") : t("config.test_connection")}
</Button>
<Button
variant="outline"
onClick={() => props.updateOpenworkServerSettings(buildOpenworkSettings())}
disabled={props.busy || !hasOpenworkChanges()}
>
Save
{t("common.save")}
</Button>
<Button variant="ghost" onClick={props.resetOpenworkServerSettings} disabled={props.busy}>
Reset
{t("common.reset")}
</Button>
</div>
@@ -580,25 +581,25 @@ export default function ConfigView(props: ConfigViewProps) {
role="status"
aria-live="polite"
>
{openworkTestState() === "testing" ? "Testing connection..." : openworkTestMessage() ?? "Connection status updated."}
{openworkTestState() === "testing" ? t("config.testing_connection") : openworkTestMessage() ?? t("config.connection_status_updated")}
</div>
</Show>
<Show when={openworkStatusLabel() !== "Connected"}>
<div class="text-xs text-gray-9">OpenWork server connection needed to sync skills, plugins, and commands.</div>
<Show when={openworkStatusLabel() !== t("config.status_connected")}>
<div class="text-xs text-gray-9">{t("config.server_needed_hint")}</div>
</Show>
</div>
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-2">
<div class="text-sm font-medium text-gray-12">Messaging identities</div>
<div class="text-sm font-medium text-gray-12">{t("config.messaging_identities_title")}</div>
<div class="text-xs text-gray-10">
Manage Telegram/Slack identities and routing in the <span class="font-medium text-gray-12">Identities</span> tab.
{t("config.messaging_identities_desc")}
</div>
</div>
<Show when={!isTauriRuntime()}>
<div class="text-xs text-gray-9">
Some config features (local server sharing + messaging bridge) require the desktop app.
{t("config.desktop_only_hint")}
</div>
</Show>
</section>

View File

@@ -7,6 +7,7 @@ import McpView from "../connections/mcp-view";
import { useConnections } from "../connections/provider";
import { useExtensions } from "../extensions/provider";
import PluginsView, { type PluginsViewProps } from "./plugins";
import { t } from "../../i18n";
export type ExtensionsSection = "all" | "mcp" | "plugins";
@@ -64,9 +65,9 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div class="space-y-1">
<Show when={props.showHeader !== false}>
<h2 class="text-3xl font-bold text-dls-text">Extensions</h2>
<h2 class="text-3xl font-bold text-dls-text">{t("extensions.title")}</h2>
<p class="text-sm text-dls-secondary mt-1.5">
Apps (MCP) and OpenCode plugins live in one place.
{t("extensions.subtitle")}
</p>
</Show>
<div class={`${props.showHeader === false ? "" : "mt-3"} flex flex-wrap items-center gap-2`}>
@@ -74,7 +75,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
<div class="inline-flex items-center gap-2 rounded-full bg-green-3 px-3 py-1">
<div class="w-2 h-2 rounded-full bg-green-9" />
<span class="text-xs font-medium text-green-11">
{connectedAppsCount()} app{connectedAppsCount() === 1 ? "" : "s"} connected
{connectedAppsCount()} {connectedAppsCount() === 1 ? t("extensions.app_count_one") : t("extensions.app_count_many")}
</span>
</div>
</Show>
@@ -82,7 +83,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
<div class="inline-flex items-center gap-2 rounded-full bg-gray-3 px-3 py-1">
<Cpu size={14} class="text-gray-11" />
<span class="text-xs font-medium text-gray-11">
{pluginCount()} plugin{pluginCount() === 1 ? "" : "s"}
{pluginCount()} {pluginCount() === 1 ? t("extensions.plugin_count_one") : t("extensions.plugin_count_many")}
</span>
</div>
</Show>
@@ -97,7 +98,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
aria-pressed={section() === "all"}
onClick={() => selectSection("all")}
>
All
{t("extensions.filter_all")}
</button>
<button
type="button"
@@ -106,7 +107,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
onClick={() => selectSection("mcp")}
>
<Box size={14} />
Apps
{t("extensions.filter_apps")}
</button>
<button
type="button"
@@ -115,11 +116,11 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
onClick={() => selectSection("plugins")}
>
<Cpu size={14} />
Plugins
{t("extensions.filter_plugins")}
</button>
</div>
<Button variant="ghost" onClick={refreshAll}>
Refresh
{t("common.refresh")}
</Button>
</div>
</div>
@@ -128,7 +129,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm font-medium text-gray-12">
<Box size={16} class="text-gray-11" />
<span>Apps (MCP)</span>
<span>{t("extensions.apps_mcp_header")}</span>
</div>
<McpView
showHeader={false}
@@ -143,7 +144,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) {
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm font-medium text-gray-12">
<Cpu size={16} class="text-gray-11" />
<span>Plugins (OpenCode)</span>
<span>{t("extensions.plugins_opencode_header")}</span>
</div>
<PluginsView
busy={props.busy}

View File

@@ -9,6 +9,8 @@ import {
Shield,
} from "lucide-solid";
import { t } from "../../i18n";
import Button from "../components/button";
import ConfirmModal from "../components/confirm-modal";
import {
@@ -217,15 +219,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const serverReady = createMemo(() => props.openworkServerStatus === "connected" && Boolean(openworkServerClient()));
const scopedWorkspaceReady = createMemo(() => Boolean(workspaceId()));
const defaultRoutingDirectory = createMemo(() => props.selectedWorkspaceRoot.trim() || "Not set");
const defaultRoutingDirectory = createMemo(() => props.selectedWorkspaceRoot.trim() || t("identities.not_set"));
let lastResetKey = "";
const statusLabel = createMemo(() => {
if (healthError()) return "Unavailable";
if (healthError()) return t("identities.health_unavailable");
const snapshot = health();
if (!snapshot) return "Unknown";
return snapshot.ok ? "Running" : "Offline";
if (!snapshot) return t("identities.health_unknown");
return snapshot.ok ? t("identities.health_running") : t("identities.health_offline");
});
const isWorkerOnline = createMemo(() => {
@@ -266,13 +268,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const ts = lastActivityAt();
if (!ts) return "\u2014";
const elapsedMs = Math.max(0, Date.now() - ts);
if (elapsedMs < 60_000) return "Just now";
if (elapsedMs < 60_000) return t("identities.just_now");
const minutes = Math.floor(elapsedMs / 60_000);
if (minutes < 60) return `${minutes}m ago`;
if (minutes < 60) return t("identities.minutes_ago", undefined, { minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
if (hours < 24) return t("identities.hours_ago", undefined, { hours });
const days = Math.floor(hours / 24);
return `${days}d ago`;
return t("identities.days_ago", undefined, { days });
});
const workspaceAgentStatus = createMemo(() => {
@@ -302,7 +304,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const id = workspaceId();
if (!id) {
resetAgentState();
setAgentError("Worker scope unavailable.");
setAgentError(t("identities.agent_worker_scope_unavailable"));
return;
}
const client = openworkServerClient();
@@ -351,7 +353,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setAgentContent(OPENCODE_ROUTER_AGENT_FILE_TEMPLATE);
setAgentDraft(OPENCODE_ROUTER_AGENT_FILE_TEMPLATE);
setAgentBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null);
setAgentStatus("Created default messaging agent file.");
setAgentStatus(t("identities.agent_created"));
} catch (error) {
setAgentError(formatRequestError(error));
} finally {
@@ -379,10 +381,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setAgentExists(true);
setAgentContent(agentDraft());
setAgentBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null);
setAgentStatus("Saved messaging behavior.");
setAgentStatus(t("identities.agent_saved"));
} catch (error) {
if (error instanceof OpenworkServerError && error.status === 409) {
setAgentError("File changed remotely. Reload and save again.");
setAgentError(t("identities.agent_file_changed"));
} else {
setAgentError(formatRequestError(error));
}
@@ -414,7 +416,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
...(sendAutoBind() ? { autoBind: true } : {}),
});
setSendResult(result);
const base = `Dispatched ${result.sent}/${result.attempted} messages.`;
const base = t("identities.dispatched_messages", undefined, { sent: result.sent, attempted: result.attempted });
setSendStatus(result.reason?.trim() ? `${base} ${result.reason.trim()}` : base);
} catch (error) {
setSendError(formatRequestError(error));
@@ -443,9 +445,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setTelegramBotUsername(null);
setTelegramPairingCode(null);
setSlackIdentities([]);
setHealthError("Worker scope unavailable. Reconnect using a worker URL or switch to a known worker.");
setTelegramIdentitiesError("Worker scope unavailable.");
setSlackIdentitiesError("Worker scope unavailable.");
setHealthError(t("identities.worker_scope_unavailable_detail"));
setTelegramIdentitiesError(t("identities.worker_scope_unavailable"));
setSlackIdentitiesError(t("identities.worker_scope_unavailable"));
resetAgentState();
setSendStatus(null);
setSendError(null);
@@ -491,7 +493,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const message =
(healthRes.json && typeof (healthRes.json as any).message === "string")
? String((healthRes.json as any).message)
: `OpenCodeRouter health unavailable (${healthRes.status})`;
: t("identities.health_unavailable_status", undefined, { status: healthRes.status });
setHealthError(message);
}
setMessagingRestartRequired(true);
@@ -505,14 +507,14 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
} else {
setTelegramIdentities([]);
setTelegramPairingCode(null);
setTelegramIdentitiesError("Telegram identities unavailable.");
setTelegramIdentitiesError(t("identities.telegram_unavailable"));
}
if (isOpenCodeRouterIdentities(slackRes)) {
setSlackIdentities(slackRes.items ?? []);
} else {
setSlackIdentities([]);
setSlackIdentitiesError("Slack identities unavailable.");
setSlackIdentitiesError(t("identities.slack_unavailable"));
}
if (!agentDirty() && !agentSaving()) {
@@ -542,13 +544,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const ok = await props.reconnectOpenworkServer();
if (!ok) {
setReconnectError("Reconnect failed. Check OpenWork URL/token and try again.");
setReconnectError(t("identities.reconnect_failed"));
return;
}
setReconnectStatus("Reconnected. Refreshing worker state...");
setReconnectStatus(t("identities.reconnected_refreshing"));
await refreshAll({ force: true });
setReconnectStatus("Reconnected.");
setReconnectStatus(t("identities.reconnected"));
};
const enableMessagingModule = async () => {
@@ -575,7 +577,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setMessagingRiskOpen(false);
setMessagingRestartAction("enable");
setMessagingRestartPromptOpen(true);
setMessagingStatus("Messaging enabled. Restart this worker to apply before configuring channels.");
setMessagingStatus(t("identities.messaging_enabled_restart"));
await refreshAll({ force: true });
} catch (error) {
setMessagingError(formatRequestError(error));
@@ -608,7 +610,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
setMessagingRestartRequired(true);
setMessagingRestartAction("disable");
setMessagingRestartPromptOpen(true);
setMessagingStatus("Messaging disabled. Restart this worker to stop the messaging sidecar.");
setMessagingStatus(t("identities.messaging_disabled_restart"));
await refreshAll({ force: true });
} catch (error) {
setMessagingError(formatRequestError(error));
@@ -625,14 +627,14 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
try {
const ok = await props.restartLocalServer();
if (!ok) {
setMessagingError("Restart failed. Please restart the worker from Settings and try again.");
setMessagingError(t("identities.restart_failed"));
return;
}
setMessagingRestartPromptOpen(false);
setMessagingRestartRequired(false);
setMessagingStatus("Worker restarted. Refreshing messaging status...");
setMessagingStatus(t("identities.worker_restarted_refreshing"));
await refreshAll({ force: true });
setMessagingStatus("Worker restarted.");
setMessagingStatus(t("identities.worker_restarted"));
} catch (error) {
setMessagingError(formatRequestError(error));
} finally {
@@ -664,7 +666,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const pairingCode = typeof result.telegram?.pairingCode === "string" ? result.telegram.pairingCode.trim() : "";
if (access === "private" && pairingCode) {
setTelegramPairingCode(pairingCode);
setTelegramStatus(`Private bot saved. Pair via /pair ${pairingCode}`);
setTelegramStatus(t("identities.telegram_private_saved_pair", undefined, { code: pairingCode }));
} else {
setTelegramPairingCode(null);
}
@@ -673,15 +675,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
const normalized = String(username).trim().replace(/^@+/, "");
setTelegramBotUsername(normalized || null);
if (access !== "private" || !pairingCode) {
setTelegramStatus(`Saved (@${normalized || String(username)})`);
setTelegramStatus(t("identities.telegram_saved_username", undefined, { username: normalized || String(username) }));
}
} else {
if (access !== "private" || !pairingCode) {
setTelegramStatus(result.applied === false ? "Saved (pending apply)." : "Saved.");
setTelegramStatus(result.applied === false ? t("identities.telegram_saved_pending") : t("identities.telegram_saved"));
}
}
} else {
setTelegramError("Failed to save.");
setTelegramError(t("identities.telegram_save_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setTelegramError(result.applyError.trim());
@@ -712,9 +714,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
if (result.ok) {
setTelegramBotUsername(null);
setTelegramPairingCode(null);
setTelegramStatus(result.applied === false ? "Deleted (pending apply)." : "Deleted.");
setTelegramStatus(result.applied === false ? t("identities.telegram_deleted_pending") : t("identities.telegram_deleted"));
} else {
setTelegramError("Failed to delete.");
setTelegramError(t("identities.telegram_delete_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setTelegramError(result.applyError.trim());
@@ -732,9 +734,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
if (!code) return;
try {
await navigator.clipboard.writeText(code);
setTelegramStatus("Pairing code copied.");
setTelegramStatus(t("identities.pairing_code_copied"));
} catch {
setTelegramError("Could not copy pairing code. Copy it manually.");
setTelegramError(t("identities.pairing_code_copy_failed"));
}
};
@@ -756,9 +758,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
try {
const result = await client.upsertOpenCodeRouterSlackIdentity(id, { botToken, appToken, enabled: slackEnabled() });
if (result.ok) {
setSlackStatus(result.applied === false ? "Saved (pending apply)." : "Saved.");
setSlackStatus(result.applied === false ? t("identities.telegram_saved_pending") : t("identities.telegram_saved"));
} else {
setSlackError("Failed to save.");
setSlackError(t("identities.telegram_save_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setSlackError(result.applyError.trim());
@@ -788,9 +790,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
try {
const result = await client.deleteOpenCodeRouterSlackIdentity(id, identityId);
if (result.ok) {
setSlackStatus(result.applied === false ? "Deleted (pending apply)." : "Deleted.");
setSlackStatus(result.applied === false ? t("identities.telegram_deleted_pending") : t("identities.telegram_deleted"));
} else {
setSlackError("Failed to delete.");
setSlackError(t("identities.telegram_delete_failed"));
}
if (typeof result.applyError === "string" && result.applyError.trim()) {
setSlackError(result.applyError.trim());
@@ -855,7 +857,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div>
<div class="flex items-center justify-between mb-1.5">
<Show when={props.showHeader !== false}>
<h1 class="text-lg font-bold text-gray-12 tracking-tight">Messaging channels</h1>
<h1 class="text-lg font-bold text-gray-12 tracking-tight">{t("identities.title")}</h1>
</Show>
<div class="flex items-center gap-2">
<Button
@@ -865,7 +867,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={props.busy || props.openworkReconnectBusy}
>
<RefreshCcw size={14} class={props.openworkReconnectBusy ? "animate-spin" : ""} />
<span class="ml-1.5">Repair & reconnect</span>
<span class="ml-1.5">{t("identities.repair_reconnect")}</span>
</Button>
<Button
variant="outline"
@@ -874,18 +876,17 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={!serverReady() || refreshing()}
>
<RefreshCcw size={14} class={refreshing() ? "animate-spin" : ""} />
<span class="ml-1.5">Refresh</span>
<span class="ml-1.5">{t("common.refresh")}</span>
</Button>
</div>
</div>
<Show when={props.showHeader !== false}>
<p class="text-sm text-gray-9 leading-relaxed">
Let people reach your worker through messaging apps. Connect a channel and
your worker will automatically read and respond to messages.
{t("identities.subtitle")}
</p>
</Show>
<div class="mt-1.5 text-[11px] text-gray-8 font-mono break-all">
Workspace scope: {scopedOpenworkBaseUrl().trim() || props.openworkServerUrl.trim() || "Not set"}
{t("identities.workspace_scope_prefix")} {scopedOpenworkBaseUrl().trim() || props.openworkServerUrl.trim() || t("identities.not_set")}
</div>
<Show when={reconnectStatus()}>
{(value) => <div class="mt-1 text-[11px] text-gray-9">{value()}</div>}
@@ -904,9 +905,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Not connected to server ---- */}
<Show when={!serverReady()}>
<div class="rounded-xl border border-gray-4 bg-gray-1 p-5">
<div class="text-sm font-semibold text-gray-12">Connect to an OpenWork server</div>
<div class="text-sm font-semibold text-gray-12">{t("identities.connect_server_title")}</div>
<div class="mt-1 text-xs text-gray-10">
Identities are available when you are connected to an OpenWork host (<code class="text-[11px] font-mono bg-gray-3 px-1 py-0.5 rounded">openwork</code>).
{t("identities.connect_server_desc")}
</div>
</div>
</Show>
@@ -914,7 +915,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<Show when={serverReady()}>
<Show when={!scopedWorkspaceReady()}>
<div class="rounded-xl border border-amber-7/20 bg-amber-1/30 px-3 py-2 text-xs text-amber-12">
Workspace ID is required to manage identities. Reconnect with a workspace URL (for example: <code class="text-[11px]">/w/&lt;workspace-id&gt;</code>) or select a workspace mapped on this host.
{t("identities.workspace_id_required")}
</div>
</Show>
@@ -929,7 +930,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
}`}
onClick={() => setActiveTab("general")}
>
General
{t("identities.tab_general")}
</button>
<button
class={`flex-1 rounded-lg px-3 py-2 text-xs font-semibold transition-colors ${
@@ -939,7 +940,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
}`}
onClick={() => setActiveTab("advanced")}
>
Advanced
{t("settings.tab_advanced")}
</button>
</div>
<Button
@@ -948,21 +949,19 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={messagingSaving()}
onClick={() => setMessagingDisableConfirmOpen(true)}
>
Disable messaging
{t("identities.disable_messaging")}
</Button>
</div>
</Show>
<Show when={!messagingEnabled()}>
<div class="rounded-xl border border-gray-4 bg-gray-1 px-4 py-4 space-y-3">
<div class="text-sm font-semibold text-gray-12">Messaging is disabled by default</div>
<div class="text-sm font-semibold text-gray-12">{t("identities.messaging_disabled_title")}</div>
<p class="text-xs text-gray-10 leading-relaxed">
Messaging bots can execute actions against your local worker. If exposed publicly, they may allow access
to files, credentials, and API keys available to this worker.
{t("identities.messaging_disabled_risk")}
</p>
<p class="text-xs text-gray-10 leading-relaxed">
Enable messaging only if you understand the risk and plan to secure access (for example, private Telegram
pairing).
{t("identities.messaging_disabled_hint")}
</p>
<div class="flex flex-wrap items-center gap-2">
<Button
@@ -971,7 +970,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={messagingSaving() || !workspaceId()}
onClick={() => setMessagingRiskOpen(true)}
>
{messagingSaving() ? "Enabling..." : "Enable messaging"}
{messagingSaving() ? t("identities.enabling") : t("identities.enable_messaging")}
</Button>
</div>
</div>
@@ -981,8 +980,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<Show when={messagingRestartRequired()}>
<div class="rounded-xl border border-gray-4 bg-gray-1 px-4 py-3 text-xs text-gray-10 leading-relaxed">
Messaging is enabled in this workspace, but the messaging sidecar is not running yet. Restart this worker,
then return to Messaging settings to connect Telegram or Slack.
{t("identities.messaging_sidecar_not_running")}
<div class="mt-3">
<Button
variant="primary"
@@ -990,7 +988,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={messagingRestartBusy()}
onClick={() => void restartMessagingWorker()}
>
{messagingRestartBusy() ? "Restarting..." : "Restart worker"}
{messagingRestartBusy() ? t("identities.restarting") : t("identities.restart_worker")}
</Button>
</div>
</div>
@@ -1009,7 +1007,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="w-2.5 h-2.5 rounded-full bg-emerald-9 animate-pulse" />
</Show>
<span class="text-[15px] font-semibold text-gray-12">
{isWorkerOnline() ? "Worker online" : healthError() ? "Worker unavailable" : "Worker offline"}
{isWorkerOnline() ? t("identities.worker_online") : healthError() ? t("identities.worker_unavailable") : t("identities.worker_offline")}
</span>
</div>
<span
@@ -1033,17 +1031,17 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="flex gap-3">
<StatusPill
label="Channels"
value={`${connectedChannelCount()} connected`}
label={t("identities.channels_label")}
value={`${connectedChannelCount()} ${t("identities.channels_connected")}`}
ok={connectedChannelCount() > 0}
/>
<StatusPill
label="Messages today"
label={t("identities.messages_today")}
value={messagesToday() == null ? "\u2014" : String(messagesToday())}
ok={(messagesToday() ?? 0) > 0}
/>
<StatusPill
label="Last activity"
label={t("identities.last_activity")}
value={lastActivityLabel()}
ok={Boolean(lastActivityAt())}
/>
@@ -1053,7 +1051,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Available channels ---- */}
<div>
<div class="text-[11px] font-semibold text-gray-9 uppercase tracking-wider mb-3">
Available channels
{t("identities.available_channels")}
</div>
<div class="flex flex-col gap-2.5">
@@ -1077,12 +1075,12 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<span class="text-[15px] font-semibold text-gray-12">Telegram</span>
<Show when={hasTelegramConnected()}>
<span class="rounded-full px-2 py-0.5 text-[10px] font-semibold bg-emerald-1/40 text-emerald-11">
Connected
{t("identities.connected_badge")}
</span>
</Show>
</div>
<div class="text-[13px] text-gray-9 mt-0.5 leading-snug">
Connect a Telegram bot in public mode (open inbox) or private mode (pairing code required).
{t("identities.telegram_desc")}
</div>
</div>
<ChevronRight
@@ -1116,7 +1114,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
</span>
</div>
<div class="text-[11px] text-gray-9 mt-0.5 pl-3.5">
{item.enabled ? "Enabled" : "Disabled"} · {item.running ? "Running" : "Stopped"} · {item.access === "private" ? "Private" : "Public"}
{item.enabled ? t("identities.enabled_label") : t("identities.disabled_label")} · {item.running ? t("identities.running_label") : t("identities.stopped_label")} · {item.access === "private" ? t("identities.private_label") : t("identities.public_label")}
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@@ -1126,7 +1124,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={telegramSaving() || item.id === "env" || !workspaceId()}
onClick={() => void deleteTelegram(item.id)}
>
Disconnect
{t("identities.disconnect")}
</Button>
</div>
</div>
@@ -1137,7 +1135,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* Connected stats summary */}
<div class="flex gap-2.5">
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Status</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.status_label")}</div>
<div class="flex items-center gap-1.5">
<div class={`w-1.5 h-1.5 rounded-full ${
telegramIdentities().some((i) => i.running) ? "bg-emerald-9" : "bg-gray-8"
@@ -1145,18 +1143,18 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<span class={`text-[13px] font-semibold ${
telegramIdentities().some((i) => i.running) ? "text-emerald-11" : "text-gray-10"
}`}>
{telegramIdentities().some((i) => i.running) ? "Active" : "Stopped"}
{telegramIdentities().some((i) => i.running) ? t("identities.status_active") : t("identities.status_stopped")}
</span>
</div>
</div>
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Identities</div>
<div class="text-[13px] font-semibold text-gray-12">{telegramIdentities().length} configured</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.identities_label")}</div>
<div class="text-[13px] font-semibold text-gray-12">{telegramIdentities().length} {t("identities.configured_suffix")}</div>
</div>
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Channel</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.channel_label")}</div>
<div class="text-[13px] font-semibold text-gray-12">
{health()?.channels.telegram ? "On" : "Off"}
{health()?.channels.telegram ? t("common.on") : t("common.off")}
</div>
</div>
</div>
@@ -1173,31 +1171,31 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="space-y-2.5">
<Show when={telegramIdentities().length === 0}>
<div class="rounded-xl border border-gray-4 bg-gray-2/60 px-3.5 py-3 space-y-2.5">
<div class="text-[12px] font-semibold text-gray-12">Quick setup</div>
<div class="text-[12px] font-semibold text-gray-12">{t("identities.quick_setup")}</div>
<ol class="space-y-2 text-[12px] text-gray-10 leading-relaxed">
<li class="flex items-start gap-2">
<span class="mt-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-gray-4 text-[10px] font-semibold text-gray-11">1</span>
<span>
Open <a href="https://t.me/BotFather" target="_blank" rel="noreferrer" class="font-medium text-gray-12 underline">@BotFather</a> and run <code class="rounded bg-gray-3 px-1 py-0.5 font-mono text-[11px]">/newbot</code>.
{t("identities.botfather_step1_open")} <a href="https://t.me/BotFather" target="_blank" rel="noreferrer" class="font-medium text-gray-12 underline">@BotFather</a> {t("identities.botfather_step1_run")} <code class="rounded bg-gray-3 px-1 py-0.5 font-mono text-[11px]">/newbot</code>.
</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-gray-4 text-[10px] font-semibold text-gray-11">2</span>
<span>Copy the bot token and paste it below.</span>
<span>{t("identities.copy_bot_token_hint")}</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-gray-4 text-[10px] font-semibold text-gray-11">3</span>
<span>Choose <span class="font-medium text-gray-12">Public</span> for open inbox or <span class="font-medium text-gray-12">Private</span> to require <code class="rounded bg-gray-3 px-1 py-0.5 font-mono text-[11px]">/pair &lt;code&gt;</code>.</span>
<span>{t("identities.botfather_step3_choose")} <span class="font-medium text-gray-12">{t("identities.botfather_step3_public")}</span> {t("identities.botfather_step3_or_private")} <span class="font-medium text-gray-12">{t("identities.botfather_step3_private")}</span> {t("identities.botfather_step3_to_require")} <code class="rounded bg-gray-3 px-1 py-0.5 font-mono text-[11px]">/pair &lt;code&gt;</code>.</span>
</li>
</ol>
</div>
</Show>
<div>
<label class="text-[12px] text-gray-9 block mb-1">Bot token</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.bot_token_label")}</label>
<input
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2.5 text-sm text-gray-12 placeholder:text-gray-8"
placeholder="Paste Telegram bot token from @BotFather"
placeholder={t("identities.bot_token_placeholder")}
type="password"
value={telegramToken()}
onInput={(e) => setTelegramToken(e.currentTarget.value)}
@@ -1210,11 +1208,11 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
checked={telegramEnabled()}
onChange={(e) => setTelegramEnabled(e.currentTarget.checked)}
/>
Enabled
{t("identities.enabled_label")}
</label>
<div class="rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2 text-[11px] text-gray-10 leading-relaxed">
Public bot: first Telegram chat auto-links. Private bot: requires a pairing code before any messages run tools.
{t("identities.telegram_bot_access_desc")}
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
@@ -1235,7 +1233,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
>
<Link size={15} />
</Show>
{telegramSaving() ? "Connecting..." : "Create public bot"}
{telegramSaving() ? t("identities.connecting") : t("identities.create_public_bot")}
</button>
<button
@@ -1256,27 +1254,27 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
>
<Shield size={15} />
</Show>
{telegramSaving() ? "Connecting..." : "Create private bot"}
{telegramSaving() ? t("identities.connecting") : t("identities.create_private_bot")}
</button>
</div>
<Show when={telegramPairingCode()}>
{(code) => (
<div class="rounded-xl border border-sky-7/25 bg-sky-1/40 px-3.5 py-3 space-y-2">
<div class="text-[12px] font-semibold text-sky-11">Private pairing code</div>
<div class="text-[12px] font-semibold text-sky-11">{t("identities.private_pairing_code")}</div>
<div class="rounded-md border border-sky-7/20 bg-sky-2/80 px-3 py-2 font-mono text-[13px] tracking-[0.08em] text-sky-12">
{code()}
</div>
<div class="text-[11px] text-sky-11/90 leading-relaxed">
In Telegram, open the chat that should control this worker and send <code class="rounded bg-sky-3/60 px-1 py-0.5 font-mono text-[10px]">/pair {code()}</code>.
{t("identities.pairing_code_instruction_prefix")} <code class="rounded bg-sky-3/60 px-1 py-0.5 font-mono text-[10px]">/pair {code()}</code>.
</div>
<div class="flex items-center gap-2">
<Button variant="outline" class="h-7 px-2.5 text-[11px]" onClick={() => void copyTelegramPairingCode()}>
<Copy size={12} />
<span class="ml-1">Copy code</span>
<span class="ml-1">{t("identities.copy_code")}</span>
</Button>
<Button variant="outline" class="h-7 px-2.5 text-[11px]" onClick={() => setTelegramPairingCode(null)}>
Hide
{t("common.hide")}
</Button>
</div>
</div>
@@ -1292,7 +1290,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
class="inline-flex items-center gap-2 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2 text-[12px] font-medium text-gray-11 hover:bg-gray-2"
>
<Link size={14} />
Open @{telegramBotUsername()} in Telegram
{t("identities.open_bot_link", undefined, { username: telegramBotUsername() ?? "" })}
</a>
)}
</Show>
@@ -1329,12 +1327,12 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<span class="text-[15px] font-semibold text-gray-12">Slack</span>
<Show when={hasSlackConnected()}>
<span class="rounded-full px-2 py-0.5 text-[10px] font-semibold bg-emerald-1/40 text-emerald-11">
Connected
{t("identities.connected_badge")}
</span>
</Show>
</div>
<div class="text-[13px] text-gray-9 mt-0.5 leading-snug">
Your worker appears as a bot in Slack channels. Team members can message it directly or mention it in threads.
{t("identities.slack_desc")}
</div>
</div>
<ChevronRight
@@ -1368,7 +1366,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
</span>
</div>
<div class="text-[11px] text-gray-9 mt-0.5 pl-3.5">
{item.enabled ? "Enabled" : "Disabled"} · {item.running ? "Running" : "Stopped"}
{item.enabled ? t("identities.enabled_label") : t("identities.disabled_label")} · {item.running ? t("identities.running_label") : t("identities.stopped_label")}
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@@ -1378,7 +1376,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
disabled={slackSaving() || item.id === "env" || !workspaceId()}
onClick={() => void deleteSlack(item.id)}
>
Disconnect
{t("identities.disconnect")}
</Button>
</div>
</div>
@@ -1389,7 +1387,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* Connected stats summary */}
<div class="flex gap-2.5">
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Status</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.status_label")}</div>
<div class="flex items-center gap-1.5">
<div class={`w-1.5 h-1.5 rounded-full ${
slackIdentities().some((i) => i.running) ? "bg-emerald-9" : "bg-gray-8"
@@ -1397,18 +1395,18 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<span class={`text-[13px] font-semibold ${
slackIdentities().some((i) => i.running) ? "text-emerald-11" : "text-gray-10"
}`}>
{slackIdentities().some((i) => i.running) ? "Active" : "Stopped"}
{slackIdentities().some((i) => i.running) ? t("identities.status_active") : t("identities.status_stopped")}
</span>
</div>
</div>
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Identities</div>
<div class="text-[13px] font-semibold text-gray-12">{slackIdentities().length} configured</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.identities_label")}</div>
<div class="text-[13px] font-semibold text-gray-12">{slackIdentities().length} {t("identities.configured_suffix")}</div>
</div>
<div class="flex-1 rounded-lg border border-gray-4 bg-gray-2/50 px-3 py-2.5">
<div class="text-[11px] text-gray-9 mb-0.5">Channel</div>
<div class="text-[11px] text-gray-9 mb-0.5">{t("identities.channel_label")}</div>
<div class="text-[13px] font-semibold text-gray-12">
{health()?.channels.slack ? "On" : "Off"}
{health()?.channels.slack ? t("common.on") : t("common.off")}
</div>
</div>
</div>
@@ -1425,13 +1423,13 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="space-y-2.5">
<Show when={slackIdentities().length === 0}>
<p class="text-[13px] text-gray-10 leading-relaxed">
Connect your Slack workspace to let team members interact with this worker in channels and DMs.
{t("identities.slack_intro")}
</p>
</Show>
<div class="space-y-2">
<div>
<label class="text-[12px] text-gray-9 block mb-1">Bot token</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.bot_token_label")}</label>
<input
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2.5 text-sm text-gray-12 placeholder:text-gray-8"
placeholder="xoxb-..."
@@ -1441,7 +1439,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
/>
</div>
<div>
<label class="text-[12px] text-gray-9 block mb-1">App token</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.app_token_label")}</label>
<input
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2.5 text-sm text-gray-12 placeholder:text-gray-8"
placeholder="xapp-..."
@@ -1458,7 +1456,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
checked={slackEnabled()}
onChange={(e) => setSlackEnabled(e.currentTarget.checked)}
/>
Enabled
{t("identities.enabled_label")}
</label>
<button
@@ -1479,7 +1477,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
>
<Link size={15} />
</Show>
{slackSaving() ? "Connecting..." : "Connect Slack"}
{slackSaving() ? t("identities.connecting") : t("identities.connect_slack")}
</button>
<Show when={slackIdentities().length === 0}>
@@ -1504,21 +1502,20 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Message routing ---- */}
<div>
<div class="text-[11px] font-semibold text-gray-9 uppercase tracking-wider mb-2">
Message routing
{t("identities.message_routing_title")}
</div>
<p class="text-[13px] text-gray-9 leading-relaxed mb-3">
Control which conversations go to which workspace folder. Messages are
routed to the worker's default folder unless you set up rules here.
{t("identities.message_routing_desc")}
</p>
<div class="rounded-xl border border-gray-4 bg-gray-2/50 px-4 py-3.5 space-y-3">
<div class="flex items-center gap-2">
<Shield size={16} class="text-gray-9" />
<span class="text-[13px] font-medium text-gray-11">Default routing</span>
<span class="text-[13px] font-medium text-gray-11">{t("identities.default_routing")}</span>
</div>
<div class="flex items-center gap-2 pl-6">
<span class="rounded-md bg-gray-4 px-2.5 py-1 text-[12px] font-medium text-gray-11">
All channels
{t("identities.all_channels")}
</span>
<ArrowRight size={14} class="text-gray-8" />
<span class="rounded-md bg-dls-accent/10 px-2.5 py-1 text-[12px] font-medium text-dls-accent">
@@ -1528,7 +1525,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
</div>
<div class="text-xs text-gray-10 mt-2.5">
Advanced: reply with <code class="text-[11px] font-mono bg-gray-3 px-1 py-0.5 rounded">/dir &lt;path&gt;</code> in Slack/Telegram to override the directory for a specific chat (limited to this workspace root).
{t("identities.routing_override_prefix")} <code class="text-[11px] font-mono bg-gray-3 px-1 py-0.5 rounded">/dir &lt;path&gt;</code> {t("identities.routing_override_suffix")}
</div>
</div>
@@ -1536,9 +1533,9 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="rounded-xl border border-gray-4 bg-gray-1 p-4 space-y-3">
<div class="flex items-center justify-between gap-2">
<div>
<div class="text-[13px] font-semibold text-gray-12">Messaging agent behavior</div>
<div class="text-[13px] font-semibold text-gray-12">{t("identities.agent_behavior_title")}</div>
<div class="text-[12px] text-gray-9 mt-0.5">
One file per workspace. Add optional first line <code class="font-mono">@agent &lt;id&gt;</code> to route via a specific OpenCode agent.
{t("identities.agent_behavior_desc")}
</div>
</div>
<span class="rounded-md border border-gray-4 bg-gray-2/50 px-2 py-1 text-[11px] font-mono text-gray-10">
@@ -1549,24 +1546,24 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<Show when={workspaceAgentStatus()}>
{(value) => (
<div class="rounded-lg border border-gray-4 bg-gray-2/40 px-3 py-2 text-[11px] text-gray-10">
Active scope: workspace · status: {value().loaded ? "loaded" : "missing"} · selected agent: {value().selected || "(none)"}
{t("identities.agent_scope_status", undefined, { status: value().loaded ? t("identities.agent_status_loaded") : t("identities.agent_status_missing"), agent: value().selected || t("identities.agent_none") })}
</div>
)}
</Show>
<Show when={agentLoading()}>
<div class="text-[11px] text-gray-9">Loading agent file…</div>
<div class="text-[11px] text-gray-9">{t("identities.agent_loading")}</div>
</Show>
<Show when={!agentExists() && !agentLoading()}>
<div class="rounded-lg border border-amber-7/20 bg-amber-1/30 px-3 py-2 text-xs text-amber-12">
Agent file not found in this workspace yet.
{t("identities.agent_not_found")}
</div>
</Show>
<textarea
class="min-h-[220px] w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2.5 text-[13px] font-mono text-gray-12 placeholder:text-gray-8"
placeholder="Add messaging behavior instructions for opencodeRouter here..."
placeholder={t("identities.agent_placeholder")}
value={agentDraft()}
onInput={(e) => setAgentDraft(e.currentTarget.value)}
/>
@@ -1578,7 +1575,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
onClick={() => void loadAgentFile()}
disabled={agentLoading() || !workspaceId()}
>
Reload
{t("identities.reload")}
</Button>
<Show when={!agentExists()}>
<Button
@@ -1587,7 +1584,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
onClick={() => void createDefaultAgentFile()}
disabled={agentSaving() || !workspaceId()}
>
Create default file
{t("identities.create_default_file")}
</Button>
</Show>
<Button
@@ -1596,10 +1593,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
onClick={() => void saveAgentFile()}
disabled={agentSaving() || !workspaceId() || !agentDirty()}
>
{agentSaving() ? "Saving..." : "Save behavior"}
{agentSaving() ? t("identities.saving") : t("identities.save_behavior")}
</Button>
<Show when={agentDirty() && !agentSaving()}>
<span class="text-[11px] text-gray-9">Unsaved changes</span>
<span class="text-[11px] text-gray-9">{t("identities.unsaved_changes")}</span>
</Show>
</div>
@@ -1614,15 +1611,15 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
{/* ---- Outbound send test ---- */}
<div class="rounded-xl border border-gray-4 bg-gray-1 p-4 space-y-3">
<div>
<div class="text-[13px] font-semibold text-gray-12">Send test message</div>
<div class="text-[13px] font-semibold text-gray-12">{t("identities.send_test_title")}</div>
<div class="text-[12px] text-gray-9 mt-0.5">
Validate outbound wiring. Use a peer ID for direct send, or leave peer ID empty to fan out by bindings in a directory.
{t("identities.send_test_desc")}
</div>
</div>
<div class="grid gap-2 sm:grid-cols-2">
<div>
<label class="text-[12px] text-gray-9 block mb-1">Channel</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.channel_label")}</label>
<select
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2 text-sm text-gray-12"
value={sendChannel()}
@@ -1633,10 +1630,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
</select>
</div>
<div>
<label class="text-[12px] text-gray-9 block mb-1">Peer ID (optional)</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.peer_id_label")}</label>
<input
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-8"
placeholder={sendChannel() === "telegram" ? "Telegram chat id (e.g. 123456789)" : "Slack peer id (e.g. D12345678|thread_ts)"}
placeholder={sendChannel() === "telegram" ? t("identities.peer_id_placeholder_telegram") : t("identities.peer_id_placeholder_slack")}
value={sendPeerId()}
onInput={(e) => setSendPeerId(e.currentTarget.value)}
/>
@@ -1645,7 +1642,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<div class="grid gap-2 sm:grid-cols-2">
<div>
<label class="text-[12px] text-gray-9 block mb-1">Directory (optional)</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.directory_label")}</label>
<input
class="w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-8"
placeholder={defaultRoutingDirectory()}
@@ -1660,16 +1657,16 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
checked={sendAutoBind()}
onChange={(e) => setSendAutoBind(e.currentTarget.checked)}
/>
Auto-bind peer to directory on direct send
{t("identities.auto_bind_label")}
</label>
</div>
</div>
<div>
<label class="text-[12px] text-gray-9 block mb-1">Message</label>
<label class="text-[12px] text-gray-9 block mb-1">{t("identities.message_label")}</label>
<textarea
class="min-h-[90px] w-full rounded-lg border border-gray-4 bg-gray-1 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-8"
placeholder="Test message content"
placeholder={t("identities.send_test_button")}
value={sendText()}
onInput={(e) => setSendText(e.currentTarget.value)}
/>
@@ -1682,7 +1679,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
onClick={() => void sendTestMessage()}
disabled={sendBusy() || !workspaceId() || !sendText().trim()}
>
{sendBusy() ? "Sending..." : "Send test message"}
{sendBusy() ? t("identities.sending") : t("identities.send_test_button")}
</Button>
<Show when={sendStatus()}>
{(value) => <span class="text-[11px] text-gray-9">{value()}</span>}
@@ -1722,10 +1719,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<ConfirmModal
open={messagingRiskOpen()}
title="Enable messaging for this worker?"
message="Messaging can expose this worker to remote commands. If a bot is public or compromised, it can access files, credentials, and API keys available to this worker."
confirmLabel={messagingSaving() ? "Enabling..." : "Enable messaging"}
cancelLabel="Cancel"
title={t("identities.enable_messaging_title")}
message={t("identities.enable_messaging_risk")}
confirmLabel={messagingSaving() ? t("identities.enabling") : t("identities.enable_messaging")}
cancelLabel={t("common.cancel")}
variant="danger"
onCancel={() => {
if (messagingSaving()) return;
@@ -1738,14 +1735,14 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<ConfirmModal
open={messagingRestartPromptOpen()}
title="Restart worker now?"
title={t("identities.restart_worker_title")}
message={
messagingRestartAction() === "enable"
? "Messaging was enabled for this workspace. Restart the worker now to start the messaging sidecar and unlock Telegram and Slack setup."
: "Messaging was disabled for this workspace. Restart the worker now to stop the messaging sidecar."
? t("identities.restart_to_enable_messaging")
: t("identities.restart_to_disable_messaging")
}
confirmLabel={messagingRestartBusy() ? "Restarting..." : "Restart worker"}
cancelLabel="Later"
confirmLabel={messagingRestartBusy() ? t("identities.restarting") : t("identities.restart_worker")}
cancelLabel={t("identities.later")}
onCancel={() => {
if (messagingRestartBusy()) return;
setMessagingRestartPromptOpen(false);
@@ -1757,10 +1754,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<ConfirmModal
open={messagingDisableConfirmOpen()}
title="Disable messaging for this worker?"
message="This will turn off messaging for this workspace. Telegram and Slack setup will be hidden until messaging is enabled again, and you will need to restart the worker to fully stop the messaging sidecar."
confirmLabel={messagingSaving() ? "Disabling..." : "Disable messaging"}
cancelLabel="Cancel"
title={t("identities.disable_messaging_title")}
message={t("identities.disable_messaging_message")}
confirmLabel={messagingSaving() ? t("identities.disabling") : t("identities.disable_messaging")}
cancelLabel={t("common.cancel")}
onCancel={() => {
if (messagingSaving()) return;
setMessagingDisableConfirmOpen(false);
@@ -1772,17 +1769,10 @@ export default function IdentitiesView(props: IdentitiesViewProps) {
<ConfirmModal
open={publicTelegramWarningOpen()}
title="Make this bot public?"
message={
<>
Your bot will be accessible to the public and anyone who gets access to your bot will be able to have
full access to your local worker including any files or API keys that you've given it. If you create a
private bot, you can limit who can access it by requiring a pairing token. Are you sure you want to make
your bot public?
</>
}
confirmLabel="Yes I understand the risk"
cancelLabel="Cancel"
title={t("identities.public_bot_warning_title")}
message={t("identities.public_bot_warning_message")}
confirmLabel={t("identities.public_bot_confirm")}
cancelLabel={t("common.cancel")}
variant="danger"
confirmButtonVariant="danger"
cancelButtonVariant="primary"

View File

@@ -5,6 +5,7 @@ import { useExtensions } from "../extensions/provider";
import Button from "../components/button";
import TextInput from "../components/text-input";
import { Cpu } from "lucide-solid";
import { t } from "../../i18n";
export type PluginsViewProps = {
busy: boolean;
@@ -37,8 +38,8 @@ export default function PluginsView(props: PluginsViewProps) {
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
<div class="flex items-start justify-between gap-4">
<div class="space-y-1">
<div class="text-sm font-medium text-gray-12">OpenCode plugins</div>
<div class="text-xs text-gray-10">Manage `opencode.json` for your project or global OpenCode plugins.</div>
<div class="text-sm font-medium text-gray-12">{t("plugins.title")}</div>
<div class="text-xs text-gray-10">{t("plugins.desc")}</div>
</div>
<div class="flex items-center gap-2">
<button
@@ -52,7 +53,7 @@ export default function PluginsView(props: PluginsViewProps) {
void extensions.refreshPlugins("project");
}}
>
Project
{t("plugins.scope_project")}
</button>
<button
disabled={!props.canUseGlobalScope}
@@ -67,24 +68,24 @@ export default function PluginsView(props: PluginsViewProps) {
void extensions.refreshPlugins("global");
}}
>
Global
{t("plugins.scope_global")}
</button>
<Button variant="ghost" onClick={() => void extensions.refreshPlugins()}>
Refresh
{t("common.refresh")}
</Button>
</div>
</div>
<div class="flex flex-col gap-1 text-xs text-gray-10">
<div>Config</div>
<div class="text-gray-7 font-mono truncate">{extensions.pluginConfigPath() ?? extensions.pluginConfig()?.path ?? "Not loaded yet"}</div>
<div>{t("plugins.config_label")}</div>
<div class="text-gray-7 font-mono truncate">{extensions.pluginConfigPath() ?? extensions.pluginConfig()?.path ?? t("plugins.not_loaded_yet")}</div>
<Show when={props.accessHint}>
<div class="text-gray-9">{props.accessHint}</div>
</Show>
</div>
<div class="space-y-3">
<div class="text-xs font-medium text-gray-11 uppercase tracking-wider">Suggested plugins</div>
<div class="text-xs font-medium text-gray-11 uppercase tracking-wider">{t("plugins.suggested_heading")}</div>
<div class="grid gap-3">
<For each={props.suggestedPlugins}>
{(plugin) => {
@@ -108,7 +109,7 @@ export default function PluginsView(props: PluginsViewProps) {
variant="ghost"
onClick={() => extensions.setActivePluginGuide(isGuideOpen() ? null : plugin.packageName)}
>
{isGuideOpen() ? "Hide setup" : "Setup"}
{isGuideOpen() ? t("plugins.hide_setup") : t("plugins.setup")}
</Button>
</Show>
<Button
@@ -121,7 +122,7 @@ export default function PluginsView(props: PluginsViewProps) {
(extensions.pluginScope() === "project" && !props.selectedWorkspaceRoot.trim())
}
>
{isInstalled() ? "Added" : "Add"}
{isInstalled() ? t("plugins.added") : t("plugins.add")}
</Button>
</div>
</div>
@@ -177,7 +178,7 @@ export default function PluginsView(props: PluginsViewProps) {
when={extensions.pluginList().length}
fallback={
<div class="rounded-xl border border-gray-6/60 bg-gray-1/40 p-4 text-sm text-gray-10">
No plugins configured yet.
{t("plugins.empty")}
</div>
}
>
@@ -187,14 +188,14 @@ export default function PluginsView(props: PluginsViewProps) {
<div class="flex items-center justify-between rounded-xl border border-gray-6/60 bg-gray-1/40 px-4 py-2.5">
<div class="text-sm text-gray-12 font-mono">{pluginName}</div>
<div class="flex items-center gap-2">
<div class="text-[10px] uppercase tracking-wide text-gray-10">Enabled</div>
<div class="text-[10px] uppercase tracking-wide text-gray-10">{t("plugins.enabled")}</div>
<Button
variant="ghost"
class="h-7 px-2 text-[11px] text-red-11 hover:text-red-12"
onClick={() => extensions.removePlugin(pluginName)}
disabled={props.busy || !props.canEditPlugins}
>
Remove
{t("plugins.remove")}
</Button>
</div>
</div>
@@ -207,11 +208,11 @@ export default function PluginsView(props: PluginsViewProps) {
<div class="flex flex-col md:flex-row gap-3">
<div class="flex-1">
<TextInput
label="Add plugin"
label={t("plugins.add_label")}
placeholder="opencode-wakatime"
value={extensions.pluginInput()}
onInput={(e) => extensions.setPluginInput(e.currentTarget.value)}
hint="Add npm package names, e.g. opencode-wakatime"
hint={t("plugins.add_hint")}
/>
</div>
<Button
@@ -220,7 +221,7 @@ export default function PluginsView(props: PluginsViewProps) {
disabled={props.busy || !extensions.pluginInput().trim() || !props.canEditPlugins}
class="md:mt-6"
>
Add
{t("plugins.add")}
</Button>
</div>
<Show when={extensions.pluginStatus()}>

View File

@@ -9,7 +9,7 @@ import {
onCleanup,
onMount,
} from "solid-js";
import { t, currentLocale } from "../../i18n";
import { t } from "../../i18n";
import type { Agent, Part, Session } from "@opencode-ai/sdk/v2/client";
import type {
ComposerDraft,
@@ -269,10 +269,10 @@ type CommandPaletteMode = "root" | "sessions";
function describePermissionRequest(permission: PendingPermission | null) {
if (!permission) {
return {
title: "Permission Required",
message: "OpenCode is requesting permission to continue.",
title: t("session.permission_required"),
message: t("session.permission_message"),
permissionLabel: "",
scopeLabel: "Scope",
scopeLabel: t("session.scope_label"),
scopeValue: "",
isDoomLoop: false,
note: null as string | null,
@@ -286,21 +286,21 @@ function describePermissionRequest(permission: PendingPermission | null) {
? permission.metadata.tool
: null;
return {
title: "Doom Loop Detected",
message: "OpenCode detected repeated tool calls with identical input and is asking whether it should continue after repeated failures.",
permissionLabel: "Doom Loop",
scopeLabel: tool ? "Tool" : "Repeated call",
scopeValue: tool ?? (patterns.length ? patterns.join(", ") : "Repeated tool call"),
title: t("session.doom_loop_title"),
message: t("session.doom_loop_message"),
permissionLabel: t("session.doom_loop_label"),
scopeLabel: tool ? t("session.doom_loop_tool_label") : t("session.doom_loop_repeated_call_label"),
scopeValue: tool ?? (patterns.length ? patterns.join(", ") : t("session.doom_loop_repeated_tool_call")),
isDoomLoop: true,
note: "Reject to stop the loop, or allow if you want the agent to keep trying.",
note: t("session.doom_loop_note"),
};
}
return {
title: "Permission Required",
message: "OpenCode is requesting permission to continue.",
title: t("session.permission_required"),
message: t("session.permission_message"),
permissionLabel: permission.permission,
scopeLabel: "Scope",
scopeLabel: t("session.scope_label"),
scopeValue: patterns.join(", "),
isDoomLoop: false,
note: null as string | null,
@@ -425,7 +425,7 @@ export default function SessionView(props: SessionViewProps) {
};
const agentLabel = createMemo(() => {
const name = sessionActions.selectedSessionAgent() ?? "Default agent";
const name = sessionActions.selectedSessionAgent() ?? t("session.default_agent");
return name.charAt(0).toUpperCase() + name.slice(1);
});
const workspaceLabel = (workspace: WorkspaceInfo) =>
@@ -433,7 +433,7 @@ export default function SessionView(props: SessionViewProps) {
workspace.openworkWorkspaceName?.trim() ||
workspace.name?.trim() ||
workspace.path?.trim() ||
"Workspace";
t("session.workspace_label");
const todoList = createMemo(() =>
props.todos.filter((todo) => todo.content.trim()),
);
@@ -627,11 +627,11 @@ export default function SessionView(props: SessionViewProps) {
const activeSearchPositionLabel = createMemo(() => {
const hits = searchHits();
if (!hits.length) return "No matches";
if (!hits.length) return t("session.no_matches");
const size = hits.length;
const raw = activeSearchHitIndex();
const index = ((raw % size) + size) % size;
return `${index + 1} of ${size}`;
return t("session.search_position", undefined, { current: index + 1, total: size });
});
const searchActive = createMemo(
@@ -1027,7 +1027,7 @@ export default function SessionView(props: SessionViewProps) {
: "";
return {
ok: false as const,
reason: `${lastError instanceof Error ? lastError.message : "File open failed"}${suffix}`,
reason: `${lastError instanceof Error ? lastError.message : t("session.file_open_failed")}${suffix}`,
};
};
@@ -1037,11 +1037,11 @@ export default function SessionView(props: SessionViewProps) {
if (!workspace || workspace.workspaceType !== "local") return;
const target = workspace.path?.trim() ?? "";
if (!target) {
showStatusToast("Workspace path is unavailable.", "warning");
showStatusToast(t("session.workspace_path_unavailable"), "warning");
return;
}
if (!isTauriRuntime()) {
showStatusToast("Reveal is available in the desktop app.", "warning");
showStatusToast(t("session.reveal_desktop_only"), "warning");
return;
}
try {
@@ -1054,14 +1054,14 @@ export default function SessionView(props: SessionViewProps) {
}
} catch (error) {
const message =
error instanceof Error ? error.message : "Unable to reveal workspace";
error instanceof Error ? error.message : t("session.unable_to_reveal");
showStatusToast(message, "error");
}
};
const todoLabel = createMemo(() => {
const total = todoCount();
if (!total) return "";
return `${todoCompletedCount()} out of ${total} tasks completed`;
return t("session.todo_progress", undefined, { completed: todoCompletedCount(), total });
});
const shareWorkspaceState = createShareWorkspaceState({
workspaces: () => props.workspaces,
@@ -1080,9 +1080,9 @@ export default function SessionView(props: SessionViewProps) {
const attachmentsDisabledReason = createMemo(() => {
if (attachmentsEnabled()) return null;
if (props.openworkServerStatus === "limited") {
return "Add a server token to attach files.";
return t("session.attachments_add_token");
}
return "Connect to OpenWork server to attach files.";
return t("session.attachments_connect_server");
});
onCleanup(() => {
@@ -1158,12 +1158,12 @@ export default function SessionView(props: SessionViewProps) {
if (!trimmed) return;
if (props.selectedWorkspaceDisplay.workspaceType === "remote") {
showStatusToast("File open is unavailable for remote workspaces.", "warning");
showStatusToast(t("session.file_open_remote_unavailable"), "warning");
return;
}
if (!isTauriRuntime()) {
showStatusToast("File open is available in the desktop app.", "warning");
showStatusToast(t("session.file_open_desktop_only"), "warning");
return;
}
@@ -1177,7 +1177,7 @@ export default function SessionView(props: SessionViewProps) {
},
);
if (!result.ok && result.reason === "missing-root") {
showStatusToast("Pick a workspace to open files.", "warning");
showStatusToast(t("session.pick_workspace_to_open"), "warning");
return;
}
if (!result.ok) {
@@ -1186,7 +1186,7 @@ export default function SessionView(props: SessionViewProps) {
}
} catch (error) {
const message =
error instanceof Error ? error.message : "Unable to open file";
error instanceof Error ? error.message : t("session.unable_to_open_file");
showStatusToast(message, "error");
}
};
@@ -1206,7 +1206,7 @@ export default function SessionView(props: SessionViewProps) {
return sorted;
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to load agents";
error instanceof Error ? error.message : t("session.failed_to_load_agents");
setAgentPickerError(message);
setAgentOptions([]);
return [];
@@ -1333,13 +1333,13 @@ export default function SessionView(props: SessionViewProps) {
const compactionStatusDetail = createMemo(() => {
if (!showCompactionIndicator()) return "";
return props.sessionCompactionState?.mode === "auto"
? "OpenCode is auto-compacting this session"
: "OpenCode is compacting this session";
? t("session.compacting_auto")
: t("session.compacting_manual");
});
const statusBarCopy = createMemo(() => {
if (showCompactionIndicator()) {
return {
label: "Compacting Context",
label: t("session.status_compacting"),
detail: compactionStatusDetail(),
dotClass: "bg-blue-9",
pingClass: "bg-blue-9/35 animate-ping",
@@ -1349,7 +1349,7 @@ export default function SessionView(props: SessionViewProps) {
if (showRunIndicator()) {
return {
label: "Session Active",
label: t("session.status_active"),
detail: undefined,
dotClass: "bg-green-9",
pingClass: "bg-green-9/45 animate-ping",
@@ -1362,7 +1362,7 @@ export default function SessionView(props: SessionViewProps) {
if (props.selectedSessionId) {
return {
label: "Session Ready",
label: t("session.status_ready_session"),
detail: undefined,
dotClass: "bg-green-9",
pingClass: "bg-green-9/35",
@@ -1371,7 +1371,7 @@ export default function SessionView(props: SessionViewProps) {
}
return {
label: "Ready",
label: t("session.status_ready"),
detail: undefined,
dotClass: "bg-gray-8",
pingClass: "bg-green-9/35",
@@ -1385,7 +1385,7 @@ export default function SessionView(props: SessionViewProps) {
(startedAt, previous) => {
if (!startedAt || startedAt === previous) return;
if (props.sessionCompactionState?.mode === "manual") return;
showStatusToast("OpenCode started compacting the session context.", "info");
showStatusToast(t("session.compaction_started"), "info");
},
),
);
@@ -1396,7 +1396,7 @@ export default function SessionView(props: SessionViewProps) {
(finishedAt, previous) => {
if (!finishedAt || finishedAt === previous) return;
if (props.sessionCompactionState?.mode === "manual") return;
showStatusToast("OpenCode finished compacting the session context.", "success");
showStatusToast(t("session.compaction_finished"), "success");
},
),
);
@@ -1444,30 +1444,30 @@ export default function SessionView(props: SessionViewProps) {
const tool = typeof record.tool === "string" ? record.tool : "";
switch (tool) {
case "task":
return "Delegating";
return t("session.status_delegating");
case "todowrite":
case "todoread":
return "Planning";
return t("session.status_planning");
case "read":
return "Gathering context";
return t("session.status_gathering_context");
case "list":
case "grep":
case "glob":
return "Searching codebase";
return t("session.status_searching_codebase");
case "webfetch":
return "Searching the web";
return t("session.status_searching_web");
case "edit":
case "write":
case "apply_patch":
return "Writing file";
return t("session.status_writing_file");
case "bash":
return "Running shell";
return t("session.status_running_shell");
default:
return "Working";
return t("session.status_working");
}
}
if (part.type === "reasoning") {
return "Thinking";
return t("session.status_thinking");
}
if (part.type === "text") {
return null;
@@ -1478,7 +1478,7 @@ export default function SessionView(props: SessionViewProps) {
const thinkingStatus = createMemo(() => {
const status = computeStatusFromPart(latestRunPart());
if (status) return status;
if (runPhase() === "thinking") return "Thinking";
if (runPhase() === "thinking") return t("session.status_thinking");
return null;
});
@@ -1523,15 +1523,15 @@ export default function SessionView(props: SessionViewProps) {
const runLabel = createMemo(() => {
switch (runPhase()) {
case "sending":
return "Sending";
return t("session.phase_sending");
case "retrying":
return "Retrying";
return t("session.phase_retrying");
case "responding":
return "Responding";
return t("session.phase_responding");
case "thinking":
return "Thinking";
return t("session.status_thinking");
case "error":
return "Run failed";
return t("session.phase_run_failed");
default:
return "";
}
@@ -1855,17 +1855,17 @@ export default function SessionView(props: SessionViewProps) {
const cancelRun = async () => {
if (abortBusy()) return;
if (!props.selectedSessionId) {
showStatusToast("No session selected", "warning");
showStatusToast(t("session.no_session_selected"), "warning");
return;
}
setAbortBusy(true);
showStatusToast("Stopping the run...", "info");
showStatusToast(t("session.stopping_run"), "info");
try {
await sessionActions.abortSession(props.selectedSessionId);
showStatusToast("Stopped.", "success");
showStatusToast(t("session.stopped"), "success");
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to stop";
const message = error instanceof Error ? error.message : t("session.failed_to_stop");
showStatusToast(message, "error");
} finally {
setAbortBusy(false);
@@ -1875,13 +1875,13 @@ export default function SessionView(props: SessionViewProps) {
const retryRun = async () => {
const text = sessionActions.lastPromptSent().trim();
if (!text) {
showStatusToast("Nothing to retry yet", "warning");
showStatusToast(t("session.nothing_to_retry"), "warning");
return;
}
if (abortBusy()) return;
setAbortBusy(true);
showStatusToast("Trying again...", "info");
showStatusToast(t("session.trying_again"), "info");
try {
if (showRunIndicator() && props.selectedSessionId) {
await sessionActions.abortSession(props.selectedSessionId);
@@ -1974,18 +1974,18 @@ export default function SessionView(props: SessionViewProps) {
const undoLastMessage = async () => {
if (historyActionBusy()) return;
if (!canUndoLastMessage()) {
showStatusToast("Nothing to undo yet.", "warning");
showStatusToast(t("session.nothing_to_undo"), "warning");
return;
}
setHistoryActionBusy("undo");
try {
await sessionActions.undoLastUserMessage();
showStatusToast("Reverted the last user message.", "success");
showStatusToast(t("session.reverted_last_message"), "success");
} catch (error) {
const message =
error instanceof Error ? error.message : props.safeStringify(error);
showStatusToast(message || "Failed to undo", "error");
showStatusToast(message || t("session.failed_to_undo"), "error");
} finally {
setHistoryActionBusy(null);
}
@@ -1994,18 +1994,18 @@ export default function SessionView(props: SessionViewProps) {
const redoLastMessage = async () => {
if (historyActionBusy()) return;
if (!canRedoLastMessage()) {
showStatusToast("Nothing to redo.", "warning");
showStatusToast(t("session.nothing_to_redo"), "warning");
return;
}
setHistoryActionBusy("redo");
try {
await sessionActions.redoLastUserMessage();
showStatusToast("Restored the reverted message.", "success");
showStatusToast(t("session.restored_message"), "success");
} catch (error) {
const message =
error instanceof Error ? error.message : props.safeStringify(error);
showStatusToast(message || "Failed to redo", "error");
showStatusToast(message || t("session.failed_to_redo"), "error");
} finally {
setHistoryActionBusy(null);
}
@@ -2014,24 +2014,24 @@ export default function SessionView(props: SessionViewProps) {
const compactSessionHistory = async () => {
if (historyActionBusy()) return;
if (!canCompactSession()) {
showStatusToast(t("app.error_compact_empty", currentLocale()), "warning");
showStatusToast(t("session.nothing_to_compact"), "warning");
return;
}
const sessionID = props.selectedSessionId;
const startedAt = perfNow();
setHistoryActionBusy("compact");
showStatusToast("Compacting session context...", "info");
showStatusToast(t("session.compacting"), "info");
try {
await sessionActions.compactCurrentSession();
showStatusToast("Session compacted.", "success");
showStatusToast(t("session.compacted"), "success");
finishPerf(props.developerMode, "session.compact", "ui-done", startedAt, {
sessionID,
});
} catch (error) {
const message =
error instanceof Error ? error.message : props.safeStringify(error);
showStatusToast(message || "Failed to compact session", "error");
showStatusToast(message || t("session.failed_to_compact"), "error");
finishPerf(
props.developerMode,
"session.compact",
@@ -2094,7 +2094,7 @@ export default function SessionView(props: SessionViewProps) {
const lastMsg = chatContainerEl?.querySelector(
'[data-message-role="assistant"]:last-child',
);
triggerFlyout(lastMsg ?? null, "sidebar-progress", "New Task", "check");
triggerFlyout(lastMsg ?? null, "sidebar-progress", t("session.flyout_new_task"), "check");
}
setPrevTodoCount(count);
});
@@ -2110,7 +2110,7 @@ export default function SessionView(props: SessionViewProps) {
triggerFlyout(
lastMsg ?? null,
"sidebar-context",
"File Modified",
t("session.flyout_file_modified"),
"folder",
);
}
@@ -2169,9 +2169,9 @@ export default function SessionView(props: SessionViewProps) {
return sessionTitleForId(pending.sessionId);
});
const sessionHeaderTitle = createMemo(() => {
if (showWorkspaceSetupEmptyState()) return "Create or connect a workspace";
if (showWorkspaceSetupEmptyState()) return t("session.create_or_connect_workspace");
if (showPendingSessionTransition()) {
return pendingSessionTransitionTitle() || "Loading session";
return pendingSessionTransitionTitle() || t("session.loading_session");
}
return selectedSessionTitle() || DEFAULT_SESSION_TITLE;
});
@@ -2253,7 +2253,7 @@ export default function SessionView(props: SessionViewProps) {
const openRenameModal = (options?: { returnFocusToComposer?: boolean }) => {
const sessionId = props.selectedSessionId;
if (!sessionId) {
showStatusToast("No session selected", "warning");
showStatusToast(t("session.no_session_selected"), "warning");
if (options?.returnFocusToComposer) {
focusComposer();
}
@@ -2291,7 +2291,7 @@ export default function SessionView(props: SessionViewProps) {
const openDeleteSessionModal = () => {
const sessionId = props.selectedSessionId;
if (!sessionId) {
showStatusToast("No session selected", "warning");
showStatusToast(t("session.no_session_selected"), "warning");
return;
}
setDeleteSessionId(sessionId);
@@ -2313,13 +2313,13 @@ export default function SessionView(props: SessionViewProps) {
await sessionActions.deleteSessionById(sessionId);
setDeleteSessionOpen(false);
setDeleteSessionId(null);
showStatusToast("Session deleted", "success");
showStatusToast(t("session.deleted"), "success");
// Route away from the deleted session id.
props.setView("session");
} catch (error) {
const message =
error instanceof Error ? error.message : props.safeStringify(error);
showStatusToast(message || "Failed to delete session", "error");
showStatusToast(message || t("session.failed_to_delete"), "error");
} finally {
setDeleteSessionBusy(false);
}
@@ -2328,7 +2328,7 @@ export default function SessionView(props: SessionViewProps) {
const requireSessionId = () => {
const sessionId = props.selectedSessionId;
if (!sessionId) {
showStatusToast("No session selected", "warning");
showStatusToast(t("session.no_session_selected"), "warning");
return null;
}
return sessionId;
@@ -2367,7 +2367,7 @@ export default function SessionView(props: SessionViewProps) {
methodIndex?: number,
): Promise<ProviderOAuthStartResult> => {
if (providerAuthActionBusy()) {
throw new Error("Provider auth is already in progress.");
throw new Error(t("session.provider_auth_in_progress"));
}
setProviderAuthActionBusy(true);
try {
@@ -2391,12 +2391,12 @@ export default function SessionView(props: SessionViewProps) {
code,
);
if (result.connected) {
showStatusToast(result.message || "Provider connected", "success");
showStatusToast(result.message || t("session.provider_connected"), "success");
props.closeProviderAuthModal();
}
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "OAuth failed";
const message = error instanceof Error ? error.message : t("session.oauth_failed");
showStatusToast(message, "error");
return { connected: false };
} finally {
@@ -2412,11 +2412,11 @@ export default function SessionView(props: SessionViewProps) {
setProviderAuthActionBusy(true);
try {
const message = await props.submitProviderApiKey(providerId, apiKey);
showStatusToast(message || "API key saved", "success");
showStatusToast(message || t("session.api_key_saved"), "success");
props.closeProviderAuthModal();
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to save API key";
error instanceof Error ? error.message : t("session.failed_to_save_api_key");
showStatusToast(message, "error");
} finally {
setProviderAuthActionBusy(false);
@@ -2446,7 +2446,7 @@ export default function SessionView(props: SessionViewProps) {
if (!client || !workspaceId) {
if (notify) {
showComposerNotice({
title: "Connect to the OpenWork server to upload files to the shared folder.",
title: t("session.upload_connect_server"),
tone: "warning",
});
}
@@ -2458,7 +2458,7 @@ export default function SessionView(props: SessionViewProps) {
files.length === 1 ? (files[0]?.name ?? "file") : `${files.length} files`;
if (notify) {
showComposerNotice({
title: `Uploading ${label} to the shared folder...`,
title: t("session.uploading_to_shared_folder", undefined, { label }),
tone: "info",
});
}
@@ -2477,8 +2477,8 @@ export default function SessionView(props: SessionViewProps) {
.join(", ");
showComposerNotice({
title: summary
? `Uploaded to the shared folder: ${summary}`
: "Uploaded to the shared folder.",
? t("session.uploaded_with_summary", undefined, { summary })
: t("session.uploaded_to_shared_folder"),
tone: "success",
});
}
@@ -2488,7 +2488,7 @@ export default function SessionView(props: SessionViewProps) {
const message =
error instanceof Error
? error.message
: "Shared folder upload failed";
: t("session.shared_folder_upload_failed");
showComposerNotice({ title: message, tone: "error" });
}
return [];
@@ -2539,9 +2539,9 @@ export default function SessionView(props: SessionViewProps) {
const items: CommandPaletteItem[] = [
{
id: "new-session",
title: "Create new session",
detail: "Start a fresh task in the current workspace",
meta: "Create",
title: t("session.cmd_new_session_title"),
detail: t("session.cmd_new_session_detail"),
meta: t("session.cmd_new_session_meta"),
action: () => {
closeCommandPalette();
void Promise.resolve(sessionActions.createSessionAndOpen())
@@ -2553,18 +2553,18 @@ export default function SessionView(props: SessionViewProps) {
const message =
error instanceof Error
? error.message
: "Failed to create session";
: t("session.failed_to_create_session");
showStatusToast(message, "error");
});
},
},
{
id: "rename-session",
title: "Rename current session",
title: t("session.cmd_rename_title"),
detail:
selectedSessionTitle().trim() ||
"Give your selected session a clearer name",
meta: "Rename",
t("session.cmd_rename_detail_fallback"),
meta: t("session.cmd_rename_meta"),
action: () => {
closeCommandPalette();
openRenameModal({ returnFocusToComposer: true });
@@ -2572,11 +2572,11 @@ export default function SessionView(props: SessionViewProps) {
},
{
id: "compact-session",
title: "Compact Conversation",
title: t("session.cmd_compact_title"),
detail: canCompactSession()
? "Send a compact instruction to OpenCode for this session"
: "No user messages to compact yet",
meta: "Compact",
? t("session.cmd_compact_detail")
: t("session.cmd_compact_detail_empty"),
meta: t("session.cmd_compact_meta"),
action: () => {
closeCommandPalette();
void compactSessionHistory();
@@ -2584,9 +2584,9 @@ export default function SessionView(props: SessionViewProps) {
},
{
id: "sessions",
title: "Search sessions",
detail: `${totalSessionCount().toLocaleString()} available across workspaces`,
meta: "Jump",
title: t("session.cmd_sessions_title"),
detail: t("session.cmd_sessions_detail", undefined, { count: totalSessionCount().toLocaleString() }),
meta: t("session.cmd_sessions_meta"),
action: () => {
setCommandPaletteMode("sessions");
setCommandPaletteQuery("");
@@ -2596,9 +2596,9 @@ export default function SessionView(props: SessionViewProps) {
},
{
id: "model",
title: "Change model",
detail: `${modelControls.selectedSessionModelLabel() || "Model"} · ${modelControls.sessionModelVariantLabel()}`,
meta: "Open",
title: t("session.cmd_model_title"),
detail: t("session.cmd_model_detail", undefined, { model: modelControls.selectedSessionModelLabel() || t("session.cmd_model_fallback"), variant: modelControls.sessionModelVariantLabel() }),
meta: t("session.cmd_model_meta"),
action: () => {
closeCommandPalette();
modelControls.openSessionModelPicker({ returnFocusTarget: "composer" });
@@ -2606,9 +2606,9 @@ export default function SessionView(props: SessionViewProps) {
},
{
id: "provider",
title: "Connect provider",
detail: "Open provider connection flow",
meta: "Open",
title: t("session.cmd_provider_title"),
detail: t("session.cmd_provider_detail"),
meta: t("session.cmd_provider_meta"),
action: () => {
closeCommandPalette();
void props
@@ -2617,7 +2617,7 @@ export default function SessionView(props: SessionViewProps) {
const message =
error instanceof Error
? error.message
: "Failed to load providers";
: t("session.failed_to_load_providers");
showStatusToast(message, "error");
focusComposer();
});
@@ -2646,8 +2646,8 @@ export default function SessionView(props: SessionViewProps) {
detail: item.workspaceTitle,
meta:
item.workspaceId === props.selectedWorkspaceId
? "Current workspace"
: "Switch",
? t("session.cmd_current_workspace")
: t("session.cmd_switch"),
action: () => {
closeCommandPalette();
openSessionFromList(item.workspaceId, item.sessionId, {
@@ -2665,14 +2665,14 @@ export default function SessionView(props: SessionViewProps) {
const commandPaletteTitle = createMemo(() => {
const mode = commandPaletteMode();
if (mode === "sessions") return "Search sessions";
return "Quick actions";
if (mode === "sessions") return t("session.palette_title_sessions");
return t("session.palette_title_actions");
});
const commandPalettePlaceholder = createMemo(() => {
const mode = commandPaletteMode();
if (mode === "sessions") return "Find by session title or workspace";
return "Search actions";
if (mode === "sessions") return t("session.palette_placeholder_sessions");
return t("session.palette_placeholder_actions");
});
createEffect(
@@ -2715,13 +2715,13 @@ export default function SessionView(props: SessionViewProps) {
const updatePillLabel = createMemo(() => {
const state = props.updateStatus?.state;
if (state === "ready") {
return props.anyActiveRuns ? "Update ready" : "Install update";
return props.anyActiveRuns ? t("session.update_ready") : t("session.install_update");
}
if (state === "downloading") {
const percent = updateDownloadPercent();
return percent == null ? "Downloading" : `Downloading ${percent}%`;
return percent == null ? t("session.downloading") : t("session.downloading_percent", undefined, { percent });
}
return "Update available";
return t("session.update_available");
});
const updatePillButtonTone = createMemo(() => {
@@ -2779,11 +2779,11 @@ export default function SessionView(props: SessionViewProps) {
const state = props.updateStatus?.state;
if (state === "ready") {
return props.anyActiveRuns
? `Update ready ${version}. Stop active runs to restart.`
: `Restart to apply update ${version}`;
? t("session.update_ready_stop_runs_title", undefined, { version })
: t("session.restart_update_title", undefined, { version });
}
if (state === "downloading") return `Downloading update ${version}`;
return `Update available ${version}`;
if (state === "downloading") return t("session.downloading_update_title", undefined, { version });
return t("session.update_available_title", undefined, { version });
});
const handleUpdatePillClick = () => {
@@ -2802,7 +2802,7 @@ export default function SessionView(props: SessionViewProps) {
const openProviderAuth = (preferredProviderId?: string) => {
void props.openProviderAuthModal({ preferredProviderId }).catch((error) => {
const message = error instanceof Error ? error.message : "Connect failed";
const message = error instanceof Error ? error.message : t("session.connect_failed");
showStatusToast(message, "error");
});
};
@@ -2987,8 +2987,8 @@ export default function SessionView(props: SessionViewProps) {
<div
class="absolute right-0 top-3 hidden h-[calc(100%-24px)] w-2 translate-x-1/2 cursor-col-resize rounded-full bg-transparent transition-colors hover:bg-gray-6/40 lg:block"
onPointerDown={startLeftSidebarResize}
title="Resize workspace column"
aria-label="Resize workspace column"
title={t("session.resize_workspace_column")}
aria-label={t("session.resize_workspace_column")}
/>
</aside>
@@ -3035,7 +3035,7 @@ export default function SessionView(props: SessionViewProps) {
{sessionHeaderTitle()}
</h1>
<span class="hidden truncate text-[13px] text-dls-secondary lg:inline">
{props.selectedWorkspaceDisplay.displayName || props.selectedWorkspaceDisplay.name || "Workspace"}
{props.selectedWorkspaceDisplay.displayName || props.selectedWorkspaceDisplay.name || t("session.workspace_fallback")}
</span>
<Show when={props.developerMode}>
<span class="hidden text-[12px] text-dls-secondary lg:inline">
@@ -3066,11 +3066,11 @@ export default function SessionView(props: SessionViewProps) {
}
window.setTimeout(() => openCommandPalette(), 0);
}}
title="Quick actions (Ctrl/Cmd+K)"
aria-label="Quick actions"
title={t("session.quick_actions_title")}
aria-label={t("session.quick_actions_label")}
>
<Menu size={15} />
<span>Menu</span>
<span>{t("session.menu_label")}</span>
<span class="ml-1 rounded border border-dls-border px-1 text-[10px] text-gray-9">
K
</span>
@@ -3089,8 +3089,8 @@ export default function SessionView(props: SessionViewProps) {
}
openSearch();
}}
title="Search conversation (Ctrl/Cmd+F)"
aria-label="Search conversation"
title={t("session.search_conversation_title")}
aria-label={t("session.search_conversation_label")}
>
<Search size={16} />
</button>
@@ -3100,8 +3100,8 @@ export default function SessionView(props: SessionViewProps) {
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[13px] font-medium text-gray-10 transition-colors hover:bg-gray-2/70 hover:text-dls-text disabled:cursor-not-allowed disabled:opacity-60"
onClick={undoLastMessage}
disabled={!canUndoLastMessage() || historyActionBusy() !== null}
title="Undo last message"
aria-label="Undo last message"
title={t("session.undo_title")}
aria-label={t("session.undo_label")}
>
<Show
when={historyActionBusy() === "undo"}
@@ -3109,15 +3109,15 @@ export default function SessionView(props: SessionViewProps) {
>
<Loader2 size={16} class="animate-spin" />
</Show>
<span class="hidden lg:inline">Revert</span>
<span class="hidden lg:inline">{t("session.revert_label")}</span>
</button>
<button
type="button"
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[13px] font-medium text-gray-10 transition-colors hover:bg-gray-2/70 hover:text-dls-text disabled:cursor-not-allowed disabled:opacity-60"
onClick={redoLastMessage}
disabled={!canRedoLastMessage() || historyActionBusy() !== null}
title="Redo last reverted message"
aria-label="Redo last reverted message"
title={t("session.redo_title")}
aria-label={t("session.redo_aria_label")}
>
<Show
when={historyActionBusy() === "redo"}
@@ -3125,7 +3125,7 @@ export default function SessionView(props: SessionViewProps) {
>
<Loader2 size={16} class="animate-spin" />
</Show>
<span class="hidden lg:inline">Redo</span>
<span class="hidden lg:inline">{t("session.redo_label")}</span>
</button>
</div>
</header>
@@ -3154,8 +3154,8 @@ export default function SessionView(props: SessionViewProps) {
}
}}
class="min-w-0 flex-1 bg-transparent text-sm text-gray-11 placeholder:text-gray-9 focus:outline-none"
placeholder="Search in this chat"
aria-label="Search in this chat"
placeholder={t("session.search_placeholder")}
aria-label={t("session.search_placeholder")}
/>
<span class="text-[11px] text-gray-10 tabular-nums">
{activeSearchPositionLabel()}
@@ -3165,24 +3165,24 @@ export default function SessionView(props: SessionViewProps) {
class="rounded-md border border-dls-border px-2 py-1 text-[11px] text-gray-10 transition-colors hover:bg-gray-2 hover:text-gray-12 disabled:opacity-60"
disabled={searchHits().length === 0}
onClick={() => moveSearchHit(-1)}
aria-label="Previous match"
aria-label={t("session.prev_match")}
>
Prev
{t("session.search_prev")}
</button>
<button
type="button"
class="rounded-md border border-dls-border px-2 py-1 text-[11px] text-gray-10 transition-colors hover:bg-gray-2 hover:text-gray-12 disabled:opacity-60"
disabled={searchHits().length === 0}
onClick={() => moveSearchHit(1)}
aria-label="Next match"
aria-label={t("session.next_match")}
>
Next
{t("session.search_next")}
</button>
<button
type="button"
class="flex h-7 w-7 items-center justify-center rounded-md text-gray-10 transition-colors hover:bg-gray-2 hover:text-gray-12"
onClick={closeSearch}
aria-label="Close search"
aria-label={t("session.close_search")}
>
<X size={14} />
</button>
@@ -3230,9 +3230,9 @@ export default function SessionView(props: SessionViewProps) {
<Loader2 size={20} class="animate-spin text-dls-secondary" />
</div>
<div class="space-y-1">
<h3 class="text-base font-medium text-dls-text">Loading session</h3>
<h3 class="text-base font-medium text-dls-text">{t("session.loading_title")}</h3>
<p class="text-sm text-dls-secondary">
Pulling in the latest messages for this task.
{t("session.loading_detail")}
</p>
</div>
</div>
@@ -3303,10 +3303,10 @@ export default function SessionView(props: SessionViewProps) {
disabled={props.loadingEarlierMessages}
>
{props.loadingEarlierMessages
? "Loading earlier messages..."
? t("session.loading_earlier")
: hiddenMessageCount() > 0
? `Show ${nextRevealCount().toLocaleString()} earlier message${nextRevealCount() === 1 ? "" : "s"}`
: "Load earlier messages"}
? t("session.show_earlier", undefined, { count: nextRevealCount().toLocaleString(), plural: nextRevealCount() === 1 ? "" : "s" })
: t("session.load_earlier")}
</button>
</div>
</Show>
@@ -3382,7 +3382,7 @@ export default function SessionView(props: SessionViewProps) {
sessionScroll.jumpToStartOfMessage("smooth");
}}
>
Jump to start of message
{t("session.jump_to_start")}
</button>
</Show>
<Show when={!sessionScroll.isAtBottom()}>
@@ -3394,7 +3394,7 @@ export default function SessionView(props: SessionViewProps) {
sessionScroll.jumpToLatest("smooth");
}}
>
Jump to latest
{t("session.jump_to_latest")}
</button>
</Show>
</div>
@@ -3483,7 +3483,7 @@ export default function SessionView(props: SessionViewProps) {
onSend={handleSendPrompt}
onStop={cancelRun}
onDraftChange={handleDraftChange}
selectedModelLabel={modelControls.selectedSessionModelLabel() || "Model"}
selectedModelLabel={modelControls.selectedSessionModelLabel() || t("session.model_fallback")}
onModelClick={() => modelControls.openSessionModelPicker()}
modelVariantLabel={modelControls.sessionModelVariantLabel()}
modelVariant={modelControls.sessionModelVariant()}
@@ -3560,7 +3560,7 @@ export default function SessionView(props: SessionViewProps) {
class="h-8 px-2 rounded-md text-xs text-dls-secondary hover:text-dls-text hover:bg-dls-hover transition-colors"
onClick={returnToCommandRoot}
>
Back
{t("session.back")}
</button>
</Show>
<Search size={14} class="text-dls-secondary shrink-0" />
@@ -3579,7 +3579,7 @@ export default function SessionView(props: SessionViewProps) {
type="button"
class="h-8 w-8 flex items-center justify-center rounded-md text-dls-secondary hover:text-dls-text hover:bg-dls-hover transition-colors"
onClick={closeCommandPalette}
aria-label="Close quick actions"
aria-label={t("session.close_quick_actions")}
>
<X size={14} />
</button>
@@ -3594,7 +3594,7 @@ export default function SessionView(props: SessionViewProps) {
when={commandPaletteItems().length > 0}
fallback={
<div class="px-3 py-6 text-sm text-dls-secondary text-center">
No matches.
{t("session.no_matches_command")}
</div>
}
>
@@ -3640,8 +3640,8 @@ export default function SessionView(props: SessionViewProps) {
</div>
<div class="border-t border-dls-border px-3 py-2 text-[11px] text-dls-secondary flex items-center justify-between gap-2">
<span>Arrow keys to navigate</span>
<span>Enter to run · Esc to close</span>
<span>{t("session.palette_hint_navigate")}</span>
<span>{t("session.palette_hint_run")}</span>
</div>
</div>
</div>
@@ -3676,14 +3676,14 @@ export default function SessionView(props: SessionViewProps) {
<ConfirmModal
open={deleteSessionOpen()}
title="Delete session?"
title={t("session.delete_session_title")}
message={
sessionTitleForId(deleteSessionId()).trim()
? `This will permanently delete \"${sessionTitleForId(deleteSessionId()).trim()}\" and its messages.`
: "This will permanently delete the selected session and its messages."
? t("session.delete_named_session_message", undefined, { title: sessionTitleForId(deleteSessionId()).trim() })
: t("session.delete_session_generic")
}
confirmLabel={deleteSessionBusy() ? "Deleting..." : "Delete"}
cancelLabel="Cancel"
confirmLabel={deleteSessionBusy() ? t("session.deleting") : t("session.delete")}
cancelLabel={t("common.cancel")}
variant="danger"
onConfirm={confirmDeleteSession}
onCancel={closeDeleteSessionModal}
@@ -3765,7 +3765,7 @@ export default function SessionView(props: SessionViewProps) {
<div class="bg-gray-1/50 rounded-xl p-4 border border-gray-6 mb-6">
<div class="text-xs text-gray-10 uppercase tracking-wider mb-2 font-semibold">
Permission
{t("session.permission_label")}
</div>
<div class="text-sm text-gray-12 font-mono">
{activePermissionPresentation().permissionLabel}
@@ -3793,7 +3793,7 @@ export default function SessionView(props: SessionViewProps) {
>
<details class="mt-4 rounded-lg bg-gray-1/20 p-2">
<summary class="cursor-pointer text-xs text-gray-11">
Details
{t("session.details_label")}
</summary>
<pre class="mt-2 whitespace-pre-wrap break-words text-xs text-gray-12">
{props.safeStringify(props.activePermission?.metadata)}
@@ -3812,7 +3812,7 @@ export default function SessionView(props: SessionViewProps) {
}
disabled={props.permissionReplyBusy}
>
Deny
{t("session.deny")}
</Button>
<div class="grid grid-cols-2 gap-2">
<Button
@@ -3824,7 +3824,7 @@ export default function SessionView(props: SessionViewProps) {
}
disabled={props.permissionReplyBusy}
>
Once
{t("session.allow_once")}
</Button>
<Button
variant="primary"
@@ -3838,7 +3838,7 @@ export default function SessionView(props: SessionViewProps) {
}
disabled={props.permissionReplyBusy}
>
Allow for session
{t("session.allow_for_session")}
</Button>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -237,7 +237,7 @@ export default function SkillsView(props: SkillsViewProps) {
});
});
const maskError = (value: unknown) => (value instanceof Error ? value.message : "Something went wrong");
const maskError = (value: unknown) => (value instanceof Error ? value.message : translate("common.something_went_wrong"));
const showToast = (title: string, tone: AppStatusToastTone = "info") => {
statusToasts.showToast({ title, tone });
};
@@ -245,7 +245,7 @@ export default function SkillsView(props: SkillsViewProps) {
const hubRepoKey = (repo: HubSkillRepo) => `${repo.owner}/${repo.repo}@${repo.ref}`;
const defaultHubRepoKey = "different-ai/openwork-hub@main";
const activeHubRepoLabel = createMemo(() => (extensions.hubRepo() ? hubRepoKey(extensions.hubRepo()!) : "No hub repo selected"));
const activeHubRepoLabel = createMemo(() => (extensions.hubRepo() ? hubRepoKey(extensions.hubRepo()!) : translate("skills.no_hub_repo_label")));
const hasDefaultHubRepo = createMemo(() => extensions.hubRepos().some((repo) => hubRepoKey(repo) === defaultHubRepoKey));
@@ -273,7 +273,7 @@ export default function SkillsView(props: SkillsViewProps) {
const repo = customRepoName().trim();
const ref = customRepoRef().trim() || "main";
if (!owner || !repo) {
setCustomRepoError("Owner and repo are required.");
setCustomRepoError(translate("skills.owner_repo_required"));
return;
}
extensions.addHubRepo({ owner, repo, ref });
@@ -387,7 +387,7 @@ export default function SkillsView(props: SkillsViewProps) {
const installFromHub = async (skill: HubSkillCard) => {
if (props.busy || installingHubSkill()) return;
setInstallingHubSkill(skill.name);
showToast(`Installing ${skill.name}`);
showToast(`${translate("skills.installing_prefix")} ${skill.name}`);
try {
const result = await extensions.installHubSkill(skill.name);
showToast(result.message, "success");
@@ -488,7 +488,7 @@ export default function SkillsView(props: SkillsViewProps) {
try {
const skill = await extensions.readSkill(target.name);
if (!skill) throw new Error("Failed to load skill");
if (!skill) throw new Error(translate("skills.skill_load_failed"));
const payload: SkillBundleV1 = {
schemaVersion: 1,
@@ -508,7 +508,7 @@ export default function SkillsView(props: SkillsViewProps) {
setShareUrl(result.url);
try {
await navigator.clipboard.writeText(result.url);
showToast("Link copied", "success");
showToast(translate("skills.link_copied"), "success");
} catch {
// ignore
}
@@ -524,9 +524,9 @@ export default function SkillsView(props: SkillsViewProps) {
if (!url) return;
try {
await navigator.clipboard.writeText(url);
showToast("Link copied", "success");
showToast(translate("skills.link_copied"), "success");
} catch {
setShareError("Failed to copy link");
setShareError(translate("skills.copy_link_failed"));
}
};
@@ -540,12 +540,12 @@ export default function SkillsView(props: SkillsViewProps) {
try {
const result = await extensions.readSkill(skill.name);
if (!result) {
setSelectedError("Failed to load skill.");
setSelectedError(translate("skills.skill_load_failed"));
return;
}
setSelectedContent(result.content);
} catch (e) {
setSelectedError(e instanceof Error ? e.message : "Failed to load skill.");
setSelectedError(e instanceof Error ? e.message : translate("skills.skill_load_failed"));
} finally {
setSelectedLoading(false);
}
@@ -574,7 +574,7 @@ export default function SkillsView(props: SkillsViewProps) {
);
setSelectedDirty(false);
} catch (e) {
setSelectedError(e instanceof Error ? e.message : "Failed to save skill.");
setSelectedError(e instanceof Error ? e.message : translate("skills.save_failed"));
}
};
@@ -619,7 +619,7 @@ export default function SkillsView(props: SkillsViewProps) {
<h2 class={pageTitleClass}>{translate("skills.title")}</h2>
</Show>
<p class="mt-2 max-w-2xl text-[14px] leading-relaxed text-dls-secondary">
Skills are the core abilities of this worker. Discover them from Hub, manage what is installed, and create new ones directly in chat.
{translate("skills.worker_profile_desc")}
</p>
</div>
@@ -631,7 +631,7 @@ export default function SkillsView(props: SkillsViewProps) {
class={pillSecondaryClass}
>
<Upload size={14} />
Import local skill
{translate("skills.import_local_skill")}
</button>
<button
type="button"
@@ -640,7 +640,7 @@ export default function SkillsView(props: SkillsViewProps) {
class={pillSecondaryClass}
>
<FolderOpen size={14} />
Reveal folder
{translate("skills.reveal_folder")}
</button>
<button
type="button"
@@ -649,7 +649,7 @@ export default function SkillsView(props: SkillsViewProps) {
class={pillPrimaryClass}
>
<Sparkles size={14} />
Create skill in chat
{translate("skills.create_in_chat")}
</button>
</div>
</div>
@@ -691,7 +691,7 @@ export default function SkillsView(props: SkillsViewProps) {
class={pillSecondaryClass}
>
<RefreshCw size={14} />
Refresh
{translate("common.refresh")}
</button>
</div>
</div>
@@ -722,10 +722,10 @@ export default function SkillsView(props: SkillsViewProps) {
<div>
<h3 class={sectionTitleClass}>{translate("skills.installed")}</h3>
<p class="mt-1 text-[13px] text-dls-secondary">
Installed skills live on this worker and can be edited or shared.
{translate("skills.installed_desc")}
</p>
</div>
<div class="text-[12px] text-dls-secondary">{filteredSkills().length} shown</div>
<div class="text-[12px] text-dls-secondary">{t("skills.shown_count", currentLocale(), { count: filteredSkills().length })}</div>
</div>
<Show
@@ -764,7 +764,7 @@ export default function SkillsView(props: SkillsViewProps) {
<span class={tagClass}>OpenWork</span>
</Show>
</div>
<Show when={skill.description} fallback={<p class="mt-2 text-[13px] text-dls-secondary">No description yet.</p>}>
<Show when={skill.description} fallback={<p class="mt-2 text-[13px] text-dls-secondary">{translate("skills.no_description")}</p>}>
<p class="mt-2 line-clamp-2 text-[13px] leading-relaxed text-dls-secondary">
{skill.description}
</p>
@@ -773,7 +773,7 @@ export default function SkillsView(props: SkillsViewProps) {
</div>
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-dls-border pt-4">
<span class={tagClass}>Installed</span>
<span class={tagClass}>{translate("skills.installed_status")}</span>
<div class="flex flex-wrap gap-2">
<button
type="button"
@@ -784,10 +784,10 @@ export default function SkillsView(props: SkillsViewProps) {
openShareLink(skill);
}}
disabled={props.busy}
title="Share"
title={translate("skills.share_title")}
>
<Share2 size={14} />
Share
{translate("skills.share_title")}
</button>
<button
type="button"
@@ -798,10 +798,10 @@ export default function SkillsView(props: SkillsViewProps) {
void openSkill(skill);
}}
disabled={props.busy}
title="Edit"
title={translate("common.edit")}
>
<Edit2 size={14} />
Edit
{translate("common.edit")}
</button>
<button
type="button"
@@ -819,7 +819,7 @@ export default function SkillsView(props: SkillsViewProps) {
title={translate("skills.uninstall")}
>
<Trash2 size={14} />
Remove
{translate("common.remove")}
</button>
</div>
</div>
@@ -961,9 +961,9 @@ export default function SkillsView(props: SkillsViewProps) {
<div class="space-y-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<h3 class={sectionTitleClass}>Available from Hub</h3>
<h3 class={sectionTitleClass}>{translate("skills.available_from_hub")}</h3>
<p class="mt-1 text-[13px] text-dls-secondary">
Browse shared skills from GitHub-backed hubs and add them to this worker.
{translate("skills.hub_desc")}
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
@@ -977,34 +977,34 @@ export default function SkillsView(props: SkillsViewProps) {
disabled={props.busy || hasDefaultHubRepo()}
>
<Plus size={14} />
Add OpenWork Hub
{translate("skills.add_openwork_hub")}
</button>
<button
type="button"
onClick={openCustomRepoModal}
disabled={props.busy}
class={pillSecondaryClass}
title="Add custom GitHub repo"
title={translate("skills.add_custom_repo")}
>
<Plus size={14} />
Add git repo
{translate("skills.add_git_repo")}
</button>
<button
type="button"
onClick={() => void extensions.refreshHubSkills({ force: true })}
disabled={props.busy}
class={pillSecondaryClass}
title="Refresh hub catalog"
title={translate("skills.refresh_hub_title")}
>
<RefreshCw size={14} />
Refresh hub
{translate("skills.refresh_hub")}
</button>
</div>
</div>
<div class="space-y-3 rounded-[20px] border border-dls-border bg-dls-surface p-4">
<div class="text-[12px] text-dls-secondary">
Source: <span class="font-mono text-dls-text">{activeHubRepoLabel()}</span>
{translate("skills.source_label")}: <span class="font-mono text-dls-text">{activeHubRepoLabel()}</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<For each={extensions.hubRepos()}>
@@ -1031,7 +1031,7 @@ export default function SkillsView(props: SkillsViewProps) {
void extensions.refreshHubSkills({ force: true });
}}
disabled={props.busy}
title="Remove saved repo"
title={translate("skills.remove_saved_repo")}
>
×
</button>
@@ -1052,7 +1052,7 @@ export default function SkillsView(props: SkillsViewProps) {
when={filteredHubSkills().length}
fallback={
<div class="rounded-[20px] border border-dashed border-dls-border bg-dls-surface px-5 py-8 text-[14px] text-dls-secondary">
{extensions.hubRepo() ? "No hub skills available." : "No hub repo selected. Add a GitHub repo to browse skills."}
{extensions.hubRepo() ? translate("skills.no_hub_skills") : translate("skills.no_hub_repo_selected")}
</div>
}
>
@@ -1069,7 +1069,7 @@ export default function SkillsView(props: SkillsViewProps) {
<h4 class="text-[14px] font-semibold text-dls-text truncate">{skill.name}</h4>
<Show
when={skill.description}
fallback={<p class="mt-2 text-[13px] text-dls-secondary">From {skill.source.owner}/{skill.source.repo}</p>}
fallback={<p class="mt-2 text-[13px] text-dls-secondary">{t("skills.from_repo", currentLocale(), { owner: skill.source.owner, repo: skill.source.repo })}</p>}
>
<p class="mt-2 line-clamp-2 text-[13px] leading-relaxed text-dls-secondary">{skill.description}</p>
</Show>
@@ -1078,8 +1078,8 @@ export default function SkillsView(props: SkillsViewProps) {
{skill.source.owner}/{skill.source.repo}
</span>
<Show when={skill.trigger}>
<span class={tagClass} title={`Trigger: ${skill.trigger}`}>
Trigger: {skill.trigger}
<span class={tagClass} title={t("skills.trigger_label", currentLocale(), { trigger: skill.trigger ?? "" })}>
{t("skills.trigger_label", currentLocale(), { trigger: skill.trigger ?? "" })}
</span>
</Show>
</div>
@@ -1087,7 +1087,7 @@ export default function SkillsView(props: SkillsViewProps) {
</div>
<div class="flex items-center justify-between gap-3 border-t border-dls-border pt-4">
<span class={tagClass}>Hub</span>
<span class={tagClass}>{translate("skills.hub_label")}</span>
<button
type="button"
class={installingHubSkill() === skill.name ? pillSecondaryClass : pillPrimaryClass}
@@ -1097,7 +1097,7 @@ export default function SkillsView(props: SkillsViewProps) {
void installFromHub(skill);
}}
disabled={props.busy || installingHubSkill() === skill.name}
title={`Install ${skill.name}`}
title={t("skills.install_name_title", currentLocale(), { name: skill.name })}
>
<Show
when={installingHubSkill() === skill.name}
@@ -1105,7 +1105,7 @@ export default function SkillsView(props: SkillsViewProps) {
>
<Loader2 size={14} class="animate-spin" />
</Show>
{installingHubSkill() === skill.name ? "Installing" : "Add skill"}
{installingHubSkill() === skill.name ? translate("skills.installing") : translate("common.add")}
</button>
</div>
</div>
@@ -1135,14 +1135,14 @@ export default function SkillsView(props: SkillsViewProps) {
disabled={!selectedDirty() || props.busy}
onClick={() => void saveSelectedSkill()}
>
Save
{translate("common.save")}
</button>
<button
type="button"
class="px-3 py-1.5 text-xs font-medium rounded-lg bg-dls-hover text-dls-text hover:bg-dls-active transition-colors"
onClick={closeSkill}
>
Close
{translate("common.close")}
</button>
</div>
</div>
@@ -1155,7 +1155,7 @@ export default function SkillsView(props: SkillsViewProps) {
</Show>
<Show
when={!selectedLoading()}
fallback={<div class="text-xs text-dls-secondary">Loading</div>}
fallback={<div class="text-xs text-dls-secondary">{translate("skills.loading")}</div>}
>
<textarea
value={selectedContent()}
@@ -1408,15 +1408,15 @@ export default function SkillsView(props: SkillsViewProps) {
<div class="bg-dls-surface border border-dls-border w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden">
<div class="p-6 space-y-4">
<div>
<h3 class="text-lg font-semibold text-dls-text">Add custom GitHub repo</h3>
<h3 class="text-lg font-semibold text-dls-text">{translate("skills.add_custom_repo")}</h3>
<p class="text-sm text-dls-secondary mt-1">
Skills are loaded from <span class="font-mono">skills/&lt;name&gt;/SKILL.md</span>.
{translate("skills.github_repo_hint")}
</p>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label class="space-y-1">
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">Owner</div>
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">{translate("skills.owner_label")}</div>
<input
type="text"
value={customRepoOwner()}
@@ -1427,7 +1427,7 @@ export default function SkillsView(props: SkillsViewProps) {
/>
</label>
<label class="space-y-1">
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">Repo</div>
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">{translate("skills.repo_label")}</div>
<input
type="text"
value={customRepoName()}
@@ -1440,7 +1440,7 @@ export default function SkillsView(props: SkillsViewProps) {
</div>
<label class="space-y-1">
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">Ref (branch/tag/commit)</div>
<div class="text-xs font-semibold uppercase tracking-widest text-dls-secondary">{translate("skills.ref_label")}</div>
<input
type="text"
value={customRepoRef()}
@@ -1462,7 +1462,7 @@ export default function SkillsView(props: SkillsViewProps) {
{translate("common.cancel")}
</Button>
<Button variant="secondary" onClick={saveCustomRepo} disabled={props.busy}>
Save and load
{translate("skills.save_and_load")}
</Button>
</div>
</div>

View File

@@ -7,7 +7,6 @@ import {
type Accessor,
} from "solid-js";
import { t } from "../../i18n";
import {
publishSkillsSetBundleFromWorkspace,
@@ -29,6 +28,7 @@ import type {
WorkspaceInfo,
} from "../lib/tauri";
import type { OpenworkServerSettings } from "../lib/openwork-server";
import { t } from "../../i18n";
import { isTauriRuntime, normalizeDirectoryPath } from "../utils";
export type ShareWorkspaceState = ReturnType<typeof createShareWorkspaceState>;
@@ -199,28 +199,28 @@ export function createShareWorkspaceState(options: ShareWorkspaceStateOptions) {
label: t("session.share_worker_url"),
value: url,
placeholder: !isTauriRuntime()
? t("session.share_desktop_required")
? t("session.share_desktop_app_required")
: t("session.share_starting_server"),
hint: mountedUrl
? t("session.share_url_worker_hint")
? t("session.share_worker_url_phones_hint")
: hostUrl
? t("session.share_url_resolving_hint")
? t("session.share_worker_url_resolving_hint")
: undefined,
},
{
label: t("session.share_password"),
value: ownerToken,
secret: true,
placeholder: isTauriRuntime() ? "-" : t("session.share_desktop_required"),
placeholder: isTauriRuntime() ? "-" : t("session.share_desktop_app_required"),
hint: mountedUrl
? t("session.share_url_worker_hint")
: t("session.share_password_owner_hint"),
? t("session.share_worker_url_phones_hint")
: t("session.share_owner_permission_hint"),
},
{
label: t("session.share_collaborator_token"),
label: t("session.share_collaborator_label"),
value: collaboratorToken,
secret: true,
placeholder: isTauriRuntime() ? "-" : t("session.share_desktop_required"),
placeholder: isTauriRuntime() ? "-" : t("session.share_desktop_app_required"),
hint: mountedUrl
? t("session.share_collaborator_hint")
: t("session.share_collaborator_host_hint"),

View File

@@ -4,18 +4,19 @@ import ja from "./locales/ja";
import zh from "./locales/zh";
import vi from "./locales/vi";
import ptBR from "./locales/pt-BR";
import th from "./locales/th";
import { LANGUAGE_PREF_KEY } from "../app/constants";
/**
* Supported languages
*/
export type Language = "en" | "ja" | "zh" | "vi" | "pt-BR";
export type Language = "en" | "ja" | "zh" | "vi" | "pt-BR" | "th";
export type Locale = Language;
/**
* All supported languages - single source of truth
*/
export const LANGUAGES: Language[] = ["en", "ja", "zh", "vi", "pt-BR"];
export const LANGUAGES: Language[] = ["en", "ja", "zh", "vi", "pt-BR", "th"];
/**
* Language options for UI - single source of truth
@@ -26,6 +27,7 @@ export const LANGUAGE_OPTIONS = [
{ value: "zh" as Language, label: "简体中文", nativeName: "简体中文" },
{ value: "vi" as Language, label: "Vietnamese", nativeName: "Tiếng Việt" },
{ value: "pt-BR" as Language, label: "Portuguese (BR)", nativeName: "Português (BR)" },
{ value: "th" as Language, label: "ไทย", nativeName: "ไทย" },
] as const;
/**
@@ -37,6 +39,7 @@ const TRANSLATIONS: Record<Language, Record<string, string>> = {
zh,
vi,
"pt-BR": ptBR,
th,
};
/**

View File

@@ -197,6 +197,19 @@ export default {
"session.rename_description": "Update the name for this session.",
"session.rename_label": "Session name",
"session.rename_placeholder": "Enter a new name",
"session.provider_connected": "Provider connected",
"session.oauth_failed": "OAuth failed",
"session.api_key_saved": "API key saved",
"session.failed_to_save_api_key": "Failed to save API key",
"session.quick_actions_title": "Quick actions (Ctrl/Cmd+K)",
"session.quick_actions_label": "Quick actions",
"session.menu_label": "Menu",
"session.search_conversation_title": "Search conversation (Ctrl/Cmd+F)",
"session.search_conversation_label": "Search conversation",
"session.undo_title": "Undo last message",
"session.revert_label": "Revert",
"session.redo_title": "Redo last reverted message",
"session.redo_aria_label": "Redo last reverted message",
// ==================== Commands ====================
"commands.new": "New",
"commands.empty_state": "Save a prompt or command so you can run it again in one tap.",
@@ -966,6 +979,145 @@ export default {
"app.error_connection_failed_url": "Connection failed. Check the URL and token.",
"app.error_connection_failed": "Connection failed",
// ==================== Status bar labels ====================
"status.openwork_ready": "OpenWork Ready",
"status.limited_mode": "Limited Mode",
"status.disconnected_label": "Disconnected",
"status.ready_for_tasks": "Ready for new tasks",
"status.developer_mode": "Developer mode",
"status.providers_connected": "{count} provider{plural} connected",
"status.mcp_connected": "{count} MCP connected",
"status.limited_hint": "Reconnect to restore full OpenWork features",
"status.limited_mcp_hint": "{count} MCP connected · reconnect for full features",
"status.disconnected_hint": "Open settings to reconnect",
"status.back": "Back to previous screen",
"status.feedback": "Feedback",
"status.send_feedback": "Send feedback",
"status.settings": "Settings",
// ==================== Message List ====================
"message.waiting_subagent": "Waiting for the subagent transcript to arrive.",
"message.tool_request_label": "Request",
"message.tool_result_label": "Result",
// ==================== Message List (additional) ====================
"message_list.copy_message": "Copy message",
// ==================== Composer ====================
"composer.no_matches": "No matches found.",
"composer.loading_agents": "Loading agents...",
"composer.run_task": "Run task",
"composer.stop": "Stop",
"composer.agent_label": "Agent",
"composer.default_agent": "Default agent",
"composer.loading_commands": "Loading commands...",
"composer.no_commands": "No commands found.",
"composer.skill_source": "Skill",
"composer.attach_files": "Attach files",
"composer.image_kind": "Image",
"composer.file_kind": "File",
"composer.expand_pasted": "Click to expand pasted text",
"composer.failed_read_attachment": "Failed to read attachment",
"composer.attachments_unavailable": "Attachments are unavailable.",
"composer.file_exceeds_limit": "{name} exceeds the 8MB limit.",
"composer.file_too_large_encoding": "{name} is too large after encoding. Try a smaller image.",
"composer.uploaded_single_file": "Uploaded {name} to the shared folder and inserted a link.",
"composer.uploaded_multiple_files": "Uploaded {count} files to the shared folder and inserted links.",
"composer.upload_failed_local_links": "Couldn't upload to the shared folder. Inserted local links instead.",
"composer.unsupported_attachment_type": "Unsupported attachment type.",
"composer.inserted_links_unsupported": "Inserted links for unsupported files.",
"composer.remote_worker_paste_warning": "This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Shared folder in the sidebar.",
"composer.upload_to_shared_folder": "Upload to shared folder",
"composer.placeholder": "Describe your task...",
"composer.behavior_label": "Behavior",
// ==================== Context Panel ====================
"context_panel.context": "Context",
"context_panel.working_files": "Working files",
"context_panel.plugins": "Plugins",
"context_panel.no_plugins": "No plugins loaded.",
"context_panel.mcp": "MCP",
"context_panel.no_mcp": "No MCP servers loaded.",
"context_panel.skills": "Skills",
"context_panel.no_skills": "No skills loaded.",
"context_panel.authorized_folders": "Authorized folders",
"context_panel.none_yet": "None yet.",
"context_panel.mcp_disabled": "Disabled",
"context_panel.mcp_disconnected": "Disconnected",
"context_panel.mcp_connected": "Connected",
"context_panel.mcp_needs_auth": "Needs auth",
"context_panel.mcp_register_client": "Register client",
"context_panel.mcp_failed": "Failed",
"context_panel.open_file": "Open {path}",
// ==================== Inbox Panel ====================
"inbox_panel.shared_folder": "Shared folder",
"inbox_panel.refresh_tooltip": "Refresh shared folder",
"inbox_panel.uploading": "Uploading...",
"inbox_panel.upload_prompt": "Drop files or click to upload",
"inbox_panel.helper_text": "Share files with this worker from the app.",
"inbox_panel.connect_to_see": "Connect to see shared files.",
"inbox_panel.no_files": "No shared files yet.",
"inbox_panel.showing_first": "Showing first {count}.",
"inbox_panel.upload_needs_worker": "Connect to a worker to upload files to the shared folder.",
"inbox_panel.uploading_label": "Uploading {label}...",
"inbox_panel.upload_success": "Uploaded to the shared folder.",
"inbox_panel.upload_failed": "Shared folder upload failed",
"inbox_panel.copy_failed": "Copy failed. Your browser may block clipboard access.",
"inbox_panel.connect_to_download": "Connect to a worker to download shared files.",
"inbox_panel.missing_file_id": "Missing shared file id.",
"inbox_panel.download": "Download",
"inbox_panel.load_failed": "Failed to load shared folder",
"inbox_panel.drop_to_upload": "Drop files here to upload",
"inbox_panel.connect_to_upload": "Connect to a worker to upload",
// ==================== Session Sidebar ====================
"sidebar.no_workspaces": "No workspaces in this session yet. Add one to get started.",
"sidebar.needs_attention": "Needs attention",
"sidebar.active": "Active",
"sidebar.switch": "Switch",
"sidebar.expand": "Expand",
"sidebar.collapse": "Collapse",
"sidebar.drag_reorder": "Drag to reorder",
"sidebar.edit_connection": "Edit connection",
"sidebar.test_connection": "Test connection",
"sidebar.stop_sandbox": "Stop sandbox",
"sidebar.remove_workspace": "Remove workspace",
"sidebar.show_fewer": "Show fewer",
"sidebar.show_more": "Show {count} more",
"sidebar.add_workspace": "Add new workspace",
"sidebar.new_worker": "New worker",
"sidebar.connect_remote": "Connect remote",
"sidebar.import_config": "Import config",
"sidebar.progress": "Progress",
"sidebar.delete_session": "Delete session",
"sidebar.no_sessions_yet": "No sessions yet",
// ==================== Workspace Session List ====================
"workspace_list.connecting": "Connecting...",
"workspace_list.workspace_fallback": "Workspace",
"workspace_list.reveal_explorer": "Reveal in Explorer",
"workspace_list.reveal_finder": "Reveal in Finder",
"workspace_list.show_more": "Show {count} more",
"workspace_list.show_more_fallback": "Show more",
"workspace_list.workspace_options": "Workspace options",
"workspace_list.session_actions": "Session actions",
"workspace_list.hide_child_sessions": "Hide child sessions",
"workspace_list.show_child_sessions": "Show child sessions",
"workspace_list.edit_name": "Edit name",
"workspace_list.share": "Share...",
"workspace_list.recover": "Recover",
"workspace_list.test_connection": "Test connection",
"workspace_list.edit_connection": "Edit connection",
"workspace_list.remove_workspace": "Remove workspace",
"workspace_list.rename_session": "Rename session",
"workspace_list.delete_session": "Delete session",
"workspace_list.add_workspace": "Add workspace",
"workspace_list.new_workspace": "New workspace",
"workspace_list.connect_remote": "Connect remote workspace",
"workspace_list.import_config": "Import config",
"workspace_list.desktop_only_hint": "Create local workspaces in the desktop app.",
// ==================== Workspace badges ====================
"workspace.sandbox_badge": "Sandbox",
"workspace.remote_badge": "Remote",
@@ -1243,6 +1395,131 @@ export default {
"session.share_password": "Password",
"session.share_password_owner_hint": "Use when the remote client must answer permission prompts.",
"session.share_collaborator_token": "Collaborator token",
// ==================== Workspace (additions) ====================
"workspace.needs_attention": "Needs attention",
"workspace.active": "Active",
"workspace.switch": "Switch",
"workspace.no_tasks": "No tasks yet.",
"workspace.new_task_inline": "+ New task",
"workspace.loading_tasks": "Loading tasks...",
"workspace.selected": "Selected",
// ==================== Common (additions) ====================
"common.copy": "Copy",
"common.copied": "Copied",
"common.reset": "Reset",
"common.add": "Add",
"common.done": "Done",
// ==================== Session view ====================
"session.permission_message": "OpenCode is requesting permission to continue.",
"session.doom_loop_title": "Doom Loop Detected",
"session.doom_loop_message": "OpenCode detected repeated tool calls with identical input and is asking whether it should continue after repeated failures.",
"session.doom_loop_label": "Doom Loop",
"session.doom_loop_tool_label": "Tool",
"session.doom_loop_repeated_call_label": "Repeated call",
"session.doom_loop_repeated_tool_call": "Repeated tool call",
"session.doom_loop_note": "Reject to stop the loop, or allow if you want the agent to keep trying.",
"session.default_agent": "Default agent",
"session.workspace_label": "Workspace",
"session.no_matches": "No matches",
"session.status_delegating": "Delegating",
"session.status_planning": "Planning",
"session.status_gathering_context": "Gathering context",
"session.status_searching_codebase": "Searching codebase",
"session.status_searching_web": "Searching the web",
"session.status_writing_file": "Writing file",
"session.status_running_shell": "Running shell",
"session.status_working": "Working",
"session.status_thinking": "Thinking",
"session.phase_sending": "Sending",
"session.phase_retrying": "Retrying",
"session.phase_responding": "Responding",
"session.phase_run_failed": "Run failed",
"session.compacting_auto": "OpenCode is auto-compacting this session",
"session.compacting_manual": "OpenCode is compacting this session",
"session.compaction_started": "OpenCode started compacting the session context.",
"session.compaction_finished": "OpenCode finished compacting the session context.",
"session.no_session_selected": "No session selected",
"session.stopping_run": "Stopping the run...",
"session.stopped": "Stopped.",
"session.failed_to_stop": "Failed to stop",
"session.nothing_to_retry": "Nothing to retry yet",
"session.trying_again": "Trying again...",
"session.nothing_to_undo": "Nothing to undo yet.",
"session.reverted_last_message": "Reverted the last user message.",
"session.failed_to_undo": "Failed to undo",
"session.nothing_to_redo": "Nothing to redo.",
"session.restored_message": "Restored the reverted message.",
"session.failed_to_redo": "Failed to redo",
"session.nothing_to_compact": "Nothing to compact yet.",
"session.compacting": "Compacting session context...",
"session.compacted": "Session compacted.",
"session.failed_to_compact": "Failed to compact session",
"session.deleted": "Session deleted",
"session.failed_to_delete": "Failed to delete session",
"session.failed_to_load_agents": "Failed to load agents",
"session.failed_to_create_session": "Failed to create session",
"session.failed_to_load_providers": "Failed to load providers",
"session.cmd_new_session_title": "Create new session",
"session.cmd_new_session_detail": "Start a fresh task in the current workspace",
"session.cmd_new_session_meta": "Create",
"session.cmd_rename_title": "Rename current session",
"session.cmd_rename_detail_fallback": "Give your selected session a clearer name",
"session.cmd_rename_meta": "Rename",
"session.cmd_compact_title": "Compact Conversation",
"session.cmd_compact_detail": "Send a compact instruction to OpenCode for this session",
"session.cmd_compact_detail_empty": "No user messages to compact yet",
"session.cmd_compact_meta": "Compact",
"session.cmd_sessions_title": "Search sessions",
"session.cmd_sessions_meta": "Jump",
"session.cmd_model_title": "Change model",
"session.cmd_model_meta": "Open",
"session.cmd_model_fallback": "Model",
"session.cmd_provider_title": "Connect provider",
"session.cmd_provider_detail": "Open provider connection flow",
"session.cmd_provider_meta": "Open",
"session.cmd_current_workspace": "Current workspace",
"session.cmd_switch": "Switch",
"session.palette_title_sessions": "Search sessions",
"session.palette_title_actions": "Quick actions",
"session.palette_placeholder_sessions": "Find by session title or workspace",
"session.palette_placeholder_actions": "Search actions",
"session.search_placeholder": "Search in this chat",
"session.search_prev": "Prev",
"session.search_next": "Next",
"session.undo_label": "Revert",
"session.redo_label": "Redo",
"session.loading_title": "Loading session",
"session.loading_detail": "Pulling in the latest messages for this task.",
"session.workspace_setup_label": "Workspace setup",
"session.workspace_setup_title": "Set up your first workspace",
"session.workspace_setup_desc": "Start with a guided OpenWork workspace, or choose an existing folder you want to work in.",
"session.create_workspace_title": "Create workspace",
"session.create_workspace_desc": "Open the workspace creator and choose how you want to start.",
"session.pick_folder_title": "Pick a folder you want to work in",
"session.pick_folder_desc": "Choose an existing project or notes folder and OpenWork will use it as your workspace.",
"session.loading_earlier": "Loading earlier messages...",
"session.load_earlier": "Load earlier messages",
"session.jump_to_start": "Jump to start of message",
"session.jump_to_latest": "Jump to latest",
"session.flyout_new_task": "New Task",
"session.flyout_file_modified": "File Modified",
"session.attachments_add_token": "Add a server token to attach files.",
"session.attachments_connect_server": "Connect to OpenWork server to attach files.",
"session.unable_to_reveal": "Unable to reveal workspace",
"session.workspace_path_unavailable": "Workspace path is unavailable.",
"session.reveal_desktop_only": "Reveal is available in the desktop app.",
"session.file_open_remote_unavailable": "File open is unavailable for remote workspaces.",
"session.file_open_desktop_only": "File open is available in the desktop app.",
"session.pick_workspace_to_open": "Pick a workspace to open files.",
"session.unable_to_open_file": "Unable to open file",
"session.share_collaborator_label": "Collaborator token",
"session.share_desktop_app_required": "Desktop app required",
"session.share_worker_url_phones_hint": "Use on phones or laptops connecting to this worker.",
"session.share_worker_url_resolving_hint": "Worker URL is resolving; host URL shown as fallback.",
"session.share_owner_permission_hint": "Use when the remote client must answer permission prompts.",
"session.share_collaborator_hint": "Routine remote access when you do not need owner-only actions.",
"session.share_collaborator_host_hint": "Routine remote access to this host without owner-only actions.",
"session.share_set_token_hint": "Set token in workspace settings",
@@ -1276,6 +1553,13 @@ export default {
"session.install_update": "Install update",
"session.downloading": "Downloading",
"session.update_available": "Update available",
"session.connect_failed": "Connect failed",
"session.upload_connect_server": "Connect to the OpenWork server to upload files to the shared folder.",
"session.uploaded_to_shared_folder": "Uploaded to the shared folder.",
"session.create_or_connect_workspace": "Create or connect a workspace",
"session.allow_once": "Once",
"session.connect_to_sync": "Connect to OpenWork server to sync remote files.",
"session.obsidian_worker_relative_only": "Only worker-relative files can be opened in Obsidian.",
"session.resize_workspace_column": "Resize workspace column",
"dashboard.close_settings": "Close settings",
"dashboard.nav_ids": "IDs",
@@ -1284,4 +1568,71 @@ export default {
"session.restart_update_title": "Restart to apply update {version}",
"session.downloading_update_title": "Downloading update {version}",
"session.update_available_title": "Update available {version}",
"session.file_open_failed": "File open failed",
"session.remote_sync_failed": "Remote file sync failed",
"session.shared_folder_upload_failed": "Shared folder upload failed",
"session.status_compacting": "Compacting Context",
"session.status_active": "Session Active",
"session.status_ready_session": "Session Ready",
"session.status_ready": "Ready",
"session.delete_session_title": "Delete session?",
"session.delete_named_session_message": "This will permanently delete \"{title}\" and its messages.",
"session.delete_session_generic": "This will permanently delete the selected session and its messages.",
"session.deleting": "Deleting...",
"session.delete": "Delete",
"session.loading_session": "Loading session",
"session.workspace_fallback": "Workspace",
"session.model_fallback": "Model",
"session.close_quick_actions": "Close quick actions",
"session.prev_match": "Previous match",
"session.next_match": "Next match",
"session.close_search": "Close search",
"session.unable_to_open_obsidian": "Unable to open file in Obsidian",
"session.search_position": "{current} of {total}",
"session.conflict_sync_toast": "Conflict syncing {path}. Saved local changes to {conflictPath}.",
"session.todo_progress": "{completed} out of {total} tasks completed",
"session.uploading_to_shared_folder": "Uploading {label} to the shared folder...",
"session.uploaded_with_summary": "Uploaded to the shared folder: {summary}",
"session.cmd_sessions_detail": "{count} available across workspaces",
"session.cmd_model_detail": "{model} · {variant}",
"session.show_earlier": "Show {count} earlier message{plural}",
"session.back": "Back",
"session.no_matches_command": "No matches.",
"session.palette_hint_navigate": "Arrow keys to navigate",
"session.palette_hint_run": "Enter to run · Esc to close",
// ==================== Message List (tool headlines & subagent) ====================
"message_list.tool_run_command": "Run {command}",
"message_list.tool_run_command_fallback": "Run command",
"message_list.tool_reviewed_file": "Reviewed {file}",
"message_list.tool_reviewed_file_fallback": "Reviewed file",
"message_list.tool_updated_file": "Updated {file}",
"message_list.tool_updated_file_fallback": "Updated file",
"message_list.tool_update_file": "Update {file}",
"message_list.tool_update_file_fallback": "Update file",
"message_list.tool_searched_pattern": "Searched {pattern}",
"message_list.tool_searched_code_fallback": "Searched code",
"message_list.tool_reviewed_path": "Reviewed {path}",
"message_list.tool_reviewed_files_fallback": "Reviewed files",
"message_list.tool_delegate_agent": "Delegate {agent}",
"message_list.tool_delegate_task_fallback": "Delegate task",
"message_list.tool_update_todo": "Update todo list",
"message_list.tool_read_todo": "Read todo list",
"message_list.tool_checked_url": "Checked {url}",
"message_list.tool_checked_web_fallback": "Checked web page",
"message_list.tool_load_skill_named": "Load skill {name}",
"message_list.tool_load_skill_fallback": "Load skill",
"message_list.subagent_type_task": "{agentType} task",
"message_list.subagent_session_fallback": "Subagent session",
"message_list.subagent_loading_transcript": "Loading transcript",
"message_list.subagent_running": "Running",
"message_list.subagent_message_count": "{count} message{plural}",
"message_list.subagent_waiting_transcript": "Waiting for transcript",
"message_list.open_session": "Open session",
"message_list.step_updates_progress": "Updates progress",
// ==================== Inbox Panel (additional) ====================
"inbox_panel.copied_path": "Copied: {path}",
"inbox_panel.download_failed": "Download failed",
} as const;

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ export default {
"dashboard.runs": "Execuções",
"dashboard.find_workspace": "Buscar workspace...",
"dashboard.workspaces": "Workspaces",
"dashboard.no_workspaces": "Nenhum workspace encontrado.",
"dashboard.no_workspaces": "Nenhum workspace correspondente.",
"dashboard.new_workspace": "Novo Workspace...",
"dashboard.new_remote_workspace": "Adicionar Workspace Remoto...",
"dashboard.forget_workspace": "Esquecer workspace",
@@ -247,7 +247,7 @@ export default {
"skills.curated_packages": "Pacotes selecionados",
"skills.view": "Visualizar",
"skills.search_placeholder": "Buscar pacotes ou listas (ex: claude, registry, community)",
"skills.no_matches": "Nenhuma correspondência. Tente uma busca diferente.",
"skills.no_matches": "Nenhuma correspondência curada. Tente uma busca diferente.",
"skills.install_package": "Instalar",
"skills.registry_notice": "Publicar no registro OpenPackage requer autenticação. Uma busca no registro e sincronização de lista está planejada.",
"skills.installed": "Skills instaladas",
@@ -393,7 +393,7 @@ export default {
"mcp.edit_config_description": "Os apps são armazenados no arquivo de configuração do seu workspace.",
"mcp.docs_link": "Saiba mais",
"mcp.scope_project": "Este workspace",
"mcp.scope_global": "Todos os workers",
"mcp.scope_global": "Todos os workspaces",
"mcp.config_label": "Config",
"mcp.config_file": "Arquivo de configuração",
"mcp.config_not_loaded": "Ainda não carregado",
@@ -532,7 +532,7 @@ export default {
"mcp.auth.applying_changes_body": "Estamos reiniciando o worker para que o novo MCP esteja pronto para autenticar.",
"mcp.auth.waiting_for_conversation_title": "Aguardando conversa ser concluída",
"mcp.auth.waiting_for_conversation_body": "Vamos redirecioná-lo para autenticar assim que possível.",
"mcp.auth.waiting_for_session": "Aguardando {sessão} terminar",
"mcp.auth.waiting_for_session": "Aguardando {session} terminar",
"mcp.auth.force_stop": "Forçar parada",
"mcp.auth.force_stopping": "Parando...",
"mcp.auth.reload_before_oauth": "Recarregue o engine para concluir a configuração deste MCP antes de iniciar o OAuth.",
@@ -581,7 +581,7 @@ export default {
"settings.from_path": "Via PATH",
"settings.from_sidecar": "Sidecar integrado",
"settings.engine_source_description": "PATH usa o OpenCode instalado (padrão). Sidecar usará um binário integrado quando disponível.",
"settings.sidecar_unsupported": "Sidecar disponível no Windows",
"settings.sidecar_unsupported": "Sidecar está disponível no Windows",
"settings.sidecar_unavailable_detail": "Sidecar é integrado quando disponível.",
"settings.model": "Modelo",
"settings.model_description": "Padrões e controles de raciocínio para execuções.",
@@ -721,7 +721,7 @@ export default {
"reload.toast_title": "Atualizações disponíveis",
"reload.toast_description": "Recarregue o workspace para aplicar as alterações de configuração.",
"reload.toast_warning": "Para todas as tarefas ativas.",
"reload.toast_warning_active": "Recarregar para {count} tarefa(s) ativa(s).",
"reload.toast_warning_active": "O recarregamento para {count} tarefas ativas.",
"reload.toast_reload": "Recarregar",
"reload.toast_reload_stopped": "Recarregar e Parar Tarefas",
"reload.toast_reloading": "Recarregando...",
@@ -737,7 +737,7 @@ export default {
"onboarding.verifying": "Verificando handshake seguro",
"onboarding.create_first_workspace": "Crie seu primeiro workspace",
"onboarding.create_workspace": "Criar um workspace",
"onboarding.workspace_description": "Escolha uma pasta e um predefinição para configurar seu workspace.",
"onboarding.workspace_description": "Escolha uma pasta e uma predefinição para configurar seu workspace.",
"onboarding.start": "Iniciar OpenWork",
"onboarding.back": "Voltar",
"onboarding.advanced_settings": "Configurações avançadas",
@@ -765,7 +765,7 @@ export default {
"onboarding.theme_light": "Claro",
"onboarding.theme_dark": "Escuro",
"onboarding.access_label": "Acesso",
"onboarding.folders_allowed": "{count} pasta(s) permitida(s)",
"onboarding.folders_allowed": "{count} pasta{plural} permitida{plural}",
"onboarding.manage_access_hint": "Você pode gerenciar o acesso nas configurações avançadas.",
"onboarding.open_settings_hint": "Precisa de opções de engine ou acesso? Abra as Configurações.",
"onboarding.open_settings": "Abrir Configurações",
@@ -873,4 +873,753 @@ export default {
"app.error.command_name_template_required": "O nome e as instruções do comando são obrigatórios.",
"app.error.workspace_commands_desktop": "Comandos requerem o app desktop.",
"app.error.command_scope_unknown": "Este comando não pode ser gerenciado neste modo.",
// ==================== App (additional) ====================
"app.compact_command_desc": "Resumir esta sessão para reduzir o tamanho do contexto.",
"app.deep_link_auth_queued": "Link de autenticação Cloud enfileirado para o OpenWork.",
"app.deep_link_remote_queued": "Link de worker remoto enfileirado. O OpenWork deve entrar no fluxo de conexão.",
"app.error_audit_load": "Falha ao carregar o log de auditoria.",
"app.error_auth_failed": "Falha na autenticação",
"app.error_auto_compact_scope": "A compactação automática de contexto só pode ser alterada para um workspace local ou um workspace de servidor OpenWork com permissão de escrita.",
"app.error_cloud_signin": "Falha ao concluir o login no OpenWork Cloud.",
"app.error_command_not_resolved": "Comando não foi resolvido.",
"app.error_compact_empty": "Nada para compactar ainda.",
"app.error_compact_no_session": "Selecione uma sessão com mensagens antes de executar /compact.",
"app.error_compact_no_session_id": "Selecione uma sessão antes de compactar.",
"app.error_connect_first": "Conecte-se a este worker antes de aplicar alterações de runtime.",
"app.error_connection_failed": "Falha na conexão",
"app.error_connection_failed_url": "Falha na conexão. Verifique a URL e o token.",
"app.error_deep_link_unrecognized": "Esse link não é um deep link ou URL de compartilhamento reconhecido do OpenWork.",
"app.error_desktop_signin": "Login no desktop concluído, mas o OpenWork Cloud não retornou um token de sessão.",
"app.error_not_connected": "Não conectado a um servidor",
"app.error_pick_local_folder": "Selecione uma pasta de worker local antes de reiniciar o servidor local.",
"app.error_rate_limit": "Limite de requisições excedido",
"app.error_remote_access": "Falha ao atualizar o acesso remoto.",
"app.error_request_failed": "Falha na requisição",
"app.error_reset_config": "Falha ao redefinir as configurações padrão do app.",
"app.error_restart_local_worker": "Falha ao reiniciar o worker local com a configuração de compartilhamento atualizada.",
"app.error_runtime_changes": "Falha ao aplicar alterações de runtime.",
"app.error_session_name_required": "O nome da sessão é obrigatório",
"app.error_update_opencode_json": "Falha ao atualizar opencode.json",
"app.import_bundle_desc": "Escolha como importar este bundle.",
"app.import_shared_bundle": "Importar bundle compartilhado",
"app.local_disabled_reason": "Crie workspaces locais no app desktop. Workspaces remotos e compartilhados ainda funcionam aqui.",
"app.local_worker_detail": "Worker local",
"app.model_behavior_desc": "Escolha o modelo primeiro para ver os controles de comportamento específicos do provedor.",
"app.model_behavior_title": "Comportamento do modelo",
"app.plugins_hint_disconnected": "Servidor OpenWork indisponível. Plugins estão em modo somente leitura.",
"app.plugins_hint_limited": "O servidor OpenWork precisa de um token para editar plugins.",
"app.plugins_hint_readonly": "O servidor OpenWork está em modo somente leitura para plugins.",
"app.reload_later": "Depois",
"app.reload_now": "Recarregar agora",
"app.reload_stop_tasks": "Recarregar e Parar Tarefas",
"app.remote_worker_detail": "Worker remoto",
"app.reset_config_ok": "Configurações padrão do app redefinidas. Reinicie o OpenWork se alguma configuração obsoleta permanecer.",
"app.shared_setup": "Configuração compartilhada",
"app.skill_added": "Skill adicionada",
"app.skills_hint_disconnected": "Servidor OpenWork indisponível. Adicione a URL/token do servidor em Avançado para gerenciar skills.",
"app.skills_hint_limited": "O servidor OpenWork precisa de um token de host para instalar/atualizar skills. Adicione-o em Avançado e reconecte.",
"app.skills_hint_readonly": "O servidor OpenWork está em modo somente leitura para skills. Adicione um token de host em Avançado para habilitar instalações.",
"app.worker_fallback": "Worker",
// ==================== Common (additional) ====================
"common.add": "Adicionar",
"common.copied": "Copiado",
"common.copy": "Copiar",
"common.done": "Concluído",
"common.reset": "Redefinir",
"common.unknown": "Desconhecido",
// ==================== Composer ====================
"composer.agent_label": "Agente",
"composer.attach_files": "Anexar arquivos",
"composer.attachments_unavailable": "Anexos não estão disponíveis.",
"composer.behavior_label": "Comportamento",
"composer.default_agent": "Agente padrão",
"composer.expand_pasted": "Clique para expandir o texto colado",
"composer.failed_read_attachment": "Falha ao ler o anexo",
"composer.file_exceeds_limit": "{name} excede o limite de 8MB.",
"composer.file_kind": "Arquivo",
"composer.file_too_large_encoding": "{name} é muito grande após codificação. Tente uma imagem menor.",
"composer.image_kind": "Imagem",
"composer.inserted_links_unsupported": "Links inseridos para arquivos não suportados.",
"composer.loading_agents": "Carregando agentes...",
"composer.loading_commands": "Carregando comandos...",
"composer.no_commands": "Nenhum comando encontrado.",
"composer.no_matches": "Nenhuma correspondência encontrada.",
"composer.placeholder": "Descreva sua tarefa...",
"composer.remote_worker_paste_warning": "Este é um worker remoto. Sandboxes também são remotos. Para compartilhar arquivos, envie-os para a Pasta Compartilhada na barra lateral.",
"composer.run_task": "Executar tarefa",
"composer.skill_source": "Skill",
"composer.stop": "Parar",
"composer.unsupported_attachment_type": "Tipo de anexo não suportado.",
"composer.upload_failed_local_links": "Não foi possível enviar para a pasta compartilhada. Links locais inseridos.",
"composer.upload_to_shared_folder": "Enviar para pasta compartilhada",
"composer.uploaded_multiple_files": "{count} arquivos enviados para a pasta compartilhada com links inseridos.",
"composer.uploaded_single_file": "{name} enviado para a pasta compartilhada com link inserido.",
// ==================== Context Panel ====================
"context_panel.authorized_folders": "Pastas autorizadas",
"context_panel.context": "Contexto",
"context_panel.mcp": "MCP",
"context_panel.mcp_connected": "Conectado",
"context_panel.mcp_disabled": "Desativado",
"context_panel.mcp_disconnected": "Desconectado",
"context_panel.mcp_failed": "Falhou",
"context_panel.mcp_needs_auth": "Requer autenticação",
"context_panel.mcp_register_client": "Registrar cliente",
"context_panel.no_mcp": "Nenhum servidor MCP carregado.",
"context_panel.no_plugins": "Nenhum plugin carregado.",
"context_panel.no_skills": "Nenhuma skill carregada.",
"context_panel.none_yet": "Nenhum ainda.",
"context_panel.open_file": "Abrir {path}",
"context_panel.plugins": "Plugins",
"context_panel.skills": "Skills",
"context_panel.working_files": "Arquivos de trabalho",
// ==================== Dashboard (additional) ====================
"dashboard.access_token": "Token de acesso",
"dashboard.access_token_optional_hint": "Adicione um token apenas se o worker exigir.",
"dashboard.chooser_local_desc": "Crie um workspace neste dispositivo e, opcionalmente, comece a partir de um template de equipe.",
"dashboard.chooser_remote_desc": "Conecte a um worker OpenWork auto-hospedado usando uma URL e token de acesso.",
"dashboard.chooser_shared_desc": "Navegue por workers na nuvem compartilhados com sua organização e conecte em um passo.",
"dashboard.close_settings": "Fechar configurações",
"dashboard.cloud_signin_button": "Continuar com Cloud",
"dashboard.cloud_signin_hint": "Acesse workers remotos compartilhados com sua organização.",
"dashboard.cloud_signin_next": "Você escolherá uma equipe e se conectará a um workspace existente em seguida.",
"dashboard.cloud_signin_title": "Entrar no OpenWork Cloud",
"dashboard.cloud_worker": "Worker na nuvem",
"dashboard.connect_remote_button": "Conectar remoto",
"dashboard.connecting": "Conectando...",
"dashboard.create_local_workspace_subtitle": "Crie um workspace neste dispositivo e, opcionalmente, comece a partir de um template de equipe.",
"dashboard.create_local_workspace_title": "Workspace local",
"dashboard.create_remote_custom_subtitle": "Conecte a um worker OpenWork auto-hospedado.",
"dashboard.create_remote_custom_title": "Conectar remoto personalizado",
"dashboard.create_shared_subtitle_signed_in": "Navegue por workers na nuvem compartilhados com sua organização e conecte em um passo.",
"dashboard.create_shared_subtitle_signed_out": "Entre no OpenWork Cloud para acessar workers compartilhados com sua organização.",
"dashboard.create_shared_title": "Workspaces compartilhados",
"dashboard.creating": "Criando...",
"dashboard.desktop_badge": "Desktop",
"dashboard.display_name_label": "Nome de exibição",
"dashboard.display_name_optional": "(opcional)",
"dashboard.docker_debug_details": "Detalhes de depuração do Docker",
"dashboard.error_choose_org": "Escolha uma organização antes de abrir um workspace.",
"dashboard.error_connect_worker": "Falha ao conectar a {name}.",
"dashboard.error_create_template": "Falha ao criar {name}.",
"dashboard.error_load_orgs": "Falha ao carregar organizações.",
"dashboard.error_load_shared_workspaces": "Falha ao carregar workspaces compartilhados.",
"dashboard.error_workspace_not_ready": "O workspace ainda não está pronto para conectar. Tente novamente em instantes.",
"dashboard.import_config": "Importar config",
"dashboard.importing": "Importando\u2026",
"dashboard.modal_back": "Voltar",
"dashboard.modal_close": "Fechar modal de adicionar workspace",
"dashboard.nav_ids": "IDs",
"dashboard.no_folder_selected": "Nenhuma pasta selecionada ainda.",
"dashboard.open_cloud_dashboard": "Abrir painel da nuvem",
"dashboard.recently_updated": "Atualizados recentemente",
"dashboard.remote_server_details_hint": "Conecte a um worker OpenWork auto-hospedado.",
"dashboard.remote_server_details_title": "Detalhes do servidor remoto",
"dashboard.sandbox_hide_logs": "Ocultar logs",
"dashboard.sandbox_live_logs": "Logs ao vivo",
"dashboard.sandbox_setup": "Configuração do sandbox",
"dashboard.sandbox_show_logs": "Mostrar logs",
"dashboard.search_shared_workspaces": "Buscar workspaces compartilhados",
"dashboard.shared_workspaces_loading": "Carregando workspaces compartilhados\u2026",
"dashboard.shared_workspaces_no_match": "Nenhum workspace compartilhado corresponde a essa busca.",
"dashboard.shared_workspaces_none": "Nenhum workspace compartilhado disponível ainda.",
"dashboard.shared_workspaces_refreshing": "Atualizando workspaces\u2026",
"dashboard.team_templates_hint": "Escolha um ponto de partida ou deixe em branco para criar um workspace vazio.",
"dashboard.team_templates_none": "Nenhum template de equipe encontrado para esta organização ainda.",
"dashboard.team_templates_syncing": "Sincronizando",
"dashboard.team_templates_title": "Templates de equipe",
"dashboard.template_selected_badge": "Selecionado",
"dashboard.unknown_creator": "Criador desconhecido",
"dashboard.worker_status_attention": "Atenção",
"dashboard.worker_status_ready": "Pronto",
"dashboard.worker_status_starting": "Iniciando",
"dashboard.worker_status_stopped": "Parado",
"dashboard.worker_status_unknown": "Desconhecido",
"dashboard.worker_url_hint": "Cole a URL do worker OpenWork ao qual deseja conectar.",
"dashboard.worker_url_label": "URL do worker",
"dashboard.workspace_connect": "Conectar",
"dashboard.workspace_connect_unavailable": "Conectar workspaces compartilhados não está disponível aqui.",
"dashboard.workspace_connecting": "Conectando",
"dashboard.workspace_folder_hint": "Escolha onde este workspace ficará no seu dispositivo.",
"dashboard.workspace_folder_title": "Pasta do workspace",
"dashboard.workspace_not_ready_title": "Este workspace ainda não está pronto para conectar.",
// ==================== Den ====================
"den.active_org_hint": "Workers na nuvem e templates de equipe estão vinculados à organização selecionada.",
"den.active_org_title": "Organização ativa",
"den.auto_reconnect_hint": "Conclua a autenticação no navegador e o OpenWork reconectará aqui automaticamente.",
"den.checking_session": "Verificando sessão",
"den.choose_org_for_templates": "Escolha uma organização para ver os templates de equipe.",
"den.cloud_account_hint": "Gerencie sua conta conectada e organização.",
"den.cloud_account_title": "Conta Cloud",
"den.cloud_control_plane_open": "Abrir no navegador",
"den.cloud_control_plane_reset": "Redefinir",
"den.cloud_control_plane_save": "Salvar URL",
"den.cloud_control_plane_url_hint": "Apenas modo desenvolvedor. Use para apontar para um plano de controle Cloud local ou auto-hospedado. Alterá-lo desconecta você para que o app possa se re-hidratar no novo plano de controle.",
"den.cloud_control_plane_url_label": "URL do plano de controle Cloud",
"den.cloud_section_desc": "Entre, escolha uma organização e abra workers Cloud ou templates de equipe.",
"den.cloud_section_title": "OpenWork Cloud",
"den.cloud_sleep_hint": "Entre no OpenWork Cloud para manter suas tarefas ativas mesmo quando seu computador entrar em suspensão.",
"den.cloud_workers_hint": "Abra workers diretamente no OpenWork usando o mesmo fluxo de conexão remota que o app já usa.",
"den.cloud_workers_title": "Workers na nuvem",
"den.create_account": "Criar conta",
"den.error_base_url": "Digite uma URL de plano de controle Cloud válida com http:// ou https://.",
"den.error_choose_org": "Escolha uma organização antes de abrir um worker.",
"den.error_load_orgs": "Falha ao carregar organizações.",
"den.error_load_templates": "Falha ao carregar templates de equipe.",
"den.error_load_workers": "Falha ao carregar workers.",
"den.error_no_session": "Nenhuma sessão Cloud ativa encontrada.",
"den.error_no_token": "Login no desktop concluído, mas o OpenWork Cloud não retornou um token de sessão.",
"den.error_open_template": "Falha ao abrir {name}.",
"den.error_open_worker": "Falha ao abrir {name} no OpenWork.",
"den.error_open_worker_fallback": "Falha ao abrir {name}.",
"den.error_paste_valid_code": "Cole um link de login do OpenWork válido ou um código de uso único.",
"den.error_signin_failed": "Falha ao concluir o login no OpenWork Cloud.",
"den.error_worker_not_ready": "O worker ainda não está pronto. Tente novamente após o provisionamento ser concluído.",
"den.finish_signin": "Concluir login",
"den.finishing": "Concluindo...",
"den.hide_signin_code": "Ocultar código de login",
"den.needs_attention": "Requer atenção",
"den.no_cloud_workers": "Nenhum worker na nuvem visível para esta organização ainda. Crie um no Cloud e atualize esta aba.",
"den.no_org_selected": "Nenhuma organização selecionada",
"den.no_team_templates": "Nenhum template de equipe ainda. Use Compartilhar \u2192 Template \u2192 Compartilhar com a equipe.",
"den.open": "Abrir",
"den.opening": "Abrindo...",
"den.org_member_suffix": "(Membro)",
"den.org_owner_suffix": "(Proprietário)",
"den.org_switched": "Alterado para {name}.",
"den.paste_signin_code": "Colar código de login",
"den.refresh": "Atualizar",
"den.sign_out": "Sair",
"den.signed_out": "Desconectado",
"den.signin_button": "Entrar",
"den.signin_code_note": "Aceita um link openwork://den-auth ou o código de uso único.",
"den.signin_link_hint": "Se o navegador não redirecionar de volta para o OpenWork automaticamente, cole o link de login ou código de uso único do OpenWork Cloud aqui.",
"den.signin_link_label": "Link de login ou código de uso único",
"den.signin_link_placeholder": "openwork://den-auth?... ou código colado",
"den.signin_title": "Entrar no OpenWork Cloud",
"den.signing_in": "Concluindo login no OpenWork Cloud...",
"den.signing_out": "Saindo...",
"den.status_base_url_updated": "URL do plano de controle Cloud atualizada. Entre novamente para continuar.",
"den.status_browser_signin": "Conclua o login no navegador para conectar o OpenWork.",
"den.status_browser_signup": "Conclua a criação da conta no navegador para conectar o OpenWork.",
"den.status_cloud_signed_in_as": "OpenWork Cloud conectado como {email}.",
"den.status_cloud_signin_done": "OpenWork Cloud conectado.",
"den.status_loaded_orgs": "{count} organização{plural} carregada{plural}.",
"den.status_loaded_templates": "{count} template{plural} carregado{plural} para {name}.",
"den.status_loaded_workers": "{count} worker{plural} carregado{plural} para {name}.",
"den.status_no_templates": "Nenhum template de equipe encontrado para {name}.",
"den.status_no_workers": "Nenhum worker encontrado para {name}.",
"den.status_opened_template": "{name} aberto de {org}.",
"den.status_opened_template_fallback": "{name} aberto dos templates de equipe.",
"den.status_opened_worker": "{name} aberto no OpenWork.",
"den.status_signed_in_as": "Conectado como {email}.",
"den.status_signed_out": "Sessão do OpenWork Cloud desconectada e limpa neste dispositivo.",
"den.team_template_badge": "Template de equipe",
"den.team_templates_hint": "Abra templates de workspace reutilizáveis compartilhados com esta organização.",
"den.team_templates_title": "Templates de equipe",
"den.worker_mine_badge": "Meu",
"den.worker_not_ready_title": "Este worker ainda não está pronto para abrir.",
"den.worker_provider_label": "Worker {provider}",
"den.worker_secondary_cloud": "Worker na nuvem",
// ==================== Den Settings ====================
"den_settings.manage_account": "Gerenciar conta",
"den_settings.no_workers": "Nenhum workspace compartilhado encontrado.",
"den_settings.open_workspace": "Abrir workspace",
"den_settings.refresh": "Atualizar",
"den_settings.sign_in": "Entrar",
"den_settings.sign_in_hint": "Entre no OpenWork Cloud para acessar workspaces compartilhados e recursos de equipe.",
"den_settings.sign_out": "Sair",
"den_settings.signed_in_as": "Conectado como",
"den_settings.title": "Cloud",
"den_settings.workers_hint": "Workspaces na nuvem compartilhados com sua organização.",
"den_settings.workers_title": "Workspaces compartilhados",
// ==================== Extensions ====================
"extensions.title": "Extensões",
// ==================== Inbox Panel ====================
"inbox_panel.connect_to_download": "Conecte a um worker para baixar arquivos compartilhados.",
"inbox_panel.connect_to_see": "Conecte para ver os arquivos compartilhados.",
"inbox_panel.connect_to_upload": "Conecte a um worker para enviar",
"inbox_panel.copied_path": "Copiado: {path}",
"inbox_panel.copy_failed": "Falha ao copiar. Seu navegador pode bloquear o acesso à área de transferência.",
"inbox_panel.download": "Baixar",
"inbox_panel.download_failed": "Falha no download",
"inbox_panel.drop_to_upload": "Solte os arquivos aqui para enviar",
"inbox_panel.helper_text": "Compartilhe arquivos com este worker pelo app.",
"inbox_panel.load_failed": "Falha ao carregar a pasta compartilhada",
"inbox_panel.missing_file_id": "ID do arquivo compartilhado ausente.",
"inbox_panel.no_files": "Nenhum arquivo compartilhado ainda.",
"inbox_panel.refresh_tooltip": "Atualizar pasta compartilhada",
"inbox_panel.shared_folder": "Pasta compartilhada",
"inbox_panel.showing_first": "Exibindo os primeiros {count}.",
"inbox_panel.upload_failed": "Falha no envio para a pasta compartilhada",
"inbox_panel.upload_needs_worker": "Conecte a um worker para enviar arquivos para a pasta compartilhada.",
"inbox_panel.upload_prompt": "Solte arquivos ou clique para enviar",
"inbox_panel.upload_success": "Enviado para a pasta compartilhada.",
"inbox_panel.uploading": "Enviando...",
"inbox_panel.uploading_label": "Enviando {label}...",
// ==================== MCP (additional) ====================
"mcp.control_chrome_browser_hint": "No Chrome 144 ou mais recente, faça isso primeiro:",
"mcp.control_chrome_browser_step_one": "Abra chrome://inspect/#remote-debugging.",
"mcp.control_chrome_browser_step_three": "Permita conexões de depuração quando o Chrome solicitar.",
"mcp.control_chrome_browser_step_two": "Ative a depuração remota.",
"mcp.control_chrome_browser_title": "1. Ativar acesso ao Chrome",
"mcp.control_chrome_connect": "Adicionar Control Chrome",
"mcp.control_chrome_docs": "Guia oficial do MCP",
"mcp.control_chrome_edit": "Editar configurações",
"mcp.control_chrome_profile_hint": "O Control Chrome normalmente abre um perfil separado do Chrome. Ative isso se quiser que o OpenWork reutilize a janela do Chrome que você já tem aberta.",
"mcp.control_chrome_profile_title": "2. Escolher qual Chrome usar",
"mcp.control_chrome_save": "Salvar configurações",
"mcp.control_chrome_setup_subtitle": "Ative o acesso ao Chrome e escolha se o OpenWork deve usar seu próprio perfil limpo ou se conectar ao Chrome que você já usa.",
"mcp.control_chrome_setup_title": "Configurar Control Chrome",
"mcp.control_chrome_toggle_hint": "Quando ativado, o OpenWork adiciona --autoConnect para que o MCP se conecte a uma instância do Chrome que você já iniciou.",
"mcp.control_chrome_toggle_label": "Usar meu perfil existente do Chrome",
"mcp.control_chrome_toggle_off": "O OpenWork abrirá um perfil separado do Chrome apenas para automação.",
"mcp.control_chrome_toggle_on": "O OpenWork reutilizará suas abas, cookies e logins atuais.",
// ==================== Message ====================
"message.tool_request_label": "Solicitação",
"message.tool_result_label": "Resultado",
"message.waiting_subagent": "Aguardando a transcrição do subagente chegar.",
// ==================== Message List ====================
"message_list.copy_message": "Copiar mensagem",
"message_list.open_session": "Abrir sessão",
"message_list.step_updates_progress": "Atualiza o progresso",
"message_list.subagent_loading_transcript": "Carregando transcrição",
"message_list.subagent_message_count": "{count} mensagem{plural}",
"message_list.subagent_running": "Em execução",
"message_list.subagent_session_fallback": "Sessão do subagente",
"message_list.subagent_type_task": "Tarefa de {agentType}",
"message_list.subagent_waiting_transcript": "Aguardando transcrição",
"message_list.tool_checked_url": "Verificou {url}",
"message_list.tool_checked_web_fallback": "Verificou página web",
"message_list.tool_delegate_agent": "Delegar {agent}",
"message_list.tool_delegate_task_fallback": "Delegar tarefa",
"message_list.tool_load_skill_fallback": "Carregar skill",
"message_list.tool_load_skill_named": "Carregar skill {name}",
"message_list.tool_read_todo": "Ler lista de tarefas",
"message_list.tool_reviewed_file": "Revisou {file}",
"message_list.tool_reviewed_file_fallback": "Revisou arquivo",
"message_list.tool_reviewed_files_fallback": "Revisou arquivos",
"message_list.tool_reviewed_path": "Revisou {path}",
"message_list.tool_run_command": "Executar {command}",
"message_list.tool_run_command_fallback": "Executar comando",
"message_list.tool_searched_code_fallback": "Pesquisou código",
"message_list.tool_searched_pattern": "Pesquisou {pattern}",
"message_list.tool_update_file": "Atualizar {file}",
"message_list.tool_update_file_fallback": "Atualizar arquivo",
"message_list.tool_update_todo": "Atualizar lista de tarefas",
"message_list.tool_updated_file": "Atualizou {file}",
"message_list.tool_updated_file_fallback": "Atualizou arquivo",
// ==================== Scheduled ====================
"scheduled.title": "Automações",
// ==================== Session (additional) ====================
"session.allow_once": "Uma vez",
"session.api_key_saved": "Chave de API salva",
"session.attachments_add_token": "Adicione um token de servidor para anexar arquivos.",
"session.attachments_connect_server": "Conecte ao servidor OpenWork para anexar arquivos.",
"session.back": "Voltar",
"session.close_quick_actions": "Fechar ações rápidas",
"session.close_search": "Fechar busca",
"session.cmd_compact_detail": "Enviar uma instrução de compactação ao OpenCode para esta sessão",
"session.cmd_compact_detail_empty": "Nenhuma mensagem de usuário para compactar ainda",
"session.cmd_compact_meta": "Compactar",
"session.cmd_compact_title": "Compactar Conversa",
"session.cmd_current_workspace": "Workspace atual",
"session.cmd_model_detail": "{model} · {variant}",
"session.cmd_model_fallback": "Modelo",
"session.cmd_model_meta": "Abrir",
"session.cmd_model_title": "Alterar modelo",
"session.cmd_new_session_detail": "Iniciar uma nova tarefa no workspace atual",
"session.cmd_new_session_meta": "Criar",
"session.cmd_new_session_title": "Criar nova sessão",
"session.cmd_provider_detail": "Abrir fluxo de conexão do provedor",
"session.cmd_provider_meta": "Abrir",
"session.cmd_provider_title": "Conectar provedor",
"session.cmd_rename_detail_fallback": "Dê um nome mais claro à sessão selecionada",
"session.cmd_rename_meta": "Renomear",
"session.cmd_rename_title": "Renomear sessão atual",
"session.cmd_sessions_detail": "{count} disponíveis entre workspaces",
"session.cmd_sessions_meta": "Ir",
"session.cmd_sessions_title": "Buscar sessões",
"session.cmd_switch": "Alternar",
"session.compacted": "Sessão compactada.",
"session.compacting": "Compactando contexto da sessão...",
"session.compacting_auto": "O OpenCode está compactando esta sessão automaticamente",
"session.compacting_manual": "O OpenCode está compactando esta sessão",
"session.compaction_finished": "O OpenCode terminou de compactar o contexto da sessão.",
"session.compaction_started": "O OpenCode começou a compactar o contexto da sessão.",
"session.conflict_sync_toast": "Conflito ao sincronizar {path}. Alterações locais salvas em {conflictPath}.",
"session.connect_failed": "Falha na conexão",
"session.connect_to_sync": "Conecte ao servidor OpenWork para sincronizar arquivos remotos.",
"session.create_or_connect_workspace": "Criar ou conectar um workspace",
"session.create_workspace_desc": "Abra o criador de workspace e escolha como deseja começar.",
"session.create_workspace_title": "Criar workspace",
"session.default_agent": "Agente padrão",
"session.delete": "Excluir",
"session.delete_named_session_message": "Isso excluirá permanentemente \"{title}\" e suas mensagens.",
"session.delete_session_generic": "Isso excluirá permanentemente a sessão selecionada e suas mensagens.",
"session.delete_session_title": "Excluir sessão?",
"session.deleted": "Sessão excluída",
"session.deleting": "Excluindo...",
"session.doom_loop_label": "Loop Infinito",
"session.doom_loop_message": "O OpenCode detectou chamadas de ferramenta repetidas com a mesma entrada e está perguntando se deve continuar após falhas repetidas.",
"session.doom_loop_note": "Rejeite para parar o loop ou permita se quiser que o agente continue tentando.",
"session.doom_loop_repeated_call_label": "Chamada repetida",
"session.doom_loop_repeated_tool_call": "Chamada de ferramenta repetida",
"session.doom_loop_title": "Loop Infinito Detectado",
"session.doom_loop_tool_label": "Ferramenta",
"session.downloading": "Baixando",
"session.downloading_percent": "Baixando {percent}%",
"session.downloading_update_title": "Baixando atualização {version}",
"session.export_already_running": "Exportação já em andamento.",
"session.export_desktop_only": "Exportação disponível apenas no app desktop.",
"session.export_desktop_only_local": "Exportação disponível para workers locais no app desktop.",
"session.export_local_only": "Exportação disponível apenas para workers locais.",
"session.failed_to_compact": "Falha ao compactar sessão",
"session.failed_to_create_session": "Falha ao criar sessão",
"session.failed_to_delete": "Falha ao excluir sessão",
"session.failed_to_load_agents": "Falha ao carregar agentes",
"session.failed_to_load_providers": "Falha ao carregar provedores",
"session.failed_to_redo": "Falha ao refazer",
"session.failed_to_save_api_key": "Falha ao salvar chave de API",
"session.failed_to_stop": "Falha ao parar",
"session.failed_to_undo": "Falha ao desfazer",
"session.file_open_desktop_only": "Abrir arquivos está disponível no app desktop.",
"session.file_open_failed": "Falha ao abrir arquivo",
"session.file_open_remote_unavailable": "Abrir arquivos não está disponível para workspaces remotos.",
"session.flyout_file_modified": "Arquivo Modificado",
"session.flyout_new_task": "Nova Tarefa",
"session.install_update": "Instalar atualização",
"session.jump_to_latest": "Ir para o mais recente",
"session.jump_to_start": "Ir para o início da mensagem",
"session.load_earlier": "Carregar mensagens anteriores",
"session.loading_detail": "Buscando as últimas mensagens desta tarefa.",
"session.loading_earlier": "Carregando mensagens anteriores...",
"session.loading_session": "Carregando sessão",
"session.loading_title": "Carregando sessão",
"session.menu_label": "Menu",
"session.model_fallback": "Modelo",
"session.next_match": "Próxima correspondência",
"session.no_matches": "Sem correspondências",
"session.no_matches_command": "Sem correspondências.",
"session.no_session_selected": "Nenhuma sessão selecionada",
"session.nothing_to_compact": "Nada para compactar ainda.",
"session.nothing_to_redo": "Nada para refazer.",
"session.nothing_to_retry": "Nada para tentar novamente ainda",
"session.nothing_to_undo": "Nada para desfazer ainda.",
"session.oauth_failed": "Falha no OAuth",
"session.obsidian_worker_relative_only": "Apenas arquivos relativos ao worker podem ser abertos no Obsidian.",
"session.palette_hint_navigate": "Setas para navegar",
"session.palette_hint_run": "Enter para executar · Esc para fechar",
"session.palette_placeholder_actions": "Buscar ações",
"session.palette_placeholder_sessions": "Buscar por título de sessão ou workspace",
"session.palette_title_actions": "Ações rápidas",
"session.palette_title_sessions": "Buscar sessões",
"session.permission_message": "O OpenCode está solicitando permissão para continuar.",
"session.phase_responding": "Respondendo",
"session.phase_retrying": "Tentando novamente",
"session.phase_run_failed": "Execução falhou",
"session.phase_sending": "Enviando",
"session.pick_folder_desc": "Escolha uma pasta de projeto ou notas existente e o OpenWork a usará como workspace.",
"session.pick_folder_title": "Escolha uma pasta para trabalhar",
"session.pick_workspace_to_open": "Selecione um workspace para abrir arquivos.",
"session.prev_match": "Correspondência anterior",
"session.provider_auth_in_progress": "Autenticação do provedor já em andamento.",
"session.provider_connected": "Provedor conectado",
"session.quick_actions_label": "Ações rápidas",
"session.quick_actions_title": "Ações rápidas (Ctrl/Cmd+K)",
"session.redo_aria_label": "Refazer última mensagem revertida",
"session.redo_label": "Refazer",
"session.redo_title": "Refazer última mensagem revertida",
"session.remote_sync_failed": "Falha na sincronização de arquivos remotos",
"session.resize_workspace_column": "Redimensionar coluna do workspace",
"session.restart_update_title": "Reinicie para aplicar a atualização {version}",
"session.restored_message": "Mensagem revertida restaurada.",
"session.reveal_desktop_only": "Mostrar está disponível no app desktop.",
"session.revert_label": "Reverter",
"session.reverted_last_message": "Última mensagem do usuário revertida.",
"session.search_conversation_label": "Buscar na conversa",
"session.search_conversation_title": "Buscar na conversa (Ctrl/Cmd+F)",
"session.search_next": "Próximo",
"session.search_placeholder": "Buscar neste chat",
"session.search_position": "{current} de {total}",
"session.search_prev": "Anterior",
"session.share_active_cloud_org": "Organização Cloud ativa",
"session.share_choose_org": "Escolha uma organização em Configurações -> Cloud antes de compartilhar com sua equipe.",
"session.share_collaborator_hint": "Acesso remoto rotineiro quando você não precisa de ações exclusivas do proprietário.",
"session.share_collaborator_host_hint": "Acesso remoto rotineiro a este host sem ações exclusivas do proprietário.",
"session.share_collaborator_label": "Token de colaborador",
"session.share_collaborator_token": "Token de colaborador",
"session.share_connected_with_hint": "Este workspace está conectado com esta senha.",
"session.share_desktop_app_required": "Requer o app desktop",
"session.share_desktop_required": "Requer o app desktop",
"session.share_host_url_and_token_required": "URL do host OpenWork e token são obrigatórios.",
"session.share_local_host_not_ready": "O host local do OpenWork ainda não está pronto.",
"session.share_missing_host_url": "URL do host OpenWork ausente.",
"session.share_missing_token": "Token do OpenWork ausente.",
"session.share_no_skills": "Nenhuma skill encontrada neste workspace.",
"session.share_note_direct_runtime": "O runtime do engine está em modo Direto. Alternar workers locais pode reiniciar o host e desconectar clientes. O token pode mudar após a reinicialização.",
"session.share_opencode_base_url": "URL base do OpenCode",
"session.share_openwork_workers_only": "Links de compartilhamento estão disponíveis para workers OpenWork.",
"session.share_owner_permission_hint": "Use quando o cliente remoto precisar responder a prompts de permissão.",
"session.share_password": "Senha",
"session.share_password_owner_hint": "Use quando o cliente remoto precisar responder a prompts de permissão.",
"session.share_publish_skills_failed": "Falha ao publicar o conjunto de skills",
"session.share_publish_workspace_failed": "Falha ao publicar o perfil do workspace",
"session.share_resolve_local_workspace_failed": "Não foi possível resolver este workspace no host local do OpenWork.",
"session.share_resolve_remote_workspace_failed": "Não foi possível resolver este workspace no host do OpenWork.",
"session.share_save_team_template_failed": "Falha ao salvar template de equipe",
"session.share_saved_to_org": "{name} salvo em {org}.",
"session.share_select_workspace": "Selecione um workspace primeiro.",
"session.share_set_token_hint": "Definir token nas configurações do workspace",
"session.share_sign_in_required": "Entre no OpenWork Cloud em Configurações para compartilhar com sua equipe.",
"session.share_skills_set_desc": "Conjunto completo de skills de um workspace OpenWork.",
"session.share_starting_server": "Iniciando servidor...",
"session.share_team_fallback_name": "seus templates de equipe",
"session.share_url_resolving_hint": "URL do worker está sendo resolvida; URL do host exibida como alternativa.",
"session.share_url_worker_hint": "Use em celulares ou laptops conectando a este worker.",
"session.share_worker_url": "URL do worker",
"session.share_worker_url_phones_hint": "Use em celulares ou laptops conectando a este worker.",
"session.share_worker_url_resolving_hint": "URL do worker está sendo resolvida; URL do host exibida como alternativa.",
"session.share_workspace_template_desc": "Template completo de workspace OpenWork com config, comandos, skills e arquivos .opencode extras.",
"session.shared_folder_upload_failed": "Falha no envio para a pasta compartilhada",
"session.show_earlier": "Mostrar {count} mensagem{plural} anterior(es)",
"session.status_active": "Sessão Ativa",
"session.status_compacting": "Compactando Contexto",
"session.status_delegating": "Delegando",
"session.status_gathering_context": "Coletando contexto",
"session.status_planning": "Planejando",
"session.status_ready": "Pronto",
"session.status_ready_session": "Sessão Pronta",
"session.status_running_shell": "Executando shell",
"session.status_searching_codebase": "Pesquisando código",
"session.status_searching_web": "Pesquisando na web",
"session.status_thinking": "Pensando",
"session.status_working": "Trabalhando",
"session.status_writing_file": "Escrevendo arquivo",
"session.stopped": "Parado.",
"session.stopping_run": "Parando a execução...",
"session.todo_progress": "{completed} de {total} tarefas concluídas",
"session.trying_again": "Tentando novamente...",
"session.unable_to_open_file": "Não foi possível abrir o arquivo",
"session.unable_to_open_obsidian": "Não foi possível abrir o arquivo no Obsidian",
"session.unable_to_reveal": "Não foi possível revelar o workspace",
"session.undo_label": "Reverter",
"session.undo_title": "Desfazer última mensagem",
"session.update_available": "Atualização disponível",
"session.update_available_title": "Atualização disponível {version}",
"session.update_ready": "Atualização pronta",
"session.update_ready_stop_runs_title": "Atualização pronta {version}. Pare as execuções ativas para reiniciar.",
"session.upload_connect_server": "Conecte ao servidor OpenWork para enviar arquivos para a pasta compartilhada.",
"session.uploaded_to_shared_folder": "Enviado para a pasta compartilhada.",
"session.uploaded_with_summary": "Enviado para a pasta compartilhada: {summary}",
"session.uploading_to_shared_folder": "Enviando {label} para a pasta compartilhada...",
"session.workspace_fallback": "Workspace",
"session.workspace_label": "Workspace",
"session.workspace_path_unavailable": "Caminho do workspace não disponível.",
"session.workspace_setup_desc": "Comece com um workspace guiado do OpenWork ou escolha uma pasta existente.",
"session.workspace_setup_label": "Configuração do workspace",
"session.workspace_setup_title": "Configure seu primeiro workspace",
// ==================== Settings (additional) ====================
"settings.tab_advanced": "Avançado",
"settings.tab_appearance": "Aparência",
"settings.tab_automations": "Automações",
"settings.tab_cloud": "Cloud",
"settings.tab_debug": "Depuração",
"settings.tab_extensions": "Extensões",
"settings.tab_general": "Configurações",
"settings.tab_messaging": "Mensagens",
"settings.tab_recovery": "Recuperação",
"settings.tab_skills": "Skills",
"settings.tab_updates": "Atualizações",
// ==================== Share ====================
"share.active_cloud_org": "Organização Cloud ativa",
"share.back_hint": "Voltar às opções de compartilhamento",
"share.chooser_subtitle": "Escolha como deseja compartilhar este workspace.",
"share.close_hint": "Fechar",
"share.cloud_signin_note": "O OpenWork Cloud abre no navegador e retorna aqui após o login.",
"share.collaborator_hint": "Acesso rotineiro sem aprovações de permissão.",
"share.connect_messaging_desc": "Use este workspace pelo Slack, Telegram e outros.",
"share.connect_messaging_title": "Conectar mensagens",
"share.connection_details_label": "Detalhes da conexão",
"share.copy_hint": "Copiar",
"share.copy_link_hint": "Copiar link",
"share.create_template_link": "Criar link de template",
"share.credentials_disabled_hint": "Ative o acesso remoto e clique em Salvar para reiniciar o worker e revelar os detalhes de conexão ativos deste workspace.",
"share.field_password": "Senha",
"share.field_worker_url": "URL do worker",
"share.hide_password": "Ocultar senha",
"share.included_in_template": "Incluído neste template",
"share.option_access_desc": "Revele os detalhes de conexão ativos necessários para acessar este workspace em execução de outra máquina.",
"share.option_access_title": "Acessar workspace remotamente",
"share.option_public_desc": "Crie um link de compartilhamento que qualquer pessoa pode usar para começar a partir deste template.",
"share.option_public_title": "Template público",
"share.option_team_desc": "Salve este template de workspace na sua organização ativa do OpenWork Cloud.",
"share.option_team_title": "Compartilhar com a equipe",
"share.option_template_desc": "Empacote esta configuração para que outra pessoa possa começar do mesmo ambiente.",
"share.option_template_title": "Compartilhar um template",
"share.optional_collaborator": "Acesso de colaborador opcional",
"share.public_intro": "Compartilhe este workspace como um link de template público.",
"share.publishing": "Publicando...",
"share.regenerate_link": "Regenerar link",
"share.remote_access_desc": "Desativado por padrão. Ative apenas quando quiser que este worker seja acessível de outra máquina.",
"share.remote_access_disabled": "O acesso remoto está desativado.",
"share.remote_access_enabled": "O acesso remoto está ativado.",
"share.remote_access_title": "Acesso remoto",
"share.remote_save": "Salvar",
"share.remote_save_busy": "Salvando...",
"share.reveal_password": "Revelar senha",
"share.save_to_team": "Salvar para a equipe",
"share.saving": "Salvando...",
"share.setup": "Configuração",
"share.sign_in_to_share": "Entrar para compartilhar com a equipe",
"share.subtitle_access": "Revele os detalhes de conexão necessários para acessar este workspace de outra máquina.",
"share.subtitle_template_public": "Crie um link público de template de workspace que qualquer pessoa pode importar.",
"share.subtitle_template_team": "Salve um template de workspace na organização ativa do OpenWork Cloud.",
"share.team_intro": "Salve este template na organização ativa do OpenWork Cloud para que colegas possam abri-lo depois pelas configurações do Cloud.",
"share.template_intro": "Compartilhe uma configuração reutilizável sem conceder acesso ao vivo a este workspace em execução.",
"share.template_item_config": "Comandos e config",
"share.template_item_config_desc": "Comandos reutilizáveis mais configuração do OpenWork/OpenCode.",
"share.template_item_settings": "Configurações do workspace",
"share.template_item_settings_desc": "O perfil compartilhado do workspace e o comportamento padrão.",
"share.template_item_skills": "Skills incluídas",
"share.template_item_skills_desc": "Skills personalizadas salvas neste workspace.",
"share.template_name_label": "Nome do template",
"share.title": "Compartilhar workspace",
"share.view_access": "Acessar workspace remotamente",
"share.view_template": "Compartilhar um template",
"share.view_template_public": "Template público",
"share.view_template_team": "Compartilhar com a equipe",
"share.warning_basic": "Compartilhe apenas com pessoas de confiança. Essas credenciais concedem acesso ao vivo a este workspace.",
"share.warning_full": "Essas credenciais concedem acesso ao vivo a este workspace. Compartilhar este workspace remotamente pode permitir que qualquer pessoa com acesso à sua rede controle seu worker.",
"share.workspace_fallback": "Workspace",
"share.workspace_template_desc": "Compartilhe a configuração base e os padrões do workspace.",
"share.workspace_template_title": "Template de workspace",
// ==================== Sidebar ====================
"sidebar.active": "Ativo",
"sidebar.add_workspace": "Adicionar novo workspace",
"sidebar.collapse": "Recolher",
"sidebar.connect_remote": "Conectar remoto",
"sidebar.delete_session": "Excluir sessão",
"sidebar.drag_reorder": "Arrastar para reordenar",
"sidebar.edit_connection": "Editar conexão",
"sidebar.expand": "Expandir",
"sidebar.import_config": "Importar config",
"sidebar.needs_attention": "Requer atenção",
"sidebar.new_worker": "Novo worker",
"sidebar.no_sessions_yet": "Nenhuma sessão ainda",
"sidebar.no_workspaces": "Nenhum workspace nesta sessão ainda. Adicione um para começar.",
"sidebar.progress": "Progresso",
"sidebar.remove_workspace": "Remover workspace",
"sidebar.show_fewer": "Mostrar menos",
"sidebar.show_more": "Mostrar mais {count}",
"sidebar.stop_sandbox": "Parar sandbox",
"sidebar.switch": "Alternar",
"sidebar.test_connection": "Testar conexão",
// ==================== Skills (additional) ====================
"skills.catalog_search_placeholder": "Buscar skills instaladas, de equipe e de hub",
"skills.cloud_add_skill": "Adicionar skill",
"skills.cloud_badge": "Cloud",
"skills.cloud_choose_org_detail": "Use o painel Cloud para escolher sua organização ativa e atualize esta lista.",
"skills.cloud_choose_org_hint": "Escolha uma organização em Configurações → Cloud para carregar skills da equipe.",
"skills.cloud_footer_label": "Equipe",
"skills.cloud_hub_label": "Hub: {name}",
"skills.cloud_install_need_server": "Conecte a um servidor OpenWork com acesso de escrita a skills para instalar skills de equipe neste worker.",
"skills.cloud_installed": "{name} instalado neste worker.",
"skills.cloud_installing": "Instalando {title}\u2026",
"skills.cloud_installing_short": "Instalando",
"skills.cloud_no_search_matches": "Nenhuma skill corresponde a essa busca.",
"skills.cloud_org_empty": "Nenhuma skill da organização disponível ainda.",
"skills.cloud_org_fallback": "OpenWork Cloud",
"skills.cloud_org_load_failed": "Falha ao carregar skills da organização.",
"skills.cloud_refresh": "Atualizar skills da equipe",
"skills.cloud_section_subtitle":
"Skills compartilhadas com você pelo OpenWork Cloud — incluindo hubs de skills da equipe que você pode acessar.",
"skills.cloud_section_title": "Da sua organização",
"skills.cloud_shared_org": "Org",
"skills.cloud_shared_public": "Público",
"skills.cloud_sign_in": "Entrar no Cloud",
"skills.cloud_sign_in_hint": "Entre no OpenWork Cloud para navegar por skills de equipe e organização.",
"skills.filter_all": "Todas",
"skills.filter_cloud": "Equipe",
"skills.filter_hub": "Hub",
"skills.filter_installed": "Instaladas",
// ==================== Status (additional) ====================
"status.back": "Voltar à tela anterior",
"status.developer_mode": "Modo desenvolvedor",
"status.disconnected_hint": "Abra as configurações para reconectar",
"status.disconnected_label": "Desconectado",
"status.feedback": "Feedback",
"status.limited_hint": "Reconecte para restaurar todos os recursos do OpenWork",
"status.limited_mcp_hint": "{count} MCP conectados · reconecte para recursos completos",
"status.limited_mode": "Modo Limitado",
"status.mcp_connected": "{count} MCP conectados",
"status.openwork_ready": "OpenWork Pronto",
"status.providers_connected": "{count} provedor{plural} conectado{plural}",
"status.ready_for_tasks": "Pronto para novas tarefas",
"status.send_feedback": "Enviar feedback",
"status.settings": "Configurações",
// ==================== Workspace (additional) ====================
"workspace.active": "Ativo",
"workspace.loading_tasks": "Carregando tarefas...",
"workspace.local_badge": "Local",
"workspace.needs_attention": "Requer atenção",
"workspace.new_task_inline": "+ Nova tarefa",
"workspace.no_tasks": "Nenhuma tarefa ainda.",
"workspace.remote_badge": "Remoto",
"workspace.sandbox_badge": "Sandbox",
"workspace.selected": "Selecionado",
"workspace.switch": "Alternar",
// ==================== Workspace List ====================
"workspace_list.add_workspace": "Adicionar workspace",
"workspace_list.connect_remote": "Conectar workspace remoto",
"workspace_list.connecting": "Conectando...",
"workspace_list.delete_session": "Excluir sessão",
"workspace_list.desktop_only_hint": "Crie workspaces locais no app desktop.",
"workspace_list.edit_connection": "Editar conexão",
"workspace_list.edit_name": "Editar nome",
"workspace_list.hide_child_sessions": "Ocultar sessões filhas",
"workspace_list.import_config": "Importar config",
"workspace_list.new_workspace": "Novo workspace",
"workspace_list.recover": "Recuperar",
"workspace_list.remove_workspace": "Remover workspace",
"workspace_list.rename_session": "Renomear sessão",
"workspace_list.reveal_explorer": "Mostrar no Explorador",
"workspace_list.reveal_finder": "Mostrar no Finder",
"workspace_list.session_actions": "Ações da sessão",
"workspace_list.share": "Compartilhar...",
"workspace_list.show_child_sessions": "Mostrar sessões filhas",
"workspace_list.show_more": "Mostrar mais {count}",
"workspace_list.show_more_fallback": "Mostrar mais",
"workspace_list.test_connection": "Testar conexão",
"workspace_list.workspace_fallback": "Workspace",
"workspace_list.workspace_options": "Opções do workspace",
// ==================== Workspace Sidebar ====================
"workspace_sidebar.automations": "Automações",
"workspace_sidebar.close_sidebar": "Fechar barra lateral",
"workspace_sidebar.collapse_sidebar": "Recolher barra lateral",
"workspace_sidebar.configuration": "configuração",
"workspace_sidebar.expand_sidebar": "Expandir barra lateral",
"workspace_sidebar.extensions": "Extensões",
"workspace_sidebar.messaging": "Mensagens",
} as const;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff