Commit dbd31e98 authored by AI-甘富林's avatar AI-甘富林

feat(settings): support workspace directory switching

parent 2c7fc20a
...@@ -131,6 +131,23 @@ function sanitizeAttachmentFileComponent(value: string): string { ...@@ -131,6 +131,23 @@ function sanitizeAttachmentFileComponent(value: string): string {
return sanitized || "attachment"; return sanitized || "attachment";
} }
function normalizeComparablePath(value?: string): string {
const trimmed = value?.trim();
if (!trimmed) {
return "";
}
try {
return path.resolve(trimmed).replace(/[\\/]+/g, "\\").toLowerCase();
} catch {
return trimmed.replace(/[\\/]+/g, "\\").toLowerCase();
}
}
function didWorkspacePathChange(previousConfig: AppConfig, nextConfig: AppConfig): boolean {
return normalizeComparablePath(previousConfig.workspacePath) !== normalizeComparablePath(nextConfig.workspacePath);
}
async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAttachment | null> { async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAttachment | null> {
const dialogOptions: OpenDialogOptions = { const dialogOptions: OpenDialogOptions = {
title: "Select image", title: "Select image",
...@@ -173,6 +190,24 @@ async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAt ...@@ -173,6 +190,24 @@ async function pickImageAttachment(window: BrowserWindow | null): Promise<ChatAt
}; };
} }
async function pickWorkspaceDirectory(window: BrowserWindow | null, currentPath?: string): Promise<string | null> {
const defaultPath = currentPath?.trim();
const dialogOptions: OpenDialogOptions = {
title: "Select workspace directory",
properties: ["openDirectory", "createDirectory"],
...(defaultPath ? { defaultPath } : {})
};
const result = window
? await dialog.showOpenDialog(window, dialogOptions)
: await dialog.showOpenDialog(dialogOptions);
if (result.canceled || !result.filePaths.length) {
return null;
}
const selectedPath = result.filePaths[0]?.trim();
return selectedPath || null;
}
function normalizeChatAttachments(attachments?: ChatAttachment[]): ChatAttachment[] { function normalizeChatAttachments(attachments?: ChatAttachment[]): ChatAttachment[] {
if (!Array.isArray(attachments)) { if (!Array.isArray(attachments)) {
return []; return [];
...@@ -335,6 +370,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -335,6 +370,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
runtimeManager, runtimeManager,
profileClient, profileClient,
secretManager, secretManager,
skillStore,
skillClient, skillClient,
expertCatalogService, expertCatalogService,
skillCatalogService, skillCatalogService,
...@@ -585,6 +621,44 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -585,6 +621,44 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
})(); })();
}; };
const syncCachedRemoteProjectsToWorkspace = async (
reason: string,
options: {
action?: RuntimeCloudFetchAction;
config?: AppConfig;
} = {}
): Promise<void> => {
const nextConfig = options.config ?? await configService.load();
if (nextConfig.setupMode !== "employee-key") {
return;
}
const apiKey = (await secretManager.getApiKey())?.trim();
if (!apiKey) {
return;
}
let runtimeCloudStatus = await runtimeCloudClient.getStatus();
let remoteSkills = runtimeCloudClient.getRemoteSkillAssets();
if (!runtimeCloudStatus.config || remoteSkills.length === 0) {
runtimeCloudStatus = await runtimeCloudClient.fetchConfig(options.action ?? "init");
remoteSkills = runtimeCloudClient.getRemoteSkillAssets();
}
if (!runtimeCloudStatus.config) {
return;
}
await skillStore.reconcile(remoteSkills, runtimeCloudStatus.config.configVersion);
await projectBundleService.syncRemoteBundles(remoteSkills, runtimeCloudStatus.config.configVersion, options.action ?? "sync");
await startupLogger.info("project-bundle", "workspace-project-sync", "Synced cached runtime cloud projects into the current workspace root.", {
reason,
workspacePath: nextConfig.workspacePath,
remoteSkillCount: remoteSkills.length,
configVersion: runtimeCloudStatus.config.configVersion
});
};
const startManagedRuntime = async ( const startManagedRuntime = async (
reason: string, reason: string,
options: { options: {
...@@ -728,6 +802,95 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -728,6 +802,95 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}; };
}; };
const saveAppConfig = async (input: SaveConfigInput): Promise<AppConfig> => {
const previousConfig = await configService.load();
const config = await configService.save(input);
const workspacePathChanged = didWorkspacePathChange(previousConfig, config);
const shouldSyncChatManagedConfig = config.setupMode === "employee-key" && config.runtimeMode !== "external-gateway" && (
typeof input.expertModelConfig?.copywriting?.baseUrl === "string"
|| typeof input.expertModelConfig?.copywriting?.apiKey === "string"
|| typeof input.expertModelConfig?.copywriting?.modelId === "string"
);
if (typeof input.apiKey === "string") {
await secretManager.setApiKey(input.apiKey || undefined);
}
if (typeof input.gatewayToken === "string") {
await secretManager.setGatewayToken(input.gatewayToken || undefined);
}
if (typeof input.authToken === "string") {
await secretManager.setAuthToken(input.authToken || undefined);
}
if (typeof input.expertModelConfig?.image?.apiKey === "string") {
await secretManager.setImageModelApiKey(input.expertModelConfig.image.apiKey || undefined);
}
if (typeof input.expertModelConfig?.video?.apiKey === "string") {
await secretManager.setVideoModelApiKey(input.expertModelConfig.video.apiKey || undefined);
}
if (typeof input.expertModelConfig?.copywriting?.apiKey === "string") {
await secretManager.setCopywritingModelApiKey(input.expertModelConfig.copywriting.apiKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcAccessKey === "string") {
await secretManager.setDigitalHumanVolcAccessKey(input.expertModelConfig.digitalHuman.volcAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcSecretKey === "string") {
await secretManager.setDigitalHumanVolcSecretKey(input.expertModelConfig.digitalHuman.volcSecretKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuAccessKey === "string") {
await secretManager.setDigitalHumanQiniuAccessKey(input.expertModelConfig.digitalHuman.qiniuAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuSecretKey === "string") {
await secretManager.setDigitalHumanQiniuSecretKey(input.expertModelConfig.digitalHuman.qiniuSecretKey || undefined);
}
if (
config.setupMode === "direct-provider"
|| previousConfig.setupMode !== config.setupMode
|| previousConfig.runtimeCloudApiBaseUrl !== config.runtimeCloudApiBaseUrl
|| (config.setupMode === "employee-key"
&& typeof input.apiKey === "string"
&& input.apiKey.trim().length > 0)
) {
await runtimeCloudClient.clearCache().catch(() => undefined);
}
await runtimeManager.setRequestedMode(config.runtimeMode);
if (shouldSyncChatManagedConfig) {
await runtimeManager.syncManagedConfig("sync");
}
if (workspacePathChanged) {
await syncCachedRemoteProjectsToWorkspace("config-save", {
action: "init",
config
}).catch(async (error) => {
const message = error instanceof Error ? error.message : String(error);
console.error("[workspace-config]", "workspace-project-sync.error", message);
await startupLogger.error("project-bundle", "workspace-project-sync.error", "Failed to sync cached runtime cloud projects after workspace path change.", {
workspacePath: config.workspacePath,
error: message
});
});
}
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await reconfigureGatewayClient(config, input.gatewayToken);
void queueWorkspaceWarmup("config-save", {
action: "init",
config,
inputToken: input.gatewayToken,
restart: config.setupMode === "employee-key"
&& typeof input.apiKey === "string"
&& input.apiKey.trim().length > 0
});
} else {
await runtimeCloudSupervisor.stop("config-save");
await reconfigureGatewayClient(config, input.gatewayToken);
if (config.setupMode === "employee-key") {
await syncRuntimeCloudSupervisor("config-save");
}
}
void dailyReportService.runDueCheck().catch(() => undefined);
return getEffectiveConfig();
};
const buildWorkspaceSummary = async (): Promise<WorkspaceSummary> => { const buildWorkspaceSummary = async (): Promise<WorkspaceSummary> => {
const config = await getEffectiveConfig(); const config = await getEffectiveConfig();
await projectStore.initialize(); await projectStore.initialize();
...@@ -1625,79 +1788,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1625,79 +1788,10 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
ipcMain.handle(IPC_CHANNELS.runtimeTelemetryGetStatus, async () => runtimeCloudSupervisor.getStatus()); ipcMain.handle(IPC_CHANNELS.runtimeTelemetryGetStatus, async () => runtimeCloudSupervisor.getStatus());
ipcMain.handle(IPC_CHANNELS.configLoad, async () => getEffectiveConfig()); ipcMain.handle(IPC_CHANNELS.configLoad, async () => getEffectiveConfig());
ipcMain.handle(IPC_CHANNELS.configSave, async (_event, input: SaveConfigInput) => { ipcMain.handle(IPC_CHANNELS.configPickWorkspaceDirectory, async (event, currentPath?: string) => {
const previousConfig = await configService.load(); return pickWorkspaceDirectory(BrowserWindow.fromWebContents(event.sender), currentPath);
const config = await configService.save(input);
const shouldSyncChatManagedConfig = config.setupMode === "employee-key" && config.runtimeMode !== "external-gateway" && (
typeof input.expertModelConfig?.copywriting?.baseUrl === "string"
|| typeof input.expertModelConfig?.copywriting?.apiKey === "string"
|| typeof input.expertModelConfig?.copywriting?.modelId === "string"
);
if (typeof input.apiKey === "string") {
await secretManager.setApiKey(input.apiKey || undefined);
}
if (typeof input.gatewayToken === "string") {
await secretManager.setGatewayToken(input.gatewayToken || undefined);
}
if (typeof input.authToken === "string") {
await secretManager.setAuthToken(input.authToken || undefined);
}
if (typeof input.expertModelConfig?.image?.apiKey === "string") {
await secretManager.setImageModelApiKey(input.expertModelConfig.image.apiKey || undefined);
}
if (typeof input.expertModelConfig?.video?.apiKey === "string") {
await secretManager.setVideoModelApiKey(input.expertModelConfig.video.apiKey || undefined);
}
if (typeof input.expertModelConfig?.copywriting?.apiKey === "string") {
await secretManager.setCopywritingModelApiKey(input.expertModelConfig.copywriting.apiKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcAccessKey === "string") {
await secretManager.setDigitalHumanVolcAccessKey(input.expertModelConfig.digitalHuman.volcAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcSecretKey === "string") {
await secretManager.setDigitalHumanVolcSecretKey(input.expertModelConfig.digitalHuman.volcSecretKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuAccessKey === "string") {
await secretManager.setDigitalHumanQiniuAccessKey(input.expertModelConfig.digitalHuman.qiniuAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuSecretKey === "string") {
await secretManager.setDigitalHumanQiniuSecretKey(input.expertModelConfig.digitalHuman.qiniuSecretKey || undefined);
}
if (
config.setupMode === "direct-provider"
|| previousConfig.setupMode !== config.setupMode
|| previousConfig.runtimeCloudApiBaseUrl !== config.runtimeCloudApiBaseUrl
|| (config.setupMode === "employee-key"
&& typeof input.apiKey === "string"
&& input.apiKey.trim().length > 0)
) {
await runtimeCloudClient.clearCache().catch(() => undefined);
}
await runtimeManager.setRequestedMode(config.runtimeMode);
if (shouldSyncChatManagedConfig) {
await runtimeManager.syncManagedConfig("sync");
}
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await reconfigureGatewayClient(config, input.gatewayToken);
void queueWorkspaceWarmup("config-save", {
action: "init",
config,
inputToken: input.gatewayToken,
restart: config.setupMode === "employee-key"
&& typeof input.apiKey === "string"
&& input.apiKey.trim().length > 0
});
} else {
await runtimeCloudSupervisor.stop("config-save");
await reconfigureGatewayClient(config, input.gatewayToken);
if (config.setupMode === "employee-key") {
await syncRuntimeCloudSupervisor("config-save");
}
}
void dailyReportService.runDueCheck().catch(() => undefined);
return getEffectiveConfig();
}); });
ipcMain.handle(IPC_CHANNELS.configSave, async (_event, input: SaveConfigInput) => saveAppConfig(input));
ipcMain.handle(IPC_CHANNELS.authGetSession, async () => authClient.getSessionSummary()); ipcMain.handle(IPC_CHANNELS.authGetSession, async () => authClient.getSessionSummary());
ipcMain.handle(IPC_CHANNELS.authSignIn, async (_event, input: SignInInput) => { ipcMain.handle(IPC_CHANNELS.authSignIn, async (_event, input: SignInInput) => {
...@@ -1834,63 +1928,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1834,63 +1928,8 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}, },
config: { config: {
load: () => getEffectiveConfig(), load: () => getEffectiveConfig(),
save: async (input: SaveConfigInput) => { pickWorkspaceDirectory: (currentPath?: string) => pickWorkspaceDirectory(null, currentPath),
const config = await configService.save(input); save: (input: SaveConfigInput) => saveAppConfig(input)
const shouldSyncChatManagedConfig = config.setupMode === "employee-key" && config.runtimeMode !== "external-gateway" && (
typeof input.expertModelConfig?.copywriting?.baseUrl === "string"
|| typeof input.expertModelConfig?.copywriting?.apiKey === "string"
|| typeof input.expertModelConfig?.copywriting?.modelId === "string"
);
if (typeof input.apiKey === "string") {
await secretManager.setApiKey(input.apiKey || undefined);
}
if (typeof input.gatewayToken === "string") {
await secretManager.setGatewayToken(input.gatewayToken || undefined);
}
if (typeof input.authToken === "string") {
await secretManager.setAuthToken(input.authToken || undefined);
}
if (typeof input.expertModelConfig?.image?.apiKey === "string") {
await secretManager.setImageModelApiKey(input.expertModelConfig.image.apiKey || undefined);
}
if (typeof input.expertModelConfig?.video?.apiKey === "string") {
await secretManager.setVideoModelApiKey(input.expertModelConfig.video.apiKey || undefined);
}
if (typeof input.expertModelConfig?.copywriting?.apiKey === "string") {
await secretManager.setCopywritingModelApiKey(input.expertModelConfig.copywriting.apiKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcAccessKey === "string") {
await secretManager.setDigitalHumanVolcAccessKey(input.expertModelConfig.digitalHuman.volcAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.volcSecretKey === "string") {
await secretManager.setDigitalHumanVolcSecretKey(input.expertModelConfig.digitalHuman.volcSecretKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuAccessKey === "string") {
await secretManager.setDigitalHumanQiniuAccessKey(input.expertModelConfig.digitalHuman.qiniuAccessKey || undefined);
}
if (typeof input.expertModelConfig?.digitalHuman?.qiniuSecretKey === "string") {
await secretManager.setDigitalHumanQiniuSecretKey(input.expertModelConfig.digitalHuman.qiniuSecretKey || undefined);
}
await runtimeManager.setRequestedMode(config.runtimeMode);
if (shouldSyncChatManagedConfig) {
await runtimeManager.syncManagedConfig("sync");
}
if (config.runtimeMode !== "external-gateway" && (await secretManager.getApiKey())) {
await reconfigureGatewayClient(config, input.gatewayToken);
void queueWorkspaceWarmup("config-save", {
action: "init",
config,
inputToken: input.gatewayToken
});
} else {
await runtimeCloudSupervisor.stop("config-save");
await reconfigureGatewayClient(config, input.gatewayToken);
await syncRuntimeCloudSupervisor("config-save");
}
void dailyReportService.runDueCheck().catch(() => undefined);
return getEffectiveConfig();
}
}, },
projects: { projects: {
list: () => projectStore.listProjects(), list: () => projectStore.listProjects(),
......
...@@ -948,6 +948,7 @@ const mockDesktopApi = { ...@@ -948,6 +948,7 @@ const mockDesktopApi = {
} }
} }
}), }),
pickWorkspaceDirectory: async (currentPath?: string) => currentPath || "D:/workspace",
save: async (input: SaveConfigInput) => ({ save: async (input: SaveConfigInput) => ({
setupMode: input.setupMode, setupMode: input.setupMode,
provider: input.provider, provider: input.provider,
...@@ -2036,6 +2037,7 @@ export default function App() { ...@@ -2036,6 +2037,7 @@ export default function App() {
const copiedTokenResetRef = useRef<number | null>(null); const copiedTokenResetRef = useRef<number | null>(null);
const composerDragDepthRef = useRef(0); const composerDragDepthRef = useRef(0);
const startupWarmupRequestedRef = useRef(false); const startupWarmupRequestedRef = useRef(false);
const lastLoadedWorkspacePathRef = useRef<string | null>(null);
const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null); const [streamSmoke, setStreamSmoke] = useState<SmokeStreamSnapshot | null>(null);
const minimizeWindow = () => void desktopApi.window.minimize(); const minimizeWindow = () => void desktopApi.window.minimize();
const maximizeWindow = () => void desktopApi.window.maximize(); const maximizeWindow = () => void desktopApi.window.maximize();
...@@ -2044,6 +2046,9 @@ export default function App() { ...@@ -2044,6 +2046,9 @@ export default function App() {
const effectiveSkills = useMemo(() => (readySkills.length ? [DEFAULT_SKILL, ...readySkills] : [DEFAULT_SKILL]), [readySkills]); const effectiveSkills = useMemo(() => (readySkills.length ? [DEFAULT_SKILL, ...readySkills] : [DEFAULT_SKILL]), [readySkills]);
const selectedSkill = useMemo(() => effectiveSkills.find((skill) => skill.id === selectedSkillId) ?? effectiveSkills[0] ?? DEFAULT_SKILL, [effectiveSkills, selectedSkillId]); const selectedSkill = useMemo(() => effectiveSkills.find((skill) => skill.id === selectedSkillId) ?? effectiveSkills[0] ?? DEFAULT_SKILL, [effectiveSkills, selectedSkillId]);
const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key"; const setupMode = workspace?.setupMode ?? config?.setupMode ?? "employee-key";
const savedWorkspacePath = config?.workspacePath ?? "";
const displayedWorkspacePath = workspacePathDraft.trim() || savedWorkspacePath || ui.none;
const hasPendingWorkspacePathChange = Boolean(config && workspacePathDraft.trim() && workspacePathDraft.trim() !== savedWorkspacePath);
const shellReady = workspace?.shellReady ?? false; const shellReady = workspace?.shellReady ?? false;
const bindingRequired = workspace?.bindingRequired ?? !Boolean(workspace?.apiKeyConfigured ?? config?.apiKeyConfigured); const bindingRequired = workspace?.bindingRequired ?? !Boolean(workspace?.apiKeyConfigured ?? config?.apiKeyConfigured);
const chatLaunchState: ChatLaunchState = workspace?.chatLaunchState ?? (!bindingRequired ? "starting" : "unbound"); const chatLaunchState: ChatLaunchState = workspace?.chatLaunchState ?? (!bindingRequired ? "starting" : "unbound");
...@@ -2305,7 +2310,6 @@ export default function App() { ...@@ -2305,7 +2310,6 @@ export default function App() {
setRuntimeTelemetry(nextTelemetry); setRuntimeTelemetry(nextTelemetry);
setSystemSummary(nextSystem); setSystemSummary(nextSystem);
setExpertDefinitions(nextExperts); setExpertDefinitions(nextExperts);
setWorkspacePathDraft((current) => current || nextConfig.workspacePath);
setGatewayStatus(statusResult); setGatewayStatus(statusResult);
const nextReadySkills = nextWorkspace.skills.filter((skill) => skill.ready); const nextReadySkills = nextWorkspace.skills.filter((skill) => skill.ready);
...@@ -2504,7 +2508,14 @@ export default function App() { ...@@ -2504,7 +2508,14 @@ export default function App() {
return; return;
} }
setWorkspacePathDraft(config.workspacePath); setWorkspacePathDraft((current) => {
const lastLoadedWorkspacePath = lastLoadedWorkspacePathRef.current;
lastLoadedWorkspacePathRef.current = config.workspacePath;
if (!current || current === lastLoadedWorkspacePath) {
return config.workspacePath;
}
return current;
});
setImageModelBaseUrlDraft(config.expertModelConfig.image.baseUrl); setImageModelBaseUrlDraft(config.expertModelConfig.image.baseUrl);
setImageModelIdDraft(config.expertModelConfig.image.modelId ?? FIXED_EXPERT_MODEL_ENDPOINTS.image.modelId); setImageModelIdDraft(config.expertModelConfig.image.modelId ?? FIXED_EXPERT_MODEL_ENDPOINTS.image.modelId);
setVideoModelBaseUrlDraft(config.expertModelConfig.video.baseUrl); setVideoModelBaseUrlDraft(config.expertModelConfig.video.baseUrl);
...@@ -3381,6 +3392,7 @@ export default function App() { ...@@ -3381,6 +3392,7 @@ export default function App() {
lobsterKey?: string; lobsterKey?: string;
workspacePath?: string; workspacePath?: string;
expertModelConfig?: SaveConfigInput["expertModelConfig"]; expertModelConfig?: SaveConfigInput["expertModelConfig"];
successMessage?: string;
}) { }) {
if (!config) { if (!config) {
return; return;
...@@ -3435,7 +3447,7 @@ export default function App() { ...@@ -3435,7 +3447,7 @@ export default function App() {
setDigitalHumanVolcSecretKeyDraft(""); setDigitalHumanVolcSecretKeyDraft("");
setDigitalHumanQiniuAccessKeyDraft(""); setDigitalHumanQiniuAccessKeyDraft("");
setDigitalHumanQiniuSecretKeyDraft(""); setDigitalHumanQiniuSecretKeyDraft("");
setInfoText(trimmedLobsterKey ? ui.saveSuccessPending : ui.saveSuccessApplied); setInfoText(options?.successMessage ?? (trimmedLobsterKey ? ui.saveSuccessPending : ui.saveSuccessApplied));
void refresh(false); void refresh(false);
} catch (error) { } catch (error) {
setErrorText(err(error)); setErrorText(err(error));
...@@ -4079,6 +4091,46 @@ export default function App() { ...@@ -4079,6 +4091,46 @@ export default function App() {
} }
} }
async function pickWorkspaceDirectory() {
if (!config || saving) {
return;
}
setErrorText("");
try {
const nextPath = await desktopApi.config.pickWorkspaceDirectory(workspacePathDraft.trim() || config.workspacePath);
if (!nextPath?.trim()) {
return;
}
setWorkspacePathDraft(nextPath.trim());
setInfoText("");
} catch (error) {
setErrorText(err(error));
}
}
function restoreWorkspaceDirectory() {
if (!config) {
return;
}
setWorkspacePathDraft(config.workspacePath);
setErrorText("");
setInfoText("");
}
async function saveWorkspaceDirectory() {
if (!hasPendingWorkspacePathChange) {
return;
}
await saveConfig({
workspacePath: workspacePathDraft,
successMessage: "工作目录已保存,正在重新预热工作区。"
});
}
const sidebarSessionLabel = viewMode === "experts" ? "专家会话" : "会话管理"; const sidebarSessionLabel = viewMode === "experts" ? "专家会话" : "会话管理";
const selectedSkillBadge = selectedSkillId === DEFAULT_SKILL.id ? "千匠问天" : "@" + selectedSkill.name; const selectedSkillBadge = selectedSkillId === DEFAULT_SKILL.id ? "千匠问天" : "@" + selectedSkill.name;
const sidebarNewSessionAction = ( const sidebarNewSessionAction = (
...@@ -4905,30 +4957,27 @@ export default function App() { ...@@ -4905,30 +4957,27 @@ export default function App() {
<div className="settings-section-headline"> <div className="settings-section-headline">
<div> <div>
<span className="settings-section-kicker">诊断与工作区</span> <span className="settings-section-kicker">诊断与工作区</span>
<h4>{ui.diagnostics}</h4>
<p>{ui.diagnosticsDesc}</p>
</div> </div>
</div> </div>
<div className="settings-static-list"> <div className="workspace-directory-card">
<div className="settings-static-item"> <div className="workspace-directory-panel">
<span>{ui.workspacePath}</span> <span className="workspace-directory-eyebrow">当前生效目录</span>
<strong>{config?.workspacePath || workspacePathDraft || ui.none}</strong> <strong className="workspace-directory-path">{displayedWorkspacePath}</strong>
<p className="workspace-directory-hint">项目会从该目录加载;保存后会重新预热工作区。</p>
</div>
{hasPendingWorkspacePathChange ? (
<div className="workspace-directory-draft-row">
<span className="workspace-directory-draft-badge">待保存</span>
<div className="workspace-directory-inline-actions">
<button disabled={saving} onClick={() => void saveWorkspaceDirectory()}>{saving ? ui.saving : ui.save}</button>
<button className="secondary workspace-directory-inline-button" disabled={saving} onClick={restoreWorkspaceDirectory}>恢复当前</button>
</div> </div>
</div> </div>
<div className="diagnostic-meta-list">
{runtimeCloudStatus ? (
<>
<div className="mini-info"><span>Runtime Cloud Target</span><strong>{runtimeCloudStatus.baseUrl || ui.none}</strong></div>
<div className="mini-info"><span>Target Source</span><strong>{runtimeCloudStatus.baseUrlSource ?? ui.none}</strong></div>
<div className="mini-info"><span>Binding Host</span><strong>{runtimeCloudStatus.baseHost ?? ui.none}</strong></div>
</>
) : null} ) : null}
</div> </div>
<div className="button-row settings-actions"> <div className="button-row settings-actions workspace-directory-actions">
<button className="secondary" onClick={() => void handleCopyText("workspace-path", config?.workspacePath || workspacePathDraft || ui.none)}> <button disabled={saving || !config} onClick={() => void pickWorkspaceDirectory()}>更改目录</button>
{copiedToken === "workspace-path" ? ui.copied : ui.copy} <button className="secondary" disabled={saving} onClick={() => void exportDiagnostics()}>{ui.export}</button>
</button>
<button className="secondary" onClick={() => void exportDiagnostics()}>{ui.export}</button>
</div> </div>
</div> </div>
</section> </section>
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
runtimeCloudFetchConfig: "runtime-cloud:fetch-config", runtimeCloudFetchConfig: "runtime-cloud:fetch-config",
runtimeTelemetryGetStatus: "runtime-telemetry:get-status", runtimeTelemetryGetStatus: "runtime-telemetry:get-status",
configLoad: "config:load", configLoad: "config:load",
configPickWorkspaceDirectory: "config:pick-workspace-directory",
configSave: "config:save", configSave: "config:save",
projectsList: "projects:list", projectsList: "projects:list",
projectsSetActive: "projects:set-active", projectsSetActive: "projects:set-active",
...@@ -802,6 +803,7 @@ export interface DesktopApi { ...@@ -802,6 +803,7 @@ export interface DesktopApi {
}; };
config: { config: {
load(): Promise<AppConfig>; load(): Promise<AppConfig>;
pickWorkspaceDirectory(currentPath?: string): Promise<string | null>;
save(input: SaveConfigInput): Promise<AppConfig>; save(input: SaveConfigInput): Promise<AppConfig>;
}; };
projects: { projects: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment