mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
feat(i18n): extract en translations — pages (#1250)
* feat(i18n): extract en translations for pages Extract hardcoded English strings to i18n keys for: - config, extensions, identities, plugins - automations (was scheduled), settings, skills pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(i18n): second-pass extraction of remaining hardcoded strings Scanned all 7 PR3 page files for hardcoded English strings missed by the reference diff. Converted to t() calls and added keys to en.ts: - identities.tsx: 25 keys (time ago, dispatched messages, health status, Telegram bot setup strings, routing override, agent scope/status, peer ID placeholders) - automations.tsx: 5 keys (scheduler install, prepare/remove job toasts, delete confirm description) - settings.tsx: 12 keys (actor labels, cap labels, workspace fallback, deeplink hint, worker ID label, inbox/outbox cap labels) - skills.tsx: 1 key (loading spinner text) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(i18n): add missing translation keys for identities page * fix(i18n): resolve rebase fallout on pages branch --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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/<workspace-id></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 <code></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 <code></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 <path></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 <path></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 <id></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"
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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/<name>/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>
|
||||
|
||||
@@ -1222,7 +1222,6 @@ export default {
|
||||
|
||||
// ==================== Dashboard page (session/share strings) ====================
|
||||
"scheduled.title": "Automations",
|
||||
"extensions.title": "Extensions",
|
||||
"settings.tab_automations": "Automations",
|
||||
"settings.tab_skills": "Skills",
|
||||
"settings.tab_extensions": "Extensions",
|
||||
@@ -1285,56 +1284,420 @@ export default {
|
||||
"session.downloading_update_title": "Downloading update {version}",
|
||||
"session.update_available_title": "Update available {version}",
|
||||
|
||||
// ==================== Automations context ====================
|
||||
"automations.server_unavailable": "OpenWork server unavailable. Connect to sync scheduled tasks.",
|
||||
"automations.server_needs_token": "OpenWork server needs a token to load scheduled tasks.",
|
||||
"automations.server_not_ready": "OpenWork server not ready.",
|
||||
"automations.failed_to_load": "Failed to load scheduled tasks.",
|
||||
"automations.desktop_required": "Scheduled tasks require the desktop app.",
|
||||
"automations.schedule_required": "Schedule is required.",
|
||||
"automations.prompt_required": "Prompt is required.",
|
||||
"automations.prompt_empty": "Automation prompt is empty.",
|
||||
// ==================== Common (PR3) ====================
|
||||
"common.reset": "Reset",
|
||||
|
||||
// ==================== Question Modal ====================
|
||||
"question_modal.question_counter": "Question {current} of {total}",
|
||||
"question_modal.custom_answer_label": "Or type a custom answer",
|
||||
"question_modal.custom_answer_placeholder": "Type your answer here...",
|
||||
// ==================== Config (PR3) ====================
|
||||
"config.auto_reload_desc": "Reload automatically after agents/skills/commands/config change (only when idle).",
|
||||
"config.auto_reload_title": "Auto reload (local)",
|
||||
"config.auto_reload_unavailable": "Available for local workspaces in the desktop app.",
|
||||
"config.collaborator_token_disabled_hint": "Stored in advance for remote sharing, but remote access is currently disabled.",
|
||||
"config.collaborator_token_label": "Collaborator token",
|
||||
"config.collaborator_token_remote_hint": "Routine remote access for phones or laptops connecting to this server.",
|
||||
"config.connection_failed": "Connection failed.",
|
||||
"config.connection_failed_check": "Connection failed. Check the host URL and token.",
|
||||
"config.connection_status_updated": "Connection status updated.",
|
||||
"config.connection_successful": "Connection successful.",
|
||||
"config.copied": "Copied",
|
||||
"config.copy": "Copy",
|
||||
"config.desktop_only_hint": "Some config features (local server sharing + messaging bridge) require the desktop app.",
|
||||
"config.diagnostics_desc": "Copy sanitized runtime state for debugging.",
|
||||
"config.diagnostics_title": "Diagnostics bundle",
|
||||
"config.enable_auto_reload_first": "Enable auto reload first",
|
||||
"config.engine_reload_desc": "Restart the OpenCode server for this workspace.",
|
||||
"config.engine_reload_title": "Engine reload",
|
||||
"config.host_admin_token_hint": "Internal host-only token for approvals CLI and admin APIs. Do not use this in the remote app connect flow.",
|
||||
"config.host_admin_token_label": "Host admin token",
|
||||
"config.host_local_only": "Local only",
|
||||
"config.host_offline": "Offline",
|
||||
"config.host_remote_enabled": "Remote enabled",
|
||||
"config.local_ip_hint": "Use your local IP on the same Wi-Fi for the fastest connection.",
|
||||
"config.mdns_hint": ".local names are easier to remember but may not resolve on all networks.",
|
||||
"config.messaging_identities_desc": "Manage Telegram/Slack identities and routing in the Identities tab.",
|
||||
"config.messaging_identities_title": "Messaging identities",
|
||||
"config.not_set": "Not set",
|
||||
"config.owner_token_disabled_hint": "Only relevant after you enable remote access for this worker.",
|
||||
"config.owner_token_label": "Owner token",
|
||||
"config.owner_token_remote_hint": "Use this when a remote client needs to answer permission prompts or take owner-only actions.",
|
||||
"config.reload_active_tasks_warning": "Reloading will stop active tasks.",
|
||||
"config.reload_availability_hint": "Reloading is only available for local workers or connected OpenWork servers.",
|
||||
"config.reload_connect_hint": "Connect to this worker to reload.",
|
||||
"config.reload_engine": "Reload engine",
|
||||
"config.reload_now_desc": "Applies config updates and reconnects your session.",
|
||||
"config.reload_now_title": "Reload now",
|
||||
"config.reloading": "Reloading...",
|
||||
"config.remote_access_off_hint": "Remote access is off. Use Share workspace to enable it before connecting from another machine.",
|
||||
"config.resolved_worker_url": "Resolved worker URL:",
|
||||
"config.resume_sessions_desc": "If a reload was queued while tasks were running, send a resume message afterward.",
|
||||
"config.resume_sessions_title": "Resume sessions after auto reload",
|
||||
"config.server_needed_hint": "OpenWork server connection needed to sync skills, plugins, and commands.",
|
||||
"config.server_section_desc": "Connect to an OpenWork server. Use the URL plus a collaborator or owner token from your server admin.",
|
||||
"config.server_section_title": "OpenWork server",
|
||||
"config.server_sharing_desc": "Share these details with a trusted device. Keep the server on the same network for the fastest setup.",
|
||||
"config.server_sharing_menu_hint": "For per-workspace sharing links, use Share... in the workspace menu.",
|
||||
"config.server_sharing_title": "OpenWork server sharing",
|
||||
"config.server_url_hint": "Use the URL shared by your OpenWork server. Local desktop workers reuse a persistent high port in the 48000-51000 range.",
|
||||
"config.server_url_input_label": "OpenWork server URL",
|
||||
"config.server_url_label": "OpenWork Server URL",
|
||||
"config.starting_server": "Starting server\\u2026",
|
||||
"config.status_connected": "Connected",
|
||||
"config.status_limited": "Limited",
|
||||
"config.status_not_connected": "Not connected",
|
||||
"config.test_connection": "Test connection",
|
||||
"config.testing": "Testing...",
|
||||
"config.testing_connection": "Testing connection...",
|
||||
"config.token_hint": "Optional. Paste a collaborator token for routine access or an owner token when this client must answer permission prompts.",
|
||||
"config.token_label": "Collaborator or owner token",
|
||||
"config.token_placeholder": "Paste your token",
|
||||
"config.unavailable": "Unavailable",
|
||||
"config.worker_id": "Worker ID:",
|
||||
"config.workspace_config_desc": "These settings affect the selected workspace. Runtime-only actions apply to whichever workspace is currently connected.",
|
||||
"config.workspace_config_title": "Workspace config",
|
||||
"config.workspace_id_prefix": "Workspace:",
|
||||
|
||||
// ==================== Common (additions) ====================
|
||||
"common.navigate": "navigate",
|
||||
"common.select": "select",
|
||||
"common.submit": "Submit",
|
||||
"common.next": "Next",
|
||||
"common.question": "Question",
|
||||
// ==================== Extensions (PR3) ====================
|
||||
"extensions.app_count_many": "apps connected",
|
||||
"extensions.app_count_one": "app connected",
|
||||
"extensions.apps_mcp_header": "Apps (MCP)",
|
||||
"extensions.filter_all": "All",
|
||||
"extensions.filter_apps": "Apps",
|
||||
"extensions.filter_plugins": "Plugins",
|
||||
"extensions.plugin_count_many": "plugins",
|
||||
"extensions.plugin_count_one": "plugin",
|
||||
"extensions.plugins_opencode_header": "Plugins (OpenCode)",
|
||||
"extensions.subtitle": "Apps (MCP) and OpenCode plugins live in one place.",
|
||||
"extensions.title": "Extensions",
|
||||
|
||||
// ==================== Providers store ====================
|
||||
"providers.api_key_label": "API key",
|
||||
"providers.not_connected": "Not connected to a server",
|
||||
"providers.connect_failed": "Failed to connect provider",
|
||||
"providers.oauth_failed": "Failed to complete OAuth",
|
||||
"providers.save_api_key_failed": "Failed to save API key",
|
||||
"providers.disconnect_failed": "Failed to disconnect provider",
|
||||
"providers.load_failed": "Failed to load providers",
|
||||
"providers.auth_failed": "Authentication failed",
|
||||
"providers.rate_limit_exceeded": "Rate limit exceeded",
|
||||
"providers.provider_error": "Provider error ({provider})",
|
||||
"providers.request_failed": "Request failed",
|
||||
"providers.api_key_required": "API key is required",
|
||||
"providers.no_providers_available": "No providers available",
|
||||
"providers.provider_id_required": "Provider ID is required",
|
||||
"providers.unknown_provider": "Unknown provider",
|
||||
"providers.no_oauth_prefix": "No OAuth flow available for",
|
||||
"providers.use_api_key_suffix": "Use an API key instead.",
|
||||
"providers.not_oauth_flow_prefix": "Selected auth method is not an OAuth flow for",
|
||||
"providers.oauth_method_required": "OAuth method is required",
|
||||
"providers.removal_unsupported": "Provider auth removal is not supported by this client.",
|
||||
"providers.disconnected_prefix": "Disconnected",
|
||||
"providers.disabled_in_config_suffix": "and disabled it in OpenCode config.",
|
||||
"providers.still_connected_suffix": ", but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.",
|
||||
// ==================== Identities (PR3) ====================
|
||||
"identities.agent_behavior_desc": "One file per workspace. Add optional first line @agent <id> to route via a specific OpenCode agent.",
|
||||
"identities.agent_behavior_title": "Messaging agent behavior",
|
||||
"identities.agent_created": "Created default messaging agent file.",
|
||||
"identities.agent_file_changed": "File changed remotely. Reload and save again.",
|
||||
"identities.agent_loading": "Loading agent file\\u2026",
|
||||
"identities.agent_not_found": "Agent file not found in this workspace yet.",
|
||||
"identities.agent_placeholder": "Add messaging behavior instructions for opencodeRouter here...",
|
||||
"identities.agent_saved": "Saved messaging behavior.",
|
||||
"identities.agent_worker_scope_unavailable": "Worker scope unavailable.",
|
||||
"identities.all_channels": "All channels",
|
||||
"identities.app_token_label": "App token",
|
||||
"identities.auto_bind_label": "Auto-bind peer to directory on direct send",
|
||||
"identities.available_channels": "Available channels",
|
||||
"identities.bot_token_label": "Bot token",
|
||||
"identities.channel_label": "Channel",
|
||||
"identities.channels_connected": "connected",
|
||||
"identities.channels_label": "Channels",
|
||||
"identities.configured_suffix": "configured",
|
||||
"identities.connect_server_desc": "Identities are available when you are connected to an OpenWork host.",
|
||||
"identities.connect_server_title": "Connect to an OpenWork server",
|
||||
"identities.connect_slack": "Connect Slack",
|
||||
"identities.connected_badge": "Connected",
|
||||
"identities.connecting": "Connecting...",
|
||||
"identities.copy_bot_token_hint": "Copy the bot token and paste it below.",
|
||||
"identities.copy_code": "Copy code",
|
||||
"identities.create_default_file": "Create default file",
|
||||
"identities.create_private_bot": "Create private bot",
|
||||
"identities.create_public_bot": "Create public bot",
|
||||
"identities.default_routing": "Default routing",
|
||||
"identities.directory_label": "Directory (optional)",
|
||||
"identities.disable_messaging": "Disable messaging",
|
||||
"identities.disable_messaging_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.",
|
||||
"identities.disable_messaging_title": "Disable messaging for this worker?",
|
||||
"identities.disabled_label": "Disabled",
|
||||
"identities.disabling": "Disabling...",
|
||||
"identities.disconnect": "Disconnect",
|
||||
"identities.enable_messaging": "Enable messaging",
|
||||
"identities.enable_messaging_risk": "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.",
|
||||
"identities.enable_messaging_title": "Enable messaging for this worker?",
|
||||
"identities.enabled_label": "Enabled",
|
||||
"identities.enabling": "Enabling...",
|
||||
"identities.health_offline": "Offline",
|
||||
"identities.health_running": "Running",
|
||||
"identities.health_unavailable": "Unavailable",
|
||||
"identities.health_unknown": "Unknown",
|
||||
"identities.identities_label": "Identities",
|
||||
"identities.just_now": "Just now",
|
||||
"identities.last_activity": "Last activity",
|
||||
"identities.later": "Later",
|
||||
"identities.message_label": "Message",
|
||||
"identities.message_routing_desc": "Control which conversations go to which workspace folder. Messages are routed to the worker's default folder unless you set up rules here.",
|
||||
"identities.message_routing_title": "Message routing",
|
||||
"identities.messages_today": "Messages today",
|
||||
"identities.messaging_disabled_hint": "Enable messaging only if you understand the risk and plan to secure access (for example, private Telegram pairing).",
|
||||
"identities.messaging_disabled_restart": "Messaging disabled. Restart this worker to stop the messaging sidecar.",
|
||||
"identities.messaging_disabled_risk": "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.",
|
||||
"identities.messaging_disabled_title": "Messaging is disabled by default",
|
||||
"identities.messaging_enabled_restart": "Messaging enabled. Restart this worker to apply before configuring channels.",
|
||||
"identities.messaging_sidecar_not_running": "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.",
|
||||
"identities.not_set": "Not set",
|
||||
"identities.pairing_code_copied": "Pairing code copied.",
|
||||
"identities.pairing_code_copy_failed": "Could not copy pairing code. Copy it manually.",
|
||||
"identities.peer_id_label": "Peer ID (optional)",
|
||||
"identities.private_label": "Private",
|
||||
"identities.private_pairing_code": "Private pairing code",
|
||||
"identities.public_bot_confirm": "Yes I understand the risk",
|
||||
"identities.public_bot_warning_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?",
|
||||
"identities.public_bot_warning_title": "Make this bot public?",
|
||||
"identities.public_label": "Public",
|
||||
"identities.quick_setup": "Quick setup",
|
||||
"identities.reconnect_failed": "Reconnect failed. Check OpenWork URL/token and try again.",
|
||||
"identities.reconnected": "Reconnected.",
|
||||
"identities.reconnected_refreshing": "Reconnected. Refreshing worker state...",
|
||||
"identities.reload": "Reload",
|
||||
"identities.repair_reconnect": "Repair & reconnect",
|
||||
"identities.restart_failed": "Restart failed. Please restart the worker from Settings and try again.",
|
||||
"identities.restart_to_disable_messaging": "Messaging was disabled for this workspace. Restart the worker now to stop the messaging sidecar.",
|
||||
"identities.restart_to_enable_messaging": "Messaging was enabled for this workspace. Restart the worker now to start the messaging sidecar and unlock Telegram and Slack setup.",
|
||||
"identities.restart_worker": "Restart worker",
|
||||
"identities.restart_worker_title": "Restart worker now?",
|
||||
"identities.restarting": "Restarting...",
|
||||
"identities.running_label": "Running",
|
||||
"identities.save_behavior": "Save behavior",
|
||||
"identities.saving": "Saving...",
|
||||
"identities.send_test_button": "Send test message",
|
||||
"identities.send_test_desc": "Validate outbound wiring. Use a peer ID for direct send, or leave peer ID empty to fan out by bindings in a directory.",
|
||||
"identities.send_test_title": "Send test message",
|
||||
"identities.sending": "Sending...",
|
||||
"identities.slack_desc": "Your worker appears as a bot in Slack channels. Team members can message it directly or mention it in threads.",
|
||||
"identities.slack_intro": "Connect your Slack workspace to let team members interact with this worker in channels and DMs.",
|
||||
"identities.slack_unavailable": "Slack identities unavailable.",
|
||||
"identities.status_active": "Active",
|
||||
"identities.status_label": "Status",
|
||||
"identities.status_stopped": "Stopped",
|
||||
"identities.stopped_label": "Stopped",
|
||||
"identities.subtitle": "Let people reach your worker through messaging apps. Connect a channel and your worker will automatically read and respond to messages.",
|
||||
"identities.tab_general": "General",
|
||||
"identities.telegram_bot_access_desc": "Public bot: first Telegram chat auto-links. Private bot: requires a pairing code before any messages run tools.",
|
||||
"identities.telegram_delete_failed": "Failed to delete.",
|
||||
"identities.telegram_deleted": "Deleted.",
|
||||
"identities.telegram_deleted_pending": "Deleted (pending apply).",
|
||||
"identities.telegram_desc": "Connect a Telegram bot in public mode (open inbox) or private mode (pairing code required).",
|
||||
"identities.telegram_save_failed": "Failed to save.",
|
||||
"identities.telegram_saved": "Saved.",
|
||||
"identities.telegram_saved_pending": "Saved (pending apply).",
|
||||
"identities.telegram_unavailable": "Telegram identities unavailable.",
|
||||
"identities.title": "Messaging channels",
|
||||
"identities.unsaved_changes": "Unsaved changes",
|
||||
"identities.worker_offline": "Worker offline",
|
||||
"identities.worker_online": "Worker online",
|
||||
"identities.worker_restarted": "Worker restarted.",
|
||||
"identities.worker_restarted_refreshing": "Worker restarted. Refreshing messaging status...",
|
||||
"identities.worker_scope_unavailable": "Worker scope unavailable.",
|
||||
"identities.worker_scope_unavailable_detail": "Worker scope unavailable. Reconnect using a worker URL or switch to a known worker.",
|
||||
"identities.worker_unavailable": "Worker unavailable",
|
||||
"identities.workspace_id_required": "Workspace ID is required to manage identities. Reconnect with a workspace URL or select a workspace mapped on this host.",
|
||||
"identities.workspace_scope_prefix": "Workspace scope:",
|
||||
|
||||
// ==================== MCP Auth Modal (additions) ====================
|
||||
"mcp.auth.authorization_link": "Authorization link",
|
||||
"mcp.auth.copied": "Copied",
|
||||
"mcp.auth.copy_link": "Copy link",
|
||||
"mcp.auth.request_timed_out": "Request timed out.",
|
||||
// ==================== Plugins (PR3) ====================
|
||||
"plugins.desc": "Manage `opencode.json` for your project or global OpenCode plugins.",
|
||||
"plugins.empty": "No plugins configured yet.",
|
||||
"plugins.not_loaded_yet": "Not loaded yet",
|
||||
"plugins.remove": "Remove",
|
||||
"plugins.suggested_heading": "Suggested plugins",
|
||||
|
||||
// ==================== Scheduled (PR3) ====================
|
||||
"scheduled.at_time": "At {time}",
|
||||
"scheduled.badge_end_of_day": "End-of-day",
|
||||
"scheduled.badge_every_few_hours": "Every few hours",
|
||||
"scheduled.badge_friday_wrapup": "Friday wrap-up",
|
||||
"scheduled.badge_weekday_evening": "Weekday evening",
|
||||
"scheduled.badge_weekday_morning": "Weekday morning",
|
||||
"scheduled.badge_weekend_review": "Weekend review",
|
||||
"scheduled.create_button": "Create",
|
||||
"scheduled.create_desc": "Automations are scheduled by running a prompt in a new thread. We'll prefill a prompt for you to send.",
|
||||
"scheduled.create_title": "Create automation",
|
||||
"scheduled.created_prefix": "Created",
|
||||
"scheduled.custom_schedule": "Custom schedule",
|
||||
"scheduled.daily_mode": "Daily",
|
||||
"scheduled.day_fri": "Fri",
|
||||
"scheduled.day_mon": "Mon",
|
||||
"scheduled.day_sat": "Sat",
|
||||
"scheduled.day_sun": "Sun",
|
||||
"scheduled.day_thu": "Thu",
|
||||
"scheduled.day_tue": "Tue",
|
||||
"scheduled.day_wed": "Wed",
|
||||
"scheduled.days_at": "{days} at {time}",
|
||||
"scheduled.default_automation_name": "Daily bug scan",
|
||||
"scheduled.delete_confirm_title": "Delete automation?",
|
||||
"scheduled.delete_error_fallback": "Failed to delete job.",
|
||||
"scheduled.delete_label": "Delete",
|
||||
"scheduled.deleting": "Deleting",
|
||||
"scheduled.desktop_required": "Scheduled tasks require the desktop app.",
|
||||
"scheduled.empty_hint": "No automations yet. Pick a template or create your own automation prompt.",
|
||||
"scheduled.every_day_at": "Every day at {time}",
|
||||
"scheduled.every_hour": "Every hour",
|
||||
"scheduled.every_n_hours": "Every {interval} hours",
|
||||
"scheduled.every_prefix": "Every",
|
||||
"scheduled.explore_more": "Explore more",
|
||||
"scheduled.failed_status": "Failed",
|
||||
"scheduled.filter_all": "All",
|
||||
"scheduled.filter_scheduled": "Scheduled",
|
||||
"scheduled.filter_templates": "Templates",
|
||||
"scheduled.hours_suffix": "hours",
|
||||
"scheduled.install_scheduler": "Install scheduler",
|
||||
"scheduled.install_scheduler_hint": "Automations run through the opencode-scheduler plugin. Add it to this workspace to enable scheduling.",
|
||||
"scheduled.install_scheduler_title": "Install the scheduler to unlock automations",
|
||||
"scheduled.installing": "Installing...",
|
||||
"scheduled.interval_mode": "Interval",
|
||||
"scheduled.last_run_prefix": "Last run",
|
||||
"scheduled.last_updated_prefix": "Last updated",
|
||||
"scheduled.name_label": "Name",
|
||||
"scheduled.never": "Never",
|
||||
"scheduled.new_automation": "New automation",
|
||||
"scheduled.no_automations_match": "No automations match this search.",
|
||||
"scheduled.no_templates_match": "No templates match this search.",
|
||||
"scheduled.not_run_yet": "Not run yet",
|
||||
"scheduled.not_synced_yet": "Not synced yet",
|
||||
"scheduled.page_description": "Schedule recurring tasks for this worker, monitor what is already registered, and start from a reusable template.",
|
||||
"scheduled.prepare_error_fallback": "Failed to prepare automation in chat.",
|
||||
"scheduled.quick_start_templates": "Quick start templates",
|
||||
"scheduled.quick_start_templates_desc": "Start from a proven recurring workflow, then tailor the prompt before you prepare it in chat.",
|
||||
"scheduled.refreshing": "Refreshing",
|
||||
"scheduled.reload_activate_hint": "OpenCode loads plugins at startup. Reload OpenWork to activate opencode-scheduler.",
|
||||
"scheduled.reload_activate_title": "Reload OpenWork to activate automations",
|
||||
"scheduled.reload_openwork": "Reload OpenWork",
|
||||
"scheduled.reloading": "Reloading...",
|
||||
"scheduled.run_label": "Run",
|
||||
"scheduled.running_status": "Running",
|
||||
"scheduled.schedule_label": "Schedule",
|
||||
"scheduled.search_placeholder": "Search automations or templates",
|
||||
"scheduled.source_local": "From local scheduler",
|
||||
"scheduled.source_remote": "From OpenWork server",
|
||||
"scheduled.subtitle_local": "Automations that run on a schedule from this device.",
|
||||
"scheduled.subtitle_remote": "Automations that run on a schedule from the connected OpenWork server.",
|
||||
"scheduled.success_status": "Success",
|
||||
"scheduled.task_summary_no_prompt": "No prompt or command found.",
|
||||
"scheduled.task_summary_prompt": "Prompt",
|
||||
"scheduled.template_badge": "Template",
|
||||
"scheduled.template_count": "{count} templates",
|
||||
"scheduled.tpl_daily_planning_desc": "Build a focused plan from your tasks and calendar.",
|
||||
"scheduled.tpl_daily_planning_name": "Daily planning brief",
|
||||
"scheduled.tpl_habit_checkin_desc": "Run a quick accountability check through the day.",
|
||||
"scheduled.tpl_habit_checkin_name": "Habit check-in",
|
||||
"scheduled.tpl_inbox_zero_desc": "Summarize unread messages and draft short replies.",
|
||||
"scheduled.tpl_inbox_zero_name": "Inbox zero helper",
|
||||
"scheduled.tpl_learning_digest_desc": "Turn saved links and notes into a weekly digest.",
|
||||
"scheduled.tpl_learning_digest_name": "Learning digest",
|
||||
"scheduled.tpl_meeting_prep_desc": "Generate prep bullets for tomorrow's meetings.",
|
||||
"scheduled.tpl_meeting_prep_name": "Meeting prep notes",
|
||||
"scheduled.tpl_weekly_wins_desc": "Create a Friday recap of wins, blockers, and next steps.",
|
||||
"scheduled.tpl_weekly_wins_name": "Weekly wins recap",
|
||||
"scheduled.view_scheduler_docs": "View scheduler docs",
|
||||
"scheduled.weekdays_at": "Weekdays at {time}",
|
||||
"scheduled.weekends_at": "Weekends at {time}",
|
||||
"scheduled.worker_root_hint": "Worker root is inferred from the selected workspace.",
|
||||
"scheduled.your_automations": "Your automations",
|
||||
|
||||
// ==================== Settings (PR3) ====================
|
||||
"settings.cap_browser_tools": "Browser tools: {value}",
|
||||
"settings.cap_commands": "Commands: {value}",
|
||||
"settings.cap_config": "Config: {value}",
|
||||
"settings.cap_file_tools": "File tools: {value}",
|
||||
"settings.cap_mcp": "MCP: {value}",
|
||||
"settings.cap_plugins": "Plugins: {value}",
|
||||
"settings.cap_proxy": "Proxy (OpenCodeRouter): {value}",
|
||||
"settings.cap_sandbox": "Sandbox: {value}",
|
||||
"settings.cap_skills": "Skills: {value}",
|
||||
"settings.debug_commit": "Commit: {sha}",
|
||||
"settings.debug_desktop_app": "Desktop app: {version}",
|
||||
"settings.debug_opencode_router_version": "OpenCodeRouter: {version}",
|
||||
"settings.debug_opencode_version": "OpenCode: {version}",
|
||||
"settings.debug_openwork_server_version": "OpenWork server: {version}",
|
||||
"settings.debug_orchestrator_version": "Orchestrator: {version}",
|
||||
"settings.diag_approval": "Approval: {mode} ({ms}ms)",
|
||||
"settings.diag_config_path": "Config path: {path}",
|
||||
"settings.diag_daemon_url": "Daemon: {url}",
|
||||
"settings.diag_health_port": "Health port: {port}",
|
||||
"settings.diag_healthy_ms": "Healthy: {ms}ms",
|
||||
"settings.diag_host_token_source": "Host token source: {source}",
|
||||
"settings.diag_last_attempt": "Last attempt: {time}",
|
||||
"settings.diag_load_sessions_ms": "Load sessions: {ms}ms",
|
||||
"settings.diag_opencode_binary": "OpenCode binary: {binary}",
|
||||
"settings.diag_opencode_url": "OpenCode: {url}",
|
||||
"settings.diag_pending_permissions_ms": "Pending permissions: {ms}ms",
|
||||
"settings.diag_pid": "PID: {pid}",
|
||||
"settings.diag_providers_ms": "Providers: {ms}ms",
|
||||
"settings.diag_read_only": "Read-only: {value}",
|
||||
"settings.diag_reason": "Reason: {reason}",
|
||||
"settings.diag_runtime_workspace": "Runtime workspace: {id}",
|
||||
"settings.diag_selected_workspace": "Selected workspace: {id}",
|
||||
"settings.diag_sidecar": "Sidecar: {info}",
|
||||
"settings.diag_started": "Started: {time}",
|
||||
"settings.diag_token_source": "Token source: {source}",
|
||||
"settings.diag_total_ms": "Total: {ms}ms",
|
||||
"settings.diag_version": "Version: {version}",
|
||||
"settings.diag_workspaces": "Workspaces: {count}",
|
||||
"settings.downloading_bytes": "Downloading {downloaded}",
|
||||
"settings.downloading_progress": "Downloading {downloaded} / {total} ({percent}%)",
|
||||
"settings.messaging_section_desc": "Manage Telegram/Slack identities and bindings in the Identities tab.",
|
||||
"settings.messaging_section_title": "Messaging",
|
||||
"settings.sandbox_result": "Result: {status}",
|
||||
"settings.sandbox_run_id": "Run ID: {id}",
|
||||
"settings.update_available_version": "Update available: v{version}",
|
||||
"settings.update_last_checked": "Last checked {time}",
|
||||
"settings.update_published": "Published {date}",
|
||||
"settings.update_ready_version": "Ready to install: v{version}",
|
||||
|
||||
// ==================== Skills (PR3) ====================
|
||||
"skills.from_repo": "From {owner}/{repo}",
|
||||
"skills.install_name_title": "Install {name}",
|
||||
"skills.shown_count": "{count} shown",
|
||||
"skills.trigger_label": "Trigger: {trigger}",
|
||||
|
||||
// ==================== Second Pass Additions ====================
|
||||
|
||||
// Identities (second pass)
|
||||
"identities.minutes_ago": "{minutes}m ago",
|
||||
"identities.hours_ago": "{hours}h ago",
|
||||
"identities.days_ago": "{days}d ago",
|
||||
"identities.dispatched_messages": "Dispatched {sent}/{attempted} messages.",
|
||||
"identities.health_unavailable_status": "OpenCodeRouter health unavailable ({status})",
|
||||
"identities.telegram_private_saved_pair": "Private bot saved. Pair via /pair {code}",
|
||||
"identities.telegram_saved_username": "Saved (@{username})",
|
||||
"identities.botfather_step1_open": "1. Open @BotFather in Telegram",
|
||||
"identities.botfather_step1_run": "and run /newbot",
|
||||
"identities.botfather_step3_choose": "3. Choose a name and username for your bot",
|
||||
"identities.botfather_step3_public": "Public",
|
||||
"identities.botfather_step3_or_private": "for open inbox or",
|
||||
"identities.botfather_step3_private": "Private",
|
||||
"identities.botfather_step3_to_require": "to require",
|
||||
"identities.botfather_step_public": "Public bots can be found by anyone.",
|
||||
"identities.botfather_step_or_private": "Or create a private bot:",
|
||||
"identities.botfather_step_private": "Private bots require an invite link to start.",
|
||||
"identities.botfather_step_to_require": "To require users to be added, enable the privacy mode via @BotFather.",
|
||||
"identities.bot_token_placeholder": "Paste Telegram bot token from @BotFather",
|
||||
"identities.pairing_code_instruction_prefix": "Send",
|
||||
"identities.open_bot_link": "Open @{username} in Telegram",
|
||||
"identities.routing_override_prefix": "All messages routed to",
|
||||
"identities.routing_override_suffix": "(override active)",
|
||||
"identities.agent_scope_status": "Active scope: workspace · status: {status} · selected agent: {agent}",
|
||||
"identities.agent_status_loaded": "loaded",
|
||||
"identities.agent_status_missing": "missing",
|
||||
"identities.agent_status_none": "none",
|
||||
"identities.agent_none": "none",
|
||||
"identities.peer_id_placeholder_telegram": "e.g. telegram:123456789",
|
||||
"identities.peer_id_placeholder_slack": "e.g. slack:U12345678",
|
||||
|
||||
// Automations/Scheduled (second pass)
|
||||
"scheduled.scheduler_install_requested": "Scheduler install requested.",
|
||||
"scheduled.prepared_automation_in_chat": "Prepared automation in chat.",
|
||||
"scheduled.prepared_job_in_chat": "Prepared {name} in chat.",
|
||||
"scheduled.removed_job": "Removed {name}.",
|
||||
"scheduled.delete_confirm_desc": "This removes the schedule and deletes the job definition from {source}.",
|
||||
|
||||
// Settings (second pass)
|
||||
"settings.actor_unknown": "unknown",
|
||||
"settings.actor_host": "host",
|
||||
"settings.actor_remote": "remote",
|
||||
"settings.cap_read": "read",
|
||||
"settings.cap_write": "write",
|
||||
"settings.workspace_fallback_name": "Workspace",
|
||||
"settings.deeplink_hint": "Accepts openwork://, openwork-dev://, or a raw supported https://share.openworklabs.com/b/... URL.",
|
||||
"settings.worker_id_label": "Worker {id}",
|
||||
"settings.cap_inbox_on": "inbox on",
|
||||
"settings.cap_inbox_off": "inbox off",
|
||||
"settings.cap_outbox_on": "outbox on",
|
||||
"settings.cap_outbox_off": "outbox off",
|
||||
|
||||
// Skills (second pass)
|
||||
"skills.loading": "Loading…",
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user