Dev#3566
Conversation
…with all student domain schemas - Created StudentId branded entity ID - Created PhoneNumber schema (country + number) - Created SingaporeAddress schema with optional fields - Created Parent schema with optional fields - Created Student schema with all required and optional fields - Created StudentRegistryDocument schema (version + students array) - Exported all schemas and inferred types - Added export to index.ts Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added studentRegistryPath: string to DesktopEnvironmentShape interface and set its value to path.join(stateDir, 'students.json') in makeDesktopEnvironment, following the same pattern as savedEnvironmentRegistryPath. Note: Desktop typecheck currently fails due to phase dependencies (Phase 1 extended DesktopBridge interface, Phase 3 will implement the methods). This subtask's changes are complete and correct. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… persistence - Created apps/desktop/src/settings/DesktopStudents.ts following DesktopSavedEnvironments pattern - Defined DesktopStudentsWriteError for error handling - Defined DesktopStudentsShape with getRegistry and setRegistry methods - Implemented StudentRegistryDocumentSchema using fromLenientJson - Implemented readRegistryDocument with graceful fallback to empty array on corrupt/missing file - Implemented atomic writeRegistryDocument with temp+rename for safe writes - Exported production layer using FileSystem + DesktopEnvironment.studentRegistryPath - Exported layerTest for in-memory testing - Stripped all encryption/SafeStorage code (students have no secrets) - Added DesktopStudents.layer to desktopFoundationLayer in main.ts Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…egistry - Added GET_STUDENTS_CHANNEL and SET_STUDENTS_CHANNEL to channels.ts - Created students.ts with getStudents and setStudents IPC methods - Added getStudents and setStudents to preload bridge implementation - Methods follow makeIpcMethod pattern with proper Effect.fn naming - Handlers yield* DesktopStudents service and call getRegistry/setRegistry Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…top/src/ Imported getStudents and setStudents from ./methods/students.ts and registered them as IPC handlers in DesktopIpcHandlers.ts. The preload.ts file already had the necessary desktopBridge methods exposed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…sistence section - Add STUDENT_REGISTRY_STORAGE_KEY constant - Import Student schema from contracts - Add BrowserStudentRegistryDocumentSchema with optional fields - Implement readBrowserStudents() returning readonly Student[] - Implement writeBrowserStudents() for localStorage persistence - Add getStudents/setStudents to LocalApi persistence section - Follow existing pattern: check window.desktopBridge first, fall back to browser storage - Update all test mocks to include new persistence methods Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created apps/web/src/routes/students.tsx with: - Auth gate using beforeLoad (checks authGateState.status) - Two-pane split layout (left: student list, right: detail/form) - SidebarInset for consistent chrome with header - Student roster loading via localApi.persistence.getStudents() - 'New Student' button in header - Empty state for when no students exist - Basic student list rendering (alphabetical) - Welcome message in right pane when no student selected The route tree was auto-generated by TanStack Router plugin. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…udentList.tsx Implemented StudentList component with: - Alphabetically sorted student list by name - Clickable rows with active student highlighting - Empty state with 'No students yet' message and 'Add your first student' button - Props: students array, selectedStudentId, onSelectStudent, onNewStudent Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… apps/we Created three field sub-components for the student form: 1. PhoneField.tsx - Country dropdown (SG, MY, CN) with phone number input. Default to Singapore (+65). Accepts value/onChange props for controlled input. 2. ParentRows.tsx - Dynamic add/remove parent rows. Each row has name, relationship, and optional phone (PhoneField). Add/remove buttons provided. Accepts parents array + onChange callback. 3. AddressFields.tsx - Singapore address fields (block, street, building, unit, postalCode). All fields optional, but if any field has value, postalCode must be 6 digits. Shows inline validation error. All components follow existing code patterns from ProviderSettingsForm.tsx and use shadcn/ui components (Input, Select, Button, Label). TypeScript compiles without errors with exactOptionalPropertyTypes enabled. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…udentForm.tsx Implemented StudentForm component with: - Create/edit mode support - Name field (required, validated for non-empty trimmed value) - Phone field using PhoneField component - Subjects field (comma-separated input) - School field (optional text) - Address fields using AddressFields component with postal code validation - Parent rows using ParentRows component (empty rows auto-dropped on save) - Notes field (textarea) - Proper UUID generation via StudentId.make(randomUUID()) - Form validation for name and postal code - Tailwind CSS styling consistent with existing forms Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…nks.ts with deep-link helpers Implemented three deep-link helper functions: - whatsAppLink: builds WhatsApp URL from PhoneNumber - telegramLink: builds Telegram URL from PhoneNumber - googleMapsLink: builds Google Maps search URL from SingaporeAddress All functions handle edge cases by returning empty string for invalid inputs. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implemented StudentDetail.tsx with: - Read-only view of all student fields in structured layout - Edit and Delete action buttons with confirmation dialog - Deep-link buttons for WhatsApp/Telegram (student + parent phones) - Google Maps button for address - All links open via localApi.shell.openExternal() - Proper field display with icons and formatting - Metadata section showing created/updated timestamps Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…t.ts with @effect/vitest tests for Student schema - StudentId branded type tests (enforce non-empty, trim whitespace) - PhoneNumber schema tests (all fields, trimming, validation) - SingaporeAddress schema tests (all/partial fields, empty object) - Parent schema tests (with/without phone, optional fields) - Student schema tests (all fields, required only, validation) - StudentRegistryDocument tests (populated/empty arrays, version validation) All tests passing using Schema.decodeUnknownEffect pattern.
…udents.test.ts - Created comprehensive test suite following DesktopSavedEnvironments.test.ts pattern - Implemented makeLayer helper with temp directory + DesktopEnvironment.layer + NodeServices.layer - Implemented withStudents helper wrapping Effect.gen with temp directory scope - Tests cover: persist/reload round-trip, missing file, empty JSON, malformed JSON, atomic write - All tests pass Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… (qa-requested)
Fixes WhatsApp/Telegram deep link generation and phone display issues.
Changes:
- PhoneField.tsx: Store country codes ("SG"/"MY"/"CN") instead of dial codes ("+65"/"+60"/"+86")
- students.ts: Tighten PhoneNumber.country schema to Schema.Union of literals ("SG", "MY", "CN")
- StudentDetail.tsx: Fix phone display mapping (was showing "++65", now shows "+65")
- StudentForm.tsx: Update type annotations to use CountryCode type
- DesktopStudents.ts: Update PhoneNumberSchema to use CountryCode
- DesktopStudents.test.ts: Update test data to use "SG" instead of "+65"
- students.test.ts: Update whitespace trimming test (literals don't trim)
Verified:
- All tests pass (contracts: 185, desktop: 164, web: 1154)
- Full typecheck passes across all packages
- WhatsApp/Telegram links now generate correctly
- Phone display shows correct dial code
QA Fix Session: 1
… helper - Created packages/contracts/src/students.ts with: - StudentId branded type following baseSchemas pattern - StudentSchema with id, name, subjects, school, and optional workspaceFolder - deriveStudentSlug() helper function for URL-safe slug generation - Updated packages/contracts/src/index.ts to export students module - Follows patterns from settings.ts for schema definitions - Verification passes with no type errors Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ktopBrid Added renderMarkdownToPdf, openPath, and ensureStudentWorkspace methods to DesktopBridge interface with corresponding Schema definitions for input/output types. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Brings the Plan 23 materials-workspace + PDF-render work onto dev alongside the student roster feature so the full flow can be tested end-to-end. Conflict resolutions: - packages/contracts/src/students.ts: kept dev's full Student schema (phone, parents, address, notes, registry doc); layered on Plan 23's optional workspaceFolder field + deriveStudentSlug helper. Dropped 002's partial standalone StudentSchema (unused by contracts consumers). - packages/contracts/src/ipc.ts: import both Student and StudentId. - apps/web/src/components/students/StudentDetail.tsx: kept dev's full detail UI + onEdit/onDelete props (the route's caller); grafted in 002's [Generate materials] button + ensureStudentWorkspace handler. - .gitignore: kept dev's block ignoring auto-claude tooling files. - .claude_settings.json / .auto-claude-status / .auto-claude-security.json: untracked (now gitignored); local .claude_settings.json preserved on disk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| phone: Schema.optionalKey(PhoneNumberSchema), | ||
| }); | ||
|
|
||
| const StudentSchema = Schema.Struct({ |
There was a problem hiding this comment.
🟠 High settings/DesktopStudents.ts:41
StudentSchema does not include the optional workspaceFolder field that exists on the Student contract. When setRegistry encodes a Student object that has workspaceFolder set, encodeStudentRegistryDocumentJson silently drops that field from the persisted JSON, and the next getRegistry read loses the saved workspace path for every student. Consider adding the missing workspaceFolder field to StudentSchema so it round-trips through encode/decode.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/settings/DesktopStudents.ts around line 41:
`StudentSchema` does not include the optional `workspaceFolder` field that exists on the `Student` contract. When `setRegistry` encodes a `Student` object that has `workspaceFolder` set, `encodeStudentRegistryDocumentJson` silently drops that field from the persisted JSON, and the next `getRegistry` read loses the saved workspace path for every student. Consider adding the missing `workspaceFolder` field to `StudentSchema` so it round-trips through encode/decode.
| data: { dismissAfterVisibleMs: 3000 }, | ||
| }); | ||
|
|
||
| await handleNewThread(defaultProjectRef, { |
There was a problem hiding this comment.
🟠 High students/StudentDetail.tsx:87
handleGenerateMaterials passes defaultProjectRef to handleNewThread, but defaultProjectRef comes from useHandleNewThread() which selects the first project across all environments via selectProjectsAcrossEnvironments. When a remote environment's project is ordered first, the thread is created in that remote environment while worktreePath points to a local desktop-only folder from window.desktopBridge.ensureStudentWorkspace. The remote runtime is then asked to use a path that does not exist on it, so the materials session cannot run. The project should be selected from the local desktop environment specifically, not whichever environment happens to be first.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/students/StudentDetail.tsx around line 87:
`handleGenerateMaterials` passes `defaultProjectRef` to `handleNewThread`, but `defaultProjectRef` comes from `useHandleNewThread()` which selects the first project across *all* environments via `selectProjectsAcrossEnvironments`. When a remote environment's project is ordered first, the thread is created in that remote environment while `worktreePath` points to a local desktop-only folder from `window.desktopBridge.ensureStudentWorkspace`. The remote runtime is then asked to use a path that does not exist on it, so the materials session cannot run. The project should be selected from the local desktop environment specifically, not whichever environment happens to be first.
| }, | ||
| }); | ||
|
|
||
| onSuccess?.(pdfPath); |
There was a problem hiding this comment.
🟡 Medium pdf/RenderPdfButton.tsx:93
onSuccess?.(pdfPath) is called inside the same try block as renderPdf. If the caller's onSuccess callback throws after the PDF was already created, execution falls into the catch block, which logs Failed to render PDF, shows an error toast, and invokes onError — even though rendering succeeded. Wrap the callback in its own try/catch so its errors aren't misattributed as render failures.
| onSuccess?.(pdfPath); | |
| try { | |
| onSuccess?.(pdfPath); | |
| } catch (callbackError) { | |
| console.error("onSuccess callback threw:", callbackError); | |
| } |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/pdf/RenderPdfButton.tsx around line 93:
`onSuccess?.(pdfPath)` is called inside the same `try` block as `renderPdf`. If the caller's `onSuccess` callback throws after the PDF was already created, execution falls into the `catch` block, which logs `Failed to render PDF`, shows an error toast, and invokes `onError` — even though rendering succeeded. Wrap the callback in its own try/catch so its errors aren't misattributed as render failures.
| const failLoadHandler = ( | ||
| _event: Electron.Event, | ||
| errorCode: number, | ||
| errorDescription: string, | ||
| ) => { | ||
| void Effect.runSync( | ||
| Deferred.fail( | ||
| didFinishLoadDeferred, | ||
| new DesktopPdfRendererError({ | ||
| cause: `Failed to load HTML: ${errorCode} ${errorDescription}`, | ||
| }), | ||
| ), | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🟡 Medium pdf/DesktopPdfRenderer.ts:76
The did-fail-load handler fails the didFinishLoadDeferred for every load failure, but Electron fires this event for subframe and subresource loads too. Since the handler ignores the isMainFrame argument, a broken image or iframe in the HTML rejects the deferred and aborts PDF generation even though the main document loaded successfully and could be printed. Consider checking isMainFrame before failing the deferred.
| const failLoadHandler = ( | |
| _event: Electron.Event, | |
| errorCode: number, | |
| errorDescription: string, | |
| ) => { | |
| void Effect.runSync( | |
| Deferred.fail( | |
| didFinishLoadDeferred, | |
| new DesktopPdfRendererError({ | |
| cause: `Failed to load HTML: ${errorCode} ${errorDescription}`, | |
| }), | |
| ), | |
| ); | |
| }; | |
| const failLoadHandler = ( | |
| _event: Electron.Event, | |
| errorCode: number, | |
| errorDescription: string, | |
| _validatedURL: string, | |
| isMainFrame: boolean, | |
| ) => { | |
| if (!isMainFrame) return; | |
| void Effect.runSync( | |
| Deferred.fail( | |
| didFinishLoadDeferred, | |
| new DesktopPdfRendererError({ | |
| cause: `Failed to load HTML: ${errorCode} ${errorDescription}`, | |
| }), | |
| ), | |
| ); | |
| }; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/pdf/DesktopPdfRenderer.ts around lines 76-89:
The `did-fail-load` handler fails the `didFinishLoadDeferred` for every load failure, but Electron fires this event for subframe and subresource loads too. Since the handler ignores the `isMainFrame` argument, a broken image or iframe in the HTML rejects the deferred and aborts PDF generation even though the main document loaded successfully and could be printed. Consider checking `isMainFrame` before failing the deferred.
| } | ||
|
|
||
| export function StudentForm({ mode, initialStudent, onSave, onCancel }: StudentFormProps) { | ||
| const [formState, setFormState] = useState<FormState>(() => ({ |
There was a problem hiding this comment.
🟠 High students/StudentForm.tsx:177
The useState initializer at line 177 only runs on mount, so when initialStudent changes while StudentForm stays mounted, the form fields keep the previous student's data. On submit, handleSubmit builds a Student using initialStudent?.id and initialStudent?.createdAt (the new student's identity) but with the stale field values from the old student, overwriting the wrong record. Consider adding a key prop based on the student ID to the StudentForm in the parent so it remounts with fresh state, or syncing formState via a useEffect when initialStudent changes.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/students/StudentForm.tsx around line 177:
The `useState` initializer at line 177 only runs on mount, so when `initialStudent` changes while `StudentForm` stays mounted, the form fields keep the previous student's data. On submit, `handleSubmit` builds a `Student` using `initialStudent?.id` and `initialStudent?.createdAt` (the new student's identity) but with the stale field values from the old student, overwriting the wrong record. Consider adding a `key` prop based on the student ID to the `StudentForm` in the parent so it remounts with fresh state, or syncing `formState` via a `useEffect` when `initialStudent` changes.
| DesktopWorkspaceShape | ||
| >()("@t3tools/desktop/workspace/DesktopWorkspace") {} | ||
|
|
||
| const ensureStudentWorkspaceImpl = Effect.fnUntraced(function* (input: { |
There was a problem hiding this comment.
🔴 Critical workspace/DesktopWorkspace.ts:32
ensureStudentWorkspaceImpl builds folderPath by joining workspaceRoot, "students", and slug without validating that slug is a safe directory name. A value like ../outside or an absolute path escapes the students directory, so makeDirectory and writeFileString create directories and files outside the intended workspace tree. Consider rejecting slug values that contain path separators or .. segments before using them in path operations.
Also found in 1 other location(s)
apps/desktop/src/ipc/methods/workspace.ts:24
ensureStudentWorkspaceusesinput.studentIddirectly as the filesystemslug, butStudentIdonly guarantees a trimmed non-empty string. A value like"../other-dir"or"nested/path"will makeDesktopWorkspace.ensureStudentWorkspacecreate folders outside the intendedstudentsdirectory because it doespath.join(workspaceRoot, "students", slug). This exposes arbitrary directory creation/traversal via the IPC method.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/workspace/DesktopWorkspace.ts around line 32:
`ensureStudentWorkspaceImpl` builds `folderPath` by joining `workspaceRoot`, `"students"`, and `slug` without validating that `slug` is a safe directory name. A value like `../outside` or an absolute path escapes the `students` directory, so `makeDirectory` and `writeFileString` create directories and files outside the intended workspace tree. Consider rejecting `slug` values that contain path separators or `..` segments before using them in path operations.
Also found in 1 other location(s):
- apps/desktop/src/ipc/methods/workspace.ts:24 -- `ensureStudentWorkspace` uses `input.studentId` directly as the filesystem `slug`, but `StudentId` only guarantees a trimmed non-empty string. A value like `"../other-dir"` or `"nested/path"` will make `DesktopWorkspace.ensureStudentWorkspace` create folders outside the intended `students` directory because it does `path.join(workspaceRoot, "students", slug)`. This exposes arbitrary directory creation/traversal via the IPC method.
| return ""; | ||
| } | ||
|
|
||
| return `${dialCode}${cleanNumber}`; |
There was a problem hiding this comment.
🟡 Medium students/links.ts:33
toE164 returns ${dialCode}${cleanNumber} without stripping a leading national prefix 0 from cleanNumber. When a user enters a domestic number like 0123456789, this produces 600123456789 instead of the valid E.164 form 60123456789, which breaks the WhatsApp and Telegram deep links. Consider removing a leading 0 from cleanNumber before concatenating with the dial code.
| return `${dialCode}${cleanNumber}`; | |
| const normalized = cleanNumber.replace(/^0+/, ""); | |
| return `${dialCode}${normalized}`; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/students/links.ts around line 33:
`toE164` returns `${dialCode}${cleanNumber}` without stripping a leading national prefix `0` from `cleanNumber`. When a user enters a domestic number like `0123456789`, this produces `600123456789` instead of the valid E.164 form `60123456789`, which breaks the WhatsApp and Telegram deep links. Consider removing a leading `0` from `cleanNumber` before concatenating with the dial code.
| await handleNewThread(defaultProjectRef, { | ||
| worktreePath: result.workspacePath, | ||
| }); |
There was a problem hiding this comment.
🟡 Medium students/StudentDetail.tsx:87
handleGenerateMaterials passes worktreePath to handleNewThread but omits envMode, which defaults to "local". The resulting draft thread is tagged as a local session even though it points at a dedicated worktree workspace, so components like GitActionsControl that branch on activeDraftThread.envMode skip worktree-specific behavior. Add envMode: "worktree" to the options so the thread mode matches the workspace.
| await handleNewThread(defaultProjectRef, { | |
| worktreePath: result.workspacePath, | |
| }); | |
| await handleNewThread(defaultProjectRef, { | |
| worktreePath: result.workspacePath, | |
| envMode: "worktree", | |
| }); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/students/StudentDetail.tsx around lines 87-89:
`handleGenerateMaterials` passes `worktreePath` to `handleNewThread` but omits `envMode`, which defaults to `"local"`. The resulting draft thread is tagged as a local session even though it points at a dedicated worktree workspace, so components like `GitActionsControl` that branch on `activeDraftThread.envMode` skip worktree-specific behavior. Add `envMode: "worktree"` to the options so the thread mode matches the workspace.
| // Construct full HTML5 document | ||
| return `<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> |
There was a problem hiding this comment.
🟡 Medium pdf/markdownToHtml.ts:36
markdownToHtml emits a full HTML document without a <base> tag or any rebasing of relative src/href URLs. When the desktop renderer loads this document via a data: URL, relative paths like ./diagram.png or ./appendix.md resolve against the data URL itself, so all relative links and images in the exported PDF are broken. Consider accepting a base URL parameter and emitting a <base href="..."> tag in the document <head> so relative paths resolve correctly.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/pdf/markdownToHtml.ts around line 36:
`markdownToHtml` emits a full HTML document without a `<base>` tag or any rebasing of relative `src`/`href` URLs. When the desktop renderer loads this document via a `data:` URL, relative paths like `./diagram.png` or `./appendix.md` resolve against the data URL itself, so all relative links and images in the exported PDF are broken. Consider accepting a base URL parameter and emitting a `<base href="...">` tag in the document `<head>` so relative paths resolve correctly.
| // Update existing student | ||
| const updatedStudents = students.map((s) => (s.id === student.id ? student : s)); | ||
| await persistStudents(updatedStudents); | ||
| setRightPaneView("detail"); |
There was a problem hiding this comment.
Silent student persist failures
High Severity
The persistStudents function silently catches errors, allowing handleSave and handleDelete to update the UI and in-memory state as if persistence succeeded. This desynchronizes the UI from actual persisted data, leading to incorrect displays (e.g., unsaved new students, stale edits, or reappearing deleted students) without user feedback.
Reviewed by Cursor Bugbot for commit e9add25. Configure here.
| return ""; | ||
| } | ||
| return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(formattedAddress)}`; | ||
| } |
There was a problem hiding this comment.
Maps link ignores postal rule
Medium Severity
The googleMapsLink function generates a Google Maps URL for addresses that may be incomplete, as it doesn't require a postalCode. It also includes building and unit in the search query. This can lead to the "Open in Google Maps" button appearing for addresses that are too vague for a useful map search.
Reviewed by Cursor Bugbot for commit e9add25. Configure here.
| const slug = input.studentId; | ||
|
|
||
| return yield* workspace.ensureStudentWorkspace({ slug }).pipe( | ||
| Effect.match({ |
There was a problem hiding this comment.
Workspace never seeds AGENTS.md
High Severity
The ensureStudentWorkspace IPC handler, used by the "Generate materials" feature, doesn't pass the agentsMarkdown content to the workspace service. This results in newly created student workspaces lacking the AGENTS.md harness file.
Reviewed by Cursor Bugbot for commit e9add25. Configure here.
| onSome: (raw) => | ||
| decodeStudentRegistryDocumentJson(raw).pipe( | ||
| Effect.map(normalizeStudentRegistryDocument), | ||
| Effect.orElseSucceed(() => ({ version: "1", students: [] })), |
There was a problem hiding this comment.
Invalid phone country wipes roster
High Severity
If students.json contains a legacy or invalid phone country (for example dial code +65 instead of SG), document decode fails and the reader substitutes an empty student list, hiding every stored student until the file is repaired manually.
Reviewed by Cursor Bugbot for commit e9add25. Configure here.
ApprovabilityVerdict: Needs human review 16 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
Self-contained plan set in .plans/atlas/ (overview + 6 feature docs: branding, render+fonts, unified workspace, students-by-chat, knowledge-base builder, worksheet generator) built on the file-seam decoupling model. Phase-0 docs (branding, render+fonts) carry concrete buildable specs plus manual-test steps. Adds brand source SVGs in assets/atlas/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the audited mechanism (auto-bootstrap already exists server-side; cwd resolves via worktreePath ?? workspaceRoot), exact file:line change set (backendCwd + T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, DesktopWorkspace root alignment, slug sanitization, picker surfaces to neutralize), a load-bearing 'do not remove' list, and a manual test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bedding utility - Import woff2 files from @fontsource-variable packages using Vite ?inline suffix - Export getFontFaces() function returning @font-face CSS with base64 data URIs - Include DM Sans Variable (weight 100-900) and Source Serif 4 Variable (weight 200-900) - Use latin-standard-normal woff2 variants - Keep local() system font fallbacks as last src entry after base64 data URI Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…t the entire CSS content from getPrintCss() inline string Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ktop user-facing strings - Updated productName in package.json - Updated APP_BASE_NAME constant in DesktopEnvironment.ts - Updated error dialog message in DesktopApp.ts - Updated update check message in DesktopApplicationMenu.ts - Updated SSH error message in DesktopSshPasswordPrompts.ts - Preserved legacy directory names as specified Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…— replace inline functions with imports from fonts.ts and print.css - Import getFontFaces from ./fonts.ts - Import printCss from ./print.css?raw - Remove inline getPrintCss() function body - Remove inline getFontFaces() function body - markdownToHtml() function remains unchanged, now uses imported modules Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… source Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tsx — imported RenderPdfButton, derived outputPath from cwd + relativePath, conditionally rendered button in subheader after markdown toggle Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…pect 'TutorAtlas' Replace all 'T3 Code' references with 'TutorAtlas' in test mocks and assertions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…(1) resolveDesktopProductName product names to TutorAtlas, (2) appId to com.tutoratlas.app, (3) artifactName to TutorAtlas-${version}-${arch}.${ext}
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
….ts product name assertions Updated product name assertions to match the new branding: - Regular builds: 'T3 Code (Alpha)' → 'TutorAtlas (Alpha)' - Nightly builds: 'T3 Code (Nightly)' → 'TutorAtlas (Nightly)' All tests passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…complete icon generator script Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…age.json Add 'gen:brand-assets' script that runs node scripts/gen-brand-assets.mjs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…F render Added comprehensive verification test suite covering: - Font embedding with data URIs (26 tests total) - Print CSS rules (@page, A4, margins, font-families) - HTML structure and component importability - Data URI embedding (no external references) Fixed print.css import issue: - Vite ?raw imports don't work for .css files in test environment - Created printCss.ts exporting CSS as string constant - Updated markdownToHtml.ts to import from printCss.ts All verification requirements met: ✓ @font-face rules with embedded fonts ✓ Complete print CSS (page size, fonts, table styling) ✓ Exact font-family names (Source Serif 4 Variable, DM Sans Variable) ✓ Weight ranges preserved (200-900, 100-900) ✓ RenderPdfButton importable, component tree compiles Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…remaining branding references This commit completes the integration verification phase and fixes 40+ user-facing "T3 Code" references that were missed in previous phases. Verification Results: - ✅ Web tests: 118 test files, 1196 tests passed - ✅ Scripts tests: 11 test files, 76 tests passed - ✅ Brand-assets test: passed (filenames unchanged) - ✅ Generator determinism: verified (clean git diff after 2 runs) - ✅ appId: com.tutoratlas.app - ✅ productName: TutorAtlas (Alpha) - ✅ Grep audit: passed (only acceptable references remain) Fixed User-Facing References (23 files): - Error messages and dialogs (linkEnvironment, RelayClientInstallDialog) - Settings UI (KeybindingsSettings, SettingsPanels, ConnectionsSettings, providerStatus) - Update dialogs and Rosetta warnings (desktopUpdate.logic) - Authentication (DesktopClerkSignIn) - SSH prompts (SshPasswordPromptDialog) - Preview and browser messages (PreviewPanel, RightPanelTabs, _chat) - Version mismatch hints (versionSkew) - Release announcements (notify-discord-release, resolve-nightly-release) - CLI help text (dev-runner) - Test assertions updated to match changes Preserved (Out of Scope): - Legacy directory name in DesktopEnvironment.ts:166 - Protocol name in build-desktop-artifact.ts:779 - Internal build descriptions All user-facing "T3 Code" strings have been replaced with "TutorAtlas" except those explicitly marked as out of scope in the specification. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The sidebar header wordmark still rendered "T3 Code DEV": a T3Wordmark SVG
glyph (drawing "T3") plus a hardcoded "Code" literal plus the stage badge.
Because the brand mark was split across an SVG + a separate text node, the
contiguous string "T3 Code" never existed in source, so the branding-swap
grep could not catch it.
Replace the SVG + "Code" with a single APP_BASE_NAME-driven wordmark
("TutorAtlas") and delete the now-dead T3Wordmark component. The stage
badge (DEV/ALPHA) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing-documentation-updates Auto claude/006 implement branding documentation updates
| - Internal build descriptions | ||
|
|
||
| All user-facing "T3 Code" strings have been replaced with "TutorAtlas" except | ||
| those explicitly marked as out of scope in the specification. |
There was a problem hiding this comment.
Agent commit log committed
Low Severity
A commit-message.txt file, an auto-claude verification log, was added to the repository root. This isn't application source or user documentation, and its presence in the tree could confuse contributors or impact release tooling.
Reviewed by Cursor Bugbot for commit 5059eb2. Configure here.
| T3 needs your SSH password to connect to{" "} | ||
| {target ? <code>{target}</code> : "the remote host"}. The password is passed to the | ||
| local SSH process for this connection attempt and is not saved by T3 Code. | ||
| local SSH process for this connection attempt and is not saved by TutorAtlas. |
There was a problem hiding this comment.
SSH prompt mixed product names
Low Severity
The SSH password dialog displays inconsistent product naming. The dialog title and description refer to "T3 needs your SSH password", but then state credentials are not saved by "TutorAtlas", which is confusing for users.
Reviewed by Cursor Bugbot for commit 5059eb2. Configure here.
| import { createRequire } from "node:module"; | ||
| const require = createRequire(import.meta.url); | ||
| const sharp = require(`${repoRoot}/node_modules/.pnpm/sharp@0.34.5/node_modules/sharp`); | ||
| ``` |
There was a problem hiding this comment.
🟡 Medium atlas/01-branding.md:79
The generator instructions hard-code the pnpm virtual-store path node_modules/.pnpm/sharp@0.34.5/node_modules/sharp. pnpm's .pnpm directory names are not stable — they change when sharp is upgraded and can include peer-resolution suffixes. Following this plan as written makes node scripts/gen-brand-assets.mjs throw MODULE_NOT_FOUND as soon as the lockfile or pnpm layout changes, breaking the promised re-runnable asset generation. Consider resolving sharp dynamically (e.g. globbing node_modules/.pnpm/sharp@*) or adding it as a proper devDependency instead.
const require = createRequire(import.meta.url);
- const sharp = require(`${repoRoot}/node_modules/.pnpm/sharp@0.34.5/node_modules/sharp`);
+ const sharpDir = fs.readdirSync(`${repoRoot}/node_modules/.pnpm`).find(d => d.startsWith("sharp@"));
+ const sharp = require(`${repoRoot}/node_modules/.pnpm/${sharpDir}/node_modules/sharp`);🤖 Copy this AI Prompt to have your agent fix this:
In file @.plans/atlas/01-branding.md around lines 79-82:
The generator instructions hard-code the pnpm virtual-store path `node_modules/.pnpm/sharp@0.34.5/node_modules/sharp`. pnpm's `.pnpm` directory names are not stable — they change when `sharp` is upgraded and can include peer-resolution suffixes. Following this plan as written makes `node scripts/gen-brand-assets.mjs` throw `MODULE_NOT_FOUND` as soon as the lockfile or pnpm layout changes, breaking the promised re-runnable asset generation. Consider resolving `sharp` dynamically (e.g. globbing `node_modules/.pnpm/sharp@*`) or adding it as a proper devDependency instead.
| ) { | ||
| const buildConfig: Record<string, unknown> = { | ||
| appId: "com.t3tools.t3code", | ||
| appId: "com.tutoratlas.app", |
There was a problem hiding this comment.
🟠 High scripts/build-desktop-artifact.ts:751
The build config now sets appId: "com.tutoratlas.app", but the runtime still hard-codes the old AppUserModelID com.t3tools.t3code in apps/desktop/src/app/DesktopEnvironment.ts and DesktopAppIdentity.ts. On Windows, Electron requires the installer-registered AppUserModelID to match the one the running app sets via setAppUserModelId. This mismatch breaks toast notifications and shell integration on packaged Windows builds because the app starts under a different ID than the installer registered. Update the runtime AppUserModelID in those files to match com.tutoratlas.app, or make both read from a single shared constant.
🤖 Copy this AI Prompt to have your agent fix this:
In file @scripts/build-desktop-artifact.ts around line 751:
The build config now sets `appId: "com.tutoratlas.app"`, but the runtime still hard-codes the old AppUserModelID `com.t3tools.t3code` in `apps/desktop/src/app/DesktopEnvironment.ts` and `DesktopAppIdentity.ts`. On Windows, Electron requires the installer-registered AppUserModelID to match the one the running app sets via `setAppUserModelId`. This mismatch breaks toast notifications and shell integration on packaged Windows builds because the app starts under a different ID than the installer registered. Update the runtime AppUserModelID in those files to match `com.tutoratlas.app`, or make both read from a single shared constant.
…r-and-fonts-module Auto claude/007 implement render and fonts module
| </TooltipPopup> | ||
| </Tooltip> | ||
| ) : null} | ||
| {isMarkdown && renderMarkdown && file.data && derivedOutputPath ? ( |
There was a problem hiding this comment.
🟡 Medium files/FilePreviewPanel.tsx:740
RenderPdfButton is shown whenever renderMarkdown is active, but the condition does not check file.data.truncated. When the file exceeds the preview byte limit, file.data.contents holds only a prefix, so clicking the button renders a PDF from an incomplete document while reporting success. Add !file.data.truncated to the render condition so the button is hidden for truncated files.
| {isMarkdown && renderMarkdown && file.data && derivedOutputPath ? ( | |
| {isMarkdown && renderMarkdown && file.data && !file.data.truncated && derivedOutputPath ? ( |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 740:
`RenderPdfButton` is shown whenever `renderMarkdown` is active, but the condition does not check `file.data.truncated`. When the file exceeds the preview byte limit, `file.data.contents` holds only a prefix, so clicking the button renders a PDF from an incomplete document while reporting success. Add `!file.data.truncated` to the render condition so the button is hidden for truncated files.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
There are 9 total unresolved issues (including 6 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7dbfd81. Configure here.
| const lastDot = filename.lastIndexOf("."); | ||
| const basename = lastDot === -1 ? filename : filename.substring(0, lastDot); | ||
| const outputDir = dir ? `${dir}/output` : "output"; | ||
| return `${cwd}/${outputDir}/${basename}.pdf`; |
There was a problem hiding this comment.
PDF path ignores Windows separators
Medium Severity
The derivedOutputPath for PDF generation constructs paths using hardcoded forward slashes. This can lead to incorrect or mixed path separators on Windows, unlike other file path resolutions in the panel that correctly normalize for the operating system. This mismatch affects the reliability of saving or opening PDF output.
Reviewed by Cursor Bugbot for commit 7dbfd81. Configure here.
| variant="ghost" | ||
| size="sm" | ||
| /> | ||
| ) : null} |
There was a problem hiding this comment.
Render PDF hidden in source view
Medium Severity
The RenderPdfButton for markdown files is only visible when the file is in rendered preview mode. Markdown files default to the source editor, requiring users to toggle the view to access PDF rendering, despite the content being available in source mode.
Reviewed by Cursor Bugbot for commit 7dbfd81. Configure here.
| variant="ghost" | ||
| size="sm" | ||
| /> | ||
| ) : null} |
There was a problem hiding this comment.
Partial PDF from truncated files
Medium Severity
When a file exceeds the preview size limit, the UI shows a banner that only the first 1 MB is loaded, but Render PDF can still run in rendered mode using that truncated file.data.contents. The deliverable PDF can silently omit the rest of the document with no block or warning on the action.
Reviewed by Cursor Bugbot for commit 7dbfd81. Configure here.


What Changed
Why
UI Changes
Checklist
Note
High Risk
Large cross-cutting change including a new desktop appId, new local persistence, and Electron PDF rendering; mistakes could affect updates, data loss, or desktop stability.
Overview
Repositions the fork as TutorAtlas: user-facing copy, splash/titles, desktop productName, and packaging now use
com.tutoratlas.app/ TutorAtlas artifact names (may break continuity with prior T3 Code installs).Adds a Students surface: sidebar link to
/students, roster CRUD UI, and persistence throughgetStudents/setStudents(desktopstudents.jsonviaDesktopStudents, weblocalStoragefallback).Ships a materials + PDF path on desktop:
ensureStudentWorkspacescaffoldsstudents/<slug>/, markdown is turned into print HTML,DesktopPdfRendereruses a hiddenBrowserWindow+printToPDF, andRenderPdfButtonis wired into markdown file preview. Materials threads hide PR/git chrome in the sidebar whenworktreePathcontains/students/.Also adds extensive
.plans/specs (licensing, Atlas roadmap, manual test guides),.gitignoreentries for Auto Claude artifacts, and anE2E-VERIFICATION-RESULTSchecklist doc.Reviewed by Cursor Bugbot for commit 7dbfd81. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Rebrand app from 'T3 Code' to 'TutorAtlas' and add student roster and PDF rendering features
students.tscontracts with schemas forStudent,PhoneNumber,SingaporeAddress, andParent; aDesktopStudentsservice with atomic JSON persistence; IPC channels; and a/studentsroute with list, detail, create, edit, and delete UI.markdownToHtmlwith embedded fonts and print CSS, aDesktopPdfRendererservice using Electron'sprintToPDFwith atomic writes, IPC wiring, and aRenderPdfButtonshown in the markdown file preview panel.DesktopWorkspaceservice that idempotently creates per-student directories under~/tutoratlas/workspace/students/<slug>and optionally seedsAGENTS.md; wired into the sidebar's new Students nav entry.com.t3tools.t3codetocom.tutoratlas.app, which will break auto-update continuity for existing installs and change OS-level data directory paths.Macroscope summarized 7dbfd81.