feat: Add session stage tags and auto-linking#78
Conversation
There was a problem hiding this comment.
Code Review
Overall this is a well-structured feature PR. The DB schema design is solid and the defensive coding (null checks, normalization, chunking) is thorough. Here are the specific comments:
1. applyStageTagsToSession is fire-and-forget (no await)
Files: server/claude-sdk.js, server/cursor-cli.js, server/gemini-cli.js, server/openai-codex.js
applyStageTagsToSession is called without await. While the function is currently synchronous (better-sqlite3 is sync), the function signature doesn't make this obvious. If it's ever made async, these calls will silently swallow errors. Consider:
- Adding a comment that it's intentionally sync/fire-and-forget
- Or wrapping in try/catch to be safe
2. Duplicate tag application — both at spawn start AND in recordIndexedSession
applyStageTagsToSession is called at the top of the spawn function, and then stageTagKeys/tagSource are also passed to recordIndexedSession, which calls applyStageTagsToSession again internally (server/utils/sessionIndex.js:86-95). Since appendSessionTagsByKeys uses INSERT OR IGNORE, there's no data corruption, but it's redundant work. The early call handles the case where the session already exists; the recordIndexedSession call handles new sessions. Worth adding a comment to explain this intentional dual-path.
3. SessionTag type has both snake_case and camelCase fields
File: src/types/app.ts:4-25
The SessionTag interface has both tag_key and tagKey, tag_type and tagType, etc. While this accommodates the server returning either format, it creates ambiguity in consumers. Throughout the codebase, every usage does tag.tagKey || tag.tag_key — this is fragile. Consider normalizing once at the API boundary (in parseTagRow on the server, which already does this) and only keeping the camelCase fields in the TypeScript type.
4. SidebarSessionItem.tsx — duplicated stage tag rendering block
File: src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx:132-156 and 205-226
The exact same stage tag rendering JSX is duplicated in two places (compact view and expanded view). Extract this into a small local component or variable to DRY it up.
5. Missing projectName validation in tag API endpoints
File: server/index.js:722-773
The GET /api/projects/:projectName/tags endpoint calls tagDb.ensureDefaultStageTags(projectName) on every request. This means any arbitrary projectName string will get default tags inserted. Consider checking that the project actually exists first, or at least documenting this as intentional (lazy initialization).
6. normalizeStageTagKeys has hardcoded alias mapping
File: server/utils/sessionIndex.js:23-26
if (value === 'presentation') return 'promotion';
if (value === 'research') return 'survey';These aliases are undocumented and could be surprising. If these come from task stage values, consider mapping them in the task normalization layer (normalizeTask in auto-research.js) instead, so the tag system stays generic.
7. hydrateSessionRowsWithTags uses chunked queries with hardcoded chunk size
File: server/database/db.js:259
chunkSize = 900 — this is a reasonable SQLite parameter limit workaround, but worth a comment explaining why 900 (SQLite's default SQLITE_MAX_VARIABLE_NUMBER is 999).
8. projectsHaveChanges now includes geminiSessions — was this missing before?
File: src/hooks/useProjectsState.ts:97-101
Adding geminiSessions to the change detection is correct, but it's a bugfix unrelated to the tag feature. Consider calling it out in the PR description or splitting it into a separate commit for clarity.
9. session-tags-updated CustomEvent — global event bus pattern
Files: src/components/ResearchLab.jsx and src/hooks/useProjectsState.ts
Using window.dispatchEvent(new CustomEvent(...)) as a cross-component communication channel works but is fragile — no type safety, no guarantees about listener lifecycle, and hard to trace in debugging. This is fine for now, but consider migrating to a shared context or state management solution if more events follow this pattern.
10. sessionTagsById useEffect may overwrite optimistic updates
File: src/components/ResearchLab.jsx:1314-1320
useEffect(() => {
const nextSessionTags = {};
projectSessions.forEach((session) => { ... });
setSessionTagsById(nextSessionTags);
}, [projectIdentity, projectSessions]);This resets sessionTagsById whenever projectSessions changes, which could overwrite optimistic updates from handleToggleSessionStageTag. If a user toggles a tag and then projectSessions re-renders (e.g., from a WebSocket update), their local state could be clobbered. Consider merging rather than replacing.
11. Minor: SESSION_TAG_SOURCE_META has unused inferred entry
File: src/components/ResearchLab.jsx:1065
The inferred source type is defined in the metadata map but no code path currently produces tags with source: 'inferred'. If this is for future use, fine — but it's dead code for now.
…omments - Fix parseTagRow to explicitly pick camelCase fields instead of spreading raw row (eliminates snake_case field leakage) - Clean SessionTag type to camelCase-only, remove all tagKey||tag_key fallback patterns across 3 frontend files and 1 server file - Extract duplicated stage tag JSX in SidebarSessionItem into shared variable - Move stage alias mapping (presentation→promotion, research→survey) from normalizeStageTagKeys to normalizeTask in auto-research.js - Fix sessionTagsById useEffect to merge instead of replace, preserving optimistic updates from tag toggling - Add explanatory comments: sync fire-and-forget calls, dual-path tag application, lazy init, SQLite chunk size, CustomEvent TODO, reserved inferred source Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Review fixes pushed: ChangesCore (Item 3): Code quality:
Comments (Items 1, 2, 5, 7, 9, 11): Added explanatory comments for sync fire-and-forget calls, dual-path tag application, lazy init, SQLite chunk size, CustomEvent TODO, and reserved Verification: |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Testing