Skip to content

feat(chats): add repository-less chats to the Manager UI and API - #483

Open
chriswritescode-dev wants to merge 6 commits into
mainfrom
om-29-scratch-chats-without-a-repository
Open

chriswritescode-dev wants to merge 6 commits into
mainfrom
om-29-scratch-chats-without-a-repository

Conversation

@chriswritescode-dev

@chriswritescode-dev chriswritescode-dev commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

User description

Summary

  • Adds chats: plain local OpenCode sessions in a dated, empty folder each, with no repository, worktree or schedule.
  • Chats get their own page at /chats/:chatId/sessions/:sessionId with transcript, composer, details, files, terminal and preview, and no repository tools.
  • Chats are reachable from a Chats group at the top of the desktop sidebar and a fourth Chats tab in the session picker; notifications and prompts for a chat open that chat.
  • Delete only; no archive. Chats are distinct from Assistant mode.

Change

  • shared/src/schemas/chats.ts: chat and chat-folder schemas plus the workspace path helper.
  • backend/src/services/chats.ts, backend/src/routes/chats.ts: ChatService owns chats/<chatId> folders; /api/chats lists paged, gets, creates and deletes chats, removing the folder and its terminals with the last session.
  • backend/src/routes/chat-terminals.ts: chat terminals use the existing terminal stack in the chat folder.
  • backend/src/services/notification.ts: chat sessions resolve to their chat so notification links open the right page.
  • Frontend: useChats and the chat API client; ChatsNavGroup in the desktop sidebar; ChatPickerContent and the fourth session-picker tab; SessionDetail chat scope with terminal-target support; activity, builtin commands and More-sheet items hide repository-only actions for chats.
  • Tests: backend route/service/terminal tests and frontend hook/component/page tests cover the new paths.

Validation

  • pnpm typecheck (cli, frontend, backend) and shared tsc --noEmit: clean
  • pnpm lint: 0 errors (41 pre-existing warnings)
  • pnpm test: CLI 456/456; frontend 324 files / 4160 tests; backend 4475 pass, 3 sandbox failures from the host CPU limit in untouched files; coverage run excluding those files 222 files / 4430 tests, 80% thresholds hold; pnpm build exit 0
  • Browser, production build with real OpenCode v2.2.5: create/list/get/delete chats and folders, cross-chat delete rejected, terminal runs in the chat folder, sidebar group and picker tab work, chat page shows only chat tools

Notes

  • Chats are delete-only; archive is out of scope.
  • Mobile support consumes /api/chats separately.

Checklist

  • Code follows project style (doc comments only, named imports)
  • TypeScript types are properly defined
  • Tests added/updated (80% coverage target)
  • pnpm lint passes locally
  • pnpm typecheck passes locally

PR Type

Enhancement, Tests


Description

  • Add repository-less chats as dated workspace folders

  • Expose /api/chats list, get, create, delete-session

  • Chat session page with transcript, terminals, files, preview

  • Sidebar Chats group and fourth session-picker tab

  • Route notifications, prompts and shortcuts into chats

  • Cover chats with backend, hook and UI tests


Diagram Walkthrough

flowchart LR
  UI["Chats sidebar group and picker tab"] -- "create, list, delete" --> API["/api/chats routes"]
  API -- "owns" --> SVC["ChatService chat folders"]
  SVC -- "creates sessions" --> OC["OpenCode client"]
  UI -- "terminal target chat" --> TERM["Chat terminal routes"]
  TERM -- "resolve chat directory" --> SVC
  SVC -- "directory lookup" --> NOTIF["Notification deep links"]
  SVC -- "filesPath and directory" --> DETAIL["SessionDetail chat scope"]
Loading

File Walkthrough

Relevant files
Enhancement
15 files
chats.ts
Define chat schemas, id pattern and response types             
+60/-0   
repo.ts
Build chat session paths and read chat ids from directories
+27/-0   
chats.ts
ChatService owns chat folders and session lifecycle           
+191/-0 
chats.ts
Add paged chat list, get, create and session delete routes
+63/-0   
chat-terminals.ts
Chat terminal REST and websocket routes in chat folder     
+144/-0 
notification.ts
Resolve chat folders to chat notification links                   
+24/-7   
useChats.ts
Chat folder pagination, session rows and mutations             
+181/-0 
chats.ts
Add chat API client for list, get, create, delete               
+40/-0   
ChatsNavGroup.tsx
Chats sidebar group with sessions and paging                         
+79/-0   
ChatPickerContent.tsx
Chat tab content for shared session picker                             
+217/-0 
SessionPickerDialog.tsx
Add Chats tab and remote-aware tab cycle                                 
+40/-276
SessionDetail.tsx
Chat session scope with terminal target and delete action
+148/-78
EventContext.tsx
Resolve local session path for repo or chat                           
+17/-17 
navigation.ts
Parse and build chat session routes                                           
+26/-2   
activeWork.ts
Surface chat sessions as active work rows                               
+39/-13 
Configuration changes
3 files
env.ts
Add chats workspace directory path helper                               
+2/-0     
index.ts
Wire chat service, routes and chats directory bootstrap   
+10/-1   
command.ts
Include chats root in sandbox project roots                           
+2/-1     
Refactor
1 files
SessionPickerList.tsx
Extract reusable session picker list and row components   
+274/-0 
Tests
1 files
chats.test.ts
Cover chat folder creation, listing and deletion                 
+265/-0 
Additional files
39 files
chat-terminals.test.ts +340/-0 
chats.test.ts +132/-0 
notification-format.test.ts +7/-0     
notification-service.test.ts +42/-2   
command.test.ts +10/-1   
repo-url.test.ts +27/-0   
App.permission-dialog.test.tsx +21/-11 
App.push-subscription-sync.test.tsx +1/-1     
App.tsx +7/-2     
ChatsNavGroup.test.tsx +229/-0 
DesktopSessionTree.test.tsx +71/-1   
DesktopSessionTree.tsx +14/-10 
RepoSessionNav.tsx +52/-22 
moreDrawerItems.test.ts +9/-1     
moreDrawerItems.ts +14/-2   
sidebar-session-tree.test.ts +21/-0   
sidebar-session-tree.ts +17/-3   
SessionDetailsSheet.test.tsx +18/-1   
SessionDetailsSheet.tsx +74/-17 
SessionPickerDialog.test.tsx +208/-2 
SessionPickerSheet.test.tsx +3/-1     
EventContext.test.tsx +29/-6   
useChats.test.tsx +292/-0 
useCommands.test.tsx +23/-3   
useCommands.ts +6/-2     
useGlobalShortcutActions.test.tsx +26/-3   
useGlobalShortcutActions.ts +4/-2     
useOpenCode.ts +2/-2     
useSessionPickerNavigation.test.tsx +14/-0   
useSessionPickerNavigation.ts +7/-0     
useSidebarServerSelection.ts +4/-2     
activeWork.test.ts +29/-0   
builtinCommands.test.ts +48/-0   
builtinCommands.ts +20/-6   
navigation.test.ts +60/-1   
terminal-target.ts +25/-5   
SessionDetail.chat.test.tsx +284/-0 
defaults.ts +1/-0     
index.ts +1/-0     

Comment on lines +182 to +190
private toChat(id: string): Chat {
const directory = path.join(this.root(), id)
return {
id,
directory,
filesPath: path.relative(getReposPath(), directory),
createdAt: parseChatIdCreatedAt(id),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Files path escapes root

filesPath is path.relative(getReposPath(), directory), which gives ../chats/<id>. The frontend passes this as the file browser basePath and as repoLocalPath. File APIs that resolve paths against the repos root and block traversal will reject it, so Files and file links would break in chats. I can't see the file routes from this diff, so this needs a check against them. No test uses the real path: the frontend tests mock filesPath as /chats/<id>.

Comment on lines +133 to +151
await this.openCodeClient.api.session.remove({ sessionID: sessionId })
try {
const remaining = await this.openCodeClient.api.session.list({
directory: chat.directory,
limit: 1,
})

if (remaining.data.length > 0) {
return { deleted: true, folderRemoved: false }
}

try {
await this.terminals.removeAll(chat.directory)
} catch (error) {
logger.warn(`Failed to remove terminals for chat ${chatId}:`, error)
}

await fs.rm(chat.directory, { recursive: true, force: true })
return { deleted: true, folderRemoved: true }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Delete/create race

After removing the session, the code checks session.list({ limit: 1 }) and then deletes the whole chat folder recursively. Nothing stops another session being created in the same folder in between, for example through the chat page's "New session" or fork. That session would be left pointing at a deleted directory, and any files it wrote would be lost.

Comment on lines +1180 to +1194
{!remote && (
<TabsContent
value="chats"
className="mt-0 flex min-h-0 w-full min-w-0 flex-1 flex-col px-0"
>
<ChatPickerContent
open={open}
activeSessionID={activeSessionID}
onNavigate={onNavigate}
onDeleteActiveSession={onDeleteActiveSession}
searchInputRef={chatsSearchInputRef}
searchTrailing={chatsTrailing}
/>
</TabsContent>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inconsistent remote check

The Chats tab trigger and the tab-cycling logic use isRemote, but the Chats TabsContent is gated on remote. If the two ever differ, the remote picker could show a Chats tab, or mount chat content for a linked server even though its trigger is hidden. Using one flag for both would avoid that.

Comment on lines +130 to +141
const confirmDelete = useCallback(() => {
if (!chatToDelete) return
if (chatToDelete.session.id === activeSessionID) onDeleteActiveSession()
void deleteChatSession
.mutateAsync({
chatId: chatToDelete.chat.id,
sessionId: chatToDelete.session.id,
directory: chatToDelete.session.location.directory,
})
.catch(() => undefined)
.finally(() => setChatToDelete(null))
}, [chatToDelete, activeSessionID, onDeleteActiveSession, deleteChatSession])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Navigates before delete

onDeleteActiveSession() runs, and navigates away from the open chat, before the delete request finishes. If the request fails, for example with a 400 for a mismatched session or a network error, the session still exists but the user has already been sent to /.

@lil-worker-cstech

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
⚠️ Risk level: Medium
✅ Merge recommendation: Needs review
🔎 Failure modes

What: A chat folder could be deleted while a new session is still being created in it. That session would then point at a directory that no longer exists.
Where: backend/src/services/chats.ts:deleteSession
Trigger: A user deletes what looks like the last session in a chat. At the same moment, a new session or fork is created in that chat from the chat page. The new session is created between the `session.list` check and the `fs.rm` call.
Detected by: Nothing. No test covers concurrent create and delete in the same chat.
Covered in this PR: No

What: The chat file browser may fail to open files. A chat's `filesPath` is built relative to the repos root, so it becomes `../chats/<id>`. Any file route that rejects paths outside the repos root would refuse it.
Where: backend/src/services/chats.ts:toChat
Trigger: Opening the Files panel, or clicking a file link, in a chat session.
Detected by: Nothing in this PR. The frontend tests mock `filesPath` as `/chats/<id>` and never call the real file API.
Covered in this PR: No

What: Notifications for sessions outside any repo now do extra synchronous filesystem calls. Each one triggers a `findByDirectory` lookup that runs `canonicalPathSync` on every notification event.
Where: backend/src/services/notification.ts:handleSSEEvent
Trigger: Frequent notification events for sessions whose directory belongs to no repo, such as worktrees or arbitrary folders.
Detected by: Nothing. The cost would only show up as event-loop latency.
Covered in this PR: No

🧪 PR contains tests
🔒 No security concerns identified

Comment on lines +1180 to +1182
{!remote && (
<TabsContent
value="chats"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The Chats tab trigger, the tab cycle and the forced reset to sessions all use isRemote, but the Chats tab content uses the remote prop. If these values differ, the Chats tab can be shown with no content, or the chat list can be mounted for a linked server and query local chat data. Use isRemote here too. [possible bug, importance: 5]

Suggested change
{!remote && (
<TabsContent
value="chats"
{!isRemote && (
<TabsContent
value="chats"

Comment on lines +107 to +110
const { id, directory } = await this.createFolder(root)
try {
const model = input.model ? parseOpenCodeModelRef(input.model) : undefined
const session = await this.openCodeClient.api.session.create({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: An invalid model string is parsed only after the chat folder exists. The parse error then creates a folder, deletes it again, and is probably returned as a server error instead of a client error. Parse the model before creating the folder so bad input fails early and leaves nothing behind. [general, importance: 4]

Suggested change
const { id, directory } = await this.createFolder(root)
try {
const model = input.model ? parseOpenCodeModelRef(input.model) : undefined
const session = await this.openCodeClient.api.session.create({
const model = input.model ? parseOpenCodeModelRef(input.model) : undefined
const { id, directory } = await this.createFolder(root)
try {
const session = await this.openCodeClient.api.session.create({

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant