Skip to content
52 changes: 27 additions & 25 deletions apps/app/src/app/app-settings/authorized-folders-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {

import { Folder, FolderLock, FolderSearch, X } from "lucide-solid";

import { currentLocale, t } from "../../i18n";
import { t } from "../../i18n";
import Button from "../components/button";
import type {
OpenworkServerCapabilities,
Expand Down Expand Up @@ -87,7 +87,9 @@ const readAuthorizedFoldersFromConfig = (opencodeConfig: Record<string, unknown>
const buildAuthorizedFoldersStatus = (preservedCount: number, action?: string) => {
const preservedLabel =
preservedCount > 0
? `Preserving ${preservedCount} non-folder permission ${preservedCount === 1 ? "entry" : "entries"}.`
? preservedCount === 1
? t("context_panel.preserving_entry")
: t("context_panel.preserving_entries", undefined, { count: preservedCount })
: null;
if (action && preservedLabel) return `${action} ${preservedLabel}`;
return action ?? preservedLabel;
Expand Down Expand Up @@ -133,13 +135,13 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
(props.openworkServerCapabilities?.config?.write ?? false),
);
const authorizedFoldersHint = createMemo(() => {
if (!openworkServerReady()) return "OpenWork server is disconnected.";
if (!openworkServerWorkspaceReady()) return "No active server workspace is selected.";
if (!openworkServerReady()) return t("context_panel.server_disconnected");
if (!openworkServerWorkspaceReady()) return t("context_panel.no_server_workspace");
if (!canReadConfig()) {
return "OpenWork server config access is unavailable for this workspace.";
return t("context_panel.config_access_unavailable");
}
if (!canWriteConfig()) {
return "OpenWork server is connected read-only for workspace config.";
return t("context_panel.config_read_only");
}
return null;
});
Expand Down Expand Up @@ -206,14 +208,14 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
const openworkWorkspaceId = props.runtimeWorkspaceId;
if (!openworkClient || !openworkWorkspaceId || !canWriteConfig()) {
setAuthorizedFoldersError(
"A writable OpenWork server workspace is required to update authorized folders.",
t("context_panel.writable_workspace_required"),
);
return false;
}

setAuthorizedFoldersSaving(true);
setAuthorizedFoldersError(null);
setAuthorizedFoldersStatus("Saving authorized folders...");
setAuthorizedFoldersStatus(t("context_panel.saving_folders"));

try {
const currentConfig = await openworkClient.getConfig(openworkWorkspaceId);
Expand All @@ -236,7 +238,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
setAuthorizedFoldersStatus(
buildAuthorizedFoldersStatus(
Object.keys(currentAuthorizedFolders.hiddenEntries).length,
"Authorized folders updated.",
t("context_panel.folders_updated"),
),
);
props.onConfigUpdated();
Expand All @@ -257,13 +259,13 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
if (!normalized) return;
if (workspaceRoot && normalized === workspaceRoot) {
setAuthorizedFolderDraft("");
setAuthorizedFoldersStatus("Workspace root is already available.");
setAuthorizedFoldersStatus(t("context_panel.workspace_root_available"));
setAuthorizedFoldersError(null);
return;
}
if (authorizedFolders().includes(normalized)) {
setAuthorizedFolderDraft("");
setAuthorizedFoldersStatus("Folder is already authorized.");
setAuthorizedFoldersStatus(t("context_panel.folder_already_authorized"));
setAuthorizedFoldersError(null);
return;
}
Expand All @@ -283,7 +285,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
if (!isTauriRuntime()) return;
try {
const selection = await pickDirectory({
title: t("onboarding.authorize_folder", currentLocale()),
title: t("onboarding.authorize_folder"),
});
const folder =
typeof selection === "string"
Expand All @@ -297,12 +299,12 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
setAuthorizedFolderDraft(normalized);
if (workspaceRoot && normalized === workspaceRoot) {
setAuthorizedFolderDraft("");
setAuthorizedFoldersStatus("Workspace root is already available.");
setAuthorizedFoldersStatus(t("context_panel.workspace_root_available"));
setAuthorizedFoldersError(null);
return;
}
if (authorizedFolders().includes(normalized)) {
setAuthorizedFoldersStatus("Folder is already authorized.");
setAuthorizedFoldersStatus(t("context_panel.folder_already_authorized"));
setAuthorizedFoldersError(null);
return;
}
Expand All @@ -321,10 +323,10 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
<div class="space-y-1">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-12">
<FolderLock size={16} class="text-gray-10" />
Authorized folders
{t("context_panel.authorized_folders")}
</div>
<div class="text-xs text-gray-9 leading-relaxed max-w-[65ch]">
Grant this workspace access to read and edit files in directories outside of its root.
{t("context_panel.authorized_folders_desc")}
</div>
</div>

Expand All @@ -333,7 +335,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
fallback={
<div class={`${softPanelClass} px-3 py-3 text-xs text-gray-10`}>
{authorizedFoldersHint() ??
"Connect to a writable OpenWork server workspace to edit authorized folders."}
t("context_panel.authorized_folders_no_access")}
</div>
}
>
Expand All @@ -353,9 +355,9 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-3/30 text-blue-11 mb-3">
<Folder size={20} />
</div>
<div class="text-sm font-medium text-gray-11">No external folders authorized</div>
<div class="text-sm font-medium text-gray-11">{t("context_panel.no_external_folders")}</div>
<div class="text-[11px] text-gray-9 mt-1 max-w-[40ch]">
Add a folder to let this workspace read and edit files outside its root directory.
{t("context_panel.add_folder_hint")}
</div>
</div>
}
Expand All @@ -380,7 +382,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
<span class="truncate text-sm font-medium text-gray-12">{folderName}</span>
<Show when={isWorkspaceRoot}>
<span class="rounded-full border border-blue-7/30 bg-blue-3/25 px-2 py-0.5 text-[10px] font-medium text-blue-11">
Workspace root
{t("context_panel.workspace_root_badge")}
</span>
</Show>
</div>
Expand All @@ -391,7 +393,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
when={!isWorkspaceRoot}
fallback={
<span class="shrink-0 text-[10px] font-medium text-gray-8">
Always available
{t("context_panel.always_available")}
</span>
}
>
Expand All @@ -404,7 +406,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
authorizedFoldersSaving() ||
!canWriteConfig()
}
aria-label={`Remove ${folderName}`}
aria-label={t("context_panel.remove_folder", undefined, { name: folderName })}
>
<X size={16} class="text-current" />
</Button>
Expand Down Expand Up @@ -446,7 +448,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
onPaste={(event) => {
event.preventDefault();
}}
placeholder="Type a folder path to authorize..."
placeholder={t("context_panel.input_placeholder")}
disabled={
authorizedFoldersLoading() ||
authorizedFoldersSaving() ||
Expand All @@ -467,7 +469,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
!canWriteConfig()
}
>
<FolderSearch size={13} class="mr-1.5" /> Browse
<FolderSearch size={13} class="mr-1.5" /> {t("context_panel.browse_button")}
</Button>
</Show>

Expand All @@ -482,7 +484,7 @@ export default function AuthorizedFoldersPanel(props: AuthorizedFoldersPanelProp
!authorizedFolderDraft().trim()
}
>
{authorizedFoldersSaving() ? "Adding..." : "Add"}
{authorizedFoldersSaving() ? t("context_panel.adding_button") : t("context_panel.add_button")}
</Button>
</form>
</div>
Expand Down
26 changes: 13 additions & 13 deletions apps/app/src/app/components/model-picker-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { For, Show, createEffect, createMemo, createSignal } from "solid-js";

import { CheckCircle2, Circle, Search, X } from "lucide-solid";
import { t, currentLocale } from "../../i18n";
import { t } from "../../i18n";

import Button from "./button";
import ProviderIcon from "./provider-icon";
Expand All @@ -24,7 +24,7 @@ export type ModelPickerModalProps = {

export default function ModelPickerModal(props: ModelPickerModalProps) {
let searchInputRef: HTMLInputElement | undefined;
const translate = (key: string) => t(key, currentLocale());
const translate = (key: string, params?: Record<string, string | number>) => t(key, undefined, params);

type RenderedItem =
| { kind: "model"; opt: ModelOption }
Expand Down Expand Up @@ -306,9 +306,9 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
<span class="truncate">{provider.title}</span>
</div>
<div class={`mt-0.5 flex items-center gap-3 text-[11px] ${index === activeIndex() ? 'text-gray-10' : 'text-gray-9 group-hover:text-gray-10'}`}>
<span class="truncate">Connect this provider to browse and save models</span>
<span class="truncate">{translate("model_picker.connect_provider_hint")}</span>
<span class="ml-auto opacity-70">
{provider.matchCount} {provider.matchCount === 1 ? "model" : "models"}
{translate(provider.matchCount === 1 ? "model_picker.model_count_one" : "model_picker.model_count", { count: provider.matchCount })}
</span>
</div>
</div>
Expand All @@ -324,12 +324,12 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-lg font-semibold text-gray-12">
{props.target === "default" ? "Default model" : "Chat model"}
{translate(props.target === "default" ? "model_picker.default_model_title" : "model_picker.chat_model_title")}
</h3>
<p class="text-sm text-gray-11 mt-1">
{props.target === "default"
? "Choose the default model for new chats, then fine-tune reasoning profiles on its card before pressing Done."
: "Choose the model for this chat. If a model supports reasoning profiles, configure them on its card."}
{translate(props.target === "default"
? "model_picker.default_model_desc"
: "model_picker.chat_model_desc")}
</p>
</div>
<Button
Expand All @@ -355,7 +355,7 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
</div>
<Show when={props.query.trim()}>
<div class="mt-2 text-xs text-dls-secondary">
{translate("settings.showing_models").replace("{count}", String(props.filteredOptions.length)).replace("{total}", String(props.options.length))}
{translate("settings.showing_models", { count: props.filteredOptions.length, total: props.options.length })}
</div>
</Show>
</div>
Expand All @@ -364,7 +364,7 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
<Show when={recommendedOptions().length > 0}>
<section class="space-y-2">
<div class="px-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-9">
Recommended
{translate("model_picker.recommended")}
</div>
<For each={recommendedOptions()}>{({ opt, index }) => renderOption(opt, index)}</For>
</section>
Expand All @@ -373,7 +373,7 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
<Show when={otherEnabledOptions().length > 0}>
<section class="space-y-2">
<div class="px-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-9">
Other connected models
{translate("model_picker.other_connected_models")}
</div>
<For each={otherEnabledOptions()}>{({ opt, index }) => renderOption(opt, index)}</For>
</section>
Expand All @@ -382,7 +382,7 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {
<Show when={otherOptions().length > 0}>
<section class="space-y-2">
<div class="px-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-9">
More providers
{translate("model_picker.more_providers")}
</div>
<For each={otherOptions()}>
{(provider) => renderProviderLink(provider, provider.index)}
Expand All @@ -392,7 +392,7 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {

<Show when={renderedItems().length === 0}>
<div class="rounded-2xl border border-gray-6/70 bg-gray-1/40 px-4 py-6 text-sm text-gray-10">
No models match your search.
{translate("model_picker.no_results")}
</div>
</Show>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Plus,
} from "lucide-solid";

import { DEFAULT_SESSION_TITLE, getDisplaySessionTitle } from "../../lib/session-title";
import { getDisplaySessionTitle } from "../../lib/session-title";
import type { WorkspaceInfo } from "../../lib/tauri";
import type {
WorkspaceConnectionState,
Expand Down Expand Up @@ -348,7 +348,7 @@ export default function WorkspaceSessionList(props: Props) {
const depth = () => row.depth;
const isSelected = () => props.selectedSessionId === session().id;
const displayTitle = () =>
getDisplaySessionTitle(session().title, DEFAULT_SESSION_TITLE);
getDisplaySessionTitle(session().title);
const hasChildren = () =>
(tree.descendantCountBySessionId.get(session().id) ?? 0) > 0;
const hiddenChildCount = () =>
Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/app/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ModelRef, SuggestedPlugin } from "./types";
import { t } from "../i18n";

export const MODEL_PREF_KEY = "openwork.defaultModel";
export const SESSION_MODEL_PREF_KEY = "openwork.sessionModels";
Expand All @@ -16,7 +17,7 @@ export const SUGGESTED_PLUGINS: SuggestedPlugin[] = [
{
name: "opencode-scheduler",
packageName: "opencode-scheduler",
description: "Run scheduled jobs with the OpenCode scheduler plugin.",
get description() { return t("plugins.scheduler_desc"); },
tags: ["automation", "jobs"],
installMode: "simple",
},
Expand Down
22 changes: 12 additions & 10 deletions apps/app/src/app/lib/model-behavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ProviderListItem } from "../types";
import type { ModelBehaviorOption } from "../types";
import { t, currentLocale } from "../../i18n";
import { t } from "../../i18n";

type ProviderModel = ProviderListItem["models"][string];

Expand All @@ -14,11 +14,13 @@ const WELL_KNOWN_VARIANT_ORDER = [
"max",
] as const;

const DEFAULT_BEHAVIOR_OPTION: ModelBehaviorOption = {
value: null,
label: "Provider default",
description: "Use the model's built-in default reasoning behavior.",
};
function defaultBehaviorOption(): ModelBehaviorOption {
return {
value: null,
label: t("settings.provider_default_label"),
description: t("settings.provider_default_desc"),
};
}

const humanize = (value: string) => {
const cleaned = value.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
Expand Down Expand Up @@ -97,7 +99,7 @@ const getVariantLabel = (providerID: string, key: string) => {

export const formatGenericBehaviorLabel = (value: string | null) => {
const normalized = normalizeModelBehaviorValue(value);
if (!normalized) return DEFAULT_BEHAVIOR_OPTION.label;
if (!normalized) return defaultBehaviorOption().label;
return getVariantLabel("generic", normalized);
};

Expand All @@ -124,7 +126,7 @@ export const getModelBehaviorOptions = (
const variantKeys = sortVariantKeys(getVariantKeys(model));
if (!variantKeys.length) return [];
return [
DEFAULT_BEHAVIOR_OPTION,
defaultBehaviorOption(),
...variantKeys.map((key) => {
const label = getVariantLabel(providerID, key);
return {
Expand Down Expand Up @@ -161,8 +163,8 @@ export const getModelBehaviorSummary = (
if (options.length > 0) {
return {
title,
label: selected?.label ?? DEFAULT_BEHAVIOR_OPTION.label,
description: selected?.description ?? DEFAULT_BEHAVIOR_OPTION.description,
label: selected?.label ?? defaultBehaviorOption().label,
description: selected?.description ?? defaultBehaviorOption().description,
options,
};
}
Expand Down
7 changes: 5 additions & 2 deletions apps/app/src/app/lib/session-title.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { t } from "../../i18n";

/** Raw English string — used for prefix matching against stored titles. */
export const DEFAULT_SESSION_TITLE = "New session";

const GENERATED_SESSION_TITLE_PREFIX = `${DEFAULT_SESSION_TITLE} - `;
Expand All @@ -11,9 +14,9 @@ export function isGeneratedSessionTitle(title: string | null | undefined) {

export function getDisplaySessionTitle(
title: string | null | undefined,
fallback = DEFAULT_SESSION_TITLE,
fallback?: string,
) {
const trimmed = title?.trim() ?? "";
if (!trimmed || isGeneratedSessionTitle(trimmed)) return fallback;
if (!trimmed || isGeneratedSessionTitle(trimmed)) return fallback ?? t("session.default_title");
return trimmed;
}
Loading
Loading