Skip to content

Dev#3566

Open
tutoratlas wants to merge 103 commits into
pingdotgg:mainfrom
tutoratlas:dev
Open

Dev#3566
tutoratlas wants to merge 103 commits into
pingdotgg:mainfrom
tutoratlas:dev

Conversation

@tutoratlas

@tutoratlas tutoratlas commented Jun 26, 2026

Copy link
Copy Markdown

What Changed

Why

UI Changes

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

High Risk
Large cross-cutting change (branding, data paths, workspace cwd, new persistence/IPC/PDF) can break existing installs, updates, and stored student data locations; PDF rendering and filesystem deletes are security- and data-loss-sensitive surfaces.

Overview
This PR pivots the fork toward TutorAtlas: user-facing branding and desktop productName move from T3 Code to TutorAtlas, app data/env shifts from T3CODE_HOME / .t3 to TUTORATLAS_HOME / .tutoratlas, and the embedded server backendCwd / workspaceRoot default to ~/tutoratlas with T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD enabled so launch can auto-open a single workspace project.

Students land as a first-class feature: @t3tools/contracts student schemas (optional workspaceFolder, deriveStudentSlug), DesktopStudents atomic JSON registry + get/set IPC, preload bridge methods, and the web Students route/UI (form validation, phone country codes, deep links). DesktopWorkspace creates students/<slug>/ under the workspace (optional AGENTS.md), bootstraps .atlas/skills, and exposes ensure/delete/open-path IPC with path checks on delete.

PDF materials: DesktopPdfRenderer renders HTML via a hidden BrowserWindow + printToPDF with temp-then-rename writes; renderMarkdownToPdf / openPath IPC and ElectronShell.openPath wire the web preview RenderPdfButton path described in the plans. Sidebar isMaterialsThread hides PR/git chrome for threads under /students/.

Also adds extensive .plans/ Atlas roadmap docs, .gitignore entries for Auto Claude, and an E2E verification write-up for the materials/PDF checklist—not runtime behavior by itself.

Reviewed by Cursor Bugbot for commit 337bb04. 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

  • Renames the product from T3 Code to TutorAtlas across all user-facing strings, config keys (T3CODE_HOMETUTORATLAS_HOME, t3HometutoratlasHome), environment paths (~/.t3~/.tutoratlas), app IDs, and build artifact names.
  • Adds a full student management system: Student schema in contracts, desktop DesktopStudents service for atomic on-disk persistence at environment.studentRegistryPath, IPC channels for get/set, browser localStorage fallback, and a /students route with list/detail/form UI.
  • Adds per-student workspace management via DesktopWorkspace: creates <workspaceRoot>/students/<slug> directories, seeds AGENTS.md, bootstraps .atlas/skills/ on startup, and supports secure deletion.
  • Adds a PDF rendering pipeline: markdownToHtml embeds base64 woff2 fonts (DM Sans Variable, Source Serif 4 Variable) and print CSS into a full HTML document, DesktopPdfRenderer renders it via an offscreen Electron BrowserWindow with atomic file write, and RenderPdfButton exposes this in the markdown preview UI.
  • Adds a gen-brand-assets script that generates ICO, ICNS, and PNG icon assets from assets/atlas/logo.svg using sharp.
  • Risk: backendCwd now points to workspaceRoot (~/tutoratlas) instead of homeDirectory/appRoot, which changes where the backend server is launched from.

Macroscope summarized 337bb04.

rexong and others added 30 commits June 16, 2026 00:44
…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>
rexong and others added 23 commits June 28, 2026 00:15
…t3Home to tutoratlasHome

Changes:
- Updated config.ts: Renamed T3CODE_HOME env var to TUTORATLAS_HOME
- Updated config.ts: Renamed t3Home field to tutoratlasHome
- Updated os-jank.ts: Changed default directory from .t3 to .tutoratlas

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…t3Home to tutoratlasHome

Renamed environment variable T3CODE_HOME to TUTORATLAS_HOME and field t3Home to tutoratlasHome across dev-runner, electron-launcher, and desktop backend configuration files. Updated all references including default home directory path, env variable assignments, flag definitions, and schema fields.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…opEnvironment, DesktopCloudAuthTokenStore, DesktopObservability, DesktopUpdates

Replace T3CODE_HOME with TUTORATLAS_HOME in env setup and DesktopConfig.layerTest calls.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…opSavedE

Replace T3CODE_HOME with TUTORATLAS_HOME in DesktopConfig.layerTest calls
for DesktopSavedEnvironments.test.ts, DesktopAppSettings.test.ts,
DesktopStudents.test.ts, and DesktopClientSettings.test.ts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…er/script test files

Renamed T3CODE_HOME → TUTORATLAS_HOME, '.t3' → '.tutoratlas', t3Home → tutoratlasHome
in all test files:
- apps/desktop/src/backend/DesktopServerExposure.test.ts
- apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
- apps/desktop/src/backend/DesktopBackendManager.test.ts
- apps/server/src/cli/config.test.ts
- scripts/dev-runner.test.ts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…pace changes

- Update DesktopAppIdentity tests to expect 'TutorAtlas (Alpha)' instead of 'T3 Code (Alpha)'
- Update DesktopEnvironment test to expect backendCwd at ~/tutoratlas (new workspace root)
- Legacy path test correctly expects 'T3 Code (Alpha)' when legacy path exists
…eplace T3CODE_HOME with TUTORATLAS_HOME

- Replaced 7 occurrences of $T3CODE_HOME/userdata/logs/ with $TUTORATLAS_HOME/userdata/logs/ in docs/operations/observability.md
- Updated .plans/atlas/03-unified-workspace.md references at lines 28 and 145
- Added R2 VSync log documentation noting ERROR:ui/gl/gl_surface_presentation_helper.cc:260 is benign noise on Linux

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…d a sect

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…faces

- Update EnsureStudentWorkspaceInputSchema to accept name (Schema.String) + id (StudentId) instead of just studentId
- Update EnsureStudentWorkspaceResultSchema to include workspaceFolder field
- Add DeleteStudentWorkspaceInputSchema (workspaceFolder: Schema.String)
- Add DeleteStudentWorkspaceResultSchema (success + error)
- Add deleteStudentWorkspace to DesktopBridge interface
- Add deleteStudentWorkspace to LocalApi materials interface

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ant to channels.ts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…eStudent

- Add deleteStudentWorkspace method to DesktopWorkspaceShape interface
- Implement path resolution via path.join(workspaceRoot, workspaceFolder)
- Use fs.realPath to resolve symlinks before deletion
- Validate resolved path is strictly inside <workspaceRoot>/students/
- Implement recursive directory removal
- Update ensureStudentWorkspace to return workspaceFolder (relative path)
- Update error class to support context field for better error messages
- Implement in both main layer and test layer

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…and add deleteStudentWorkspace

- Updated ensureStudentWorkspace to accept name+id input
- Derive slug server-side via deriveStudentSlug(name) + sanitize + id suffix
- Return both workspacePath (absolute) and workspaceFolder (relative) in result
- Added deleteStudentWorkspace IPC handler with path validation
- Both handlers follow the existing IPC method pattern with Effect.match

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… in prel

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…orkspace, remove sort/group menus

- Rename sidebar header from 'Projects' to 'Workspace'
- Remove 'Sort projects' MenuGroup block from ProjectSortMenu
- Remove 'Group projects' MenuGroup block from ProjectSortMenu
- Remove SIDEBAR_SORT_LABELS constant (orphaned)
- Remove PROJECT_GROUPING_MODE_LABELS constant (replaced with inline strings)
- Remove projectGroupingModeDescription function (replaced with inline logic)
- Remove handleProjectSortOrderChange handler (orphaned)
- Remove handleProjectGroupingModeChange handler (orphaned)
- Remove orphaned props from ProjectSortMenu component
- Remove orphaned props from SidebarProjectsContent component
- Remove SidebarProjectSortOrder type import (orphaned)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…Palette

- Remove 'New thread in...' project-picker submenu
- Remove projectSearchItems useMemo (lines 628-643)
- Remove projectThreadItems useMemo (lines 645-679)
- Remove 'Add project' action item block (lines 1011-1039)
- Remove buildProjectActionItems function from CommandPalette.logic.ts
- Clean up orphaned imports: buildProjectActionItems, ProjectFavicon

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…pace wrapper

Added deleteStudentWorkspace method in materials section following the same pattern as ensureStudentWorkspace:
- Gates on window.desktopBridge availability
- Delegates to bridge.deleteStudentWorkspace(input)
- Throws descriptive error in browser-only mode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…dleGenerateMaterials

- Removed 'Generate materials' button JSX (lines 119-127)
- Removed handleGenerateMaterials handler function (lines 50-107)
- Removed isGenerating state
- Removed useHandleNewThread hook import and usage
- Removed orphaned imports: useState, FolderIcon, toastManager

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…create b

Integrated student workspace folder lifecycle into students.tsx route:
- In handleSave (create): Call ensureStudentWorkspace IPC after initial persist, store workspaceFolder on student record
- In handleDelete: Check for workspaceFolder, show folder-aware confirmation, call deleteStudentWorkspace IPC before roster removal
- Handle pre-R4 students (no workspaceFolder) by skipping folder operations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…s and R4 schema changes

Changes:
1. apps/desktop/src/workspace/DesktopWorkspace.ts:
   - Fixed SystemError construction to use Effect.die for security violations
   - Removed 'reason' field (not supported by SystemError)

2. apps/web/src/localApi.test.ts:
   - Added workspaceFolder field to ensureStudentWorkspace mock
   - Added deleteStudentWorkspace mock method

3. apps/web/src/components/settings/SettingsPanels.browser.tsx:
   - Added workspaceFolder field to ensureStudentWorkspace mock
   - Added deleteStudentWorkspace mock method

All typechecks pass (desktop, web, server, scripts, contracts).
External dependency errors (@noble, jose, yaml) are unrelated to our changes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…he-content-of-the-file-to-generate-

Auto claude/009 i need to see the content of the file to generate
Replace the Deferred-based did-finish-load/did-fail-load wait with an
Effect.callback whose finalizer removes both listeners when the load is
interrupted (e.g. by PDF_TIMEOUT_MS), preventing a webContents listener
leak on timed-out renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed-workspace-feature

Auto claude/008 implement unified workspace feature

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 7 potential issues.

There are 12 total unresolved issues (including 5 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

// Silent fail - continue with roster removal
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roster deleted after failed workspace

High Severity

When deleting a student, the application removes them from the roster and persists this change even if the deleteStudentWorkspace call fails. This can leave student workspace files on disk while the UI shows the student as deleted, leading to an inconsistent state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

const confirmed = await localApi.dialogs.confirm(
`Delete student "${student.name}" and workspace folder "${student.workspaceFolder}"? This action cannot be undone.`,
);
if (!confirmed) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two delete confirmation dialogs

Medium Severity

Deleting a student with an associated workspace folder triggers two native confirmation dialogs. The first dialog omits mention of the workspace folder, while the second explicitly asks about deleting it, leading to redundant confirmation for users.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

const baseDir = Option.getOrElse(config.tutoratlasHome, () => path.join(homeDirectory, ".tutoratlas"));
const workspaceRoot = Option.getOrElse(config.tutoratlasWorkspace, () =>
path.join(homeDirectory, "tutoratlas"),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy app data not migrated

High Severity

The app's base directory for user state now defaults to ~/.tutoratlas (or TUTORATLAS_HOME), no longer falling back to the previous ~/.t3 (or T3CODE_HOME). Upgraded installs don't migrate existing data, so rosters, settings, and tokens appear empty.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.


const handleProjectButtonContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project context menu disabled wrongly

Low Severity

The handleProjectButtonContextMenu for project rows now immediately returns, preventing the custom context menu and its actions (like rename or delete) from appearing. This also allows the browser's default context menu to show.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

</div>

{isManualProjectSorting ? (
{false ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual project reorder always disabled

Medium Severity

The sidebar project list always renders the non-draggable branch because the condition was changed from isManualProjectSorting to literal false. Users who set manual project sort in settings can no longer drag-reorder workspace projects.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

}

// Recursively remove the directory
yield* input.fileSystem.remove(realPath, { recursive: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete wipes all student folders

High Severity

The delete path guard treats a resolved path equal to the shared students directory as valid, then recursively removes it. A workspaceFolder value of students (allowed by the contract) can delete every student workspace under the tutor root, not one slug folder.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 337bb04. Configure here.

["VITE_DEV_SERVER_URL", process.env.VITE_DEV_SERVER_URL],
["T3CODE_PORT", process.env.T3CODE_PORT],
["T3CODE_HOME", process.env.T3CODE_HOME],
["TUTORATLAS_HOME", process.env.TUTORATLAS_HOME],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/electron-launcher.mjs:107

The diff renames the exported env var on line 107 from T3CODE_HOME to TUTORATLAS_HOME, but LAUNCHER_VERSION was not bumped. buildMacLauncher() reuses an existing cached bundle whenever metadata.json matches, and expectedMetadata only includes launcherVersion, source bundle mtimes, icon mtime, bundle id, and protocol schemes — not the generated script contents or env var names. Developers who already have .electron-runtime/T3 Code (Dev).app will keep launching the old wrapper that still exports T3CODE_HOME, while the desktop app now reads TUTORATLAS_HOME. On those machines the app silently falls back to the default home directory instead of the configured one until the cached launcher is manually deleted. Bump LAUNCHER_VERSION so the cached launcher is invalidated and regenerated with the new env var name.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/scripts/electron-launcher.mjs around line 107:

The diff renames the exported env var on line 107 from `T3CODE_HOME` to `TUTORATLAS_HOME`, but `LAUNCHER_VERSION` was not bumped. `buildMacLauncher()` reuses an existing cached bundle whenever `metadata.json` matches, and `expectedMetadata` only includes `launcherVersion`, source bundle mtimes, icon mtime, bundle id, and protocol schemes — not the generated script contents or env var names. Developers who already have `.electron-runtime/T3 Code (Dev).app` will keep launching the old wrapper that still exports `T3CODE_HOME`, while the desktop app now reads `TUTORATLAS_HOME`. On those machines the app silently falls back to the default home directory instead of the configured one until the cached launcher is manually deleted. Bump `LAUNCHER_VERSION` so the cached launcher is invalidated and regenerated with the new env var name.

Comment on lines +84 to +87
return yield* Effect.die(
`Security: Path '${realPath}' is not strictly inside students directory '${realStudentsDir}'`,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium workspace/DesktopWorkspace.ts:84

The security check uses Effect.die(...) for an invalid path, but deleteStudentWorkspace is wrapped only with Effect.mapError(...). Defects are not caught by mapError, so an out-of-tree workspaceFolder such as ../other terminates the effect instead of surfacing as a DesktopWorkspaceError. Consider using Effect.fail(...) with a DesktopWorkspaceError so the invalid-path condition is handled as a normal error.

Suggested change
return yield* Effect.die(
`Security: Path '${realPath}' is not strictly inside students directory '${realStudentsDir}'`,
);
}
return yield* Effect.fail(
new DesktopWorkspaceError({
cause: new PlatformError.PlatformError({ reason: "AccessDenied", error: new Error(`Security: Path '${realPath}' is not strictly inside students directory '${realStudentsDir}'`) }),
context: "to delete student workspace",
}),
);
Also found in 1 other location(s)

apps/desktop/src/ipc/methods/workspace.ts:60

deleteStudentWorkspace wraps workspace.deleteStudentWorkspace(...) with Effect.match, but the implementation in DesktopWorkspace uses Effect.die(...) when workspaceFolder resolves outside the students directory. Effect.match does not catch defects, so an invalid or malicious workspaceFolder makes this handler reject the IPC call instead of returning the declared { success: false, error } result shape.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/workspace/DesktopWorkspace.ts around lines 84-87:

The security check uses `Effect.die(...)` for an invalid path, but `deleteStudentWorkspace` is wrapped only with `Effect.mapError(...)`. Defects are not caught by `mapError`, so an out-of-tree `workspaceFolder` such as `../other` terminates the effect instead of surfacing as a `DesktopWorkspaceError`. Consider using `Effect.fail(...)` with a `DesktopWorkspaceError` so the invalid-path condition is handled as a normal error.

Also found in 1 other location(s):
- apps/desktop/src/ipc/methods/workspace.ts:60 -- `deleteStudentWorkspace` wraps `workspace.deleteStudentWorkspace(...)` with `Effect.match`, but the implementation in `DesktopWorkspace` uses `Effect.die(...)` when `workspaceFolder` resolves outside the `students` directory. `Effect.match` does not catch defects, so an invalid or malicious `workspaceFolder` makes this handler reject the IPC call instead of returning the declared `{ success: false, error }` result shape.

: Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config"));
const baseDir = Option.getOrElse(config.t3Home, () => path.join(homeDirectory, ".t3"));
const baseDir = Option.getOrElse(config.tutoratlasHome, () => path.join(homeDirectory, ".tutoratlas"));
const workspaceRoot = Option.getOrElse(config.tutoratlasWorkspace, () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium app/DesktopEnvironment.ts:158

workspaceRoot is used directly from config.tutoratlasWorkspace without expanding ~ or resolving to an absolute path. When a user sets TUTORATLAS_WORKSPACE=~/tutoratlas, the value ~/tutoratlas is used as-is, so backendCwd is passed to the backend process as the literal relative path ~/tutoratlas, causing startup to fail with ENOENT. Consider expanding ~ to homeDirectory and resolving to an absolute path before using the value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/app/DesktopEnvironment.ts around line 158:

`workspaceRoot` is used directly from `config.tutoratlasWorkspace` without expanding `~` or resolving to an absolute path. When a user sets `TUTORATLAS_WORKSPACE=~/tutoratlas`, the value `~/tutoratlas` is used as-is, so `backendCwd` is passed to the backend process as the literal relative path `~/tutoratlas`, causing startup to fail with `ENOENT`. Consider expanding `~` to `homeDirectory` and resolving to an absolute path before using the value.

noBrowser: Schema.Boolean,
port: PortSchema,
t3Home: Schema.String,
tutoratlasHome: Schema.String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/desktopBootstrap.ts:9

Renaming the required field t3Home to tutoratlasHome breaks wire compatibility. Effect Schema.Struct requires listed fields and disallows extra properties, so an older producer that sends { "t3Home": "..." } will be rejected as missing tutoratlasHome — a mixed-version desktop/server pair or stored bootstrap fixture will fail startup. Consider restoring the original field name or adding a compatibility layer (e.g., Schema.propertySignature with Schema.optional for both names, or decoding both and re-encoding under the new name) so older payloads still decode.

🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/desktopBootstrap.ts around line 9:

Renaming the required field `t3Home` to `tutoratlasHome` breaks wire compatibility. Effect `Schema.Struct` requires listed fields and disallows extra properties, so an older producer that sends `{ "t3Home": "..." }` will be rejected as missing `tutoratlasHome` — a mixed-version desktop/server pair or stored bootstrap fixture will fail startup. Consider restoring the original field name or adding a compatibility layer (e.g., `Schema.propertySignature` with `Schema.optional` for both names, or decoding both and re-encoding under the new name) so older payloads still decode.


yield* waitForLoad.pipe(
Effect.timeout(PDF_TIMEOUT_MS),
Effect.catchTag("TimeoutError", () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium pdf/DesktopPdfRenderer.ts:102

Effect.timeout fails with the tag "TimeoutException", but Effect.catchTag at line 102 matches "TimeoutError", so the catch never triggers. When the page load exceeds PDF_TIMEOUT_MS, the raw TimeoutException escapes instead of being converted to DesktopPdfRendererError, violating the advertised error type. Use Effect.catchTag("TimeoutException", …) so timeouts are mapped correctly.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/pdf/DesktopPdfRenderer.ts around line 102:

`Effect.timeout` fails with the tag `"TimeoutException"`, but `Effect.catchTag` at line 102 matches `"TimeoutError"`, so the catch never triggers. When the page load exceeds `PDF_TIMEOUT_MS`, the raw `TimeoutException` escapes instead of being converted to `DesktopPdfRendererError`, violating the advertised error type. Use `Effect.catchTag("TimeoutException", …)` so timeouts are mapped correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants