Skip to content
186 changes: 95 additions & 91 deletions packages/app/src/app/app.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { For, Show, createEffect, createSignal } from "solid-js";

import { CheckCircle2, FolderPlus, Loader2 } from "lucide-solid";

import { currentLocale, t } from "../../i18n";
import Button from "./button";

const tr = (key: string) => t(key, currentLocale());

export default function OnboardingWorkspaceSelector(props: {
defaultPath: string;
onConfirm: (preset: "starter" | "automation" | "minimal", folder: string | null) => void;
Expand All @@ -16,13 +19,13 @@ export default function OnboardingWorkspaceSelector(props: {
const options = () => [
{
id: "starter" as const,
name: "Starter worker",
desc: "Preconfigured to show you how to use plugins, commands, and skills.",
name: tr("dashboard.starter_workspace"),
desc: tr("dashboard.starter_workspace_desc"),
},
{
id: "minimal" as const,
name: "Empty worker",
desc: "Start with a blank folder and add what you need.",
name: tr("dashboard.empty_workspace"),
desc: tr("dashboard.empty_workspace_desc"),
},
];

Expand Down
19 changes: 11 additions & 8 deletions packages/app/src/app/components/session/artifacts-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { For, Show, createMemo, createSignal } from "solid-js";
import { Paperclip } from "lucide-solid";
import { currentLocale, t } from "../../../i18n";

const tr = (key: string, params?: Record<string, string | number>) => t(key, currentLocale(), params);

export type ArtifactsPanelProps = {
files: string[];
Expand Down Expand Up @@ -98,7 +101,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
return (
<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">Artifacts</span>
<span class="text-[11px] font-semibold uppercase tracking-wider text-gray-10">{tr("context.artifacts_label")}</span>
<Show when={normalizedArtifacts().length > 0}>
<span class="text-[11px] font-medium bg-gray-4/60 text-gray-10 px-1.5 rounded">
{normalizedArtifacts().length}
Expand All @@ -109,7 +112,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
<div class="space-y-1">
<Show
when={visibleArtifacts().length > 0}
fallback={<div class="text-xs text-gray-10 px-2 py-1">No artifacts yet.</div>}
fallback={<div class="text-xs text-gray-10 px-2 py-1">{tr("context.no_artifacts")}</div>}
>
<For each={visibleArtifacts()}>
{(artifact) => {
Expand All @@ -131,12 +134,12 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
<div class="truncate text-xs font-medium text-gray-11">{base()}</div>
<Show when={md()}>
<span class="shrink-0 rounded-md border border-gray-6 bg-gray-2 px-1.5 py-0.5 text-[10px] font-mono text-gray-10">
MD
{tr("context.markdown_badge")}
</span>
</Show>
<Show when={img()}>
<span class="shrink-0 rounded-md border border-gray-6 bg-gray-2 px-1.5 py-0.5 text-[10px] font-mono text-gray-10">
IMG
{tr("context.image_badge")}
</span>
</Show>
</div>
Expand All @@ -150,7 +153,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
type="button"
class="rounded-md border border-gray-6 bg-gray-2 px-1.5 py-0.5 text-[10px] font-medium text-gray-10 hover:text-gray-12 hover:border-gray-7 transition-colors"
onClick={() => props.onOpenInObsidian?.(artifact.path)}
title="Open in Obsidian"
title={tr("context.open_in_obsidian")}
>
Obsidian
</button>
Expand All @@ -160,9 +163,9 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
type="button"
class="rounded-md border border-gray-6 bg-gray-2 px-1.5 py-0.5 text-[10px] font-medium text-gray-10 hover:text-gray-12 hover:border-gray-7 transition-colors"
onClick={() => props.onRevealArtifact?.(artifact.path)}
title={img() ? "Reveal image in Finder" : "Reveal file in Finder"}
title={img() ? tr("context.reveal_image_in_finder") : tr("context.reveal_file_in_finder")}
>
Reveal
{tr("context.reveal_button")}
</button>
</Show>
</div>
Expand All @@ -178,7 +181,7 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) {
class="w-full mt-1 rounded-lg px-2 py-1.5 text-xs text-gray-10 hover:text-gray-11 hover:bg-gray-3 transition-colors"
onClick={() => setShowAll((prev) => !prev)}
>
{showAll() ? "Show fewer" : `Show ${hiddenCount()} more`}
{showAll() ? tr("context.show_fewer") : tr("context.show_more", { count: hiddenCount() })}
</button>
</Show>
</div>
Expand Down
40 changes: 22 additions & 18 deletions packages/app/src/app/components/session/context-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { ChevronDown, Circle, File, Folder, Package } from "lucide-solid";
import { SUGGESTED_PLUGINS } from "../../constants";
import type { McpServerEntry, McpStatus, McpStatusMap, SkillCard } from "../../types";
import { stripPluginVersion } from "../../utils/plugins";
import { currentLocale, t } from "../../../i18n";

// Translation helper
const tr = (key: string, params?: Record<string, string | number>) => t(key, currentLocale(), params);

export type ContextPanelProps = {
activePlugins: string[];
Expand Down Expand Up @@ -108,19 +112,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 tr("context.status_disabled");
if (!status) return tr("context.status_disconnected");
switch (status.status) {
case "connected":
return "Connected";
return tr("context.status_connected");
case "needs_auth":
return "Needs auth";
return tr("context.status_needs_auth");
case "needs_client_registration":
return "Register client";
return tr("context.status_register_client");
case "failed":
return "Failed";
return tr("context.status_failed");
default:
return "Disconnected";
return tr("context.status_disconnected");
}
};

Expand Down Expand Up @@ -152,7 +156,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>{tr("context.title")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.context ? "rotate-180" : ""}`.trim()}
Expand All @@ -162,12 +166,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>{tr("context.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">{tr("context.none_yet")}</div>}
>
<For each={props.workingFiles}>
{(file) => {
Expand Down Expand Up @@ -204,7 +208,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>{tr("context.plugins")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.plugins ? "rotate-180" : ""}`.trim()}
Expand All @@ -217,7 +221,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 ?? tr("context.no_plugins")}
</div>
}
>
Expand Down Expand Up @@ -254,7 +258,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>{tr("context.mcp")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.mcp ? "rotate-180" : ""}`.trim()}
Expand All @@ -267,7 +271,7 @@ export default function ContextPanel(props: ContextPanelProps) {
when={props.mcpServers.length}
fallback={
<div class="text-xs text-gray-9">
{props.mcpStatus ?? "No MCP servers loaded."}
{props.mcpStatus ?? tr("context.no_mcp_servers")}
</div>
}
>
Expand Down Expand Up @@ -304,7 +308,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>{tr("context.skills")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${props.expandedSections.skills ? "rotate-180" : ""}`.trim()}
Expand All @@ -317,7 +321,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 ?? tr("context.no_skills")}
</div>
}
>
Expand Down Expand Up @@ -353,7 +357,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>{tr("context.authorized_folders")}</span>
<ChevronDown
size={16}
class={`transition-transform text-gray-10 ${
Expand All @@ -366,7 +370,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">{tr("context.none_yet")}</div>}
>
<For each={props.authorizedDirs.slice(0, 3)}>
{(folder) => (
Expand Down
27 changes: 16 additions & 11 deletions packages/app/src/app/components/session/inbox-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { getOpenWorkDeployment } from "../../lib/openwork-deployment";
import type { OpenworkInboxItem, OpenworkServerClient } from "../../lib/openwork-server";
import WebUnavailableSurface from "../web-unavailable-surface";
import { formatBytes, formatRelativeTime } from "../../utils";
import { currentLocale, t } from "../../../i18n";

// Translation helper
const tr = (key: string, params?: Record<string, string | number>) =>
t(key, currentLocale(), params);

export type InboxPanelProps = {
id?: string;
Expand Down Expand Up @@ -48,7 +53,7 @@ export default function InboxPanel(props: InboxPanelProps) {
});

const connected = createMemo(() => Boolean(props.client && (props.workspaceId ?? "").trim()));
const helperText = "Share files with your remote worker.";
const helperText = tr("context.inbox_helper_text");

const visibleItems = createMemo(() => (items() ?? []).slice(0, maxPreview()));
const hiddenCount = createMemo(() => Math.max(0, (items() ?? []).length - visibleItems().length));
Expand All @@ -71,7 +76,7 @@ export default function InboxPanel(props: InboxPanelProps) {
const result = await client.listInbox(workspaceId);
setItems(result.items ?? []);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load inbox";
const message = err instanceof Error ? err.message : tr("context.inbox_load_failed");
setError(message);
setItems([]);
} finally {
Expand All @@ -83,7 +88,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 inbox files.");
toast(tr("context.inbox_connect_to_upload"));
return;
}
if (!files.length) return;
Expand All @@ -92,14 +97,14 @@ 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(tr("context.inbox_uploading", { label }));
for (const file of files) {
await client.uploadInbox(workspaceId, file);
}
toast("Uploaded to worker inbox.");
toast(tr("context.inbox_uploaded"));
await refresh();
} catch (err) {
const message = err instanceof Error ? err.message : "Inbox upload failed";
const message = err instanceof Error ? err.message : tr("context.inbox_upload_failed");
setError(message);
toast(message);
} finally {
Expand All @@ -111,22 +116,22 @@ export default function InboxPanel(props: InboxPanelProps) {
const path = toInboxWorkspacePath(item);
try {
await navigator.clipboard.writeText(path);
toast(`Copied: ${path}`);
toast(tr("context.inbox_copied", { path }));
} catch {
toast("Copy failed. Your browser may block clipboard access.");
toast(tr("context.inbox_copy_failed"));
}
};

const downloadItem = async (item: OpenworkInboxItem) => {
const client = props.client;
const workspaceId = (props.workspaceId ?? "").trim();
if (!client || !workspaceId) {
toast("Connect to a worker to download inbox files.");
toast(tr("context.inbox_connect_to_download"));
return;
}
const id = String(item.id ?? "").trim();
if (!id) {
toast("Missing inbox item id.");
toast(tr("context.inbox_missing_id"));
return;
}

Expand All @@ -142,7 +147,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 : tr("context.inbox_download_failed");
toast(message);
}
};
Expand Down
Loading