mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix(app): remove more dead settings shell code (#1350)
* fix(app): remove more dead settings shell code * fix(app): prune more dead helpers and props
This commit is contained in:
@@ -2069,7 +2069,6 @@ export default function App() {
|
||||
const settingsTabs = new Set<SettingsTab>([
|
||||
"general",
|
||||
"den",
|
||||
"model",
|
||||
"automations",
|
||||
"skills",
|
||||
"extensions",
|
||||
|
||||
@@ -10,7 +10,6 @@ export type QuestionModalProps = {
|
||||
open: boolean;
|
||||
questions: QuestionInfo[];
|
||||
busy: boolean;
|
||||
onClose: () => void;
|
||||
onReply: (answers: string[][]) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js";
|
||||
import { Download, RefreshCw, UploadCloud } from "lucide-solid";
|
||||
|
||||
import { getOpenWorkDeployment } from "../../lib/openwork-deployment";
|
||||
import { t } from "../../../i18n";
|
||||
import type { OpenworkInboxItem, OpenworkServerClient } from "../../lib/openwork-server";
|
||||
import WebUnavailableSurface from "../web-unavailable-surface";
|
||||
import { formatBytes, formatRelativeTime } from "../../utils";
|
||||
|
||||
export type InboxPanelProps = {
|
||||
id?: string;
|
||||
client: OpenworkServerClient | null;
|
||||
workspaceId: string | null;
|
||||
onToast?: (message: string) => void;
|
||||
maxPreview?: number;
|
||||
};
|
||||
|
||||
const INBOX_PREFIX = ".opencode/openwork/inbox/";
|
||||
|
||||
function safeName(item: OpenworkInboxItem): string {
|
||||
return String(item.name ?? item.path ?? "file").trim() || "file";
|
||||
}
|
||||
|
||||
function safeRelPath(item: OpenworkInboxItem): string {
|
||||
const raw = String(item.path ?? item.name ?? "").trim();
|
||||
if (!raw) return "";
|
||||
return raw.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function toInboxWorkspacePath(item: OpenworkInboxItem): string {
|
||||
const rel = safeRelPath(item);
|
||||
return rel ? `${INBOX_PREFIX}${rel}` : INBOX_PREFIX;
|
||||
}
|
||||
|
||||
export default function InboxPanel(props: InboxPanelProps) {
|
||||
const [items, setItems] = createSignal<OpenworkInboxItem[]>([]);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
const [dragOver, setDragOver] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const webDeployment = createMemo(() => getOpenWorkDeployment() === "web");
|
||||
|
||||
let fileInputRef: HTMLInputElement | undefined;
|
||||
|
||||
const maxPreview = createMemo(() => {
|
||||
const raw = props.maxPreview ?? 6;
|
||||
if (!Number.isFinite(raw)) return 6;
|
||||
return Math.min(12, Math.max(3, Math.floor(raw)));
|
||||
});
|
||||
|
||||
const connected = createMemo(() => Boolean(props.client && (props.workspaceId ?? "").trim()));
|
||||
const helperText = () => t("inbox_panel.helper_text");
|
||||
|
||||
const visibleItems = createMemo(() => (items() ?? []).slice(0, maxPreview()));
|
||||
const hiddenCount = createMemo(() => Math.max(0, (items() ?? []).length - visibleItems().length));
|
||||
|
||||
const toast = (message: string) => {
|
||||
props.onToast?.(message);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const client = props.client;
|
||||
const workspaceId = (props.workspaceId ?? "").trim();
|
||||
if (!client || !workspaceId) {
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await client.listInbox(workspaceId);
|
||||
setItems(result.items ?? []);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : t("inbox_panel.load_failed");
|
||||
setError(message);
|
||||
setItems([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFiles = async (files: File[]) => {
|
||||
const client = props.client;
|
||||
const workspaceId = (props.workspaceId ?? "").trim();
|
||||
if (!client || !workspaceId) {
|
||||
toast(t("inbox_panel.upload_needs_worker"));
|
||||
return;
|
||||
}
|
||||
if (!files.length) return;
|
||||
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const label = files.length === 1 ? files[0]?.name ?? "file" : `${files.length} files`;
|
||||
toast(t("inbox_panel.uploading_label", undefined, { label }));
|
||||
for (const file of files) {
|
||||
await client.uploadInbox(workspaceId, file);
|
||||
}
|
||||
toast(t("inbox_panel.upload_success"));
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : t("inbox_panel.upload_failed");
|
||||
setError(message);
|
||||
toast(message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyPath = async (item: OpenworkInboxItem) => {
|
||||
const path = toInboxWorkspacePath(item);
|
||||
try {
|
||||
await navigator.clipboard.writeText(path);
|
||||
toast(t("inbox_panel.copied_path", undefined, { path }));
|
||||
} catch {
|
||||
toast(t("inbox_panel.copy_failed"));
|
||||
}
|
||||
};
|
||||
|
||||
const downloadItem = async (item: OpenworkInboxItem) => {
|
||||
const client = props.client;
|
||||
const workspaceId = (props.workspaceId ?? "").trim();
|
||||
if (!client || !workspaceId) {
|
||||
toast(t("inbox_panel.connect_to_download"));
|
||||
return;
|
||||
}
|
||||
const id = String(item.id ?? "").trim();
|
||||
if (!id) {
|
||||
toast(t("inbox_panel.missing_file_id"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await client.downloadInboxItem(workspaceId, id);
|
||||
const blob = new Blob([result.data], { type: result.contentType ?? "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = result.filename ?? safeName(item);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : t("inbox_panel.download_failed");
|
||||
toast(message);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
// Refresh when switching workers.
|
||||
void props.workspaceId;
|
||||
void props.client;
|
||||
void refresh();
|
||||
});
|
||||
|
||||
return (
|
||||
<WebUnavailableSurface unavailable={webDeployment()} compact>
|
||||
<div id={props.id}>
|
||||
<div class="flex items-center justify-between px-2 mb-3">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-gray-10">{t("inbox_panel.shared_folder")}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={(items() ?? []).length > 0}>
|
||||
<span class="text-[11px] font-medium bg-gray-4/60 text-gray-10 px-1.5 rounded">
|
||||
{(items() ?? []).length}
|
||||
</span>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md p-1 text-gray-9 hover:text-gray-11 hover:bg-gray-3 transition-colors"
|
||||
onClick={() => void refresh()}
|
||||
title={t("inbox_panel.refresh_tooltip")}
|
||||
aria-label={t("inbox_panel.refresh_tooltip")}
|
||||
disabled={!connected() || loading()}
|
||||
>
|
||||
<RefreshCw size={14} class={loading() ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
class="hidden"
|
||||
onChange={(event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
const files = Array.from(target.files ?? []);
|
||||
if (files.length) void uploadFiles(files);
|
||||
target.value = "";
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full border border-dashed border-gray-7 rounded-xl px-4 py-4 text-left transition-colors ${
|
||||
dragOver() ? "bg-gray-3" : "bg-gray-2/60 hover:bg-gray-2"
|
||||
} ${!connected() ? "opacity-70" : ""}`}
|
||||
onClick={() => fileInputRef?.click()}
|
||||
onDragOver={(event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragLeave={(event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
}}
|
||||
onDrop={(event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
const files = Array.from(event.dataTransfer?.files ?? []);
|
||||
if (files.length) void uploadFiles(files);
|
||||
}}
|
||||
disabled={uploading()}
|
||||
title={connected() ? t("inbox_panel.drop_to_upload") : t("inbox_panel.connect_to_upload")}
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<UploadCloud size={18} class="text-gray-9 mb-2" />
|
||||
<span class="text-[13px] font-medium text-gray-11">
|
||||
{uploading() ? t("inbox_panel.uploading") : t("inbox_panel.upload_prompt")}
|
||||
</span>
|
||||
<span class="mt-0.5 text-[11px] text-gray-9">{helperText()}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="mt-2 space-y-1">
|
||||
<Show when={error()}>
|
||||
<div class="text-xs text-red-11 px-1 py-1">{error()}</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={visibleItems().length > 0}
|
||||
fallback={
|
||||
<div class="text-xs text-gray-10 px-1 py-1">
|
||||
<Show when={connected()} fallback={t("inbox_panel.connect_to_see")}>
|
||||
{t("inbox_panel.no_files")}
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={visibleItems()}>
|
||||
{(item) => {
|
||||
const name = () => safeName(item);
|
||||
const rel = () => safeRelPath(item);
|
||||
const bytes = () => (typeof item.size === "number" ? item.size : null);
|
||||
const updatedAt = () => (typeof item.updatedAt === "number" ? item.updatedAt : null);
|
||||
|
||||
return (
|
||||
<div class="group flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 hover:bg-gray-2 transition-colors border border-transparent hover:border-gray-6/80">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left"
|
||||
onClick={() => void copyPath(item)}
|
||||
title={rel() ? `Copy ${INBOX_PREFIX}${rel()}` : "Copy shared folder path"}
|
||||
aria-label={rel() ? `Copy ${INBOX_PREFIX}${rel()}` : "Copy shared folder path"}
|
||||
disabled={!connected()}
|
||||
>
|
||||
<div class="truncate text-xs font-medium text-gray-11">{name()}</div>
|
||||
<div class="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-gray-9">
|
||||
<Show when={bytes() != null}>
|
||||
<span class="font-mono">{formatBytes(bytes() as number)}</span>
|
||||
</Show>
|
||||
<Show when={updatedAt() != null}>
|
||||
<span>{formatRelativeTime(updatedAt() as number)}</span>
|
||||
</Show>
|
||||
<Show when={rel()}>
|
||||
<span class="min-w-0 truncate font-mono">{rel()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md p-1 text-gray-9 opacity-0 group-hover:opacity-100 hover:text-gray-11 hover:bg-gray-3"
|
||||
onClick={() => void downloadItem(item)}
|
||||
title={t("inbox_panel.download")}
|
||||
aria-label={t("inbox_panel.download")}
|
||||
disabled={!connected()}
|
||||
>
|
||||
<Download size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={hiddenCount() > 0}>
|
||||
<div class="text-[11px] text-gray-10 px-1 py-1">{t("inbox_panel.showing_first", undefined, { count: maxPreview() })}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</WebUnavailableSurface>
|
||||
);
|
||||
}
|
||||
@@ -12,9 +12,6 @@ type StatusBarProps = {
|
||||
settingsOpen: boolean;
|
||||
onSendFeedback: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onOpenMessaging: () => void;
|
||||
onOpenProviders: () => Promise<void> | void;
|
||||
onOpenMcp: () => void;
|
||||
providerConnectedIds: string[];
|
||||
statusLabel?: string;
|
||||
statusDetail?: string;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { WorkspaceInfo } from "../lib/tauri";
|
||||
|
||||
import { t, currentLocale } from "../../i18n";
|
||||
|
||||
import { ChevronDown, Folder, Globe, Loader2, Zap } from "lucide-solid";
|
||||
|
||||
function iconForWorkspace(preset: string, workspaceType: string) {
|
||||
if (workspaceType === "remote") return Globe;
|
||||
if (preset === "starter") return Zap;
|
||||
if (preset === "automation") return Folder;
|
||||
if (preset === "minimal") return Globe;
|
||||
return Folder;
|
||||
}
|
||||
|
||||
export default function WorkspaceChip(props: {
|
||||
workspace: WorkspaceInfo;
|
||||
onClick: () => void;
|
||||
connecting?: boolean;
|
||||
}) {
|
||||
const Icon = iconForWorkspace(props.workspace.preset, props.workspace.workspaceType);
|
||||
const subtitle = () =>
|
||||
props.workspace.workspaceType === "remote"
|
||||
? props.workspace.baseUrl ?? props.workspace.path
|
||||
: props.workspace.path;
|
||||
const isSandboxWorkspace =
|
||||
props.workspace.workspaceType === "remote" &&
|
||||
(props.workspace.sandboxBackend === "docker" ||
|
||||
Boolean(props.workspace.sandboxRunId?.trim()) ||
|
||||
Boolean(props.workspace.sandboxContainerName?.trim()));
|
||||
const translate = (key: string) => t(key, currentLocale());
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
class="flex items-center gap-2 pl-3 pr-2 py-1.5 bg-gray-2 border border-gray-6 rounded-lg hover:border-gray-7 hover:bg-gray-4 transition-all group"
|
||||
>
|
||||
<div
|
||||
class={`p-1 rounded ${
|
||||
props.workspace.workspaceType !== "remote" && props.workspace.preset === "starter"
|
||||
? "bg-amber-7/10 text-amber-6"
|
||||
: "bg-indigo-7/10 text-indigo-6"
|
||||
}`}
|
||||
>
|
||||
<Icon size={14} />
|
||||
</div>
|
||||
<div class="flex flex-col items-start mr-2 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-12 leading-none truncate max-w-[9.5rem]">
|
||||
{props.workspace.name}
|
||||
</span>
|
||||
{props.workspace.workspaceType === "remote" ? (
|
||||
<span class="text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-4 text-gray-11">
|
||||
{isSandboxWorkspace ? translate("workspace.sandbox_badge") : translate("dashboard.remote")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-10 font-mono leading-none max-w-[120px] truncate">
|
||||
{subtitle()}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown size={14} class="text-gray-10 group-hover:text-gray-11" />
|
||||
{props.connecting ? <Loader2 size={14} class="text-gray-10 animate-spin" /> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { Show, type JSX } from "solid-js";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
Box,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
History,
|
||||
MessageCircle,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-solid";
|
||||
|
||||
import type { SettingsTab } from "../types";
|
||||
import type { OpenworkServerClient } from "../lib/openwork-server";
|
||||
import InboxPanel from "./session/inbox-panel";
|
||||
|
||||
type Props = {
|
||||
expanded: boolean;
|
||||
mobile?: boolean;
|
||||
showSelection?: boolean;
|
||||
settingsTab?: SettingsTab;
|
||||
developerMode: boolean;
|
||||
activeWorkspaceLabel: string;
|
||||
activeWorkspaceType: "local" | "remote";
|
||||
openworkServerClient: OpenworkServerClient | null;
|
||||
runtimeWorkspaceId: string | null;
|
||||
inboxId: string;
|
||||
onToggleExpanded: () => void;
|
||||
onCloseMobile?: () => void;
|
||||
onOpenAutomations: () => void;
|
||||
onOpenSkills: () => void;
|
||||
onOpenExtensions: () => void;
|
||||
onOpenMessaging: () => void;
|
||||
onOpenAdvanced: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onInboxToast?: (message: string) => void;
|
||||
};
|
||||
|
||||
export default function WorkspaceRightSidebar(props: Props) {
|
||||
const mobile = () => props.mobile ?? false;
|
||||
const showSelection = () => props.showSelection ?? true;
|
||||
const closeMobile = () => props.onCloseMobile?.();
|
||||
const sidebarButton = (
|
||||
label: string,
|
||||
icon: JSX.Element,
|
||||
active: boolean,
|
||||
onClick: () => void,
|
||||
) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full border text-[13px] font-medium transition-[background-color,border-color,box-shadow,color] ${
|
||||
active
|
||||
? "border-dls-border bg-dls-surface text-dls-text shadow-[var(--dls-card-shadow)]"
|
||||
: "border-transparent text-gray-10 hover:border-dls-border hover:bg-dls-surface hover:text-dls-text"
|
||||
} ${
|
||||
props.expanded
|
||||
? "flex min-h-11 items-center justify-start gap-2.5 rounded-[16px] px-3.5"
|
||||
: "flex h-12 items-center justify-center rounded-[16px] px-0"
|
||||
}`}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
if (mobile()) closeMobile();
|
||||
}}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
>
|
||||
{icon}
|
||||
<Show when={props.expanded}>
|
||||
<span class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span class="truncate">{label}</span>
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={`flex h-full w-full flex-col overflow-hidden rounded-[24px] border border-dls-border bg-dls-sidebar p-3 ${mobile() ? "shadow-2xl" : "transition-[width] duration-200"}`}>
|
||||
<div class={`flex items-center pb-3 ${props.expanded ? "justify-between gap-3" : "justify-center"}`}>
|
||||
<Show when={props.expanded}>
|
||||
<div class="min-w-0 px-1">
|
||||
<div class="truncate text-[11px] font-medium uppercase tracking-[0.18em] text-gray-8">
|
||||
{(props.activeWorkspaceLabel || t("dashboard.workspaces"))} {t("workspace_sidebar.configuration")}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-[16px] text-gray-10 transition-colors hover:bg-dls-surface hover:text-dls-text"
|
||||
onClick={mobile() ? closeMobile : props.onToggleExpanded}
|
||||
title={mobile() ? t("workspace_sidebar.close_sidebar") : props.expanded ? t("workspace_sidebar.collapse_sidebar") : t("workspace_sidebar.expand_sidebar")}
|
||||
aria-label={mobile() ? t("workspace_sidebar.close_sidebar") : props.expanded ? t("workspace_sidebar.collapse_sidebar") : t("workspace_sidebar.expand_sidebar")}
|
||||
>
|
||||
<Show
|
||||
when={mobile()}
|
||||
fallback={<Show when={props.expanded} fallback={<ChevronLeft size={18} />}><ChevronRight size={18} /></Show>}
|
||||
>
|
||||
<X size={18} />
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
<div class={`flex-1 overflow-y-auto ${props.expanded ? "space-y-5 pt-1" : "space-y-3 pt-1"}`}>
|
||||
<div class="mb-2 space-y-1">
|
||||
{sidebarButton(
|
||||
t("workspace_sidebar.automations"),
|
||||
<History size={18} />,
|
||||
showSelection() && props.settingsTab === "automations",
|
||||
props.onOpenAutomations,
|
||||
)}
|
||||
{sidebarButton(
|
||||
t("dashboard.skills"),
|
||||
<Zap size={18} />,
|
||||
showSelection() && props.settingsTab === "skills",
|
||||
props.onOpenSkills,
|
||||
)}
|
||||
{sidebarButton(
|
||||
t("workspace_sidebar.extensions"),
|
||||
<Box size={18} />,
|
||||
showSelection() && props.settingsTab === "extensions",
|
||||
props.onOpenExtensions,
|
||||
)}
|
||||
{sidebarButton(
|
||||
t("workspace_sidebar.messaging"),
|
||||
<MessageCircle size={18} />,
|
||||
showSelection() && props.settingsTab === "messaging",
|
||||
props.onOpenMessaging,
|
||||
)}
|
||||
<Show when={props.developerMode}>
|
||||
{sidebarButton(
|
||||
t("settings.advanced_title"),
|
||||
<SlidersHorizontal size={18} />,
|
||||
showSelection() && props.settingsTab === "advanced",
|
||||
props.onOpenAdvanced,
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={props.expanded && props.activeWorkspaceType === "remote"}>
|
||||
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)]">
|
||||
<InboxPanel
|
||||
id={props.inboxId}
|
||||
client={props.openworkServerClient}
|
||||
workspaceId={props.runtimeWorkspaceId}
|
||||
onToast={props.onInboxToast}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={`pt-3 ${props.expanded ? "mt-3 border-t border-dls-border/70" : "mt-2"}`}>
|
||||
{sidebarButton(
|
||||
t("dashboard.settings"),
|
||||
<Settings size={18} />,
|
||||
showSelection() && props.settingsTab === "general",
|
||||
props.onOpenSettings,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, type ParentProps } from "solid-js";
|
||||
import { createContext, type ParentProps } from "solid-js";
|
||||
|
||||
import type { OpenworkServerStore } from "./openwork-server-store";
|
||||
|
||||
@@ -11,11 +11,3 @@ export function OpenworkServerProvider(props: ParentProps<{ store: OpenworkServe
|
||||
</OpenworkServerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenworkServer() {
|
||||
const context = useContext(OpenworkServerContext);
|
||||
if (!context) {
|
||||
throw new Error("useOpenworkServer must be used within an OpenworkServerProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
/**
|
||||
* OpenWork server connection state.
|
||||
*
|
||||
* Encapsulates all signals, effects, and helpers related to the OpenWork
|
||||
* server lifecycle: connection status, capabilities polling, host info,
|
||||
* diagnostics, audit entries, and the server client instance.
|
||||
*/
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
||||
|
||||
import {
|
||||
createOpenworkServerClient,
|
||||
hydrateOpenworkServerSettingsFromEnv,
|
||||
normalizeOpenworkServerUrl,
|
||||
readOpenworkServerSettings,
|
||||
OpenworkServerError,
|
||||
type OpenworkAuditEntry,
|
||||
type OpenworkServerCapabilities,
|
||||
type OpenworkServerClient,
|
||||
type OpenworkServerDiagnostics,
|
||||
type OpenworkServerSettings,
|
||||
type OpenworkServerStatus,
|
||||
} from "../lib/openwork-server";
|
||||
import {
|
||||
openworkServerInfo,
|
||||
orchestratorStatus,
|
||||
opencodeRouterInfo,
|
||||
type OrchestratorStatus,
|
||||
type OpenworkServerInfo,
|
||||
type OpenCodeRouterInfo,
|
||||
} from "../lib/tauri";
|
||||
import { isTauriRuntime } from "../utils";
|
||||
import type { StartupPreference } from "../types";
|
||||
|
||||
export type OpenworkServerStore = ReturnType<typeof createOpenworkServerStore>;
|
||||
|
||||
export function createOpenworkServerStore(options: {
|
||||
startupPreference: () => StartupPreference | null;
|
||||
developerMode: () => boolean;
|
||||
documentVisible: () => boolean;
|
||||
refreshEngine?: () => Promise<void>;
|
||||
}) {
|
||||
const [settings, setSettings] = createSignal<OpenworkServerSettings>({});
|
||||
const [url, setUrl] = createSignal("");
|
||||
const [status, setStatus] = createSignal<OpenworkServerStatus>("disconnected");
|
||||
const [capabilities, setCapabilities] = createSignal<OpenworkServerCapabilities | null>(null);
|
||||
const [checkedAt, setCheckedAt] = createSignal<number | null>(null);
|
||||
const [workspaceId, setWorkspaceId] = createSignal<string | null>(null);
|
||||
const [hostInfo, setHostInfo] = createSignal<OpenworkServerInfo | null>(null);
|
||||
const [diagnostics, setDiagnostics] = createSignal<OpenworkServerDiagnostics | null>(null);
|
||||
const [reconnectBusy, setReconnectBusy] = createSignal(false);
|
||||
const [routerInfo, setRouterInfo] = createSignal<OpenCodeRouterInfo | null>(null);
|
||||
const [orchStatus, setOrchStatus] = createSignal<OrchestratorStatus | null>(null);
|
||||
const [auditEntries, setAuditEntries] = createSignal<OpenworkAuditEntry[]>([]);
|
||||
const [auditStatus, setAuditStatus] = createSignal<"idle" | "loading" | "error">("idle");
|
||||
const [auditError, setAuditError] = createSignal<string | null>(null);
|
||||
const [devtoolsWorkspaceId, setDevtoolsWorkspaceId] = createSignal<string | null>(null);
|
||||
|
||||
// -- Derived --
|
||||
|
||||
const baseUrl = createMemo(() => {
|
||||
const pref = options.startupPreference();
|
||||
const info = hostInfo();
|
||||
const settingsUrl = normalizeOpenworkServerUrl(settings().urlOverride ?? "") ?? "";
|
||||
|
||||
if (pref === "local") return info?.baseUrl ?? "";
|
||||
if (pref === "server") return settingsUrl;
|
||||
return info?.baseUrl ?? settingsUrl;
|
||||
});
|
||||
|
||||
const auth = createMemo(() => {
|
||||
const pref = options.startupPreference();
|
||||
const info = hostInfo();
|
||||
const settingsToken = settings().token?.trim() ?? "";
|
||||
const clientToken = info?.clientToken?.trim() ?? "";
|
||||
const hostToken = info?.hostToken?.trim() ?? "";
|
||||
|
||||
if (pref === "local") {
|
||||
return { token: clientToken || undefined, hostToken: hostToken || undefined };
|
||||
}
|
||||
if (pref === "server") {
|
||||
return { token: settingsToken || undefined, hostToken: undefined };
|
||||
}
|
||||
if (info?.baseUrl) {
|
||||
return { token: clientToken || undefined, hostToken: hostToken || undefined };
|
||||
}
|
||||
return { token: settingsToken || undefined, hostToken: undefined };
|
||||
});
|
||||
|
||||
const client = createMemo(() => {
|
||||
const base = baseUrl().trim();
|
||||
if (!base) return null;
|
||||
const a = auth();
|
||||
return createOpenworkServerClient({ baseUrl: base, token: a.token, hostToken: a.hostToken });
|
||||
});
|
||||
|
||||
const devtoolsClient = createMemo(() => client());
|
||||
|
||||
// -- Effects --
|
||||
|
||||
// Hydrate settings from env/localStorage on mount
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
hydrateOpenworkServerSettingsFromEnv();
|
||||
setSettings(readOpenworkServerSettings());
|
||||
});
|
||||
|
||||
// Derive URL from preference + host info
|
||||
createEffect(() => {
|
||||
const pref = options.startupPreference();
|
||||
const info = hostInfo();
|
||||
const hostUrl = info?.connectUrl ?? info?.lanUrl ?? info?.mdnsUrl ?? info?.baseUrl ?? "";
|
||||
const settingsUrl = normalizeOpenworkServerUrl(settings().urlOverride ?? "") ?? "";
|
||||
|
||||
if (pref === "local") {
|
||||
setUrl(hostUrl);
|
||||
return;
|
||||
}
|
||||
if (pref === "server") {
|
||||
setUrl(settingsUrl);
|
||||
return;
|
||||
}
|
||||
setUrl(hostUrl || settingsUrl);
|
||||
});
|
||||
|
||||
// Poll server health + capabilities
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!options.documentVisible()) return;
|
||||
const serverUrl = baseUrl().trim();
|
||||
const a = auth();
|
||||
const token = a.token;
|
||||
const ht = a.hostToken;
|
||||
|
||||
if (!serverUrl) {
|
||||
setStatus("disconnected");
|
||||
setCapabilities(null);
|
||||
setCheckedAt(Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
let busy = false;
|
||||
let timeoutId: number | undefined;
|
||||
let delayMs = 10_000;
|
||||
|
||||
const scheduleNext = () => {
|
||||
if (!active) return;
|
||||
timeoutId = window.setTimeout(run, delayMs);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const result = await checkServer(serverUrl, token, ht);
|
||||
if (!active) return;
|
||||
setStatus(result.status);
|
||||
setCapabilities(result.capabilities);
|
||||
delayMs =
|
||||
result.status === "connected" || result.status === "limited"
|
||||
? 10_000
|
||||
: Math.min(delayMs * 2, 60_000);
|
||||
} catch {
|
||||
delayMs = Math.min(delayMs * 2, 60_000);
|
||||
} finally {
|
||||
if (!active) return;
|
||||
setCheckedAt(Date.now());
|
||||
busy = false;
|
||||
scheduleNext();
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
if (timeoutId) window.clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
|
||||
// Poll host info (Tauri only)
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (!options.documentVisible()) return;
|
||||
let active = true;
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const info = await openworkServerInfo();
|
||||
if (active) setHostInfo(info);
|
||||
} catch {
|
||||
if (active) setHostInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
// Poll diagnostics (developer mode only)
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!options.documentVisible()) return;
|
||||
if (!options.developerMode()) {
|
||||
setDiagnostics(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const c = client();
|
||||
if (!c || status() === "disconnected") {
|
||||
setDiagnostics(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
let busy = false;
|
||||
|
||||
const run = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const s = await c.status();
|
||||
if (active) setDiagnostics(s);
|
||||
} catch {
|
||||
if (active) setDiagnostics(null);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
// Poll engine (developer mode, Tauri only)
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (!options.developerMode()) return;
|
||||
if (!options.documentVisible()) return;
|
||||
|
||||
let busy = false;
|
||||
const run = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
await options.refreshEngine?.();
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
// Poll OpenCode Router info (developer mode, Tauri only)
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (!options.developerMode()) {
|
||||
setRouterInfo(null);
|
||||
return;
|
||||
}
|
||||
if (!options.documentVisible()) return;
|
||||
|
||||
let active = true;
|
||||
const run = async () => {
|
||||
try {
|
||||
const info = await opencodeRouterInfo();
|
||||
if (active) setRouterInfo(info);
|
||||
} catch {
|
||||
if (active) setRouterInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
// Poll orchestrator status (developer mode, Tauri only)
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (!options.developerMode()) {
|
||||
setOrchStatus(null);
|
||||
return;
|
||||
}
|
||||
if (!options.documentVisible()) return;
|
||||
|
||||
let active = true;
|
||||
const run = async () => {
|
||||
try {
|
||||
const s = await orchestratorStatus();
|
||||
if (active) setOrchStatus(s);
|
||||
} catch {
|
||||
if (active) setOrchStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
// Signals (read)
|
||||
settings,
|
||||
url,
|
||||
status,
|
||||
capabilities,
|
||||
checkedAt,
|
||||
workspaceId,
|
||||
hostInfo,
|
||||
diagnostics,
|
||||
reconnectBusy,
|
||||
routerInfo,
|
||||
orchestratorStatus: orchStatus,
|
||||
auditEntries,
|
||||
auditStatus,
|
||||
auditError,
|
||||
devtoolsWorkspaceId,
|
||||
|
||||
// Derived
|
||||
baseUrl,
|
||||
auth,
|
||||
client,
|
||||
devtoolsClient,
|
||||
|
||||
// Setters (for external use)
|
||||
setSettings,
|
||||
setUrl,
|
||||
setStatus,
|
||||
setCapabilities,
|
||||
setCheckedAt,
|
||||
setWorkspaceId,
|
||||
setHostInfo,
|
||||
setDiagnostics,
|
||||
setReconnectBusy,
|
||||
setRouterInfo,
|
||||
setOrchestratorStatus: setOrchStatus,
|
||||
setAuditEntries,
|
||||
setAuditStatus,
|
||||
setAuditError,
|
||||
setDevtoolsWorkspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
async function checkServer(
|
||||
url: string,
|
||||
token?: string,
|
||||
hostToken?: string,
|
||||
): Promise<{ status: OpenworkServerStatus; capabilities: OpenworkServerCapabilities | null }> {
|
||||
const c = createOpenworkServerClient({ baseUrl: url, token, hostToken });
|
||||
try {
|
||||
await c.health();
|
||||
} catch (error) {
|
||||
if (error instanceof OpenworkServerError && (error.status === 401 || error.status === 403)) {
|
||||
return { status: "limited", capabilities: null };
|
||||
}
|
||||
return { status: "disconnected", capabilities: null };
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return { status: "limited", capabilities: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const caps = await c.capabilities();
|
||||
return { status: "connected", capabilities: caps };
|
||||
} catch (error) {
|
||||
if (error instanceof OpenworkServerError && (error.status === 401 || error.status === 403)) {
|
||||
return { status: "limited", capabilities: null };
|
||||
}
|
||||
return { status: "disconnected", capabilities: null };
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { createContext, useContext, type ParentProps } from "solid-js";
|
||||
import type { SetStoreFunction, Store } from "solid-js/store";
|
||||
|
||||
import { useGlobalSync, type WorkspaceState } from "./global-sync";
|
||||
|
||||
type SyncContextValue = {
|
||||
directory: string;
|
||||
data: Store<WorkspaceState>;
|
||||
set: SetStoreFunction<WorkspaceState>;
|
||||
};
|
||||
|
||||
const SyncContext = createContext<SyncContextValue | undefined>(undefined);
|
||||
|
||||
export function SyncProvider(props: ParentProps & { directory: string }) {
|
||||
const globalSync = useGlobalSync();
|
||||
const [store, setStore] = globalSync.child(props.directory);
|
||||
|
||||
const value: SyncContextValue = {
|
||||
directory: props.directory,
|
||||
data: store,
|
||||
set: setStore,
|
||||
};
|
||||
|
||||
return <SyncContext.Provider value={value}>{props.children}</SyncContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSync() {
|
||||
const context = useContext(SyncContext);
|
||||
if (!context) {
|
||||
throw new Error("Sync context is missing");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -690,30 +690,6 @@ export function clearOpenworkServerSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveOpenworkServerUrl(
|
||||
opencodeBaseUrl: string,
|
||||
settings?: OpenworkServerSettings,
|
||||
) {
|
||||
const override = settings?.urlOverride?.trim();
|
||||
if (override) {
|
||||
return normalizeOpenworkServerUrl(override);
|
||||
}
|
||||
|
||||
const base = opencodeBaseUrl.trim();
|
||||
if (!base) return null;
|
||||
try {
|
||||
const url = new URL(base);
|
||||
const port = settings?.portOverride ?? DEFAULT_OPENWORK_SERVER_PORT;
|
||||
url.port = String(port);
|
||||
url.pathname = "";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenworkServerError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Safe execution utilities for error handling.
|
||||
*
|
||||
* Replaces bare `catch {}` blocks with structured handling that
|
||||
* at least logs errors in development, while keeping the same
|
||||
* "swallow and continue" behavior in production.
|
||||
*/
|
||||
|
||||
const isDev = typeof import.meta !== "undefined" && import.meta.env?.DEV;
|
||||
|
||||
/**
|
||||
* Run an async function, returning the result or a fallback on error.
|
||||
* Logs errors in development mode.
|
||||
*
|
||||
* @example
|
||||
* const sessions = await safeAsync(() => loadSessions(), []);
|
||||
*/
|
||||
export async function safeAsync<T>(
|
||||
fn: () => Promise<T>,
|
||||
fallback: T,
|
||||
label?: string,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (isDev) {
|
||||
console.warn(`[safeAsync]${label ? ` ${label}:` : ""}`, error);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a synchronous function, returning the result or a fallback on error.
|
||||
* Logs errors in development mode.
|
||||
*
|
||||
* @example
|
||||
* const parsed = safeSync(() => JSON.parse(raw), null);
|
||||
*/
|
||||
export function safeSync<T>(
|
||||
fn: () => T,
|
||||
fallback: T,
|
||||
label?: string,
|
||||
): T {
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
if (isDev) {
|
||||
console.warn(`[safeSync]${label ? ` ${label}:` : ""}`, error);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget an async operation. Logs errors in development.
|
||||
* Use for cleanup, best-effort writes, etc.
|
||||
*
|
||||
* @example
|
||||
* fireAndForget(() => saveDraft(content), "save draft");
|
||||
*/
|
||||
export function fireAndForget(
|
||||
fn: () => Promise<unknown>,
|
||||
label?: string,
|
||||
): void {
|
||||
fn().catch((error) => {
|
||||
if (isDev) {
|
||||
console.warn(`[fireAndForget]${label ? ` ${label}:` : ""}`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3510,12 +3510,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
onSendFeedback={openFeedback}
|
||||
showSettingsButton={true}
|
||||
onOpenSettings={props.toggleSettings}
|
||||
onOpenMessaging={() => {
|
||||
props.setSettingsTab("messaging");
|
||||
props.setView("settings");
|
||||
}}
|
||||
onOpenProviders={openProviderAuth}
|
||||
onOpenMcp={openMcp}
|
||||
providerConnectedIds={props.providerConnectedIds}
|
||||
statusLabel={statusBarCopy().label}
|
||||
statusDetail={statusBarCopy().detail}
|
||||
@@ -3834,7 +3828,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
open={Boolean(props.activeQuestion)}
|
||||
questions={props.activeQuestion?.questions ?? []}
|
||||
busy={props.questionReplyBusy}
|
||||
onClose={() => {}}
|
||||
onReply={(answers) => {
|
||||
if (props.activeQuestion) {
|
||||
props.respondQuestion(props.activeQuestion.id, answers);
|
||||
|
||||
@@ -217,31 +217,6 @@ const DISCORD_INVITE_URL = "https://discord.gg/VEhNQXxYMB";
|
||||
const BUG_REPORT_URL =
|
||||
"https://github.com/different-ai/openwork/issues/new?template=bug.yml";
|
||||
|
||||
// OpenCodeRouter Settings Component
|
||||
//
|
||||
// Messaging identities + routing are managed in the Identities tab.
|
||||
export function OpenCodeRouterSettings(_props: {
|
||||
busy: boolean;
|
||||
openworkServerStatus: OpenworkServerStatus;
|
||||
openworkServerUrl: string;
|
||||
openworkServerSettings: OpenworkServerSettings;
|
||||
runtimeWorkspaceId: string | null;
|
||||
openworkServerHostInfo: OpenworkServerInfo | null;
|
||||
developerMode: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<MessageCircle size={16} class="text-gray-11" />
|
||||
<div class="text-sm font-medium text-gray-12">{t("settings.messaging_section_title")}</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
{t("settings.messaging_section_desc")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsView(props: SettingsViewProps) {
|
||||
const modelControls = useModelControls();
|
||||
const { showThinking, toggleShowThinking } = useSessionDisplayPreferences();
|
||||
@@ -807,8 +782,6 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
switch (tab) {
|
||||
case "den":
|
||||
return translate("settings.tab_cloud");
|
||||
case "model":
|
||||
return translate("settings.tab_model");
|
||||
case "automations":
|
||||
return translate("settings.tab_automations");
|
||||
case "skills":
|
||||
@@ -1372,8 +1345,6 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
switch (tab) {
|
||||
case "den":
|
||||
return translate("settings.tab_description_den");
|
||||
case "model":
|
||||
return translate("settings.tab_description_model");
|
||||
case "automations":
|
||||
return translate("settings.tab_description_automations");
|
||||
case "skills":
|
||||
|
||||
@@ -431,16 +431,8 @@ export default function SettingsShell(props: SettingsShellProps) {
|
||||
props.setSettingsTab(tab);
|
||||
};
|
||||
|
||||
const openMessaging = () => {
|
||||
openSettings("messaging");
|
||||
};
|
||||
|
||||
const openExtensions = () => {
|
||||
openSettings("extensions");
|
||||
};
|
||||
|
||||
const openAdvanced = () => {
|
||||
openSettings(props.developerMode ? "advanced" : "messaging");
|
||||
openSettings("advanced");
|
||||
};
|
||||
|
||||
const revealWorkspaceInFinder = async (workspaceId: string) => {
|
||||
@@ -1348,62 +1340,8 @@ export default function SettingsShell(props: SettingsShellProps) {
|
||||
showSettingsButton={true}
|
||||
onSendFeedback={openFeedback}
|
||||
onOpenSettings={props.toggleSettings}
|
||||
onOpenMessaging={openMessaging}
|
||||
onOpenProviders={() => props.openProviderAuthModal()}
|
||||
onOpenMcp={openExtensions}
|
||||
providerConnectedIds={props.providerConnectedIds}
|
||||
/>
|
||||
<nav class="hidden border-t border-dls-border bg-dls-surface">
|
||||
<div class={`mx-auto max-w-5xl px-4 py-3 grid gap-2 ${props.developerMode ? "grid-cols-5" : "grid-cols-4"}`}>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.settingsTab === "automations" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => openSettings("automations")}
|
||||
>
|
||||
<History size={18} />
|
||||
{t("scheduled.title")}
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.settingsTab === "skills" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => openSettings("skills")}
|
||||
>
|
||||
<Zap size={18} />
|
||||
{t("dashboard.skills")}
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.settingsTab === "extensions" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => openSettings("extensions")}
|
||||
>
|
||||
<Box size={18} />
|
||||
{t("extensions.title")}
|
||||
</button>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.settingsTab === "messaging" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={() => openSettings("messaging")}
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
{t("dashboard.nav_ids")}
|
||||
</button>
|
||||
<Show when={props.developerMode}>
|
||||
<button
|
||||
class={`flex flex-col items-center gap-1 text-xs ${
|
||||
props.settingsTab === "advanced" ? "text-gray-12" : "text-gray-10"
|
||||
}`}
|
||||
onClick={openAdvanced}
|
||||
>
|
||||
<SlidersHorizontal size={18} />
|
||||
{t("settings.tab_advanced")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</nav>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { createExtensionsStore } from "../context/extensions";
|
||||
@@ -1 +0,0 @@
|
||||
export { createSessionStore } from "../context/session";
|
||||
@@ -1 +0,0 @@
|
||||
export { createSystemState } from "../system-state";
|
||||
@@ -161,7 +161,6 @@ export type OnboardingStep = "welcome" | "local" | "server" | "connecting";
|
||||
export type SettingsTab =
|
||||
| "general"
|
||||
| "den"
|
||||
| "model"
|
||||
| "automations"
|
||||
| "skills"
|
||||
| "extensions"
|
||||
|
||||
@@ -274,21 +274,6 @@ export function formatRelativeTime(timestampMs: number) {
|
||||
return new Date(timestampMs).toLocaleDateString();
|
||||
}
|
||||
|
||||
export function commandPathFromWorkspaceRoot(workspaceRoot: string, commandName: string) {
|
||||
const root = workspaceRoot.trim().replace(/\/+$/, "");
|
||||
const name = commandName.trim().replace(/^\/+/, "");
|
||||
if (!root || !name) return null;
|
||||
return `${root}/.opencode/commands/${name}.md`;
|
||||
}
|
||||
|
||||
export function safeParseJson<T>(raw: string): T | null {
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addOpencodeCacheHint(message: string) {
|
||||
const lower = message.toLowerCase();
|
||||
const cacheSignals = [
|
||||
@@ -439,64 +424,6 @@ export function upsertSession(list: Session[], next: Session) {
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function upsertMessage(list: MessageWithParts[], nextInfo: MessageInfo) {
|
||||
const idx = list.findIndex((m) => m.info.id === nextInfo.id);
|
||||
if (idx === -1) {
|
||||
return list.concat({ info: nextInfo, parts: [] });
|
||||
}
|
||||
|
||||
const copy = list.slice();
|
||||
copy[idx] = { ...copy[idx], info: nextInfo };
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function upsertPart(list: MessageWithParts[], nextPart: Part) {
|
||||
const msgIdx = list.findIndex((m) => m.info.id === nextPart.messageID);
|
||||
if (msgIdx === -1) {
|
||||
// avoids missing streaming events before message.updated
|
||||
const placeholder: PlaceholderAssistantMessage = {
|
||||
id: nextPart.messageID,
|
||||
sessionID: nextPart.sessionID,
|
||||
role: "assistant",
|
||||
time: { created: Date.now() },
|
||||
parentID: "",
|
||||
modelID: "",
|
||||
providerID: "",
|
||||
mode: "",
|
||||
agent: "",
|
||||
path: { cwd: "", root: "" },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
};
|
||||
|
||||
return list.concat({ info: placeholder, parts: [nextPart] });
|
||||
}
|
||||
|
||||
const copy = list.slice();
|
||||
const msg = copy[msgIdx];
|
||||
const parts = msg.parts.slice();
|
||||
const partIdx = parts.findIndex((p) => p.id === nextPart.id);
|
||||
|
||||
if (partIdx === -1) {
|
||||
parts.push(nextPart);
|
||||
} else {
|
||||
parts[partIdx] = nextPart;
|
||||
}
|
||||
|
||||
copy[msgIdx] = { ...msg, parts };
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function removePart(list: MessageWithParts[], messageID: string, partID: string) {
|
||||
const msgIdx = list.findIndex((m) => m.info.id === messageID);
|
||||
if (msgIdx === -1) return list;
|
||||
|
||||
const copy = list.slice();
|
||||
const msg = copy[msgIdx];
|
||||
copy[msgIdx] = { ...msg, parts: msg.parts.filter((p) => p.id !== partID) };
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function normalizeSessionStatus(status: unknown) {
|
||||
if (!status || typeof status !== "object") return "idle";
|
||||
const record = status as Record<string, unknown>;
|
||||
|
||||
@@ -99,22 +99,6 @@ export const Persist = {
|
||||
},
|
||||
};
|
||||
|
||||
export function removePersisted(target: { storage?: string; key: string }) {
|
||||
const platform = usePlatform();
|
||||
const isDesktop = platform.platform === "desktop" && !!platform.storage;
|
||||
|
||||
if (isDesktop) {
|
||||
return platform.storage?.(target.storage)?.removeItem(target.key);
|
||||
}
|
||||
|
||||
if (!target.storage) {
|
||||
localStorage.removeItem(target.key);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorageWithPrefix(target.storage).removeItem(target.key);
|
||||
}
|
||||
|
||||
export function persisted<T>(
|
||||
target: string | PersistTarget,
|
||||
store: [Store<T>, SetStoreFunction<T>],
|
||||
|
||||
@@ -78,11 +78,6 @@ export function loadPluginsFromConfig(
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePluginsFromConfig(config: OpencodeConfigFile | null) {
|
||||
if (!config?.content) return [] as string[];
|
||||
return parsePluginListFromContent(config.content);
|
||||
}
|
||||
|
||||
export function parsePluginListFromContent(content: string) {
|
||||
try {
|
||||
const parsed = parse(content) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -8,7 +8,6 @@ import ModelPickerModal from "../../app/src/app/components/model-picker-modal";
|
||||
import ShareWorkspaceModal from "../../app/src/app/components/share-workspace-modal";
|
||||
import StatusBar from "../../app/src/app/components/status-bar";
|
||||
import Composer from "../../app/src/app/components/session/composer";
|
||||
import InboxPanel from "../../app/src/app/components/session/inbox-panel";
|
||||
import MessageList from "../../app/src/app/components/session/message-list";
|
||||
import WorkspaceSessionList from "../../app/src/app/components/session/workspace-session-list";
|
||||
import { MCP_QUICK_CONNECT, SUGGESTED_PLUGINS } from "../../app/src/app/constants";
|
||||
@@ -1536,7 +1535,6 @@ export default function NewLayoutApp() {
|
||||
mcpServers={storyMcpServers()}
|
||||
mcpStatus="Story-book MCP sandbox ready."
|
||||
mcpLastUpdatedAt={now}
|
||||
mcpStatuses={storyMcpStatuses()}
|
||||
mcpConnectingName={null}
|
||||
selectedMcp={selectedMcp()}
|
||||
setSelectedMcp={setSelectedMcp}
|
||||
@@ -1583,13 +1581,8 @@ export default function NewLayoutApp() {
|
||||
developerMode
|
||||
/>
|
||||
<Show when={selectedWorkspaceId() === remoteWorkspace.id}>
|
||||
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)]">
|
||||
<InboxPanel
|
||||
id="settings-inbox"
|
||||
client={mockOpenworkServerClient}
|
||||
workspaceId={selectedWorkspaceId()}
|
||||
onToast={(message) => setComposerToast(message)}
|
||||
/>
|
||||
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)] text-sm text-dls-secondary">
|
||||
Remote inbox preview has been removed from the app shell.
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={selectedWorkspaceId() !== remoteWorkspace.id}>
|
||||
@@ -1730,20 +1723,7 @@ export default function NewLayoutApp() {
|
||||
setSettingsTab("general");
|
||||
setShowingSettings((prev) => !prev);
|
||||
}}
|
||||
onOpenMessaging={() => {
|
||||
setSettingsTab("messaging");
|
||||
setShowingSettings(true);
|
||||
}}
|
||||
onOpenProviders={() => {
|
||||
setSettingsTab("general");
|
||||
setShowingSettings(true);
|
||||
}}
|
||||
onOpenMcp={() => {
|
||||
setSettingsTab("extensions");
|
||||
setShowingSettings(true);
|
||||
}}
|
||||
providerConnectedIds={["anthropic", "openai"]}
|
||||
mcpStatuses={storyMcpStatuses()}
|
||||
statusLabel="Session Ready"
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -21,7 +21,6 @@ import ModelPickerModal from "../../app/src/app/components/model-picker-modal";
|
||||
import ShareWorkspaceModal from "../../app/src/app/components/share-workspace-modal";
|
||||
import StatusBar from "../../app/src/app/components/status-bar";
|
||||
import Composer from "../../app/src/app/components/session/composer";
|
||||
import InboxPanel from "../../app/src/app/components/session/inbox-panel";
|
||||
import MessageList from "../../app/src/app/components/session/message-list";
|
||||
import WorkspaceSessionList from "../../app/src/app/components/session/workspace-session-list";
|
||||
import { createWorkspaceShellLayout } from "../../app/src/app/lib/workspace-shell-layout";
|
||||
@@ -858,13 +857,8 @@ export default function StoryBookApp() {
|
||||
</div>
|
||||
|
||||
<Show when={expanded && selectedWorkspaceId() === remoteWorkspace.id}>
|
||||
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)]">
|
||||
<InboxPanel
|
||||
id="sidebar-inbox"
|
||||
client={null}
|
||||
workspaceId={null}
|
||||
onToast={(message) => setComposerToast(message)}
|
||||
/>
|
||||
<div class="rounded-[20px] border border-dls-border bg-dls-surface p-3 shadow-[var(--dls-card-shadow)] text-sm text-dls-secondary">
|
||||
Remote inbox preview has been removed from the app shell.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -1105,11 +1099,7 @@ export default function StoryBookApp() {
|
||||
if (!rightSidebarExpanded()) toggleRightSidebar();
|
||||
setRightRailNav("advanced");
|
||||
}}
|
||||
onOpenMessaging={() => undefined}
|
||||
onOpenProviders={() => undefined}
|
||||
onOpenMcp={() => undefined}
|
||||
providerConnectedIds={["anthropic", "openai"]}
|
||||
mcpStatuses={mcpStatuses}
|
||||
statusLabel="Session Ready"
|
||||
/>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user