feat(md): Think/Research mode auto-save, hyperlinks & skill integration#166
feat(md): Think/Research mode auto-save, hyperlinks & skill integration#166
Conversation
Zhang-Henry
left a comment
There was a problem hiding this comment.
Code Review: PR #166
Security Issues
1. Path traversal via projectPath — server/routes/quick-qa.js:95
projectPath from the request body is passed directly as cwd to the query() SDK call with no validation. An attacker could set it to any directory on the filesystem:
const { selectedText, question, projectPath, mode = 'fast' } = req.body;
// ...
options: { cwd: projectPath || process.cwd(), ... }Should validate that projectPath is a descendant of the user's allowed project directories.
2. Race condition on process.env — server/routes/quick-qa.js:124-180
The code mutates process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT per-request and restores it in finally. If two concurrent requests arrive, they race on this global state — one request's finally can restore a stale value while the other is still in-flight.
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
// ... async work ...
finally {
// Another request may have already changed this
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
}3. Markdown injection in linkLabel — MarkdownSelectionPopup.jsx:1143-1151
The auto-generated linkLabel is inserted directly into markdown as [${linkLabel}](...). If the AI-generated content contains ] or ) characters, it can break the link syntax or inject arbitrary markdown.
const annotation = `\n> 📎 [${linkLabel}](${newFileName})\n`;Should sanitize linkLabel to escape [, ], (, ).
Bugs / Correctness
4. Stale closure in handleDeleteLink — CodeEditor.jsx:495-538
handleDeleteLink captures content in its closure. If the user deletes two links quickly, the second delete operates on the stale content from before the first delete completed, potentially overwriting the first change:
const handleDeleteLink = useCallback(async (href, linkText) => {
// 'content' is captured at closure creation time
let updatedContent = content.replace(annotationPattern, '\n');
// ...
setContent(updatedContent); // may overwrite the first delete's result
}, [file.projectName, file.path, resolvedPath, content]);Fix: Use a functional state updater setContent(prev => ...) and perform the regex replacements on prev instead of the captured content.
5. findNextSuffix doesn't check the filesystem — MarkdownSelectionPopup.jsx:803-815
Only scans the current markdown content for existing _XX.md patterns. If a _01.md file exists on disk but isn't linked from the current content, a collision occurs and the save silently overwrites the existing file.
function findNextSuffix(existingContent, baseName) {
const used = new Set();
// Only scans existingContent, not the filesystemConsider listing files in the directory via an API call, or at minimum doing a HEAD check before writing.
6. Non-atomic delete-then-save — CodeEditor.jsx:504-534
If api.deleteFile() succeeds but api.saveFile() fails, the linked .md file is deleted while the hyperlink still exists in the source document, leaving a broken link. Consider reversing the order (remove the link first, then delete the file) so the failure mode is a harmless orphan file rather than a broken link.
7. Auto-save effect doesn't handle unmount — MarkdownSelectionPopup.jsx:1087-1164
The async IIFE inside the effect calls onContentChange() and onSaveFile() without checking if the component is still mounted. If the user closes the popup during the save, these calls fire on stale/unmounted state. Add a mounted flag in the effect's cleanup.
8. findContainingBlock only handles tables — MarkdownSelectionPopup.jsx:734-795
Block-level matching only looks for table blocks (lines starting with |). Selecting text inside fenced code blocks, blockquotes, or list blocks won't produce an annotation — the hyperlink is silently dropped.
Design / Maintainability
9. 782-line popup component — MarkdownSelectionPopup.jsx
The file mixes utility functions (findMarkdownSpan, findContainingBlock, findNextSuffix), SSE streaming logic, and complex UI rendering in one component. Consider extracting:
- Text matching utilities to a separate module (also makes them unit-testable)
- SSE streaming to a custom hook (
useQuickQA)
10. buildMarkdownComponents re-render cascade — CodeEditor.jsx:398-425
buildMarkdownComponents is wrapped in useMemo, but onDeleteLink changes whenever content changes (it's in the dependency array of handleDeleteLink). This means every keystroke or content mutation recreates all markdown components, forcing ReactMarkdown to re-render the entire document tree.
11. Inline <style> tag — MarkdownSelectionPopup.jsx:627-641, line 1195
popupStyles is injected as a <style> element on every render of the popup. Should be a CSS module or a global stylesheet entry.
Minor Notes
server/routes/quick-qa.jsline 33 — Comment says "opus" for research mode butMODE_CONFIGactually usesmodel: 'sonnet'(line 71). Misleading.MdLinkWithTooltiptooltip dismiss timeout is 200ms — could feel sluggish or cause tooltip to persist unexpectedly.- The click-outside effect (line 916) doesn't include
handleClosein its dependency array — could reference a stalequeryIdwhen aborting.
Recommendation
Items 1–4 (path traversal, env race, stale closure, markdown injection) should be addressed before merge. The rest are lower priority but worth tracking.
The feature design is solid — auto-save _XX.md with hyperlink insertion, hover tooltips with delete confirmation, and overlay navigation are well-thought-out UX patterns.
- Validate projectPath against registered projects to prevent path traversal - Set CLAUDE_CODE_STREAM_CLOSE_TIMEOUT at module level to fix env race condition - Escape []() in linkLabel to prevent markdown injection - Use functional state updater in handleDeleteLink to fix stale closure - Check filesystem for existing _XX.md files to avoid suffix collisions - Reverse delete order: remove link first, then delete file (no broken links on failure) - Add mounted flag to auto-save effect to prevent updates after unmount - Extend findContainingBlock to support fenced code blocks and blockquotes - Fix click-outside effect missing handleClose in dependency array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code review — blocking issues before mergeI read through the PR branch ( 🛑 1. Runtime crash in
|
… skill integration - Auto-save Think/Research results as _XX.md files (01-99) in the same directory - Insert hyperlinks on selected text pointing to generated files - Smart markdown-aware text matching (handles bold, backticks, tables, CJK) - Block-level annotation for tables with best-match scoring across all table blocks - Hover tooltip on .md hyperlinks with Open and Delete actions - Delete removes both the hyperlink/annotation and the linked file - Preserve scroll position when navigating between linked files - Keep Think Mode popup state alive while viewing linked files - Allow clipboard copy (Cmd+C) of selected text before popup interaction - Deep Research mode now uses inno-deep-research skill automatically - Summary extraction for annotation labels (skips headings and labels) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Validate projectPath against registered projects to prevent path traversal - Set CLAUDE_CODE_STREAM_CLOSE_TIMEOUT at module level to fix env race condition - Escape []() in linkLabel to prevent markdown injection - Use functional state updater in handleDeleteLink to fix stale closure - Check filesystem for existing _XX.md files to avoid suffix collisions - Reverse delete order: remove link first, then delete file (no broken links on failure) - Add mounted flag to auto-save effect to prevent updates after unmount - Extend findContainingBlock to support fenced code blocks and blockquotes - Fix click-outside effect missing handleClose in dependency array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes "ReferenceError: Cannot access 'handleClose' before initialization" on mount. const declarations aren't hoisted, and the click-outside useEffect references handleClose in its dependency array, which is evaluated synchronously during the first render. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5e883ac to
1e595d6
Compare
Summary
_XX.mdfiles (01–99) in the same directory as the source file, with hyperlinks automatically inserted on the selected textinno-deep-researchskill automatically for structured, citation-rich reportsDetails
Auto-save & Hyperlink insertion
[text](file.md)— smart markdown-aware matching handles**bold**,`code`, CJK/punctuation boundaries> 📎 [summary](file.md)annotation after the block, with best-match scoring across all table blocksUX improvements
Deep Research skill
skills/inno-deep-research/SKILL.mdat server startupTest plan
_01.mdcreated and hyperlink insertedcodeformatting → verify hyperlink wraps correctly> 📎 [summary](file.md)appears after the correct table🤖 Generated with Claude Code