mirror of
https://github.com/different-ai/openwork
synced 2026-05-11 17:46:23 +02:00
feat(app): refactor sidebar to group sessions under workspaces
- Restructure sidebar with SESSIONS header and workspace groups - Add 3-dot menu for workspaces (edit name, edit connection, remove) - Add rename workspace modal with Tauri command support - Fix session indentation and add selected session highlighting - Implement progressive 'Show more' loading for sessions - Prevent workspace switch overlay for same-workspace clicks - Display remote connection errors within modal - Remove unused workspaceStatus memo from dashboard/session views
This commit is contained in:
@@ -14,6 +14,7 @@ import { useLocation, useNavigate } from "@solidjs/router";
|
||||
import type {
|
||||
Agent,
|
||||
Part,
|
||||
Session,
|
||||
TextPartInput,
|
||||
FilePartInput,
|
||||
AgentPartInput,
|
||||
@@ -29,6 +30,7 @@ import ResetModal from "./components/reset-modal";
|
||||
import WorkspaceSwitchOverlay from "./components/workspace-switch-overlay";
|
||||
import CreateRemoteWorkspaceModal from "./components/create-remote-workspace-modal";
|
||||
import CreateWorkspaceModal from "./components/create-workspace-modal";
|
||||
import RenameWorkspaceModal from "./components/rename-workspace-modal";
|
||||
import McpAuthModal from "./components/mcp-auth-modal";
|
||||
import ReloadWorkspaceToast from "./components/reload-workspace-toast";
|
||||
import OnboardingView from "./pages/onboarding";
|
||||
@@ -64,6 +66,7 @@ import type {
|
||||
SkillCard,
|
||||
TodoItem,
|
||||
View,
|
||||
WorkspaceSessionGroup,
|
||||
WorkspaceDisplay,
|
||||
McpServerEntry,
|
||||
McpStatusMap,
|
||||
@@ -643,18 +646,6 @@ export default function App() {
|
||||
setPendingPermissions,
|
||||
} = sessionStore;
|
||||
|
||||
const [sessionsLoaded, setSessionsLoaded] = createSignal(false);
|
||||
const loadSessionsWithReady = async (scopeRoot?: string) => {
|
||||
await loadSessions(scopeRoot);
|
||||
setSessionsLoaded(true);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (!client()) {
|
||||
setSessionsLoaded(false);
|
||||
}
|
||||
});
|
||||
|
||||
const artifacts = createMemo(() => deriveArtifacts(messages()));
|
||||
const workingFiles = createMemo(() => deriveWorkingFiles(artifacts()));
|
||||
const activeSessionId = createMemo(() => selectedSessionId());
|
||||
@@ -665,6 +656,49 @@ export default function App() {
|
||||
const activeArtifacts = createMemo(() => artifacts());
|
||||
const activeWorkingFiles = createMemo(() => workingFiles());
|
||||
|
||||
const sessionActivity = (session: Session) =>
|
||||
session.time?.updated ?? session.time?.created ?? 0;
|
||||
const sortSessionsByActivity = (list: Session[]) =>
|
||||
list
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const delta = sessionActivity(b) - sessionActivity(a);
|
||||
if (delta !== 0) return delta;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
const [sidebarSessions, setSidebarSessions] = createSignal<Session[]>([]);
|
||||
const refreshSidebarSessions = async () => {
|
||||
const c = client();
|
||||
if (!c) {
|
||||
setSidebarSessions([]);
|
||||
return;
|
||||
}
|
||||
const list = unwrap(await c.session.list());
|
||||
setSidebarSessions(sortSessionsByActivity(list));
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (!client()) {
|
||||
setSidebarSessions([]);
|
||||
return;
|
||||
}
|
||||
refreshSidebarSessions().catch(() => undefined);
|
||||
});
|
||||
|
||||
const [sessionsLoaded, setSessionsLoaded] = createSignal(false);
|
||||
const loadSessionsWithReady = async (scopeRoot?: string) => {
|
||||
await loadSessions(scopeRoot);
|
||||
setSessionsLoaded(true);
|
||||
await refreshSidebarSessions().catch(() => undefined);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (!client()) {
|
||||
setSessionsLoaded(false);
|
||||
}
|
||||
});
|
||||
|
||||
const [prompt, setPrompt] = createSignal("");
|
||||
const [lastPromptSent, setLastPromptSent] = createSignal("");
|
||||
|
||||
@@ -799,6 +833,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
await renameSession(sessionID, trimmed);
|
||||
await refreshSidebarSessions().catch(() => undefined);
|
||||
}
|
||||
|
||||
async function deleteSessionById(sessionID: string) {
|
||||
@@ -813,6 +848,7 @@ export default function App() {
|
||||
const params = root ? { sessionID: trimmed, directory: root } : { sessionID: trimmed };
|
||||
unwrap(await c.session.delete(params));
|
||||
await loadSessions(root || undefined).catch(() => undefined);
|
||||
await refreshSidebarSessions().catch(() => undefined);
|
||||
|
||||
const nextStatus = { ...sessionStatusById() };
|
||||
if (nextStatus[trimmed]) {
|
||||
@@ -1289,6 +1325,46 @@ export default function App() {
|
||||
engineRuntime,
|
||||
});
|
||||
|
||||
const sidebarWorkspaceGroups = createMemo<WorkspaceSessionGroup[]>(() => {
|
||||
const workspaces = workspaceStore.workspaces();
|
||||
const sessionList = sidebarSessions();
|
||||
const workspaceByRoot = new Map<string, string>();
|
||||
const sessionsByWorkspaceId = new Map<string, Session[]>();
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
const root = normalizeDirectoryPath(
|
||||
workspace.workspaceType === "remote" ? workspace.directory ?? "" : workspace.path
|
||||
);
|
||||
if (root) {
|
||||
workspaceByRoot.set(root, workspace.id);
|
||||
}
|
||||
sessionsByWorkspaceId.set(workspace.id, []);
|
||||
}
|
||||
|
||||
for (const session of sessionList) {
|
||||
const root = normalizeDirectoryPath(session.directory ?? "");
|
||||
if (!root) continue;
|
||||
const workspaceId = workspaceByRoot.get(root);
|
||||
if (!workspaceId) continue;
|
||||
const bucket = sessionsByWorkspaceId.get(workspaceId);
|
||||
if (bucket) bucket.push(session);
|
||||
}
|
||||
|
||||
return workspaces.map((workspace) => {
|
||||
const groupSessions = sortSessionsByActivity(sessionsByWorkspaceId.get(workspace.id) ?? []);
|
||||
return {
|
||||
workspace,
|
||||
sessions: groupSessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
slug: session.slug,
|
||||
time: session.time,
|
||||
directory: session.directory,
|
||||
})),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const workspaceId = workspaceStore.activeWorkspaceId();
|
||||
@@ -1514,6 +1590,11 @@ export default function App() {
|
||||
|
||||
const [editRemoteWorkspaceOpen, setEditRemoteWorkspaceOpen] = createSignal(false);
|
||||
const [editRemoteWorkspaceId, setEditRemoteWorkspaceId] = createSignal<string | null>(null);
|
||||
const [editRemoteWorkspaceError, setEditRemoteWorkspaceError] = createSignal<string | null>(null);
|
||||
const [renameWorkspaceOpen, setRenameWorkspaceOpen] = createSignal(false);
|
||||
const [renameWorkspaceId, setRenameWorkspaceId] = createSignal<string | null>(null);
|
||||
const [renameWorkspaceName, setRenameWorkspaceName] = createSignal("");
|
||||
const [renameWorkspaceBusy, setRenameWorkspaceBusy] = createSignal(false);
|
||||
|
||||
const editRemoteWorkspaceDefaults = createMemo(() => {
|
||||
const workspaceId = editRemoteWorkspaceId();
|
||||
@@ -1528,6 +1609,49 @@ export default function App() {
|
||||
};
|
||||
});
|
||||
|
||||
const openRenameWorkspace = (workspaceId: string) => {
|
||||
const workspace = workspaceStore.workspaces().find((item) => item.id === workspaceId) ?? null;
|
||||
if (!workspace) return;
|
||||
setRenameWorkspaceId(workspaceId);
|
||||
setRenameWorkspaceName(
|
||||
workspace.displayName?.trim() ||
|
||||
workspace.openworkWorkspaceName?.trim() ||
|
||||
workspace.name?.trim() ||
|
||||
""
|
||||
);
|
||||
setRenameWorkspaceOpen(true);
|
||||
};
|
||||
|
||||
const closeRenameWorkspace = () => {
|
||||
if (renameWorkspaceBusy()) return;
|
||||
setRenameWorkspaceOpen(false);
|
||||
setRenameWorkspaceId(null);
|
||||
setRenameWorkspaceName("");
|
||||
};
|
||||
|
||||
const saveRenameWorkspace = async () => {
|
||||
const workspaceId = renameWorkspaceId();
|
||||
if (!workspaceId) return;
|
||||
const nextName = renameWorkspaceName().trim();
|
||||
if (!nextName) return;
|
||||
if (renameWorkspaceBusy()) return;
|
||||
|
||||
setRenameWorkspaceBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const ok = await workspaceStore.updateWorkspaceDisplayName(workspaceId, nextName);
|
||||
if (!ok) return;
|
||||
setRenameWorkspaceOpen(false);
|
||||
setRenameWorkspaceId(null);
|
||||
setRenameWorkspaceName("");
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : safeStringify(e);
|
||||
setError(addOpencodeCacheHint(message));
|
||||
} finally {
|
||||
setRenameWorkspaceBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testOpenworkServerConnection = async (next: OpenworkServerSettings) => {
|
||||
const derived = normalizeOpenworkServerUrl(next.urlOverride ?? "");
|
||||
if (!derived) {
|
||||
@@ -1560,11 +1684,13 @@ export default function App() {
|
||||
const workspace = workspaceStore.workspaces().find((item) => item.id === workspaceId) ?? null;
|
||||
if (workspace?.workspaceType === "remote" && workspace.remoteType === "openwork") {
|
||||
setEditRemoteWorkspaceId(workspace.id);
|
||||
setEditRemoteWorkspaceError(null);
|
||||
setEditRemoteWorkspaceOpen(true);
|
||||
return;
|
||||
}
|
||||
if (workspace?.workspaceType === "remote") {
|
||||
setEditRemoteWorkspaceId(workspace.id);
|
||||
setEditRemoteWorkspaceError(null);
|
||||
setEditRemoteWorkspaceOpen(true);
|
||||
return;
|
||||
}
|
||||
@@ -3534,14 +3660,11 @@ export default function App() {
|
||||
setCreateWorkspaceOpen: workspaceStore.setCreateWorkspaceOpen,
|
||||
createWorkspaceFlow: workspaceStore.createWorkspaceFlow,
|
||||
pickWorkspaceFolder: workspaceStore.pickWorkspaceFolder,
|
||||
sessions: activeSessions().map((s) => ({
|
||||
id: s.id,
|
||||
slug: s.slug,
|
||||
title: s.title,
|
||||
time: s.time,
|
||||
directory: s.directory,
|
||||
})),
|
||||
sessionStatusById: activeSessionStatusById(),
|
||||
workspaceSessionGroups: sidebarWorkspaceGroups(),
|
||||
selectedSessionId: activeSessionId(),
|
||||
openRenameWorkspace,
|
||||
editWorkspaceConnection: openWorkspaceConnectionSettings,
|
||||
forgetWorkspace: workspaceStore.forgetWorkspace,
|
||||
scheduledJobs: scheduledJobs(),
|
||||
scheduledJobsSource: scheduledJobsSource(),
|
||||
scheduledJobsSourceReady: scheduledJobsSourceReady(),
|
||||
@@ -3665,19 +3788,6 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const workspaceLabelForDirectory = (directory?: string | null) => {
|
||||
const normalized = normalizeDirectoryPath(directory);
|
||||
if (!normalized) return null;
|
||||
const match = workspaceStore.workspaces().find((workspace) => {
|
||||
const workspacePath = normalizeDirectoryPath(
|
||||
workspace.workspaceType === "remote" ? workspace.directory ?? "" : workspace.path
|
||||
);
|
||||
return workspacePath === normalized;
|
||||
});
|
||||
return match?.displayName ?? match?.name ?? null;
|
||||
};
|
||||
|
||||
|
||||
const sessionProps = () => ({
|
||||
selectedSessionId: activeSessionId(),
|
||||
setView,
|
||||
@@ -3717,13 +3827,8 @@ export default function App() {
|
||||
createSessionAndOpen: createSessionAndOpen,
|
||||
sendPromptAsync: sendPrompt,
|
||||
newTaskDisabled: newTaskDisabled(),
|
||||
sessions: activeSessions().map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
slug: session.slug,
|
||||
time: { updated: session.time.updated },
|
||||
workspaceLabel: workspaceLabelForDirectory(session.directory),
|
||||
})),
|
||||
workspaceSessionGroups: sidebarWorkspaceGroups(),
|
||||
openRenameWorkspace,
|
||||
selectSession: selectSession,
|
||||
messages: activeMessages(),
|
||||
todos: activeTodos(),
|
||||
@@ -3994,25 +4099,48 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
|
||||
<RenameWorkspaceModal
|
||||
open={renameWorkspaceOpen()}
|
||||
title={renameWorkspaceName()}
|
||||
busy={renameWorkspaceBusy()}
|
||||
canSave={renameWorkspaceName().trim().length > 0 && !renameWorkspaceBusy()}
|
||||
onClose={closeRenameWorkspace}
|
||||
onSave={saveRenameWorkspace}
|
||||
onTitleChange={setRenameWorkspaceName}
|
||||
/>
|
||||
|
||||
<CreateRemoteWorkspaceModal
|
||||
open={editRemoteWorkspaceOpen()}
|
||||
onClose={() => {
|
||||
setEditRemoteWorkspaceOpen(false);
|
||||
setEditRemoteWorkspaceId(null);
|
||||
setEditRemoteWorkspaceError(null);
|
||||
}}
|
||||
onConfirm={(input) => {
|
||||
const workspaceId = editRemoteWorkspaceId();
|
||||
if (!workspaceId) return;
|
||||
setEditRemoteWorkspaceError(null);
|
||||
void (async () => {
|
||||
const ok = await workspaceStore.updateRemoteWorkspaceFlow(workspaceId, input);
|
||||
if (ok) {
|
||||
setEditRemoteWorkspaceOpen(false);
|
||||
setEditRemoteWorkspaceId(null);
|
||||
try {
|
||||
const ok = await workspaceStore.updateRemoteWorkspaceFlow(workspaceId, input);
|
||||
if (ok) {
|
||||
setEditRemoteWorkspaceOpen(false);
|
||||
setEditRemoteWorkspaceId(null);
|
||||
setEditRemoteWorkspaceError(null);
|
||||
} else {
|
||||
setEditRemoteWorkspaceError(error() || "Connection failed. Check the URL and token.");
|
||||
setError(null);
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "Connection failed";
|
||||
setEditRemoteWorkspaceError(message);
|
||||
setError(null);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
initialValues={editRemoteWorkspaceDefaults() ?? undefined}
|
||||
submitting={busy() && busyLabel() === "status.connecting"}
|
||||
error={editRemoteWorkspaceError()}
|
||||
title={t("dashboard.edit_remote_workspace_title", currentLocale())}
|
||||
subtitle={t("dashboard.edit_remote_workspace_subtitle", currentLocale())}
|
||||
confirmLabel={t("dashboard.edit_remote_workspace_confirm", currentLocale())}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
displayName?: string | null;
|
||||
};
|
||||
submitting?: boolean;
|
||||
error?: string | null;
|
||||
inline?: boolean;
|
||||
showClose?: boolean;
|
||||
title?: string;
|
||||
@@ -146,26 +147,33 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-gray-6 bg-gray-1 flex justify-end gap-3">
|
||||
<Show when={showClose()}>
|
||||
<Button variant="ghost" onClick={props.onClose} disabled={submitting()}>
|
||||
{translate("common.cancel")}
|
||||
</Button>
|
||||
<div class="p-6 border-t border-gray-6 bg-gray-1 space-y-3">
|
||||
<Show when={props.error}>
|
||||
<div class="p-3 rounded-lg bg-red-3/50 border border-red-6 text-sm text-red-11">
|
||||
{props.error}
|
||||
</div>
|
||||
</Show>
|
||||
<Button
|
||||
onClick={() =>
|
||||
props.onConfirm({
|
||||
openworkHostUrl: openworkHostUrl().trim(),
|
||||
openworkToken: openworkToken().trim(),
|
||||
directory: directory().trim() ? directory().trim() : null,
|
||||
displayName: displayName().trim() ? displayName().trim() : null,
|
||||
})
|
||||
}
|
||||
disabled={!canSubmit()}
|
||||
title={!openworkHostUrl().trim() ? translate("dashboard.remote_base_url_required") : undefined}
|
||||
>
|
||||
{confirmLabel()}
|
||||
</Button>
|
||||
<div class="flex justify-end gap-3">
|
||||
<Show when={showClose()}>
|
||||
<Button variant="ghost" onClick={props.onClose} disabled={submitting()}>
|
||||
{translate("common.cancel")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Button
|
||||
onClick={() =>
|
||||
props.onConfirm({
|
||||
openworkHostUrl: openworkHostUrl().trim(),
|
||||
openworkToken: openworkToken().trim(),
|
||||
directory: directory().trim() ? directory().trim() : null,
|
||||
displayName: displayName().trim() ? displayName().trim() : null,
|
||||
})
|
||||
}
|
||||
disabled={!canSubmit()}
|
||||
title={!openworkHostUrl().trim() ? translate("dashboard.remote_base_url_required") : undefined}
|
||||
>
|
||||
{confirmLabel()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
77
packages/app/src/app/components/rename-workspace-modal.tsx
Normal file
77
packages/app/src/app/components/rename-workspace-modal.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Show, createEffect } from "solid-js";
|
||||
import { X } from "lucide-solid";
|
||||
import { t, currentLocale } from "../../i18n";
|
||||
|
||||
import Button from "./button";
|
||||
import TextInput from "./text-input";
|
||||
|
||||
export type RenameWorkspaceModalProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
busy: boolean;
|
||||
canSave: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
onTitleChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export default function RenameWorkspaceModal(props: RenameWorkspaceModalProps) {
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
const translate = (key: string) => t(key, currentLocale());
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open) {
|
||||
requestAnimationFrame(() => {
|
||||
inputRef?.focus();
|
||||
if (inputRef) {
|
||||
inputRef.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<div class="fixed inset-0 z-50 bg-gray-1/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-gray-2 border border-gray-6/70 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-12">{translate("workspace.rename_title")}</h3>
|
||||
<p class="text-sm text-gray-11 mt-1">{translate("workspace.rename_description")}</p>
|
||||
</div>
|
||||
<Button variant="ghost" class="!p-2 rounded-full" onClick={props.onClose}>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
label={translate("workspace.rename_label")}
|
||||
value={props.title}
|
||||
onInput={(e) => props.onTitleChange(e.currentTarget.value)}
|
||||
placeholder={translate("workspace.rename_placeholder")}
|
||||
class="bg-gray-3"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
if (props.canSave) props.onSave();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={props.onClose} disabled={props.busy}>
|
||||
{translate("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={props.onSave} disabled={!props.canSave}>
|
||||
{translate("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
workspaceOpenworkRead,
|
||||
workspaceOpenworkWrite,
|
||||
workspaceSetActive,
|
||||
workspaceUpdateDisplayName,
|
||||
workspaceUpdateRemote,
|
||||
type EngineDoctorResult,
|
||||
type EngineInfo,
|
||||
@@ -1492,6 +1493,44 @@ export function createWorkspaceStore(options: {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWorkspaceDisplayName(workspaceId: string, displayName: string | null) {
|
||||
const id = workspaceId.trim();
|
||||
if (!id) return false;
|
||||
const workspace = workspaces().find((item) => item.id === id) ?? null;
|
||||
if (!workspace) return false;
|
||||
|
||||
const nextDisplayName = displayName?.trim() || null;
|
||||
options.setError(null);
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
try {
|
||||
const ws = await workspaceUpdateDisplayName({ workspaceId: id, displayName: nextDisplayName });
|
||||
setWorkspaces(ws.workspaces);
|
||||
if (ws.activeId) {
|
||||
updateWorkspaceConnectionState(ws.activeId, { status: "connected", message: null });
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : safeStringify(e);
|
||||
options.setError(addOpencodeCacheHint(message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setWorkspaces((prev) =>
|
||||
prev.map((entry) =>
|
||||
entry.id === id
|
||||
? {
|
||||
...entry,
|
||||
displayName: nextDisplayName,
|
||||
name: nextDisplayName ?? entry.name,
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function stopHost() {
|
||||
options.setError(null);
|
||||
options.setBusy(true);
|
||||
@@ -1995,6 +2034,7 @@ export function createWorkspaceStore(options: {
|
||||
createWorkspaceFlow,
|
||||
createRemoteWorkspaceFlow,
|
||||
updateRemoteWorkspaceFlow,
|
||||
updateWorkspaceDisplayName,
|
||||
forgetWorkspace,
|
||||
pickWorkspaceFolder,
|
||||
exportWorkspaceConfig,
|
||||
|
||||
@@ -204,6 +204,16 @@ export async function workspaceUpdateRemote(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function workspaceUpdateDisplayName(input: {
|
||||
workspaceId: string;
|
||||
displayName?: string | null;
|
||||
}): Promise<WorkspaceList> {
|
||||
return invoke<WorkspaceList>("workspace_update_display_name", {
|
||||
workspaceId: input.workspaceId,
|
||||
displayName: input.displayName ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function workspaceForget(workspaceId: string): Promise<WorkspaceList> {
|
||||
return invoke<WorkspaceList>("workspace_forget", { workspaceId });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ScheduledJob,
|
||||
SkillCard,
|
||||
StartupPreference,
|
||||
WorkspaceSessionGroup,
|
||||
View,
|
||||
} from "../types";
|
||||
import type { McpDirectoryInfo } from "../constants";
|
||||
@@ -33,10 +34,8 @@ import StatusBar from "../components/status-bar";
|
||||
import ProviderAuthModal from "../components/provider-auth-modal";
|
||||
import {
|
||||
Box,
|
||||
ChevronRight,
|
||||
Edit2,
|
||||
History,
|
||||
Layout,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
@@ -100,14 +99,11 @@ export type DashboardViewProps = {
|
||||
openCreateWorkspace: () => void;
|
||||
exportWorkspaceConfig: () => void;
|
||||
exportWorkspaceBusy: boolean;
|
||||
sessions: Array<{
|
||||
id: string;
|
||||
slug?: string | null;
|
||||
title: string;
|
||||
time: { updated: number };
|
||||
directory?: string | null;
|
||||
}>;
|
||||
sessionStatusById: Record<string, string>;
|
||||
workspaceSessionGroups: WorkspaceSessionGroup[];
|
||||
selectedSessionId: string | null;
|
||||
openRenameWorkspace: (workspaceId: string) => void;
|
||||
editWorkspaceConnection: (workspaceId: string) => void;
|
||||
forgetWorkspace: (workspaceId: string) => void;
|
||||
scheduledJobs: ScheduledJob[];
|
||||
scheduledJobsSource: "local" | "remote";
|
||||
scheduledJobsSourceReady: boolean;
|
||||
@@ -243,17 +239,6 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
}
|
||||
});
|
||||
|
||||
const workspaceStatus = createMemo(() => {
|
||||
switch (props.openworkServerStatus) {
|
||||
case "connected":
|
||||
return { label: "Live", className: "bg-emerald-3 text-emerald-11" };
|
||||
case "limited":
|
||||
return { label: "Limited", className: "bg-amber-3 text-amber-11" };
|
||||
case "disconnected":
|
||||
default:
|
||||
return { label: "Offline", className: "bg-red-3 text-red-11" };
|
||||
}
|
||||
});
|
||||
const workspaceLabel = (workspace: WorkspaceInfo) =>
|
||||
workspace.displayName?.trim() ||
|
||||
workspace.openworkWorkspaceName?.trim() ||
|
||||
@@ -263,11 +248,20 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
const workspaceKindLabel = (workspace: WorkspaceInfo) =>
|
||||
workspace.workspaceType === "remote" ? "Remote" : "Local";
|
||||
|
||||
const openSessionFromList = (sessionId: string) => {
|
||||
// Defer view switch to avoid click-through on the same event frame.
|
||||
window.setTimeout(() => {
|
||||
const openSessionFromList = (workspaceId: string, sessionId: string) => {
|
||||
// For same-workspace clicks, just select the session without workspace activation
|
||||
if (workspaceId === props.activeWorkspaceId) {
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
return;
|
||||
}
|
||||
// For different workspace, activate workspace first
|
||||
window.setTimeout(() => {
|
||||
void (async () => {
|
||||
await Promise.resolve(props.activateWorkspace(workspaceId));
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
})();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@@ -275,11 +269,40 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
const [lastRefreshedTab, setLastRefreshedTab] = createSignal<string | null>(null);
|
||||
const [refreshInProgress, setRefreshInProgress] = createSignal(false);
|
||||
const [providerAuthActionBusy, setProviderAuthActionBusy] = createSignal(false);
|
||||
const [sessionsExpanded, setSessionsExpanded] = createSignal(true);
|
||||
const [showAllSessions, setShowAllSessions] = createSignal(false);
|
||||
const visibleSessions = createMemo(() =>
|
||||
showAllSessions() ? props.sessions : props.sessions.slice(0, 5)
|
||||
);
|
||||
const MAX_SESSIONS_PREVIEW = 3;
|
||||
const [previewCountByWorkspaceId, setPreviewCountByWorkspaceId] = createSignal<
|
||||
Record<string, number>
|
||||
>({});
|
||||
const previewCount = (workspaceId: string) =>
|
||||
previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW;
|
||||
const previewSessions = (workspaceId: string, sessions: WorkspaceSessionGroup["sessions"]) =>
|
||||
sessions.slice(0, previewCount(workspaceId));
|
||||
const showMoreSessions = (workspaceId: string, total: number) => {
|
||||
setPreviewCountByWorkspaceId((current) => {
|
||||
const next = { ...current };
|
||||
const existing = next[workspaceId] ?? MAX_SESSIONS_PREVIEW;
|
||||
next[workspaceId] = Math.min(existing + MAX_SESSIONS_PREVIEW, total);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const showMoreLabel = (workspaceId: string, total: number) => {
|
||||
const remaining = Math.max(0, total - previewCount(workspaceId));
|
||||
const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining);
|
||||
return nextCount > 0 ? `Show ${nextCount} more` : "Show more";
|
||||
};
|
||||
const [workspaceMenuId, setWorkspaceMenuId] = createSignal<string | null>(null);
|
||||
let workspaceMenuRef: HTMLDivElement | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
if (!workspaceMenuId()) return;
|
||||
const closeMenu = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (workspaceMenuRef && target && workspaceMenuRef.contains(target)) return;
|
||||
setWorkspaceMenuId(null);
|
||||
};
|
||||
window.addEventListener("click", closeMenu);
|
||||
onCleanup(() => window.removeEventListener("click", closeMenu));
|
||||
});
|
||||
|
||||
const handleProviderAuthSelect = async (providerId: string) => {
|
||||
if (providerAuthActionBusy()) return;
|
||||
@@ -388,65 +411,10 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
{navItem("mcp", "Apps", <Box size={18} />)}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 tracking-tight">
|
||||
<span>Workspaces</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Workspace settings"
|
||||
onClick={() => openSettings("general")}
|
||||
class="text-dls-secondary hover:text-dls-text"
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<For each={props.workspaces}>
|
||||
{(workspace) => {
|
||||
const isActive = () => props.activeWorkspaceId === workspace.id;
|
||||
const isConnecting = () => props.connectingWorkspaceId === workspace.id;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full flex items-center justify-between h-10 px-3 rounded-lg text-left transition-colors ${
|
||||
isActive()
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => props.activateWorkspace(workspace.id)}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">{workspaceLabel(workspace)}</div>
|
||||
<div class="text-[11px] text-dls-secondary">
|
||||
{workspaceKindLabel(workspace)}
|
||||
{isActive() ? ` · ${workspaceStatus().label}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={isConnecting()}>
|
||||
<Loader2 size={14} class="animate-spin text-dls-secondary" />
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.openCreateWorkspace}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add a workspace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 mb-3 tracking-tight">
|
||||
<span>Sessions</span>
|
||||
<div class="flex gap-2 text-dls-secondary">
|
||||
<button type="button" class="hover:text-dls-text" aria-label="Session layout">
|
||||
<Layout size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-dls-text"
|
||||
@@ -459,103 +427,173 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSessionsExpanded((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
setSessionsExpanded((current) => !current);
|
||||
}}
|
||||
class="group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer text-dls-text transition-colors hover:bg-dls-hover"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
size={14}
|
||||
class={`text-dls-secondary transition-transform ${
|
||||
sessionsExpanded() ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
<span class="text-sm font-medium">{props.activeWorkspaceDisplay.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-secondary transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 mb-3">
|
||||
<For each={props.workspaceSessionGroups}>
|
||||
{(group) => {
|
||||
const workspace = () => group.workspace;
|
||||
const isConnecting = () => props.connectingWorkspaceId === workspace().id;
|
||||
const isMenuOpen = () => workspaceMenuId() === workspace().id;
|
||||
|
||||
<Show when={sessionsExpanded()}>
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-4">
|
||||
<Show
|
||||
when={props.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={visibleSessions()}>
|
||||
{(session) => (
|
||||
return (
|
||||
<div class="space-y-1">
|
||||
<div class="relative group">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class="group flex items-center justify-between h-8 px-3 hover:bg-dls-hover rounded-lg cursor-pointer relative overflow-hidden ml-5 w-[calc(100%-1.25rem)]"
|
||||
onClick={() => openSessionFromList(session.id)}
|
||||
class="w-full flex items-center justify-between h-10 px-3 rounded-lg text-left transition-colors text-dls-text hover:bg-dls-hover"
|
||||
onClick={() => props.activateWorkspace(workspace().id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(session.id);
|
||||
props.activateWorkspace(workspace().id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time.updated)}
|
||||
</span>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">{workspaceLabel(workspace())}</div>
|
||||
<div class="text-[11px] text-dls-secondary">
|
||||
{workspaceKindLabel(workspace())}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={isConnecting()}>
|
||||
<Loader2 size={14} class="animate-spin text-dls-secondary" />
|
||||
</Show>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md text-dls-secondary hover:text-dls-text hover:bg-dls-active opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setWorkspaceMenuId((current) =>
|
||||
current === workspace().id ? null : workspace().id
|
||||
);
|
||||
}}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<Show when={isMenuOpen()}>
|
||||
<div
|
||||
ref={(el) => (workspaceMenuRef = el)}
|
||||
class="absolute right-2 top-[calc(100%+4px)] z-20 w-44 rounded-lg border border-dls-border bg-dls-surface shadow-lg p-1"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover"
|
||||
onClick={() => {
|
||||
props.openRenameWorkspace(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
Edit name
|
||||
</button>
|
||||
<Show when={workspace().workspaceType === "remote"}>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover"
|
||||
onClick={() => {
|
||||
props.editWorkspaceConnection(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
Edit connection
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover text-red-11"
|
||||
onClick={() => {
|
||||
props.forgetWorkspace(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
Remove workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-2">
|
||||
<Show
|
||||
when={group.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary ml-2">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={previewSessions(workspace().id, group.sessions)}>
|
||||
{(session) => {
|
||||
const isSelected = () => props.selectedSessionId === session.id;
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class={`group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer relative overflow-hidden ml-2 w-[calc(100%-0.5rem)] ${
|
||||
isSelected()
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => openSessionFromList(workspace().id, session.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(workspace().id, session.id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time?.updated ?? Date.now())}
|
||||
</span>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
<Show when={group.sessions.length > previewCount(workspace().id)}>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 w-[calc(100%-0.5rem)] px-3 py-2 text-xs text-dls-secondary hover:text-dls-text hover:bg-dls-hover rounded-lg transition-colors text-left"
|
||||
onClick={() => showMoreSessions(workspace().id, group.sessions.length)}
|
||||
>
|
||||
{showMoreLabel(workspace().id, group.sessions.length)}
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={props.sessions.length > 5}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllSessions((current) => !current)}
|
||||
class="px-3 py-1.5 text-xs text-dls-secondary hover:text-dls-text font-medium"
|
||||
>
|
||||
{showAllSessions() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.openCreateWorkspace}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add a workspace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-dls-border">
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
View,
|
||||
WorkspaceConnectionState,
|
||||
WorkspaceDisplay,
|
||||
WorkspaceSessionGroup,
|
||||
} from "../types";
|
||||
|
||||
import type { WorkspaceInfo } from "../lib/tauri";
|
||||
@@ -24,11 +25,9 @@ import type { WorkspaceInfo } from "../lib/tauri";
|
||||
import {
|
||||
Box,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Edit2,
|
||||
HardDrive,
|
||||
History,
|
||||
Layout,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
@@ -78,13 +77,8 @@ export type SessionViewProps = {
|
||||
createSessionAndOpen: () => void;
|
||||
sendPromptAsync: (draft: ComposerDraft) => Promise<void>;
|
||||
newTaskDisabled: boolean;
|
||||
sessions: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
workspaceLabel?: string | null;
|
||||
time?: { updated: number };
|
||||
}>;
|
||||
workspaceSessionGroups: WorkspaceSessionGroup[];
|
||||
openRenameWorkspace: (workspaceId: string) => void;
|
||||
selectSession: (sessionId: string) => Promise<void> | void;
|
||||
messages: MessageWithParts[];
|
||||
todos: TodoItem[];
|
||||
@@ -169,17 +163,6 @@ export default function SessionView(props: SessionViewProps) {
|
||||
const [unreadCount, setUnreadCount] = createSignal(0);
|
||||
|
||||
const agentLabel = createMemo(() => props.selectedSessionAgent ?? "Default agent");
|
||||
const workspaceStatus = createMemo(() => {
|
||||
switch (props.openworkServerStatus) {
|
||||
case "connected":
|
||||
return { label: "Live", className: "bg-emerald-3 text-emerald-11" };
|
||||
case "limited":
|
||||
return { label: "Limited", className: "bg-amber-3 text-amber-11" };
|
||||
case "disconnected":
|
||||
default:
|
||||
return { label: "Offline", className: "bg-red-3 text-red-11" };
|
||||
}
|
||||
});
|
||||
const workspaceLabel = (workspace: WorkspaceInfo) =>
|
||||
workspace.displayName?.trim() ||
|
||||
workspace.openworkWorkspaceName?.trim() ||
|
||||
@@ -188,11 +171,40 @@ export default function SessionView(props: SessionViewProps) {
|
||||
"Workspace";
|
||||
const workspaceKindLabel = (workspace: WorkspaceInfo) =>
|
||||
workspace.workspaceType === "remote" ? "Remote" : "Local";
|
||||
const [sessionsExpanded, setSessionsExpanded] = createSignal(true);
|
||||
const [showAllSessions, setShowAllSessions] = createSignal(false);
|
||||
const visibleSessions = createMemo(() =>
|
||||
showAllSessions() ? props.sessions : props.sessions.slice(0, 5)
|
||||
);
|
||||
const MAX_SESSIONS_PREVIEW = 3;
|
||||
const [previewCountByWorkspaceId, setPreviewCountByWorkspaceId] = createSignal<
|
||||
Record<string, number>
|
||||
>({});
|
||||
const previewCount = (workspaceId: string) =>
|
||||
previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW;
|
||||
const previewSessions = (workspaceId: string, sessions: WorkspaceSessionGroup["sessions"]) =>
|
||||
sessions.slice(0, previewCount(workspaceId));
|
||||
const showMoreSessions = (workspaceId: string, total: number) => {
|
||||
setPreviewCountByWorkspaceId((current) => {
|
||||
const next = { ...current };
|
||||
const existing = next[workspaceId] ?? MAX_SESSIONS_PREVIEW;
|
||||
next[workspaceId] = Math.min(existing + MAX_SESSIONS_PREVIEW, total);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const showMoreLabel = (workspaceId: string, total: number) => {
|
||||
const remaining = Math.max(0, total - previewCount(workspaceId));
|
||||
const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining);
|
||||
return nextCount > 0 ? `Show ${nextCount} more` : "Show more";
|
||||
};
|
||||
const [workspaceMenuId, setWorkspaceMenuId] = createSignal<string | null>(null);
|
||||
let workspaceMenuRef: HTMLDivElement | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
if (!workspaceMenuId()) return;
|
||||
const closeMenu = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (workspaceMenuRef && target && workspaceMenuRef.contains(target)) return;
|
||||
setWorkspaceMenuId(null);
|
||||
};
|
||||
window.addEventListener("click", closeMenu);
|
||||
onCleanup(() => window.removeEventListener("click", closeMenu));
|
||||
});
|
||||
const attachmentsEnabled = createMemo(() => {
|
||||
if (props.activeWorkspaceDisplay.workspaceType !== "remote") return true;
|
||||
return props.openworkServerStatus === "connected";
|
||||
@@ -614,7 +626,11 @@ export default function SessionView(props: SessionViewProps) {
|
||||
const selectedSessionTitle = createMemo(() => {
|
||||
const id = props.selectedSessionId;
|
||||
if (!id) return "";
|
||||
return props.sessions.find((session) => session.id === id)?.title ?? "";
|
||||
for (const group of props.workspaceSessionGroups) {
|
||||
const match = group.sessions.find((session) => session.id === id);
|
||||
if (match) return match.title ?? "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const renameCanSave = createMemo(() => {
|
||||
@@ -729,10 +745,20 @@ export default function SessionView(props: SessionViewProps) {
|
||||
props.setPrompt(draft.text);
|
||||
};
|
||||
|
||||
const openSessionFromList = (sessionId: string) => {
|
||||
const openSessionFromList = (workspaceId: string, sessionId: string) => {
|
||||
if (!sessionId) return;
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
// For same-workspace clicks, just select the session without workspace activation
|
||||
if (workspaceId === props.activeWorkspaceId) {
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
return;
|
||||
}
|
||||
// For different workspace, activate workspace first
|
||||
void (async () => {
|
||||
await Promise.resolve(props.activateWorkspace(workspaceId));
|
||||
void props.selectSession(sessionId);
|
||||
props.setView("session", sessionId);
|
||||
})();
|
||||
};
|
||||
|
||||
const openSettings = (tab: SettingsTab = "general") => {
|
||||
@@ -810,65 +836,10 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 tracking-tight">
|
||||
<span>Workspaces</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Workspace settings"
|
||||
onClick={() => openSettings("general")}
|
||||
class="text-dls-secondary hover:text-dls-text"
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<For each={props.workspaces}>
|
||||
{(workspace) => {
|
||||
const isActive = () => props.activeWorkspaceId === workspace.id;
|
||||
const isConnecting = () => props.connectingWorkspaceId === workspace.id;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`w-full flex items-center justify-between h-10 px-3 rounded-lg text-left transition-colors ${
|
||||
isActive()
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "text-dls-text hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => props.activateWorkspace(workspace.id)}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">{workspaceLabel(workspace)}</div>
|
||||
<div class="text-[11px] text-dls-secondary">
|
||||
{workspaceKindLabel(workspace)}
|
||||
{isActive() ? ` · ${workspaceStatus().label}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={isConnecting()}>
|
||||
<Loader2 size={14} class="animate-spin text-dls-secondary" />
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.openCreateWorkspace}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add a workspace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex items-center justify-between text-[11px] font-bold text-dls-secondary uppercase px-3 mb-3 tracking-tight">
|
||||
<span>Sessions</span>
|
||||
<div class="flex gap-2 text-dls-secondary">
|
||||
<button type="button" class="hover:text-dls-text" aria-label="Session layout">
|
||||
<Layout size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="hover:text-dls-text"
|
||||
@@ -881,105 +852,175 @@ export default function SessionView(props: SessionViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSessionsExpanded((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
setSessionsExpanded((current) => !current);
|
||||
}}
|
||||
class="group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer text-dls-text transition-colors hover:bg-dls-hover"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
size={14}
|
||||
class={`text-dls-secondary transition-transform ${
|
||||
sessionsExpanded() ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
<span class="text-sm font-medium">{props.activeWorkspaceDisplay.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-secondary transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 mb-3">
|
||||
<For each={props.workspaceSessionGroups}>
|
||||
{(group) => {
|
||||
const workspace = () => group.workspace;
|
||||
const isConnecting = () => props.connectingWorkspaceId === workspace().id;
|
||||
const isMenuOpen = () => workspaceMenuId() === workspace().id;
|
||||
|
||||
<Show when={sessionsExpanded()}>
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-4">
|
||||
<Show
|
||||
when={props.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={visibleSessions()}>
|
||||
{(session) => (
|
||||
return (
|
||||
<div class="space-y-1">
|
||||
<div class="relative group">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class="group flex items-center justify-between h-8 px-3 hover:bg-dls-hover rounded-lg cursor-pointer relative overflow-hidden ml-5 w-[calc(100%-1.25rem)]"
|
||||
onClick={() => openSessionFromList(session.id)}
|
||||
class="w-full flex items-center justify-between h-10 px-3 rounded-lg text-left transition-colors text-dls-text hover:bg-dls-hover"
|
||||
onClick={() => props.activateWorkspace(workspace().id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(session.id);
|
||||
props.activateWorkspace(workspace().id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<Show when={session.time?.updated}>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time?.updated ?? Date.now())}
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium truncate">{workspaceLabel(workspace())}</div>
|
||||
<div class="text-[11px] text-dls-secondary">
|
||||
{workspaceKindLabel(workspace())}
|
||||
</div>
|
||||
</div>
|
||||
<Show when={isConnecting()}>
|
||||
<Loader2 size={14} class="animate-spin text-dls-secondary" />
|
||||
</Show>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md text-dls-secondary hover:text-dls-text hover:bg-dls-active opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setWorkspaceMenuId((current) =>
|
||||
current === workspace().id ? null : workspace().id
|
||||
);
|
||||
}}
|
||||
aria-label="Workspace options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<Show when={isMenuOpen()}>
|
||||
<div
|
||||
ref={(el) => (workspaceMenuRef = el)}
|
||||
class="absolute right-2 top-[calc(100%+4px)] z-20 w-44 rounded-lg border border-dls-border bg-dls-surface shadow-lg p-1"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover"
|
||||
onClick={() => {
|
||||
props.openRenameWorkspace(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
Edit name
|
||||
</button>
|
||||
<Show when={workspace().workspaceType === "remote"}>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover"
|
||||
onClick={() => {
|
||||
props.editWorkspaceConnection(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
Edit connection
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
class="w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-dls-hover text-red-11"
|
||||
onClick={() => {
|
||||
props.forgetWorkspace(workspace().id);
|
||||
setWorkspaceMenuId(null);
|
||||
}}
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
Remove workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 space-y-0.5 border-l border-dls-border ml-2">
|
||||
<Show
|
||||
when={group.sessions.length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-dls-secondary ml-2">
|
||||
No sessions yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={previewSessions(workspace().id, group.sessions)}>
|
||||
{(session) => {
|
||||
const isSelected = () => props.selectedSessionId === session.id;
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
class={`group flex items-center justify-between h-8 px-3 rounded-lg cursor-pointer relative overflow-hidden ml-2 w-[calc(100%-0.5rem)] ${
|
||||
isSelected()
|
||||
? "bg-dls-active text-dls-text"
|
||||
: "hover:bg-dls-hover"
|
||||
}`}
|
||||
onClick={() => openSessionFromList(workspace().id, session.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
openSessionFromList(workspace().id, session.id);
|
||||
}}
|
||||
>
|
||||
<span class="text-sm text-dls-text truncate mr-2 font-medium group-hover:pr-14 transition-all">
|
||||
{session.title}
|
||||
</span>
|
||||
<Show when={session.time?.updated}>
|
||||
<span class="text-xs text-dls-secondary whitespace-nowrap group-hover:opacity-0 transition-opacity">
|
||||
{formatRelativeTime(session.time?.updated ?? Date.now())}
|
||||
</span>
|
||||
</Show>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Session options"
|
||||
>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 hover:bg-dls-active rounded-md text-dls-text transition-colors"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="Rename session"
|
||||
>
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
<Show when={group.sessions.length > previewCount(workspace().id)}>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 w-[calc(100%-0.5rem)] px-3 py-2 text-xs text-dls-secondary hover:text-dls-text hover:bg-dls-hover rounded-lg transition-colors text-left"
|
||||
onClick={() => showMoreSessions(workspace().id, group.sessions.length)}
|
||||
>
|
||||
{showMoreLabel(workspace().id, group.sessions.length)}
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={props.sessions.length > 5}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllSessions((current) => !current)}
|
||||
class="px-3 py-1.5 text-xs text-dls-secondary hover:text-dls-text font-medium"
|
||||
>
|
||||
{showAllSessions() ? "Show less" : "Show more"}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.openCreateWorkspace}
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-dls-secondary hover:text-dls-text hover:bg-dls-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add a workspace
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-dls-border">
|
||||
|
||||
@@ -13,6 +13,22 @@ export type Client = ReturnType<typeof createClient>;
|
||||
|
||||
export type ProviderListItem = ProviderListResponse["all"][number];
|
||||
|
||||
export type SidebarSessionItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
time?: {
|
||||
updated?: number | null;
|
||||
created?: number | null;
|
||||
};
|
||||
directory?: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceSessionGroup = {
|
||||
workspace: WorkspaceInfo;
|
||||
sessions: SidebarSessionItem[];
|
||||
};
|
||||
|
||||
export type PlaceholderAssistantMessage = {
|
||||
id: string;
|
||||
sessionID: string;
|
||||
|
||||
@@ -89,6 +89,12 @@ export default {
|
||||
"dashboard.empty_workspace": "Empty workspace",
|
||||
"dashboard.empty_workspace_desc": "Start with a blank folder and add what you need.",
|
||||
|
||||
// ==================== Workspace ====================
|
||||
"workspace.rename_title": "Edit workspace name",
|
||||
"workspace.rename_description": "Update the name shown in the sidebar.",
|
||||
"workspace.rename_label": "Workspace name",
|
||||
"workspace.rename_placeholder": "Design team workspace",
|
||||
|
||||
// ==================== Session ====================
|
||||
"session.no_selected": "No session selected",
|
||||
"session.back_to_dashboard": "Back to dashboard",
|
||||
|
||||
@@ -89,6 +89,12 @@ export default {
|
||||
"dashboard.empty_workspace": "空白工作区",
|
||||
"dashboard.empty_workspace_desc": "从空白文件夹开始,添加您需要的内容。",
|
||||
|
||||
// ==================== Workspace ====================
|
||||
"workspace.rename_title": "编辑工作区名称",
|
||||
"workspace.rename_description": "更新侧边栏中显示的名称。",
|
||||
"workspace.rename_label": "工作区名称",
|
||||
"workspace.rename_placeholder": "设计团队工作区",
|
||||
|
||||
// ==================== Session ====================
|
||||
"session.no_selected": "未选择会话",
|
||||
"session.back_to_dashboard": "返回仪表盘",
|
||||
|
||||
@@ -127,6 +127,41 @@ pub fn workspace_set_active(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn workspace_update_display_name(
|
||||
app: tauri::AppHandle,
|
||||
workspace_id: String,
|
||||
display_name: Option<String>,
|
||||
) -> Result<WorkspaceList, String> {
|
||||
println!("[workspace] update display name request: {workspace_id}");
|
||||
let mut state = load_workspace_state(&app)?;
|
||||
let id = workspace_id.trim();
|
||||
|
||||
if id.is_empty() {
|
||||
return Err("workspaceId is required".to_string());
|
||||
}
|
||||
|
||||
let next_name = display_name
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
let workspace = state.workspaces.iter_mut().find(|w| w.id == id);
|
||||
match workspace {
|
||||
Some(entry) => {
|
||||
entry.display_name = next_name;
|
||||
}
|
||||
None => return Err("Unknown workspaceId".to_string()),
|
||||
}
|
||||
|
||||
save_workspace_state(&app, &state)?;
|
||||
println!("[workspace] update display name complete: {id}");
|
||||
|
||||
Ok(WorkspaceList {
|
||||
active_id: state.active_id,
|
||||
workspaces: state.workspaces,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn workspace_create(
|
||||
app: tauri::AppHandle,
|
||||
@@ -246,7 +281,9 @@ pub fn workspace_create_remote(
|
||||
.or_else(|| display_name.clone())
|
||||
.unwrap_or_else(|| {
|
||||
if remote_type == RemoteType::Openwork {
|
||||
openwork_host_url.clone().unwrap_or_else(|| base_url.clone())
|
||||
openwork_host_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| base_url.clone())
|
||||
} else {
|
||||
base_url.clone()
|
||||
}
|
||||
@@ -515,10 +552,7 @@ fn is_secret_name(name: &str) -> bool {
|
||||
if lower == ".env" || lower.starts_with(".env.") {
|
||||
return true;
|
||||
}
|
||||
if lower == "credentials.json"
|
||||
|| lower == "credentials.yml"
|
||||
|| lower == "credentials.yaml"
|
||||
{
|
||||
if lower == "credentials.json" || lower == "credentials.yml" || lower == "credentials.yaml" {
|
||||
return true;
|
||||
}
|
||||
if lower.ends_with(".key")
|
||||
@@ -607,7 +641,10 @@ pub fn workspace_export_config(
|
||||
|
||||
let workspace_root = PathBuf::from(&workspace.path);
|
||||
if !workspace_root.exists() {
|
||||
return Err(format!("Workspace path not found: {}", workspace_root.display()));
|
||||
return Err(format!(
|
||||
"Workspace path not found: {}",
|
||||
workspace_root.display()
|
||||
));
|
||||
}
|
||||
|
||||
let output_path = PathBuf::from(&output_path);
|
||||
@@ -628,8 +665,8 @@ pub fn workspace_export_config(
|
||||
let mut included_paths: Vec<String> = Vec::new();
|
||||
|
||||
for (src, rel) in entries {
|
||||
let mut input = fs::File::open(&src)
|
||||
.map_err(|e| format!("Failed to read {}: {e}", src.display()))?;
|
||||
let mut input =
|
||||
fs::File::open(&src).map_err(|e| format!("Failed to read {}: {e}", src.display()))?;
|
||||
zip.start_file(rel.clone(), options)
|
||||
.map_err(|e| format!("Failed to add {}: {e}", rel))?;
|
||||
let mut buffer = Vec::new();
|
||||
@@ -701,8 +738,7 @@ pub fn workspace_import_config(
|
||||
|
||||
let file = fs::File::open(&archive_path)
|
||||
.map_err(|e| format!("Failed to open {}: {e}", archive_path))?;
|
||||
let mut archive = ZipArchive::new(file)
|
||||
.map_err(|e| format!("Failed to read archive: {e}"))?;
|
||||
let mut archive = ZipArchive::new(file).map_err(|e| format!("Failed to read archive: {e}"))?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut entry = archive.by_index(i).map_err(|e| e.to_string())?;
|
||||
@@ -738,7 +774,8 @@ pub fn workspace_import_config(
|
||||
continue;
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
entry.read_to_end(&mut buffer)
|
||||
entry
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|e| format!("Failed to read archive entry: {e}"))?;
|
||||
fs::write(&out_path, buffer)
|
||||
.map_err(|e| format!("Failed to write {}: {e}", out_path.display()))?;
|
||||
@@ -760,7 +797,10 @@ pub fn workspace_import_config(
|
||||
config.authorized_roots = vec![target_dir.clone()];
|
||||
if let Some(workspace) = &config.workspace {
|
||||
if workspace_name.is_none() {
|
||||
workspace_name = workspace.name.clone().filter(|value| !value.trim().is_empty());
|
||||
workspace_name = workspace
|
||||
.name
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
}
|
||||
if let Some(next_preset) = &workspace.preset {
|
||||
if !next_preset.trim().is_empty() {
|
||||
|
||||
@@ -34,7 +34,7 @@ use commands::updater::updater_environment;
|
||||
use commands::workspace::{
|
||||
workspace_add_authorized_root, workspace_bootstrap, workspace_create, workspace_create_remote,
|
||||
workspace_export_config, workspace_forget, workspace_import_config, workspace_openwork_read,
|
||||
workspace_openwork_write, workspace_set_active, workspace_update_remote,
|
||||
workspace_openwork_write, workspace_set_active, workspace_update_display_name, workspace_update_remote,
|
||||
};
|
||||
use engine::manager::EngineManager;
|
||||
use openwrk::manager::OpenwrkManager;
|
||||
@@ -83,6 +83,7 @@ pub fn run() {
|
||||
workspace_set_active,
|
||||
workspace_create,
|
||||
workspace_create_remote,
|
||||
workspace_update_display_name,
|
||||
workspace_update_remote,
|
||||
workspace_forget,
|
||||
workspace_add_authorized_root,
|
||||
|
||||
Reference in New Issue
Block a user