Skip to content

Projects Prototype (Frontend-only, localStorage)#421

Draft
marksftw wants to merge 3 commits intomasterfrom
feature/projects-prototype
Draft

Projects Prototype (Frontend-only, localStorage)#421
marksftw wants to merge 3 commits intomasterfrom
feature/projects-prototype

Conversation

@marksftw
Copy link
Contributor

@marksftw marksftw commented Feb 11, 2026

Projects Prototype (Frontend-only, localStorage)

Video Walkthrough:

maple-projects-poc-walkthrough.mov

Adds a complete Projects feature prototype to evaluate the UX before building
the backend. All data is persisted in localStorage — no API changes.

What it does

  • Organize chats into projects — create, rename, and delete projects from
    the sidebar or project detail page
  • Custom instructions per project — set context that applies to all chats
    within a project (chats don't share history with each other, but they all
    use the project's custom instructions and files as context)
  • File attachments per project — add reference files to a project that
    are available across all its chats
  • Unified chat context menu — rename, delete, move between projects, or
    remove from a project, consistent everywhere (sidebar + project page)
  • Collapsible sidebar sections — both Projects and Recents are
    independently collapsible with persisted state
  • Start chats from project page — new chats are automatically assigned to
    the project

How it works

Projects are stored in localStorage (maple_projects). When a chat is
started from a project page, a project_id URL param triggers
auto-assignment after conversation creation via a custom event. Chats
assigned to projects are filtered out of the Recents list. Deleting a project
marks its chats as deleted.

New files (10)

  • state/ProjectsContext.tsx + state/useProjects.ts — state management
  • components/ChatContextMenu.tsx — shared context menu used across sidebar
    and project page
  • components/projects/ — ProjectsList, ProjectDetailPage, and 4 dialog
    components
  • routes/_auth.project.$projectId.tsx — project detail route

Modified files (5)

  • app.tsx — added ProjectsProvider
  • Sidebar.tsx — projects section, create-project-for-chat event handling
  • ChatHistoryList.tsx — collapsible Recents, exclude project chats, shared
    context menu
  • UnifiedChat.tsx — auto-assign chat to project via URL param

Open with Devin

Summary by CodeRabbit

Release Notes

  • New Features
    • Added project management system to organize and group chats
    • Create, rename, and delete projects with dedicated detail pages
    • Move chats between projects or remove them from projects
    • Set custom instructions per project
    • Attach and manage files within projects
    • Access chat context menus with project-related actions

Build the complete Projects UI/UX flow as a frontend-only prototype
using localStorage for data persistence. This allows evaluating the
design and interactions before committing to a full backend implementation.

- Add ProjectsContext/useProjects for state management (localStorage)
- Add project CRUD: create, rename, delete with confirmation dialogs
- Add project detail page with chat input, custom instructions, and files
- Add collapsible Projects and Recents sections in sidebar
- Add unified ChatContextMenu shared across all chat context menus
- Add "Move to project" drill-down submenu with animated slide transition
- Add chat rename/delete support from both sidebar and project page
- Auto-assign chats to projects via URL param after conversation creation
- Filter project chats from Recents list, track deleted project chats
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces a comprehensive project management system to the frontend application. It adds a new ProjectsContext for managing project data in localStorage, creates UI components for project creation/deletion/management, integrates project functionality into chat workflows with event-driven assignment, and establishes a new route for viewing and managing projects with their associated chats.

Changes

Cohort / File(s) Summary
Project Context & State Management
frontend/src/state/ProjectsContext.tsx, frontend/src/state/useProjects.ts, frontend/src/app.tsx
Introduces ProjectsContext for managing projects, files, and chat assignments with localStorage persistence. Provides CRUD operations, file management, and chat association methods. Wraps app provider tree with ProjectsProvider and exports useProjects hook.
Project Dialog Components
frontend/src/components/projects/CreateProjectDialog.tsx, frontend/src/components/projects/DeleteProjectDialog.tsx, frontend/src/components/projects/CustomInstructionsDialog.tsx, frontend/src/components/projects/RemoveFileDialog.tsx
Introduces reusable dialog components for project creation/deletion, editing custom instructions, and removing files. Each component handles form state, validation, and callbacks for parent integration.
Project Detail Page & List
frontend/src/components/projects/ProjectDetailPage.tsx, frontend/src/components/projects/ProjectsList.tsx
Adds comprehensive project management UI with ProjectDetailPage rendering a project detail view (sidebar, chat list, file manager, custom instructions). ProjectsList displays projects with associated chats, search filtering, and per-project/per-chat actions.
Chat Integration
frontend/src/components/ChatContextMenu.tsx, frontend/src/components/ChatHistoryList.tsx, frontend/src/components/UnifiedChat.tsx, frontend/src/components/Sidebar.tsx
Integrates project functionality into chat workflows. Adds ChatContextMenu component with project move/assignment actions. Extends ChatHistoryList with project filters and conversation callbacks. Updates Sidebar with project dialog flows and assigns chats to projects. UnifiedChat dispatches auto-assignment events when project_id parameter exists.
Routing
frontend/src/routeTree.gen.ts, frontend/src/routes/_auth.project.$projectId.tsx
Adds new authenticated route /_auth/project/$projectId that renders ProjectDetailPage with the extracted projectId parameter, enabling project-specific views.

Sequence Diagrams

sequenceDiagram
    participant User
    participant ChatContextMenu
    participant Sidebar
    participant ProjectsContext
    participant CreateProjectDialog
    participant ProjectDetailPage

    User->>ChatContextMenu: Click "New project" in context menu
    ChatContextMenu->>Sidebar: Emit "createprojectforchat" event
    Sidebar->>CreateProjectDialog: Open dialog with createProjectForChatId
    User->>CreateProjectDialog: Enter project name & submit
    CreateProjectDialog->>ProjectsContext: createProject(name)
    ProjectsContext->>ProjectsContext: Save to localStorage
    ProjectsContext->>User: Return projectId
    Sidebar->>ProjectsContext: assignChatToProject(chatId, projectId)
    Sidebar->>ProjectDetailPage: Navigate to project detail page
    ProjectDetailPage->>User: Display project with assigned chat
Loading
sequenceDiagram
    participant User
    participant ProjectsList
    participant ChatContextMenu
    participant ProjectsContext
    participant OpenAIAPI
    participant EventSystem

    User->>ProjectsList: Click chat "Move to project"
    ProjectsList->>ChatContextMenu: Render context menu
    User->>ChatContextMenu: Select target project
    ChatContextMenu->>ProjectsContext: moveChatToProject(chatId, targetProjectId)
    ProjectsContext->>ProjectsContext: Remove from old project
    ProjectsContext->>ProjectsContext: Add to new project
    ProjectsContext->>EventSystem: Dispatch state change event
    ProjectsList->>User: Update chat list
    
    User->>ProjectsList: Click chat "Rename"
    ProjectsList->>ChatContextMenu: Render context menu
    User->>ChatContextMenu: Rename chat
    ChatContextMenu->>OpenAIAPI: Update chat metadata
    OpenAIAPI->>EventSystem: Emit update event
    ProjectsList->>User: Reflect renamed chat
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Responses final #263: Overlapping modifications to ChatHistoryList, Sidebar, and UnifiedChat components with shared project integration logic.

Poem

🐰 A project garden takes its form,
With chats now grouped, organized, warm,
Dialog flowers bloom on the page,
Context menus dance, localStorage storage—
The warren's burrows grow wise and free! 🌱✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Projects Prototype (Frontend-only, localStorage)' directly and clearly summarizes the main change: introduction of a projects feature prototype with frontend-only implementation and localStorage persistence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/projects-prototype

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 11, 2026

Greptile Overview

Greptile Summary

Added a complete Projects feature prototype using localStorage for organizing chats into projects with custom instructions and file attachments.

Key changes:

  • Created state management for projects (ProjectsContext.tsx) with CRUD operations and localStorage persistence
  • Added project detail page with chat creation flow and project settings UI
  • Integrated projects into sidebar with collapsible sections filtering project chats from recents
  • Implemented auto-assignment of new chats to projects via URL param mechanism
  • Added shared ChatContextMenu component for unified chat actions across sidebar and project pages

Critical issues found:

  • Custom instructions are stored but never actually used - they're not integrated with UnifiedChat.tsx to be sent in API calls, so setting them has no effect on AI responses
  • File attachments only store metadata - actual file contents aren't persisted or uploaded, making them unavailable for chat context
  • Missing paid user check - custom instructions should be restricted to paid users but no billing check exists
  • Fragile timing in chat creation - nested setTimeout chain in ProjectDetailPage.tsx may fail across different devices/browsers

Confidence Score: 2/5

  • This PR has significant functional gaps that prevent core features from working as intended
  • Score reflects multiple critical issues: custom instructions are completely non-functional (stored but never sent to AI), file attachments only store metadata without actual content, missing required paid user restrictions, and fragile timing dependencies in chat creation flow. While the UI/UX implementation is solid, the backend integration is incomplete.
  • Pay close attention to frontend/src/state/ProjectsContext.tsx and frontend/src/components/projects/ProjectDetailPage.tsx - they contain the most critical functional gaps

Important Files Changed

Filename Overview
frontend/src/state/ProjectsContext.tsx Core state management for projects with localStorage persistence - custom instructions and files stored but not integrated with chat API, missing critical functionality
frontend/src/components/projects/ProjectDetailPage.tsx Project detail page with chat creation flow - uses fragile nested setTimeout chain for message submission that may fail due to timing issues
frontend/src/components/projects/ProjectsList.tsx Sidebar projects list with collapsible sections and search - well-structured component, no major issues found
frontend/src/components/projects/CustomInstructionsDialog.tsx Dialog for editing project custom instructions - missing paid user check required by custom rules
frontend/src/components/ChatHistoryList.tsx Updated to filter project-assigned chats and use shared ChatContextMenu - integration looks solid
frontend/src/components/UnifiedChat.tsx Added auto-assignment logic via project_id URL param - minimal, focused change that integrates cleanly

Sequence Diagram

sequenceDiagram
    participant User
    participant ProjectDetailPage
    participant Browser
    participant UnifiedChat
    participant ProjectsContext
    participant localStorage

    User->>ProjectDetailPage: Click "New chat in Project"
    ProjectDetailPage->>Browser: navigate({ to: "/" })
    ProjectDetailPage->>Browser: setTimeout(0ms)
    Browser->>Browser: history.replaceState("/?project_id=...")
    Browser->>Browser: dispatchEvent("newchat")
    Browser->>UnifiedChat: Event received
    ProjectDetailPage->>Browser: setTimeout(100ms)
    Browser->>Browser: Find textarea, set value
    ProjectDetailPage->>Browser: setTimeout(50ms)
    Browser->>Browser: Dispatch form submit
    UnifiedChat->>UnifiedChat: Create conversation
    UnifiedChat->>Browser: dispatchEvent("conversationcreated")
    UnifiedChat->>Browser: Check project_id param
    UnifiedChat->>Browser: dispatchEvent("assignchattoproject")
    Browser->>ProjectsContext: Event received
    ProjectsContext->>ProjectsContext: Assign chat to project
    ProjectsContext->>localStorage: Save updated projects
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

15 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 148 to 178
const handleStartChat = useCallback(() => {
const message = newChatInput.trim();
if (!message) return;
setNewChatInput("");
// Navigate to home with project_id param so UnifiedChat can auto-assign
navigate({ to: "/" });
setTimeout(() => {
window.history.replaceState(null, "", `/?project_id=${projectId}`);
window.dispatchEvent(new Event("newchat"));
// Set the message to be sent
setTimeout(() => {
const textarea = document.getElementById("message") as HTMLTextAreaElement;
if (textarea) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
)?.set;
nativeInputValueSetter?.call(textarea, message);
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.focus();
// Auto-submit after a brief delay
setTimeout(() => {
const form = textarea.closest("form");
if (form) {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
}
}, 50);
}
}, 100);
}, 0);
}, [newChatInput, projectId, navigate]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Chain of nested setTimeout calls creates fragile timing dependencies that may fail across different devices/browsers. The navigation and message submission rely on arbitrary delays (0ms, 100ms, 50ms) to coordinate state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/projects/ProjectDetailPage.tsx
Line: 148:178

Comment:
Chain of nested `setTimeout` calls creates fragile timing dependencies that may fail across different devices/browsers. The navigation and message submission rely on arbitrary delays (0ms, 100ms, 50ms) to coordinate state.

How can I resolve this? If you propose a fix, please make it concise.

name: string;
createdAt: number;
updatedAt: number;
customInstructions: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Custom instructions are stored but never actually sent with chat messages - they're not integrated with UnifiedChat.tsx to be included in the system prompt or API calls. Users can set them but they won't affect AI responses.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/state/ProjectsContext.tsx
Line: 18:18

Comment:
Custom instructions are stored but never actually sent with chat messages - they're not integrated with `UnifiedChat.tsx` to be included in the system prompt or API calls. Users can set them but they won't affect AI responses.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1 to +66
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";

interface CustomInstructionsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
currentInstructions: string;
onSave: (instructions: string) => void;
}

export function CustomInstructionsDialog({
open,
onOpenChange,
currentInstructions,
onSave
}: CustomInstructionsDialogProps) {
const [instructions, setInstructions] = useState("");

useEffect(() => {
if (open) {
setInstructions(currentInstructions);
}
}, [open, currentInstructions]);

const handleSave = () => {
onSave(instructions);
onOpenChange(false);
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[525px]">
<DialogHeader>
<DialogTitle>Custom instructions</DialogTitle>
<DialogDescription>
Set context and customize how Maple responds in this project.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="e.g., 'Use concise bullet points. Focus on practical examples.'"
className="min-h-[200px] resize-y"
autoFocus
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Custom instructions should only be editable by paid users per requirements, but there's no billing/subscription check in this dialog.

Context Used: Context from dashboard - System prompts should only be editable by paid users and should not be accessible to free users. Ens... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/projects/CustomInstructionsDialog.tsx
Line: 1:66

Comment:
Custom instructions should only be editable by paid users per requirements, but there's no billing/subscription check in this dialog.

**Context Used:** Context from `dashboard` - System prompts should only be editable by paid users and should not be accessible to free users. Ens... ([source](https://app.greptile.com/review/custom-context?memory=85e73147-9381-44e9-86f6-50b68c27d659))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +5 to +11
export interface ProjectFile {
id: string;
name: string;
type: string; // e.g. "pdf", "txt", "md"
size: number;
addedAt: number;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

File metadata is stored but file contents are never persisted or uploaded - only name, type, and size are saved. The actual file data isn't available for chat context.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/state/ProjectsContext.tsx
Line: 5:11

Comment:
File metadata is stored but file contents are never persisted or uploaded - only name, type, and size are saved. The actual file data isn't available for chat context.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app.tsx (1)

100-115: ⚠️ Potential issue | 🟡 Minor

Fix indentation — pipeline failure.

The <ProjectsProvider> wrapper doesn't indent its children, which is what's triggering the Prettier failure. The <OpenAIProvider> through </OpenAIProvider> block should be indented one level inside <ProjectsProvider>.

🔧 Proposed fix
         <LocalStateProvider>
-          <ProjectsProvider>
-          <OpenAIProvider>
-            <QueryClientProvider client={queryClient}>
-              <TooltipProvider>
-                <TTSProvider>
-                  <BillingServiceProvider>
-                    <ProxyEventListener />
-                    <UpdateEventListener />
-                    <DeepLinkHandler />
-                    <InnerApp />
-                  </BillingServiceProvider>
-                </TTSProvider>
-              </TooltipProvider>
-            </QueryClientProvider>
-          </OpenAIProvider>
-          </ProjectsProvider>
+          <ProjectsProvider>
+            <OpenAIProvider>
+              <QueryClientProvider client={queryClient}>
+                <TooltipProvider>
+                  <TTSProvider>
+                    <BillingServiceProvider>
+                      <ProxyEventListener />
+                      <UpdateEventListener />
+                      <DeepLinkHandler />
+                      <InnerApp />
+                    </BillingServiceProvider>
+                  </TTSProvider>
+                </TooltipProvider>
+              </QueryClientProvider>
+            </OpenAIProvider>
+          </ProjectsProvider>
         </LocalStateProvider>
🤖 Fix all issues with AI agents
In `@frontend/src/components/ChatContextMenu.tsx`:
- Around line 53-63: The button in ChatContextMenu wrapping the MoreHorizontal
icon lacks an accessible label, so update the <button> element inside the
ChatContextMenu component to include an appropriate aria-label (e.g., "Open
message menu" or similar) so screen readers can announce its purpose; locate the
button using the MoreHorizontal icon and isMobile usage and add the aria-label
attribute and ensure it remains descriptive and concise.

In `@frontend/src/components/projects/ProjectDetailPage.tsx`:
- Around line 148-178: handleStartChat is using fragile nested setTimeouts,
direct history.replaceState, and a prototype value-setter hack to push a message
into UnifiedChat; replace this with a declarative handoff: stop manipulating the
DOM (remove document.getElementById("message"), HTMLTextAreaElement.prototype
access, manual form submit and history.replaceState), and instead navigate to
"/" with URL search params (e.g. project_id and initial_message) or write the
projectId + message into a shared context/store that UnifiedChat reads on mount;
update handleStartChat (referencing newChatInput, projectId, navigate) to
setNewChatInput("") then navigate({ to: "/", search:
`?project_id=${projectId}&initial_message=${encodeURIComponent(message)}` }) or
dispatch to the shared state, and change UnifiedChat to consume those
params/state and perform the focus/send action declaratively on mount.
- Around line 206-217: handleFileUpload currently only saves file metadata
(name, type, size) and discards the File blob; either persist the file content
or make the metadata-only limitation explicit. Update handleFileUpload to read
the file content (e.g., await file.text() or use FileReader) and pass that
content into addFile (extend addFile payload with a content or base64 field) if
you intend to store content, or alternatively annotate the saved metadata with a
metadataOnly: true flag and add a visible label "(metadata only — content not
yet stored)" in the UI where addFile-driven attachments are rendered so
users/developers know the limitation; adjust the addFile signature and any
consumers (and ProjectDetailPage’s render) accordingly.
- Around line 124-144: The useEffect that loads conversations depends on
project?.chatIds.length which misses cases where the array contents change but
length stays the same; update the dependency to track the actual IDs (e.g.,
depend on project?.chatIds or a stable serialization like
JSON.stringify(project?.chatIds) or [...(project?.chatIds || [])]) so
fetchConversations runs whenever chat IDs change; ensure you still include
opensecret in the dependency array and keep the same logic inside
fetchConversations/setConversationMap to populate the conversationMap.

In `@frontend/src/components/projects/ProjectsList.tsx`:
- Around line 152-166: handleRenameChat dispatches a
CustomEvent("conversationrenamed") after updating the server but no component
listens for it, so the local conversations state in ChatHistoryList never
updates; either add an event listener in ChatHistoryList that listens for
"conversationrenamed" and updates its conversations state (or triggers a
refresh) when detail.conversationId/ title are received, or instead of
dispatching the CustomEvent from handleRenameChat call the existing cache/query
invalidation routine (e.g., the same function ChatHistoryList uses to refresh
conversations) so the sidebar refreshes immediately after
openai.conversations.update completes.
- Around line 25-33: The local Conversation interface in ProjectsList.tsx is
duplicated; remove this local definition and import the shared Conversation type
from ChatHistoryList.tsx (which exports Conversation) instead; update the top of
ProjectsList.tsx to add an import for Conversation from ChatHistoryList, delete
the interface block, and ensure all usages in ProjectsList (and any references
to ConversationData elsewhere) use the imported Conversation type so the file
relies on the single source of truth exported by ChatHistoryList.

In `@frontend/src/components/UnifiedChat.tsx`:
- Around line 2349-2359: The block that auto-assigns a chat to a project (uses
projectParam, conversationId, CustomEvent with "assignchattoproject", and
window.history.replaceState) has lines exceeding the 100-char Prettier limit;
refactor the long lines by breaking the CustomEvent creation and the
replaceState call into multiple shorter statements (e.g., create a const event =
new CustomEvent(...) on its own line and build the cleaned URL string on a
separate line) to satisfy formatting rules, then run just format, just lint, and
just build to verify and commit the reformatted changes.

In `@frontend/src/state/ProjectsContext.tsx`:
- Around line 130-145: The setProjects updater in deleteProject currently
performs a localStorage write (using DELETED_CHATS_KEY) inside the state
updater, which is a side effect and unsafe in StrictMode; refactor deleteProject
to first find the target project from the outer projects state (e.g. const
project = projects.find(p => p.id === projectId)), perform the localStorage
read/merge/write for project.chatIds outside the setProjects callback (wrapped
in try/catch), then call setProjects(prev => prev.filter(p => p.id !==
projectId)) to update state; also add projects to the deleteProject dependency
array so the finder uses current state.
🧹 Nitpick comments (10)
frontend/src/components/Sidebar.tsx (2)

46-48: excludeChatIds is a new Set on every render, defeating downstream memoization.

getAllAssignedChatIds(), getDeletedChatIds(), and the spread into a new Set all run every render, producing a fresh object reference each time. This causes filteredConversations in ChatHistoryList (which depends on excludeChatIds) to recompute on every parent re-render, even when nothing changed.

Consider memoizing this:

♻️ Suggested fix
-  const assignedIds = getAllAssignedChatIds();
-  const deletedIds = getDeletedChatIds();
-  const excludeChatIds = new Set([...assignedIds, ...deletedIds]);
+  const assignedIds = getAllAssignedChatIds();
+  const deletedIds = getDeletedChatIds();
+  const excludeChatIds = useMemo(
+    () => new Set([...assignedIds, ...deletedIds]),
+    [assignedIds, deletedIds]
+  );

Note: this alone isn't sufficient since getAllAssignedChatIds and getDeletedChatIds also return new Set instances on every call (see ProjectsContext). You'd need stable references there too (e.g., memoize the returned sets in the context) for this to be truly effective.


54-61: Custom DOM events for cross-component communication are fragile.

The createprojectforchat event uses an untyped CustomEvent with a detail.chatId property, relying on string-based event names. This pattern is used in multiple places across the PR (also assignchattoproject, conversationselected, etc.) and is hard to refactor, type-check, or trace.

For a prototype this is acceptable, but before graduating this feature consider replacing these with a more traceable pattern (e.g., a shared event bus with typed events, or lifting callbacks through context).

frontend/src/components/projects/ProjectDetailPage.tsx (2)

40-46: ConversationData duplicates the Conversation type from ChatHistoryList.

This interface is essentially the same shape as Conversation exported from ChatHistoryList.tsx (and also duplicated in ProjectsList.tsx). Consider importing the shared type to avoid drift.


226-235: handleSelectConversation is duplicated across three components.

This exact pattern (build URL params → pushState → dispatch conversationselected event) appears in ProjectDetailPage, ProjectsList, and ChatHistoryList. Extract it into a shared utility or hook to reduce duplication and ensure consistent behavior.

frontend/src/components/ChatContextMenu.tsx (1)

66-116: Sliding panel approach may cause height jumps between main menu and submenu.

When the main menu is active, the submenu is position: absolute (out of flow), and vice versa. If the two panels have different heights, the dropdown will snap between heights on transition. This can cause a jarring visual effect, especially when the project list is long.

Consider setting a min-height on the container based on the taller panel, or measuring both panels and animating height.

Also applies to: 119-161

frontend/src/components/ChatHistoryList.tsx (2)

869-890: Duplicated visibility condition for Recents section and loading indicator.

The expression (isRecentsExpanded || (searchQuery.trim() && filteredConversations.length > 0)) appears at Lines 891 and 967. Extract it to a const for readability and to avoid divergence:

♻️ Suggested improvement
+ const showRecents = isRecentsExpanded || (searchQuery.trim() && filteredConversations.length > 0);
+
  {/* Recents header */}
  {filteredConversations.length > 0 && (
    <button
      onClick={toggleRecentsExpanded}
      ...
    >
      <span className="font-medium">Recents</span>
      <span
        className={
-          !(isRecentsExpanded || (searchQuery.trim() && filteredConversations.length > 0)) || isMobile
+          !showRecents || isMobile
            ? ""
            : "opacity-0 group-hover/header:opacity-100 transition-opacity"
        }
      >
-       {isRecentsExpanded || (searchQuery.trim() && filteredConversations.length > 0) ? (
+       {showRecents ? (
          <ChevronDown className="h-3.5 w-3.5" />
        ...

And similarly at line 967.

Also applies to: 891-891, 967-967


516-521: onConversationsLoaded effect may cause unnecessary updates.

This effect fires every time conversations or onConversationsLoaded changes. If onConversationsLoaded were ever an unstable reference (e.g., an inline arrow function instead of setConversations), it would trigger an infinite loop. Currently safe because Sidebar passes setConversations directly, but this coupling is implicit.

Consider guarding with a ref or adding a comment clarifying the stability requirement.

frontend/src/state/ProjectsContext.tsx (3)

225-249: getAllAssignedChatIds and getDeletedChatIds create new objects every call, preventing downstream memoization.

Both functions return a new Set each time they are invoked. Since Sidebar calls them on every render to build excludeChatIds, no amount of downstream useMemo will help — the inputs are always "new."

Memoize the results inside the provider:

♻️ Suggested fix
- const getAllAssignedChatIds = useCallback((): Set<string> => {
-   const ids = new Set<string>();
-   for (const p of projects) {
-     for (const chatId of p.chatIds) {
-       ids.add(chatId);
-     }
-   }
-   return ids;
- }, [projects]);
+ const allAssignedChatIds = useMemo(() => {
+   const ids = new Set<string>();
+   for (const p of projects) {
+     for (const chatId of p.chatIds) {
+       ids.add(chatId);
+     }
+   }
+   return ids;
+ }, [projects]);
+
+ const getAllAssignedChatIds = useCallback(
+   () => allAssignedChatIds,
+   [allAssignedChatIds]
+ );

Similarly for getDeletedChatIds — though that one reads localStorage directly, so consider tracking deleted IDs in React state instead.


211-216: moveChatToProject is an unnecessary wrapper around assignChatToProject.

It delegates entirely to assignChatToProject with the same signature. Consider removing it and using assignChatToProject directly.


61-68: No schema validation on localStorage.getItem parse — corrupt data will silently produce invalid state.

loadProjects parses localStorage JSON but doesn't validate the shape. If the stored data is corrupt or from an incompatible version (e.g., missing chatIds field), downstream code will throw when accessing project.chatIds.length, project.files.map(), etc.

Consider adding minimal validation (e.g., Array.isArray(parsed) && parsed.every(p => p.id && Array.isArray(p.chatIds))) or wrapping access with defaults.

Comment on lines +53 to +63
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Trigger button lacks an accessible label.

The <button> wrapping <MoreHorizontal /> has no aria-label, so screen readers will announce it as an unlabeled button. Add an aria-label for accessibility.

♻️ Suggested fix
         <button
           className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
             isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
           }`}
+          aria-label="Chat actions"
           onClick={(e) => {
             e.preventDefault();
             e.stopPropagation();
           }}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
<button
className={`z-50 bg-background/80 absolute right-2 top-1/2 transform -translate-y-1/2 text-primary transition-opacity p-2 ${
isMobile ? "opacity-100" : "opacity-0 group-hover:opacity-100"
}`}
aria-label="Chat actions"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<MoreHorizontal size={16} />
</button>
🤖 Prompt for AI Agents
In `@frontend/src/components/ChatContextMenu.tsx` around lines 53 - 63, The button
in ChatContextMenu wrapping the MoreHorizontal icon lacks an accessible label,
so update the <button> element inside the ChatContextMenu component to include
an appropriate aria-label (e.g., "Open message menu" or similar) so screen
readers can announce its purpose; locate the button using the MoreHorizontal
icon and isMobile usage and add the aria-label attribute and ensure it remains
descriptive and concise.

Comment on lines +124 to +144
useEffect(() => {
if (!opensecret || !project?.chatIds.length) return;
let cancelled = false;

async function fetchConversations() {
try {
const response = await opensecret.listConversations({ limit: 100 });
if (cancelled) return;
const map = new Map<string, ConversationData>();
for (const conv of response.data || []) {
map.set(conv.id, conv);
}
setConversationMap(map);
} catch {
// Silently fail — names just won't show
}
}

fetchConversations();
return () => { cancelled = true; };
}, [opensecret, project?.chatIds.length]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

useEffect dependency on project?.chatIds.length misses same-length mutations.

If a chat is removed and another added in the same operation (length unchanged), the effect won't re-run and the conversationMap will be stale. Depend on the chat IDs themselves instead:

♻️ Suggested fix
-  }, [opensecret, project?.chatIds.length]);
+  }, [opensecret, project?.chatIds.join(",")]);

Alternatively, use JSON.stringify(project?.chatIds) or spread the array into the dependency.

🤖 Prompt for AI Agents
In `@frontend/src/components/projects/ProjectDetailPage.tsx` around lines 124 -
144, The useEffect that loads conversations depends on project?.chatIds.length
which misses cases where the array contents change but length stays the same;
update the dependency to track the actual IDs (e.g., depend on project?.chatIds
or a stable serialization like JSON.stringify(project?.chatIds) or
[...(project?.chatIds || [])]) so fetchConversations runs whenever chat IDs
change; ensure you still include opensecret in the dependency array and keep the
same logic inside fetchConversations/setConversationMap to populate the
conversationMap.

Comment on lines 148 to 178
const handleStartChat = useCallback(() => {
const message = newChatInput.trim();
if (!message) return;
setNewChatInput("");
// Navigate to home with project_id param so UnifiedChat can auto-assign
navigate({ to: "/" });
setTimeout(() => {
window.history.replaceState(null, "", `/?project_id=${projectId}`);
window.dispatchEvent(new Event("newchat"));
// Set the message to be sent
setTimeout(() => {
const textarea = document.getElementById("message") as HTMLTextAreaElement;
if (textarea) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
)?.set;
nativeInputValueSetter?.call(textarea, message);
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.focus();
// Auto-submit after a brief delay
setTimeout(() => {
const form = textarea.closest("form");
if (form) {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
}
}, 50);
}
}, 100);
}, 0);
}, [newChatInput, projectId, navigate]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

handleStartChat relies on fragile nested timeouts and React-internal hacks — high breakage risk.

This function:

  1. Navigates to "/", then uses setTimeout(…, 0) to wait for render.
  2. Manually sets window.history to inject project_id.
  3. Accesses HTMLTextAreaElement.prototype.value setter via Object.getOwnPropertyDescriptor — a React internals hack to bypass controlled-input behavior.
  4. Chains another setTimeout(…, 100) for DOM lookup, then setTimeout(…, 50) for synthetic form submission.

This is inherently timing-dependent and will silently break if UnifiedChat's DOM changes (e.g., textarea ID, form structure) or if navigation timing shifts. The nativeInputValueSetter hack is especially fragile and has been known to break across React versions.

A more robust approach would be to use shared state (context or URL search params) to pass both project_id and the initial message, letting UnifiedChat consume them declaratively on mount.

🤖 Prompt for AI Agents
In `@frontend/src/components/projects/ProjectDetailPage.tsx` around lines 148 -
178, handleStartChat is using fragile nested setTimeouts, direct
history.replaceState, and a prototype value-setter hack to push a message into
UnifiedChat; replace this with a declarative handoff: stop manipulating the DOM
(remove document.getElementById("message"), HTMLTextAreaElement.prototype
access, manual form submit and history.replaceState), and instead navigate to
"/" with URL search params (e.g. project_id and initial_message) or write the
projectId + message into a shared context/store that UnifiedChat reads on mount;
update handleStartChat (referencing newChatInput, projectId, navigate) to
setNewChatInput("") then navigate({ to: "/", search:
`?project_id=${projectId}&initial_message=${encodeURIComponent(message)}` }) or
dispatch to the shared state, and change UnifiedChat to consume those
params/state and perform the focus/send action declaratively on mount.

Comment on lines 206 to 217
const handleFileUpload = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
for (const file of Array.from(files)) {
const ext = file.name.split(".").pop()?.toLowerCase() || "file";
addFile(projectId, { name: file.name, type: ext, size: file.size });
}
e.target.value = "";
},
[projectId, addFile]
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

File upload only stores metadata — file content is discarded.

handleFileUpload records name, type, and size but never reads the File blob. The UI presents these as attached files, but they won't be available for chat context since the content isn't persisted (not even to localStorage or IndexedDB).

If this is intentional scaffolding for the prototype, add a visible indicator (e.g., "(metadata only — content not yet stored)") or a comment, so users and future developers understand the limitation.

🤖 Prompt for AI Agents
In `@frontend/src/components/projects/ProjectDetailPage.tsx` around lines 206 -
217, handleFileUpload currently only saves file metadata (name, type, size) and
discards the File blob; either persist the file content or make the
metadata-only limitation explicit. Update handleFileUpload to read the file
content (e.g., await file.text() or use FileReader) and pass that content into
addFile (extend addFile payload with a content or base64 field) if you intend to
store content, or alternatively annotate the saved metadata with a metadataOnly:
true flag and add a visible label "(metadata only — content not yet stored)" in
the UI where addFile-driven attachments are rendered so users/developers know
the limitation; adjust the addFile signature and any consumers (and
ProjectDetailPage’s render) accordingly.

Comment on lines +25 to +33
interface Conversation {
id: string;
object: "conversation";
created_at: number;
metadata?: {
title?: string;
[key: string]: unknown;
};
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Conversation interface is duplicated in three files.

This exact interface exists in ChatHistoryList.tsx (which exports it at line 1077), ProjectDetailPage.tsx (as ConversationData), and here. Import from ChatHistoryList to have a single source of truth.

♻️ Suggested fix
-interface Conversation {
-  id: string;
-  object: "conversation";
-  created_at: number;
-  metadata?: {
-    title?: string;
-    [key: string]: unknown;
-  };
-}
+import type { Conversation } from "@/components/ChatHistoryList";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface Conversation {
id: string;
object: "conversation";
created_at: number;
metadata?: {
title?: string;
[key: string]: unknown;
};
}
import type { Conversation } from "@/components/ChatHistoryList";
🤖 Prompt for AI Agents
In `@frontend/src/components/projects/ProjectsList.tsx` around lines 25 - 33, The
local Conversation interface in ProjectsList.tsx is duplicated; remove this
local definition and import the shared Conversation type from
ChatHistoryList.tsx (which exports Conversation) instead; update the top of
ProjectsList.tsx to add an import for Conversation from ChatHistoryList, delete
the interface block, and ensure all usages in ProjectsList (and any references
to ConversationData elsewhere) use the imported Conversation type so the file
relies on the single source of truth exported by ChatHistoryList.

Comment on lines +152 to +166
const handleRenameChat = useCallback(
async (chatId: string, newTitle: string) => {
if (!openai) return;
await openai.conversations.update(chatId, {
metadata: { title: newTitle }
});
// Dispatch event so ChatHistoryList and other consumers refresh
window.dispatchEvent(
new CustomEvent("conversationrenamed", {
detail: { conversationId: chatId, title: newTitle }
})
);
},
[openai]
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

conversationrenamed event appears to have no listener.

handleRenameChat dispatches a CustomEvent("conversationrenamed", …) after the API call succeeds, but none of the provided files listen for this event. The rename will persist server-side but the sidebar's local conversations state won't update — the old title will show until the next poll (up to 60s).

Either add a listener in ChatHistoryList to handle this event, or trigger a query invalidation instead.

🤖 Prompt for AI Agents
In `@frontend/src/components/projects/ProjectsList.tsx` around lines 152 - 166,
handleRenameChat dispatches a CustomEvent("conversationrenamed") after updating
the server but no component listens for it, so the local conversations state in
ChatHistoryList never updates; either add an event listener in ChatHistoryList
that listens for "conversationrenamed" and updates its conversations state (or
triggers a refresh) when detail.conversationId/ title are received, or instead
of dispatching the CustomEvent from handleRenameChat call the existing
cache/query invalidation routine (e.g., the same function ChatHistoryList uses
to refresh conversations) so the sidebar refreshes immediately after
openai.conversations.update completes.

Comment on lines 2349 to 2359

// Auto-assign to project if project_id param is present
const projectParam = new URLSearchParams(window.location.search).get("project_id");
if (projectParam) {
window.dispatchEvent(new CustomEvent("assignchattoproject", {
detail: { chatId: conversationId, projectId: projectParam }
}));
const cleaned = new URLSearchParams(window.location.search);
cleaned.delete("project_id");
window.history.replaceState(null, "", `${window.location.pathname}?${cleaned.toString()}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Auto-assign logic is sound; fix formatting for Prettier.

The event-based project assignment and URL cleanup work correctly. window.location.search at this point already has conversation_id from the replaceState on Line 2341, so the cleaned params won't be empty.

However, this block likely contributes to the Prettier pipeline failure — the new CustomEvent(...) and replaceState lines appear to exceed the 100-character line limit. Run just format to resolve. As per coding guidelines, "Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation."

🤖 Prompt for AI Agents
In `@frontend/src/components/UnifiedChat.tsx` around lines 2349 - 2359, The block
that auto-assigns a chat to a project (uses projectParam, conversationId,
CustomEvent with "assignchattoproject", and window.history.replaceState) has
lines exceeding the 100-char Prettier limit; refactor the long lines by breaking
the CustomEvent creation and the replaceState call into multiple shorter
statements (e.g., create a const event = new CustomEvent(...) on its own line
and build the cleaned URL string on a separate line) to satisfy formatting
rules, then run just format, just lint, and just build to verify and commit the
reformatted changes.

Comment on lines +130 to +145
const deleteProject = useCallback((projectId: string) => {
setProjects((prev) => {
const project = prev.find((p) => p.id === projectId);
if (project && project.chatIds.length > 0) {
// Mark project's chats as deleted in localStorage
try {
const existing = JSON.parse(localStorage.getItem(DELETED_CHATS_KEY) || "[]");
const updated = [...new Set([...existing, ...project.chatIds])];
localStorage.setItem(DELETED_CHATS_KEY, JSON.stringify(updated));
} catch {
// ignore
}
}
return prev.filter((p) => p.id !== projectId);
});
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Side effect (localStorage write) inside setProjects updater — unsafe in StrictMode.

The deleteProject callback performs a localStorage.setItem inside the function passed to setProjects. React state updater functions should be pure — in StrictMode, React may invoke them twice, causing the deleted chat IDs to be written to localStorage redundantly (in this case idempotent due to Set dedup, but still an anti-pattern).

Move the side effect outside the updater:

♻️ Suggested fix
  const deleteProject = useCallback((projectId: string) => {
+   const project = projects.find((p) => p.id === projectId);
+   if (project && project.chatIds.length > 0) {
+     try {
+       const existing = JSON.parse(localStorage.getItem(DELETED_CHATS_KEY) || "[]");
+       const updated = [...new Set([...existing, ...project.chatIds])];
+       localStorage.setItem(DELETED_CHATS_KEY, JSON.stringify(updated));
+     } catch {
+       // ignore
+     }
+   }
    setProjects((prev) => {
-     const project = prev.find((p) => p.id === projectId);
-     if (project && project.chatIds.length > 0) {
-       try {
-         const existing = JSON.parse(localStorage.getItem(DELETED_CHATS_KEY) || "[]");
-         const updated = [...new Set([...existing, ...project.chatIds])];
-         localStorage.setItem(DELETED_CHATS_KEY, JSON.stringify(updated));
-       } catch {
-         // ignore
-       }
-     }
      return prev.filter((p) => p.id !== projectId);
    });
- }, []);
+ }, [projects]);

Note: this requires adding projects to the dependency array since we now read projects outside the updater.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deleteProject = useCallback((projectId: string) => {
setProjects((prev) => {
const project = prev.find((p) => p.id === projectId);
if (project && project.chatIds.length > 0) {
// Mark project's chats as deleted in localStorage
try {
const existing = JSON.parse(localStorage.getItem(DELETED_CHATS_KEY) || "[]");
const updated = [...new Set([...existing, ...project.chatIds])];
localStorage.setItem(DELETED_CHATS_KEY, JSON.stringify(updated));
} catch {
// ignore
}
}
return prev.filter((p) => p.id !== projectId);
});
}, []);
const deleteProject = useCallback((projectId: string) => {
const project = projects.find((p) => p.id === projectId);
if (project && project.chatIds.length > 0) {
try {
const existing = JSON.parse(localStorage.getItem(DELETED_CHATS_KEY) || "[]");
const updated = [...new Set([...existing, ...project.chatIds])];
localStorage.setItem(DELETED_CHATS_KEY, JSON.stringify(updated));
} catch {
// ignore
}
}
setProjects((prev) => {
return prev.filter((p) => p.id !== projectId);
});
}, [projects]);
🤖 Prompt for AI Agents
In `@frontend/src/state/ProjectsContext.tsx` around lines 130 - 145, The
setProjects updater in deleteProject currently performs a localStorage write
(using DELETED_CHATS_KEY) inside the state updater, which is a side effect and
unsafe in StrictMode; refactor deleteProject to first find the target project
from the outer projects state (e.g. const project = projects.find(p => p.id ===
projectId)), perform the localStorage read/merge/write for project.chatIds
outside the setProjects callback (wrapped in try/catch), then call
setProjects(prev => prev.filter(p => p.id !== projectId)) to update state; also
add projects to the deleteProject dependency array so the finder uses current
state.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@AnthonyRonning AnthonyRonning marked this pull request as draft February 11, 2026 23:54
@AnthonyRonning
Copy link
Contributor

not bad, appreciate you doing that.

i have a few thoughts:

  1. shared files context, this significantly increases scope here. We really only support a single doc at a time. And it's part of the context. We're really not well equiped to be able to handle multi document, let along shared across chats.
  2. it's weird that a chat list is showing up under the chatbox. i think the chatbox is better off at the bottom where it typically is and the chat list either above it or to the right panel too
  3. Mobile view. Has all of the flows and design been tested for a mobile size view?

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 12, 2026

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: b7d4bc2
Status: ✅  Deploy successful!
Preview URL: https://e2cd86cb.maple-ca8.pages.dev
Branch Preview URL: https://feature-projects-prototype.maple-ca8.pages.dev

View logs

@marksftw
Copy link
Contributor Author

marksftw commented Feb 12, 2026

not bad, appreciate you doing that.

i have a few thoughts:

  1. shared files context, this significantly increases scope here. We really only support a single doc at a time. And it's part of the context. We're really not well equiped to be able to handle multi document, let along shared across chats.

For the UX perspective, it would be awesome to add multiple files into the context. From the technical perspective, I can understand the limitation right now. The feature is still very useful without the file attachment feature. Would we be able to do a single file or should we just leave it to the user to attach the file to a single chat like they already do?

  1. it's weird that a chat list is showing up under the chatbox. i think the chatbox is better off at the bottom where it typically is and the chat list either above it or to the right panel too

Yeah I can see that. Both ChatGPT and Claude have it above like this, so I was following their lead. That said, putting the chat box below could be fine.

  1. Mobile view. Has all of the flows and design been tested for a mobile size view?

Yes, I tested it on mobile and just pushed a fix for a small layout issue. Everything else works great on mobile.

@AnthonyRonning
Copy link
Contributor

I'm not a fan of attempting to extract just the chatbox out of the main display. This isn't an extractable component and it's designed this way for a reason.

I think I would prefer there to be something like a folder icon on the chatbox that you can auto assign a new chat to a project. And maybe you can have a "new chat" button on the project page that jumps to the main page but preloads the correct project.

@marksftw
Copy link
Contributor Author

I'm not a fan of attempting to extract just the chatbox out of the main display. This isn't an extractable component and it's designed this way for a reason.

I think I would prefer there to be something like a folder icon on the chatbox that you can auto assign a new chat to a project. And maybe you can have a "new chat" button on the project page that jumps to the main page but preloads the correct project.

I'm good with that solution. I'll adjust the prototype with that behavior.

@marksftw
Copy link
Contributor Author

Made improvements to the UX:

  1. Main chatbox has a Project selector dropdown (folder icon)
  2. Project Page simplified: chat input replaced with "New chat in {project}" button
  3. Files section removed from Project Page
  4. Sidebar cleanup: project chats collapsed by default, auto-expand on active chat (similar to ChatGPT behavior)
  5. Layout/responsive improvements for the Project Page

@AnthonyRonning
Copy link
Contributor

hmm, looks like cloudflare didn't publish it? can you do an empty commit and push when you have a chance

Move project selection from a duplicated chat input on the Project Page
into a folder icon dropdown in the main chat toolbar. Simplify the
Project Page to show a "New chat" button instead. Collapse project chats
in the sidebar by default, expanding only when active. Add date
subtitles and separators to the project chat list. Remove Files section.
@marksftw marksftw force-pushed the feature/projects-prototype branch from 6ffb5a5 to b7d4bc2 Compare February 13, 2026 14:40
@marksftw
Copy link
Contributor Author

hmm, looks like cloudflare didn't publish it? can you do an empty commit and push when you have a chance

yep, done

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants