diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index 2f2429d..0b99e58 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -6,5 +6,20 @@ fn main() { println!("cargo:rustc-env={key}={val}"); } } + + // On Linux, apply a version script to hide statically linked ICU/HarfBuzz/ + // FreeType/Fontconfig symbols from the dynamic symbol table. This prevents + // symbol collisions with the system copies loaded by WebKit2GTK (segfault). + // See: https://github.com/delibae/claude-prism/issues/100 + #[cfg(target_os = "linux")] + { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); + println!( + "cargo:rustc-link-arg=-Wl,--version-script={}/symbols.map", + manifest_dir + ); + println!("cargo:rerun-if-changed=symbols.map"); + } + tauri_build::build() } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 55457d0..4b93ce6 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -21,28 +21,61 @@ "dialog:allow-message", "dialog:allow-ask", "fs:default", - "fs:allow-read", - "fs:allow-write", - "fs:allow-exists", - "fs:allow-mkdir", - "fs:allow-remove", - "fs:allow-rename", - "fs:allow-copy-file", - "fs:allow-read-dir", - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-read-file", - "fs:allow-write-file", - "fs:allow-stat", - "fs:allow-watch", { - "identifier": "fs:scope", - "allow": [ - { "path": "$HOME/**" }, - { "path": "$DOCUMENT/**" }, - { "path": "$DESKTOP/**" }, - { "path": "$TEMP/**" } - ] + "identifier": "fs:allow-read", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-exists", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-mkdir", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-remove", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-rename", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-copy-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-dir", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write-text-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-stat", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-watch", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] }, "shell:default", "shell:allow-open", diff --git a/apps/desktop/src-tauri/symbols.map b/apps/desktop/src-tauri/symbols.map new file mode 100644 index 0000000..32a364c --- /dev/null +++ b/apps/desktop/src-tauri/symbols.map @@ -0,0 +1,50 @@ +/* Version script to hide statically linked library symbols. + * + * Prevents symbol collisions between our statically linked + * ICU/HarfBuzz/FreeType/Fontconfig (from vcpkg, used by Tectonic) + * and the dynamically linked versions used by WebKit2GTK at runtime. + * + * Without this, the ELF dynamic linker resolves WebKit2GTK's ICU + * calls to our static symbols (different version), causing a segfault + * at startup. Fixes #100; preserves #91 fix (static ICU = no + * libicuuc.so.70 not-found error on newer distros). + */ +{ + local: + /* ICU4C */ + u_*; + ucnv_*; + ucol_*; + ures_*; + udata_*; + ucal_*; + udat_*; + unum_*; + uset_*; + ustr_*; + ubidi_*; + ubrk_*; + uidna_*; + uloc_*; + unorm_*; + unorm2_*; + utrans_*; + icu_*; + UCNV_*; + + /* HarfBuzz */ + hb_*; + + /* FreeType */ + FT_*; + ft_*; + + /* Fontconfig */ + Fc*; + + /* Graphite2 */ + gr_*; + + /* libpng (often pulled in by FreeType) */ + png_*; +}; diff --git a/apps/desktop/src/components/claude-chat/markdown-renderer.tsx b/apps/desktop/src/components/claude-chat/markdown-renderer.tsx index bc4d83b..4c08078 100644 --- a/apps/desktop/src/components/claude-chat/markdown-renderer.tsx +++ b/apps/desktop/src/components/claude-chat/markdown-renderer.tsx @@ -220,7 +220,7 @@ const CodeBlock: FC<{ language: string; code: string }> = ({ Run in{" "} - {projectRoot?.split("/").pop()}/ + {projectRoot?.split(/[/\\]/).pop()}/ diff --git a/apps/desktop/src/components/project-wizard.tsx b/apps/desktop/src/components/project-wizard.tsx index f583381..e318349 100644 --- a/apps/desktop/src/components/project-wizard.tsx +++ b/apps/desktop/src/components/project-wizard.tsx @@ -3,6 +3,7 @@ import { open } from "@tauri-apps/plugin-dialog"; import { mkdir, writeTextFile } from "@tauri-apps/plugin-fs"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { homeDir } from "@tauri-apps/api/path"; +import { toast } from "sonner"; import { ArrowLeftIcon, FolderOpenIcon, @@ -133,11 +134,11 @@ function ScratchForm({ onBack }: { onBack: () => void }) { } else { homeDir() .then((home) => join(home, "Documents", "ClaudePrism")) - .then((dir) => { - mkdir(dir, { recursive: true }).catch(() => {}); + .then(async (dir) => { + await mkdir(dir, { recursive: true }).catch(() => {}); setProjectFolder(dir); }) - .catch(() => {}); + .catch((err) => console.warn("Failed to resolve default project folder:", err)); } }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -234,7 +235,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { try { const projectPath = await join(projectFolder, projectName.trim()); - await mkdir(projectPath, { recursive: true }).catch(() => {}); + await mkdir(projectPath, { recursive: true }); // Create CLAUDE.md for Claude Code context const claudeMdPath = await join(projectPath, "CLAUDE.md"); @@ -259,12 +260,12 @@ function ScratchForm({ onBack }: { onBack: () => void }) { if (attachments.length > 0) { const attachmentsDir = await join(projectPath, "attachments"); - await mkdir(attachmentsDir, { recursive: true }).catch(() => {}); + await mkdir(attachmentsDir, { recursive: true }); } if (purpose.trim()) { const attachmentNames = attachments - .map((p) => p.split("/").pop()) + .map((p) => p.split(/[/\\]/).pop()) .filter(Boolean); const attachmentSection = attachmentNames.length > 0 @@ -303,6 +304,9 @@ function ScratchForm({ onBack }: { onBack: () => void }) { } } catch (err) { console.error("Failed to create project:", err); + toast.error("Failed to create project", { + description: err instanceof Error ? err.message : String(err), + }); } finally { setIsCreating(false); } @@ -383,7 +387,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { > - {path.split("/").pop()} + {path.split(/[/\\]/).pop()} handleRemoveAttachment(path)} @@ -447,7 +451,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { {!locationOpen && projectFolder && projectName.trim() && ( - .../{projectFolder.split("/").pop()}/{projectName.trim()} + .../{projectFolder.split(/[/\\]/).pop()}/{projectName.trim()} )} setProjectFolder(dir)) - .catch(() => {}); + homeDir() + .then((home) => join(home, "Documents", "ClaudePrism")) + .then(async (dir) => { + await mkdir(dir, { recursive: true }).catch(() => {}); + setProjectFolder(dir); + }) + .catch((err) => + console.warn("Failed to resolve default project folder:", err), + ); } }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -400,7 +407,7 @@ export function TemplatePreview() { try { const projectPath = await join(projectFolder, projectName.trim()); - await mkdir(projectPath, { recursive: true }).catch(() => {}); + await mkdir(projectPath, { recursive: true }); const mainTexPath = await join(projectPath, template.mainFileName); const mainExists = await exists(mainTexPath); @@ -418,12 +425,12 @@ export function TemplatePreview() { if (attachments.length > 0) { const attachmentsDir = await join(projectPath, "attachments"); - await mkdir(attachmentsDir, { recursive: true }).catch(() => {}); + await mkdir(attachmentsDir, { recursive: true }); } if (purpose.trim()) { const attachmentNames = attachments - .map((p) => p.split("/").pop()) + .map((p) => p.split(/[/\\]/).pop()) .filter(Boolean); const attachmentSection = attachmentNames.length > 0 @@ -465,6 +472,9 @@ export function TemplatePreview() { closePreview(); } catch (err) { console.error("Failed to create project:", err); + toast.error("Failed to create project", { + description: err instanceof Error ? err.message : String(err), + }); } finally { setIsCreating(false); } @@ -650,7 +660,7 @@ export function TemplatePreview() { > - {path.split("/").pop()} + {path.split(/[/\\]/).pop()} handleRemoveAttachment(path)} @@ -713,7 +723,7 @@ export function TemplatePreview() { {!locationOpen && projectFolder && projectName.trim() && ( - .../{projectFolder.split("/").pop()}/ + .../{projectFolder.split(/[/\\]/).pop()}/ {projectName.trim()} )} diff --git a/apps/desktop/src/components/uv-setup.tsx b/apps/desktop/src/components/uv-setup.tsx index 8d23265..6c614da 100644 --- a/apps/desktop/src/components/uv-setup.tsx +++ b/apps/desktop/src/components/uv-setup.tsx @@ -152,8 +152,7 @@ export function UvSetupDialog({ open, onClose }: UvSetupDialogProps) { title={pythonPath} > Python:{" "} - {pythonPath.split("/").pop() || - pythonPath.split("\\").pop()} + {pythonPath.split(/[/\\]/).pop()} )} diff --git a/apps/desktop/src/components/workspace/history-panel.tsx b/apps/desktop/src/components/workspace/history-panel.tsx index 5e77fc4..39b0af8 100644 --- a/apps/desktop/src/components/workspace/history-panel.tsx +++ b/apps/desktop/src/components/workspace/history-panel.tsx @@ -344,7 +344,7 @@ function SnapshotRow({ {hasFiles && ( {snapshot.changed_files - .map((f) => f.split("/").pop()) + .map((f) => f.split(/[/\\]/).pop()) .join(", ")} )} diff --git a/apps/desktop/src/stores/document-store.ts b/apps/desktop/src/stores/document-store.ts index b77fbb7..dfe74a6 100644 --- a/apps/desktop/src/stores/document-store.ts +++ b/apps/desktop/src/stores/document-store.ts @@ -241,7 +241,7 @@ export const useDocumentStore = create()((set, get) => ({ for (const f of fsFiles) { const pf: ProjectFile = { id: f.relativePath, - name: f.relativePath.split("/").pop() || f.relativePath, + name: f.relativePath.split(/[/\\]/).pop() || f.relativePath, relativePath: f.relativePath, absolutePath: f.absolutePath, type: f.type, @@ -796,7 +796,7 @@ export const useDocumentStore = create()((set, get) => ({ const newAbsPath = await join(state.projectRoot, newRelativePath); await renameFileOnDisk(file.absolutePath, newAbsPath); - const newName = newRelativePath.split("/").pop() || file.name; + const newName = newRelativePath.split(/[/\\]/).pop() || file.name; migratePdfBytesKey(fileId, newRelativePath); set((s) => { const compileErrorCache = migrateCacheKey( @@ -833,7 +833,7 @@ export const useDocumentStore = create()((set, get) => ({ const state = get(); if (!state.projectRoot) return; - const folderName = folderPath.split("/").pop()!; + const folderName = folderPath.split(/[/\\]/).pop()!; const newFolderPath = targetFolder ? `${targetFolder}/${folderName}` : folderName; @@ -911,7 +911,7 @@ export const useDocumentStore = create()((set, get) => ({ // New file on disk const pf: ProjectFile = { id: fsFile.relativePath, - name: fsFile.relativePath.split("/").pop() || fsFile.relativePath, + name: fsFile.relativePath.split(/[/\\]/).pop() || fsFile.relativePath, relativePath: fsFile.relativePath, absolutePath: fsFile.absolutePath, type: fsFile.type, diff --git a/apps/desktop/src/stores/project-store.ts b/apps/desktop/src/stores/project-store.ts index 59e90c7..e55e6b0 100644 --- a/apps/desktop/src/stores/project-store.ts +++ b/apps/desktop/src/stores/project-store.ts @@ -26,7 +26,7 @@ export const useProjectStore = create()( setLastProjectFolder: (path) => set({ lastProjectFolder: path }), addRecentProject: (path) => { - const name = path.split("/").pop() || path; + const name = path.split(/[/\\]/).pop() || path; set((state) => { const filtered = state.recentProjects.filter((p) => p.path !== path); return {
- {projectRoot?.split("/").pop()}/ + {projectRoot?.split(/[/\\]/).pop()}/