From fab35ac860a125544628319d065f25f8d94f46c6 Mon Sep 17 00:00:00 2001 From: Aayush Prajapati Date: Sat, 21 Feb 2026 17:24:28 +0530 Subject: [PATCH 1/2] fix: Fixed editor mode file open failing --- .../session/artifact-markdown-editor.tsx | 49 ++++++++- .../components/session/artifacts-panel.tsx | 6 + packages/app/src/app/pages/session.tsx | 104 +++++++++++++++++- packages/app/src/app/utils/index.ts | 50 ++++++++- 4 files changed, 193 insertions(+), 16 deletions(-) diff --git a/packages/app/src/app/components/session/artifact-markdown-editor.tsx b/packages/app/src/app/components/session/artifact-markdown-editor.tsx index 1370194b..ca18a9cb 100644 --- a/packages/app/src/app/components/session/artifact-markdown-editor.tsx +++ b/packages/app/src/app/components/session/artifact-markdown-editor.tsx @@ -63,12 +63,19 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp const client = props.client; const workspaceId = props.workspaceId; + console.debug("[ArtifactMarkdownEditor] load requested:", { target, workspaceId, hasClient: !!client }); + if (!client || !workspaceId) { + console.warn("[ArtifactMarkdownEditor] load blocked: no client or workspaceId", { workspaceId, hasClient: !!client }); setError(writeDisabledReason()); return; } - if (!target) return; + if (!target) { + console.warn("[ArtifactMarkdownEditor] load blocked: empty target path"); + return; + } if (!isMarkdown(target)) { + console.warn("[ArtifactMarkdownEditor] load blocked: not a markdown file:", target); setError("Only markdown files are supported."); return; } @@ -80,7 +87,9 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp let result: OpenworkWorkspaceFileContent; let actualPath = target; try { + console.debug("[ArtifactMarkdownEditor] fetching file:", { workspaceId, target }); result = (await client.readWorkspaceFile(workspaceId, target)) as OpenworkWorkspaceFileContent; + console.debug("[ArtifactMarkdownEditor] file loaded successfully:", target); } catch (err) { // Artifacts are frequently referenced as workspace-relative paths (e.g. `learned/foo.md`), // but on disk they may live under the OpenWork outbox dir: `.opencode/openwork/outbox/`. @@ -91,14 +100,29 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp err instanceof OpenworkServerError && err.status === 404; + console.debug("[ArtifactMarkdownEditor] primary fetch failed:", { + target, + error: err instanceof Error ? err.message : String(err), + status: err instanceof OpenworkServerError ? err.status : null, + willTryOutbox: shouldTryOutbox, + candidateOutbox, + }); + if (!shouldTryOutbox) { throw err; } actualPath = candidateOutbox; try { + console.debug("[ArtifactMarkdownEditor] trying outbox path:", actualPath); result = (await client.readWorkspaceFile(workspaceId, actualPath)) as OpenworkWorkspaceFileContent; + console.debug("[ArtifactMarkdownEditor] outbox file loaded successfully:", actualPath); } catch (second) { + console.warn("[ArtifactMarkdownEditor] outbox fetch also failed:", { + actualPath, + error: second instanceof Error ? second.message : String(second), + status: second instanceof OpenworkServerError ? second.status : null, + }); if (second instanceof OpenworkServerError && second.status === 404) { throw new OpenworkServerError(404, "file_not_found", "File not found (workspace root or outbox)."); } @@ -113,6 +137,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp setBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null); } catch (err) { const message = err instanceof Error ? err.message : "Failed to load file"; + console.error("[ArtifactMarkdownEditor] load error:", { target, error: message, rawError: err }); setError(message); setLoadedPath(target); } finally { @@ -193,21 +218,37 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp } const target = path(); - if (!target) return; - if (loading() || pendingReason() === "switch") return; + if (!target) { + console.debug("[ArtifactMarkdownEditor] effect: no target path, skipping"); + return; + } + if (loading()) { + console.debug("[ArtifactMarkdownEditor] effect: already loading, skipping"); + return; + } + if (pendingReason() === "switch") { + console.debug("[ArtifactMarkdownEditor] effect: pending switch, skipping"); + return; + } const active = loadedPath(); if (!active) { + console.debug("[ArtifactMarkdownEditor] effect: no active path, triggering load for:", target); void load(target); return; } - if (target === active) return; + if (target === active) { + console.debug("[ArtifactMarkdownEditor] effect: target matches active, no-op"); + return; + } if (!dirty()) { + console.debug("[ArtifactMarkdownEditor] effect: different target, not dirty, loading:", target); void load(target); return; } + console.debug("[ArtifactMarkdownEditor] effect: different target with dirty content, prompting switch"); setPendingPath(target); setPendingReason("switch"); }); diff --git a/packages/app/src/app/components/session/artifacts-panel.tsx b/packages/app/src/app/components/session/artifacts-panel.tsx index 259d977e..39e5e83c 100644 --- a/packages/app/src/app/components/session/artifacts-panel.tsx +++ b/packages/app/src/app/components/session/artifacts-panel.tsx @@ -133,6 +133,12 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) { openable() ? "hover:bg-dls-active" : "cursor-default" }`} onClick={() => { + console.debug("[ArtifactsPanel] click:", { + path: artifact.path, + kind: artifact.kind, + hasOnOpenMarkdown: typeof props.onOpenMarkdown === "function", + hasOnOpenImage: typeof props.onOpenImage === "function", + }); if (md()) props.onOpenMarkdown?.(artifact.path); else if (img()) props.onOpenImage?.(artifact.path); }} diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index e9c74304..1d9b8892 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -709,51 +709,143 @@ export default function SessionView(props: SessionViewProps) { const toWorkspaceRelativeForApi = (file: string) => { const normalized = normalizeSidebarPath(file).replace(/^file:\/\//i, ""); - if (!normalized) return ""; + if (!normalized) { + console.debug("[toWorkspaceRelativeForApi] empty after normalization:", { original: file }); + return ""; + } const root = normalizeSidebarPath(props.activeWorkspaceRoot).replace(/\/+$/, ""); const rootKey = root.toLowerCase(); const fileKey = normalized.toLowerCase(); + console.debug("[toWorkspaceRelativeForApi] processing:", { file, normalized, root, rootKey, fileKey }); + + // Case 1: Full absolute path that starts with workspace root if (root && fileKey.startsWith(`${rootKey}/`)) { - return normalized.slice(root.length + 1); + const result = normalized.slice(root.length + 1); + console.debug("[toWorkspaceRelativeForApi] stripped workspace root, result:", result); + return result; } if (root && fileKey === rootKey) { + console.debug("[toWorkspaceRelativeForApi] path equals workspace root, returning empty"); return ""; } + // Case 2: Truncated absolute path - check if any suffix of the workspace root + // matches a prefix of the file path. This handles cases like: + // - root: "/Users/foo/Library/Application Support/com.differentai.openwork.dev/workspaces/starter" + // - file: "Support/com.differentai.openwork.dev/workspaces/starter/test.md" + if (root) { + const rootSegments = root.split("/").filter(Boolean); + const fileSegments = normalized.split("/"); + + // Try to find where the file path overlaps with the workspace root + for (let i = 1; i < rootSegments.length; i++) { + const rootSuffix = rootSegments.slice(i).join("/").toLowerCase(); + const rootSuffixWithSlash = `${rootSuffix}/`; + + if (fileKey.startsWith(rootSuffixWithSlash)) { + const result = normalized.slice(rootSuffix.length + 1); + console.debug("[toWorkspaceRelativeForApi] found overlapping suffix, stripped:", { + rootSuffix, + result, + }); + return result; + } + if (fileKey === rootSuffix) { + console.debug("[toWorkspaceRelativeForApi] file matches root suffix exactly"); + return ""; + } + } + + // Also check for workspace name/id match in file path segments + // e.g., path contains "workspaces/starter/" where "starter" is the workspace folder name + const workspaceFolderName = rootSegments[rootSegments.length - 1]?.toLowerCase(); + if (workspaceFolderName) { + const workspacesPattern = new RegExp(`workspaces/${workspaceFolderName}/`, "i"); + const match = normalized.match(workspacesPattern); + if (match && match.index !== undefined) { + const result = normalized.slice(match.index + match[0].length); + console.debug("[toWorkspaceRelativeForApi] found workspaces/name pattern, result:", result); + return result; + } + } + } + let relative = normalized.replace(/^\.\/+/, ""); - if (!relative) return ""; + if (!relative) { + console.debug("[toWorkspaceRelativeForApi] empty after stripping ./"); + return ""; + } // Tool output paths sometimes carry git-style prefixes (a/ or b/). if (/^[ab]\/.+\.(md|mdx|markdown)$/i.test(relative)) { relative = relative.slice(2); + console.debug("[toWorkspaceRelativeForApi] stripped git prefix (a/ or b/):", relative); } // Some tool outputs include a leading "workspace/" prefix. if (/^workspace\//i.test(relative)) { relative = relative.replace(/^workspace\//i, ""); + console.debug("[toWorkspaceRelativeForApi] stripped workspace/ prefix:", relative); } // Other surfaces include an absolute-style "/workspace/" prefix. if (/^\/+workspace\//i.test(relative)) { relative = relative.replace(/^\/+workspace\//i, ""); + console.debug("[toWorkspaceRelativeForApi] stripped /workspace/ prefix:", relative); } - if (relative.startsWith("/") || relative.startsWith("~") || /^[a-zA-Z]:\//.test(relative)) return ""; - if (relative.split("/").some((part) => part === "." || part === "..")) return ""; + + if (relative.startsWith("/") || relative.startsWith("~") || /^[a-zA-Z]:\//.test(relative)) { + console.debug("[toWorkspaceRelativeForApi] rejected absolute path:", relative); + return ""; + } + if (relative.split("/").some((part) => part === "." || part === "..")) { + console.debug("[toWorkspaceRelativeForApi] rejected path with . or .. segments:", relative); + return ""; + } + + // Reject paths that look like truncated system paths (contain app bundle identifiers) + if (/com\.[^/]+\.(openwork|opencode)/i.test(relative)) { + console.debug("[toWorkspaceRelativeForApi] rejected truncated system path:", relative); + return ""; + } + + console.debug("[toWorkspaceRelativeForApi] final result:", relative); return relative; }; const openMarkdownEditor = (file: string) => { + console.debug("[openMarkdownEditor] requested file:", file, { + workspaceRoot: props.activeWorkspaceRoot, + hasClient: !!props.openworkServerClient, + workspaceId: props.openworkServerWorkspaceId, + }); + + // Check prerequisites before attempting to open + if (!props.openworkServerClient) { + console.warn("[openMarkdownEditor] no openwork server client available"); + setToastMessage("Cannot open file: not connected to OpenWork server."); + return; + } + if (!props.openworkServerWorkspaceId) { + console.warn("[openMarkdownEditor] no workspace ID available"); + setToastMessage("Cannot open file: no workspace selected."); + return; + } + const relative = toWorkspaceRelativeForApi(file); if (!relative) { - setToastMessage("Only worker-relative files can be opened here."); + console.warn("[openMarkdownEditor] path resolution failed for:", file); + setToastMessage(`Cannot open file: path "${file}" is not within the workspace.`); return; } if (!/\.(md|mdx|markdown)$/i.test(relative)) { + console.warn("[openMarkdownEditor] not a markdown file:", relative); setToastMessage("Only markdown files can be edited here right now."); return; } + console.debug("[openMarkdownEditor] opening resolved path:", relative); setMarkdownEditorPath(relative); setMarkdownEditorOpen(true); }; diff --git a/packages/app/src/app/utils/index.ts b/packages/app/src/app/utils/index.ts index a53c9df0..98efd229 100644 --- a/packages/app/src/app/utils/index.ts +++ b/packages/app/src/app/utils/index.ts @@ -800,6 +800,44 @@ const ARTIFACT_PATH_PATTERN = const ARTIFACT_OUTPUT_SCAN_LIMIT = 4000; const ARTIFACT_OUTPUT_SKIP_TOOLS = new Set(["webfetch"]); +// Patterns that indicate a path is a truncated system/absolute path rather than a workspace-relative path +const TRUNCATED_SYSTEM_PATH_PATTERNS = [ + /com\.[^/]+\.(openwork|opencode)/i, // macOS app bundle identifiers + /\.openwork\.dev\//i, // OpenWork dev paths + /Application Support\//i, // macOS Application Support + /AppData[/\\]/i, // Windows AppData + /\.local\/share\//i, // Linux XDG data + /workspaces\/[^/]+\/workspaces\//i, // Nested workspaces paths (clearly malformed) +]; + +/** + * Clean up an artifact path to extract the workspace-relative portion. + * Returns null if the path should be rejected entirely. + */ +function cleanArtifactPath(rawPath: string): string | null { + const normalized = rawPath.trim().replace(/[\\/]+/g, "/"); + if (!normalized) return null; + + // Check if this looks like a truncated system path + for (const pattern of TRUNCATED_SYSTEM_PATH_PATTERNS) { + if (pattern.test(normalized)) { + // Try to extract just the relative part after "workspaces//" + const workspacesMatch = normalized.match(/workspaces\/[^/]+\/(.+)$/i); + if (workspacesMatch && workspacesMatch[1]) { + const relative = workspacesMatch[1]; + // Validate the extracted path doesn't still contain system patterns + if (!TRUNCATED_SYSTEM_PATH_PATTERNS.some((p) => p.test(relative))) { + return relative; + } + } + // Reject the path entirely if we can't extract a clean relative path + return null; + } + } + + return normalized; +} + type DeriveArtifactsOptions = { maxMessages?: number; }; @@ -906,19 +944,19 @@ export function deriveArtifacts(list: MessageWithParts[], options: DeriveArtifac if (matches.size === 0) return; matches.forEach((match) => { - const normalizedPath = match.trim().replace(/[\\/]+/g, "/"); - if (!normalizedPath) return; + const cleanedPath = cleanArtifactPath(match); + if (!cleanedPath) return; - const key = normalizedPath.toLowerCase(); - const name = normalizedPath.split("/").pop() ?? normalizedPath; - const id = `artifact-${encodeURIComponent(normalizedPath)}`; + const key = cleanedPath.toLowerCase(); + const name = cleanedPath.split("/").pop() ?? cleanedPath; + const id = `artifact-${encodeURIComponent(cleanedPath)}`; // Delete and re-add to move to end (most recent) if (results.has(key)) results.delete(key); results.set(key, { id, name, - path: normalizedPath, + path: cleanedPath, kind: "file" as const, size: state.size ? String(state.size) : undefined, messageId: messageId || undefined, From 307fafda914bb8748ee2a53df8e0aa0b37e01d91 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Sat, 21 Feb 2026 10:58:13 -0800 Subject: [PATCH 2/2] refactor(session): simplify artifact path resolution noise --- .../session/artifact-markdown-editor.tsx | 44 +--------- .../components/session/artifacts-panel.tsx | 6 -- packages/app/src/app/pages/session.tsx | 88 +++---------------- 3 files changed, 12 insertions(+), 126 deletions(-) diff --git a/packages/app/src/app/components/session/artifact-markdown-editor.tsx b/packages/app/src/app/components/session/artifact-markdown-editor.tsx index ca18a9cb..3f26426f 100644 --- a/packages/app/src/app/components/session/artifact-markdown-editor.tsx +++ b/packages/app/src/app/components/session/artifact-markdown-editor.tsx @@ -63,19 +63,14 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp const client = props.client; const workspaceId = props.workspaceId; - console.debug("[ArtifactMarkdownEditor] load requested:", { target, workspaceId, hasClient: !!client }); - if (!client || !workspaceId) { - console.warn("[ArtifactMarkdownEditor] load blocked: no client or workspaceId", { workspaceId, hasClient: !!client }); setError(writeDisabledReason()); return; } if (!target) { - console.warn("[ArtifactMarkdownEditor] load blocked: empty target path"); return; } if (!isMarkdown(target)) { - console.warn("[ArtifactMarkdownEditor] load blocked: not a markdown file:", target); setError("Only markdown files are supported."); return; } @@ -87,9 +82,7 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp let result: OpenworkWorkspaceFileContent; let actualPath = target; try { - console.debug("[ArtifactMarkdownEditor] fetching file:", { workspaceId, target }); result = (await client.readWorkspaceFile(workspaceId, target)) as OpenworkWorkspaceFileContent; - console.debug("[ArtifactMarkdownEditor] file loaded successfully:", target); } catch (err) { // Artifacts are frequently referenced as workspace-relative paths (e.g. `learned/foo.md`), // but on disk they may live under the OpenWork outbox dir: `.opencode/openwork/outbox/`. @@ -100,29 +93,14 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp err instanceof OpenworkServerError && err.status === 404; - console.debug("[ArtifactMarkdownEditor] primary fetch failed:", { - target, - error: err instanceof Error ? err.message : String(err), - status: err instanceof OpenworkServerError ? err.status : null, - willTryOutbox: shouldTryOutbox, - candidateOutbox, - }); - if (!shouldTryOutbox) { throw err; } actualPath = candidateOutbox; try { - console.debug("[ArtifactMarkdownEditor] trying outbox path:", actualPath); result = (await client.readWorkspaceFile(workspaceId, actualPath)) as OpenworkWorkspaceFileContent; - console.debug("[ArtifactMarkdownEditor] outbox file loaded successfully:", actualPath); } catch (second) { - console.warn("[ArtifactMarkdownEditor] outbox fetch also failed:", { - actualPath, - error: second instanceof Error ? second.message : String(second), - status: second instanceof OpenworkServerError ? second.status : null, - }); if (second instanceof OpenworkServerError && second.status === 404) { throw new OpenworkServerError(404, "file_not_found", "File not found (workspace root or outbox)."); } @@ -137,7 +115,6 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp setBaseUpdatedAt(typeof result.updatedAt === "number" ? result.updatedAt : null); } catch (err) { const message = err instanceof Error ? err.message : "Failed to load file"; - console.error("[ArtifactMarkdownEditor] load error:", { target, error: message, rawError: err }); setError(message); setLoadedPath(target); } finally { @@ -218,37 +195,20 @@ export default function ArtifactMarkdownEditor(props: ArtifactMarkdownEditorProp } const target = path(); - if (!target) { - console.debug("[ArtifactMarkdownEditor] effect: no target path, skipping"); - return; - } - if (loading()) { - console.debug("[ArtifactMarkdownEditor] effect: already loading, skipping"); - return; - } - if (pendingReason() === "switch") { - console.debug("[ArtifactMarkdownEditor] effect: pending switch, skipping"); - return; - } + if (!target || loading() || pendingReason() === "switch") return; const active = loadedPath(); if (!active) { - console.debug("[ArtifactMarkdownEditor] effect: no active path, triggering load for:", target); void load(target); return; } - if (target === active) { - console.debug("[ArtifactMarkdownEditor] effect: target matches active, no-op"); - return; - } + if (target === active) return; if (!dirty()) { - console.debug("[ArtifactMarkdownEditor] effect: different target, not dirty, loading:", target); void load(target); return; } - console.debug("[ArtifactMarkdownEditor] effect: different target with dirty content, prompting switch"); setPendingPath(target); setPendingReason("switch"); }); diff --git a/packages/app/src/app/components/session/artifacts-panel.tsx b/packages/app/src/app/components/session/artifacts-panel.tsx index 39e5e83c..259d977e 100644 --- a/packages/app/src/app/components/session/artifacts-panel.tsx +++ b/packages/app/src/app/components/session/artifacts-panel.tsx @@ -133,12 +133,6 @@ export default function ArtifactsPanel(props: ArtifactsPanelProps) { openable() ? "hover:bg-dls-active" : "cursor-default" }`} onClick={() => { - console.debug("[ArtifactsPanel] click:", { - path: artifact.path, - kind: artifact.kind, - hasOnOpenMarkdown: typeof props.onOpenMarkdown === "function", - hasOnOpenImage: typeof props.onOpenImage === "function", - }); if (md()) props.onOpenMarkdown?.(artifact.path); else if (img()) props.onOpenImage?.(artifact.path); }} diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 1d9b8892..b570be9e 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -709,143 +709,75 @@ export default function SessionView(props: SessionViewProps) { const toWorkspaceRelativeForApi = (file: string) => { const normalized = normalizeSidebarPath(file).replace(/^file:\/\//i, ""); - if (!normalized) { - console.debug("[toWorkspaceRelativeForApi] empty after normalization:", { original: file }); - return ""; - } + if (!normalized) return ""; const root = normalizeSidebarPath(props.activeWorkspaceRoot).replace(/\/+$/, ""); const rootKey = root.toLowerCase(); const fileKey = normalized.toLowerCase(); - console.debug("[toWorkspaceRelativeForApi] processing:", { file, normalized, root, rootKey, fileKey }); - - // Case 1: Full absolute path that starts with workspace root if (root && fileKey.startsWith(`${rootKey}/`)) { - const result = normalized.slice(root.length + 1); - console.debug("[toWorkspaceRelativeForApi] stripped workspace root, result:", result); - return result; + return normalized.slice(root.length + 1); } if (root && fileKey === rootKey) { - console.debug("[toWorkspaceRelativeForApi] path equals workspace root, returning empty"); return ""; } - // Case 2: Truncated absolute path - check if any suffix of the workspace root - // matches a prefix of the file path. This handles cases like: - // - root: "/Users/foo/Library/Application Support/com.differentai.openwork.dev/workspaces/starter" - // - file: "Support/com.differentai.openwork.dev/workspaces/starter/test.md" if (root) { const rootSegments = root.split("/").filter(Boolean); - const fileSegments = normalized.split("/"); - - // Try to find where the file path overlaps with the workspace root - for (let i = 1; i < rootSegments.length; i++) { - const rootSuffix = rootSegments.slice(i).join("/").toLowerCase(); - const rootSuffixWithSlash = `${rootSuffix}/`; - - if (fileKey.startsWith(rootSuffixWithSlash)) { - const result = normalized.slice(rootSuffix.length + 1); - console.debug("[toWorkspaceRelativeForApi] found overlapping suffix, stripped:", { - rootSuffix, - result, - }); - return result; - } - if (fileKey === rootSuffix) { - console.debug("[toWorkspaceRelativeForApi] file matches root suffix exactly"); - return ""; - } - } - - // Also check for workspace name/id match in file path segments - // e.g., path contains "workspaces/starter/" where "starter" is the workspace folder name const workspaceFolderName = rootSegments[rootSegments.length - 1]?.toLowerCase(); if (workspaceFolderName) { - const workspacesPattern = new RegExp(`workspaces/${workspaceFolderName}/`, "i"); - const match = normalized.match(workspacesPattern); - if (match && match.index !== undefined) { - const result = normalized.slice(match.index + match[0].length); - console.debug("[toWorkspaceRelativeForApi] found workspaces/name pattern, result:", result); - return result; - } + const workspaceMarker = `workspaces/${workspaceFolderName}/`; + const markerIndex = fileKey.indexOf(workspaceMarker); + if (markerIndex >= 0) return normalized.slice(markerIndex + workspaceMarker.length); + if (fileKey.endsWith(`workspaces/${workspaceFolderName}`)) return ""; } } let relative = normalized.replace(/^\.\/+/, ""); - if (!relative) { - console.debug("[toWorkspaceRelativeForApi] empty after stripping ./"); - return ""; - } + if (!relative) return ""; // Tool output paths sometimes carry git-style prefixes (a/ or b/). if (/^[ab]\/.+\.(md|mdx|markdown)$/i.test(relative)) { relative = relative.slice(2); - console.debug("[toWorkspaceRelativeForApi] stripped git prefix (a/ or b/):", relative); } // Some tool outputs include a leading "workspace/" prefix. if (/^workspace\//i.test(relative)) { relative = relative.replace(/^workspace\//i, ""); - console.debug("[toWorkspaceRelativeForApi] stripped workspace/ prefix:", relative); } // Other surfaces include an absolute-style "/workspace/" prefix. if (/^\/+workspace\//i.test(relative)) { relative = relative.replace(/^\/+workspace\//i, ""); - console.debug("[toWorkspaceRelativeForApi] stripped /workspace/ prefix:", relative); } - if (relative.startsWith("/") || relative.startsWith("~") || /^[a-zA-Z]:\//.test(relative)) { - console.debug("[toWorkspaceRelativeForApi] rejected absolute path:", relative); - return ""; - } - if (relative.split("/").some((part) => part === "." || part === "..")) { - console.debug("[toWorkspaceRelativeForApi] rejected path with . or .. segments:", relative); - return ""; - } + if (relative.startsWith("/") || relative.startsWith("~") || /^[a-zA-Z]:\//.test(relative)) return ""; + if (relative.split("/").some((part) => part === "." || part === "..")) return ""; - // Reject paths that look like truncated system paths (contain app bundle identifiers) - if (/com\.[^/]+\.(openwork|opencode)/i.test(relative)) { - console.debug("[toWorkspaceRelativeForApi] rejected truncated system path:", relative); - return ""; - } + if (/com\.[^/]+\.(openwork|opencode)/i.test(relative)) return ""; - console.debug("[toWorkspaceRelativeForApi] final result:", relative); return relative; }; const openMarkdownEditor = (file: string) => { - console.debug("[openMarkdownEditor] requested file:", file, { - workspaceRoot: props.activeWorkspaceRoot, - hasClient: !!props.openworkServerClient, - workspaceId: props.openworkServerWorkspaceId, - }); - - // Check prerequisites before attempting to open if (!props.openworkServerClient) { - console.warn("[openMarkdownEditor] no openwork server client available"); setToastMessage("Cannot open file: not connected to OpenWork server."); return; } if (!props.openworkServerWorkspaceId) { - console.warn("[openMarkdownEditor] no workspace ID available"); setToastMessage("Cannot open file: no workspace selected."); return; } const relative = toWorkspaceRelativeForApi(file); if (!relative) { - console.warn("[openMarkdownEditor] path resolution failed for:", file); setToastMessage(`Cannot open file: path "${file}" is not within the workspace.`); return; } if (!/\.(md|mdx|markdown)$/i.test(relative)) { - console.warn("[openMarkdownEditor] not a markdown file:", relative); setToastMessage("Only markdown files can be edited here right now."); return; } - console.debug("[openMarkdownEditor] opening resolved path:", relative); setMarkdownEditorPath(relative); setMarkdownEditorOpen(true); };