mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-17 06:17:03 +08:00
feat(openclaw): add Workspace Files panel for managing bootstrap md files
Add a dedicated panel to read/write OpenClaw's 6 workspace bootstrap files (AGENTS.md, SOUL.md, USER.md, IDENTITY.md, TOOLS.md, MEMORY.md) directly from ~/.openclaw/workspace/, with a whitelist-secured backend and Markdown editor UI. Also fix prompt auto-import missing OpenCode/OpenClaw and the PromptFormPanel filenameMap type exclusion.
This commit is contained in:
@@ -20,6 +20,7 @@ mod stream_check;
|
|||||||
mod sync_support;
|
mod sync_support;
|
||||||
mod usage;
|
mod usage;
|
||||||
mod webdav_sync;
|
mod webdav_sync;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use deeplink::*;
|
pub use deeplink::*;
|
||||||
@@ -40,3 +41,4 @@ pub use skill::*;
|
|||||||
pub use stream_check::*;
|
pub use stream_check::*;
|
||||||
pub use usage::*;
|
pub use usage::*;
|
||||||
pub use webdav_sync::*;
|
pub use webdav_sync::*;
|
||||||
|
pub use workspace::*;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::config::write_text_file;
|
||||||
|
use crate::openclaw_config::get_openclaw_dir;
|
||||||
|
|
||||||
|
/// Allowed workspace filenames (whitelist for security)
|
||||||
|
const ALLOWED_FILES: &[&str] = &[
|
||||||
|
"AGENTS.md",
|
||||||
|
"SOUL.md",
|
||||||
|
"USER.md",
|
||||||
|
"IDENTITY.md",
|
||||||
|
"TOOLS.md",
|
||||||
|
"MEMORY.md",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn validate_filename(filename: &str) -> Result<(), String> {
|
||||||
|
if !ALLOWED_FILES.contains(&filename) {
|
||||||
|
return Err(format!(
|
||||||
|
"Invalid workspace filename: {filename}. Allowed: {}",
|
||||||
|
ALLOWED_FILES.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an OpenClaw workspace file content.
|
||||||
|
/// Returns None if the file does not exist.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_workspace_file(filename: String) -> Result<Option<String>, String> {
|
||||||
|
validate_filename(&filename)?;
|
||||||
|
|
||||||
|
let path = get_openclaw_dir().join("workspace").join(&filename);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::read_to_string(&path).map(Some).map_err(|e| {
|
||||||
|
format!("Failed to read workspace file {filename}: {e}")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write content to an OpenClaw workspace file (atomic write).
|
||||||
|
/// Creates the workspace directory if it does not exist.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn write_workspace_file(filename: String, content: String) -> Result<(), String> {
|
||||||
|
validate_filename(&filename)?;
|
||||||
|
|
||||||
|
let workspace_dir = get_openclaw_dir().join("workspace");
|
||||||
|
|
||||||
|
// Ensure workspace directory exists
|
||||||
|
std::fs::create_dir_all(&workspace_dir).map_err(|e| {
|
||||||
|
format!("Failed to create workspace directory: {e}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let path = workspace_dir.join(&filename);
|
||||||
|
|
||||||
|
write_text_file(&path, &content).map_err(|e| {
|
||||||
|
format!("Failed to write workspace file {filename}: {e}")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -582,6 +582,8 @@ pub fn run() {
|
|||||||
crate::app_config::AppType::Claude,
|
crate::app_config::AppType::Claude,
|
||||||
crate::app_config::AppType::Codex,
|
crate::app_config::AppType::Codex,
|
||||||
crate::app_config::AppType::Gemini,
|
crate::app_config::AppType::Gemini,
|
||||||
|
crate::app_config::AppType::OpenCode,
|
||||||
|
crate::app_config::AppType::OpenClaw,
|
||||||
] {
|
] {
|
||||||
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
match crate::services::prompt::PromptService::import_from_file_on_first_launch(
|
||||||
&app_state,
|
&app_state,
|
||||||
@@ -1023,6 +1025,9 @@ pub fn run() {
|
|||||||
commands::get_current_omo_provider_id,
|
commands::get_current_omo_provider_id,
|
||||||
commands::get_omo_provider_count,
|
commands::get_omo_provider_count,
|
||||||
commands::disable_current_omo,
|
commands::disable_current_omo,
|
||||||
|
// Workspace files (OpenClaw)
|
||||||
|
commands::read_workspace_file,
|
||||||
|
commands::write_workspace_file,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
|
|||||||
+20
-2
@@ -16,6 +16,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
FolderArchive,
|
FolderArchive,
|
||||||
Search,
|
Search,
|
||||||
|
FolderOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Provider, VisibleApps } from "@/types";
|
import type { Provider, VisibleApps } from "@/types";
|
||||||
import type { EnvConflict } from "@/types/env";
|
import type { EnvConflict } from "@/types/env";
|
||||||
@@ -56,6 +57,7 @@ import { McpIcon } from "@/components/BrandIcons";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
||||||
import { useDisableCurrentOmo } from "@/lib/query/omo";
|
import { useDisableCurrentOmo } from "@/lib/query/omo";
|
||||||
|
import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel";
|
||||||
|
|
||||||
type View =
|
type View =
|
||||||
| "providers"
|
| "providers"
|
||||||
@@ -66,14 +68,15 @@ type View =
|
|||||||
| "mcp"
|
| "mcp"
|
||||||
| "agents"
|
| "agents"
|
||||||
| "universal"
|
| "universal"
|
||||||
| "sessions";
|
| "sessions"
|
||||||
|
| "workspace";
|
||||||
|
|
||||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||||
const HEADER_HEIGHT = 64; // px
|
const HEADER_HEIGHT = 64; // px
|
||||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
||||||
|
|
||||||
const STORAGE_KEY = "cc-switch-last-app";
|
const STORAGE_KEY = "cc-switch-last-app";
|
||||||
const VALID_APPS: AppId[] = ["claude", "codex", "gemini", "opencode"];
|
const VALID_APPS: AppId[] = ["claude", "codex", "gemini", "opencode", "openclaw"];
|
||||||
|
|
||||||
const getInitialApp = (): AppId => {
|
const getInitialApp = (): AppId => {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY) as AppId | null;
|
const saved = localStorage.getItem(STORAGE_KEY) as AppId | null;
|
||||||
@@ -94,6 +97,7 @@ const VALID_VIEWS: View[] = [
|
|||||||
"agents",
|
"agents",
|
||||||
"universal",
|
"universal",
|
||||||
"sessions",
|
"sessions",
|
||||||
|
"workspace",
|
||||||
];
|
];
|
||||||
|
|
||||||
const getInitialView = (): View => {
|
const getInitialView = (): View => {
|
||||||
@@ -625,6 +629,8 @@ function App() {
|
|||||||
|
|
||||||
case "sessions":
|
case "sessions":
|
||||||
return <SessionManagerPage />;
|
return <SessionManagerPage />;
|
||||||
|
case "workspace":
|
||||||
|
return <WorkspaceFilesPanel />;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="px-6 flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
<div className="px-6 flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
||||||
@@ -783,6 +789,7 @@ function App() {
|
|||||||
defaultValue: "统一供应商",
|
defaultValue: "统一供应商",
|
||||||
})}
|
})}
|
||||||
{currentView === "sessions" && t("sessionManager.title")}
|
{currentView === "sessions" && t("sessionManager.title")}
|
||||||
|
{currentView === "workspace" && t("workspace.title")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -985,6 +992,17 @@ function App() {
|
|||||||
>
|
>
|
||||||
<Book className="w-4 h-4" />
|
<Book className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{activeApp === "openclaw" && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentView("workspace")}
|
||||||
|
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
title={t("workspace.manage")}
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const appName = t(`apps.${appId}`);
|
const appName = t(`apps.${appId}`);
|
||||||
const filenameMap: Record<Exclude<AppId, "openclaw">, string> = {
|
const filenameMap: Record<AppId, string> = {
|
||||||
claude: "CLAUDE.md",
|
claude: "CLAUDE.md",
|
||||||
codex: "AGENTS.md",
|
codex: "AGENTS.md",
|
||||||
gemini: "GEMINI.md",
|
gemini: "GEMINI.md",
|
||||||
opencode: "AGENTS.md",
|
opencode: "AGENTS.md",
|
||||||
|
openclaw: "AGENTS.md",
|
||||||
};
|
};
|
||||||
const filename = filenameMap[appId as Exclude<AppId, "openclaw">];
|
const filename = filenameMap[appId];
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||||
|
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||||
|
import { workspaceApi } from "@/lib/api/workspace";
|
||||||
|
|
||||||
|
interface WorkspaceFileEditorProps {
|
||||||
|
filename: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkspaceFileEditor: React.FC<WorkspaceFileEditorProps> = ({
|
||||||
|
filename,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !filename) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
workspaceApi
|
||||||
|
.readFile(filename)
|
||||||
|
.then((data) => {
|
||||||
|
setContent(data ?? "");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to read workspace file:", err);
|
||||||
|
toast.error(t("workspace.loadFailed"));
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [isOpen, filename, t]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await workspaceApi.writeFile(filename, content);
|
||||||
|
toast.success(t("workspace.saveSuccess"));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save workspace file:", err);
|
||||||
|
toast.error(t("workspace.saveFailed"));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [filename, content, t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenPanel
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={t("workspace.editing", { filename })}
|
||||||
|
onClose={onClose}
|
||||||
|
footer={
|
||||||
|
<Button onClick={handleSave} disabled={saving || loading}>
|
||||||
|
{saving ? t("common.saving") : t("common.save")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
||||||
|
{t("prompts.loading")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<MarkdownEditor
|
||||||
|
value={content}
|
||||||
|
onChange={setContent}
|
||||||
|
darkMode={isDarkMode}
|
||||||
|
placeholder={`# ${filename}\n\n...`}
|
||||||
|
minHeight="calc(100vh - 240px)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FullScreenPanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceFileEditor;
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
FileCode,
|
||||||
|
Heart,
|
||||||
|
User,
|
||||||
|
IdCard,
|
||||||
|
Wrench,
|
||||||
|
Brain,
|
||||||
|
CheckCircle2,
|
||||||
|
Circle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { workspaceApi } from "@/lib/api/workspace";
|
||||||
|
import WorkspaceFileEditor from "./WorkspaceFileEditor";
|
||||||
|
|
||||||
|
interface WorkspaceFile {
|
||||||
|
filename: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
descKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_FILES: WorkspaceFile[] = [
|
||||||
|
{ filename: "AGENTS.md", icon: FileCode, descKey: "workspace.files.agents" },
|
||||||
|
{ filename: "SOUL.md", icon: Heart, descKey: "workspace.files.soul" },
|
||||||
|
{ filename: "USER.md", icon: User, descKey: "workspace.files.user" },
|
||||||
|
{
|
||||||
|
filename: "IDENTITY.md",
|
||||||
|
icon: IdCard,
|
||||||
|
descKey: "workspace.files.identity",
|
||||||
|
},
|
||||||
|
{ filename: "TOOLS.md", icon: Wrench, descKey: "workspace.files.tools" },
|
||||||
|
{ filename: "MEMORY.md", icon: Brain, descKey: "workspace.files.memory" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const WorkspaceFilesPanel: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [editingFile, setEditingFile] = useState<string | null>(null);
|
||||||
|
const [fileExists, setFileExists] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const checkFileExistence = async () => {
|
||||||
|
const results: Record<string, boolean> = {};
|
||||||
|
await Promise.all(
|
||||||
|
WORKSPACE_FILES.map(async (f) => {
|
||||||
|
try {
|
||||||
|
const content = await workspaceApi.readFile(f.filename);
|
||||||
|
results[f.filename] = content !== null;
|
||||||
|
} catch {
|
||||||
|
results[f.filename] = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setFileExists(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void checkFileExistence();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditorClose = () => {
|
||||||
|
setEditingFile(null);
|
||||||
|
// Re-check file existence after closing editor (file may have been created)
|
||||||
|
void checkFileExistence();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-6 pt-4 pb-8">
|
||||||
|
<p className="text-sm text-muted-foreground mb-6">
|
||||||
|
~/.openclaw/workspace/
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{WORKSPACE_FILES.map((file) => {
|
||||||
|
const Icon = file.icon;
|
||||||
|
const exists = fileExists[file.filename];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={file.filename}
|
||||||
|
onClick={() => setEditingFile(file.filename)}
|
||||||
|
className="flex items-start gap-3 p-4 rounded-xl border border-border bg-card hover:bg-accent/50 transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-sm text-foreground">
|
||||||
|
{file.filename}
|
||||||
|
</span>
|
||||||
|
{exists ? (
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Circle className="w-3.5 h-3.5 text-muted-foreground/40 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t(file.descKey)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WorkspaceFileEditor
|
||||||
|
filename={editingFile ?? ""}
|
||||||
|
isOpen={!!editingFile}
|
||||||
|
onClose={handleEditorClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceFilesPanel;
|
||||||
@@ -1115,6 +1115,22 @@
|
|||||||
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
|
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"workspace": {
|
||||||
|
"title": "Workspace Files",
|
||||||
|
"manage": "Workspace",
|
||||||
|
"files": {
|
||||||
|
"agents": "Agent instructions and rules",
|
||||||
|
"soul": "Agent personality and communication style",
|
||||||
|
"user": "User profile and preferences",
|
||||||
|
"identity": "Agent name and avatar",
|
||||||
|
"tools": "Local tool documentation",
|
||||||
|
"memory": "Long-term memory and decisions"
|
||||||
|
},
|
||||||
|
"editing": "Edit {{filename}}",
|
||||||
|
"saveSuccess": "Saved successfully",
|
||||||
|
"saveFailed": "Failed to save",
|
||||||
|
"loadFailed": "Failed to load"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"warning": {
|
"warning": {
|
||||||
"title": "Environment Variable Conflicts Detected",
|
"title": "Environment Variable Conflicts Detected",
|
||||||
|
|||||||
@@ -1115,6 +1115,22 @@
|
|||||||
"deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
"deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"workspace": {
|
||||||
|
"title": "ワークスペースファイル",
|
||||||
|
"manage": "ワークスペース",
|
||||||
|
"files": {
|
||||||
|
"agents": "エージェントの操作指示とルール",
|
||||||
|
"soul": "エージェントの人格とコミュニケーションスタイル",
|
||||||
|
"user": "ユーザープロファイルと設定",
|
||||||
|
"identity": "エージェントの名前とアバター",
|
||||||
|
"tools": "ローカルツールドキュメント",
|
||||||
|
"memory": "長期記憶と意思決定記録"
|
||||||
|
},
|
||||||
|
"editing": "{{filename}} を編集",
|
||||||
|
"saveSuccess": "保存しました",
|
||||||
|
"saveFailed": "保存に失敗しました",
|
||||||
|
"loadFailed": "読み込みに失敗しました"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"warning": {
|
"warning": {
|
||||||
"title": "競合する環境変数を検出しました",
|
"title": "競合する環境変数を検出しました",
|
||||||
|
|||||||
@@ -1115,6 +1115,22 @@
|
|||||||
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
|
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"workspace": {
|
||||||
|
"title": "Workspace 文件管理",
|
||||||
|
"manage": "Workspace",
|
||||||
|
"files": {
|
||||||
|
"agents": "Agent 操作指令和规则",
|
||||||
|
"soul": "Agent 人格和沟通风格",
|
||||||
|
"user": "用户档案和偏好",
|
||||||
|
"identity": "Agent 名称和头像",
|
||||||
|
"tools": "本地工具文档",
|
||||||
|
"memory": "长期记忆和决策记录"
|
||||||
|
},
|
||||||
|
"editing": "编辑 {{filename}}",
|
||||||
|
"saveSuccess": "保存成功",
|
||||||
|
"saveFailed": "保存失败",
|
||||||
|
"loadFailed": "读取失败"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"warning": {
|
"warning": {
|
||||||
"title": "检测到系统环境变量冲突",
|
"title": "检测到系统环境变量冲突",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export { vscodeApi } from "./vscode";
|
|||||||
export { proxyApi } from "./proxy";
|
export { proxyApi } from "./proxy";
|
||||||
export { openclawApi } from "./openclaw";
|
export { openclawApi } from "./openclaw";
|
||||||
export { sessionsApi } from "./sessions";
|
export { sessionsApi } from "./sessions";
|
||||||
|
export { workspaceApi } from "./workspace";
|
||||||
export * as configApi from "./config";
|
export * as configApi from "./config";
|
||||||
export type { ProviderSwitchEvent } from "./providers";
|
export type { ProviderSwitchEvent } from "./providers";
|
||||||
export type { Prompt } from "./prompts";
|
export type { Prompt } from "./prompts";
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export const workspaceApi = {
|
||||||
|
async readFile(filename: string): Promise<string | null> {
|
||||||
|
return invoke<string | null>("read_workspace_file", { filename });
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeFile(filename: string, content: string): Promise<void> {
|
||||||
|
return invoke<void>("write_workspace_file", { filename, content });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -433,6 +433,7 @@ type = "stdio"
|
|||||||
codex: false,
|
codex: false,
|
||||||
gemini: false,
|
gemini: false,
|
||||||
opencode: false,
|
opencode: false,
|
||||||
|
openclaw: false,
|
||||||
});
|
});
|
||||||
expect(onSave).toHaveBeenCalledTimes(1);
|
expect(onSave).toHaveBeenCalledTimes(1);
|
||||||
expect(toastErrorMock).not.toHaveBeenCalled();
|
expect(toastErrorMock).not.toHaveBeenCalled();
|
||||||
|
|||||||
Reference in New Issue
Block a user