Conversations, Projects & Search

Organize chats into projects, search everything, fork, and delete turns

Every conversation is an append-only transcript you can group into projects, search full-text, fork from any turn, or trim a message at a time.

python
from praisonaiagents import Agent

agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
)
# Each Desktop conversation is one transcript this agent appends to.
agent.start("Start a new conversation")
Diagram

💬 Conversations

📁 Projects

🔍 ⌘K Search

🌿 Fork

Quick Start

  1. Group into a project

    Right-click a conversation and choose Move to project. An in-app text prompt appears — type a project name (or leave it blank to remove the chat from its current project) and press OK. Press Esc or click outside to cancel without changing anything. If the prompt never appears on an older build, see Troubleshooting.

  2. Search everything

    Press ⌘K and type — the app searches every conversation’s text and jumps to the first match.

  3. Fork from a turn

    Fork any turn to branch a new conversation without changing the original.

The Move to project prompt routes on what you type:

Diagram
graph LR
    Right[🖱️ Right-click chat] --> Menu[📋 Move to project]
    Menu --> Ask[📝 In-app text prompt]
    Ask -->|Name entered| Post[📨 POST /project/{cid}]
    Ask -->|Blank| Remove[🧹 Remove from project]
    Ask -->|Cancel / Esc| None[⛔ No request]

    classDef action fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef ui fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Right,Menu action
    class Ask ui
    class Post,Remove ok
    class None warn

How It Works

Transcripts are append-only JSON on disk, written with a write-then-rename pattern so a crash mid-write can never truncate an existing chat.

Diagram

💬 Completed turn

chat.tmp

rename → chat.json

📁 chats/

Transcripts live at ~/Library/Application Support/PraisonAI/chats/ as one JSON file per conversation.


Reopening a chat restores history

The transcript you see on screen and the history the model reads are two different things — reopening a chat now aligns them.

  • The transcript on disk is what the sidebar renders.
  • The agent’s in-memory chat_history is what the model actually sees.
  • Reopen a chat after a restart, a settings change, or toggling tools, and the transcript is replayed into the fresh agent so the model has the same context you can read on screen.
  • Blank or whitespace-only assistant messages (left behind by failed turns) are skipped, so the model is never taught that silence is a valid answer.
  • A mid-session agent already holds those turns and is not replayed over — nothing is duplicated.
Diagram

💾 Transcript

🔁 Reopen chat

🤖 Fresh Agent

🧠 Model sees history


Actions

ActionEndpointEffect
SearchGET /search?q=Full-text across every conversation
ForkPOST /fork/{cid}/{idx}Copies the transcript up to message N into a new chat
Delete messageDELETE /messages/{cid}/{idx}Removes one exchange (a user turn and its reply)
Move to projectPOST /project/{cid}Assigns the conversation to a project; a blank name removes it from its current project
Delete chatDELETE /chats/{cid}Removes the whole conversation

Deleting a conversation

The app now tells the truth about a delete: the screen follows what the engine actually did on disk, not the click.

Answering 200 OK for every case was how a chat closed on screen and reappeared on refresh — the sidebar delete and Clear-all now check the engine’s answer first.

  • The sidebar delete follows the server’s answer. If the engine refuses the delete — a read-only data dir, a permission-changed folder, or a synced folder mid-conflict — the row stays, the open transcript stays, and a toast reads “Could not delete that conversation.” On success, the transcript is cleared only if the deleted chat was the active one; the sidebar refreshes either way.
  • Clear-all tolerates partial failure. Every conversation that couldn’t be deleted is counted, and a toast reads “Could not delete N conversation(s).” The active transcript is blanked when its own delete succeeded, even if another one failed — the transcript on screen follows disk state, not the batch result.
Diagram

Yes

No

🗑️ Delete chat

🧠 Engine deleted it?

✅ Transcript cleared if active

⚠️ Row + transcript kept

🔔 Could not delete toast

The raw response codes behind these paths — 200, 400, 500 — are in Engine API.


Ranked ⌘K search

⌘K search delegates to the library’s SqliteSessionStore (FTS5/bm25) over your on-disk chats instead of a substring scan.

Diagram

💬 ⌘K query

🗃️ In-memory index

🎯 FTS5 + bm25

📊 Ranked + snippets

  • Ranked by FTS5/bm25 relevance instead of directory order.
  • Each result carries a snippet centred on the match.
  • The index is rebuilt in-memory per query, so an edited chat is never served stale.
  • Falls back to the old substring scan if SqliteSessionStore is unavailable, so search never breaks.

The same engine powers the terminal — see Session Search.


Best Practices

Fork instead of editing history

A fork copies the transcript up to a chosen turn into a new conversation and leaves the original untouched — branch an idea without losing the thread.

Group with projects

Move related conversations into a project so the sidebar stays legible as chats accumulate.

Trust auto-titling

With auto_title on, the first message becomes the chat title. Leave it on and rename only the few that need it.


Related