Skip to content

Chat

Chat in LatchAI is not a sidebar bolted onto an editor. It runs inside the engine, on the same provider, the same tool registry, the same multi-root filesystem, and the same event bus as workflow runs — which is why a chat turn can edit a file, run a test, and show up in the same review surfaces a scheduled run does.

Open it with ⌘⇧L, or from the ▤ chat button in the status bar. A conversation can also live as a tab in the editor area: New Chat Tab in the command palette starts one, and in the dock’s header moves the conversation you’re already having into a tab. A chat is in exactly one place at a time — dock or tab, never both.

The chat panel

Tool calls appear inline as collapsible rows; the full entries land in the events panel.

One dropdown at the bottom of the panel decides that, and it has three parts:

  • Default · <model> leads the list. It is the model pinned on the Models page, and picking it sends the literal name default — so the engine re-resolves the pin on every turn. Move the pin and this conversation follows it, with nothing to re-select.
  • Models — everything else in the Library: models downloaded to this Mac (listed as local:<name>, with marking the one currently loaded in memory and the ones that aren’t) and connected providers. Talking to a model directly gets LatchAI’s built-in assistant persona.
  • Agents — your agent definitions, grouped by their group frontmatter. Picking one swaps in its system prompt, its tool allowlist and its model for this conversation.

A local model for routine questions and a large hosted one for the hard problem is a per-conversation decision, not a global setting — and the conversation remembers it: a session’s model, agent, and permission mode are stored in its envelope, so reopening a chat restores the pickers to what you had.

Pick a local: model that isn’t resident and the panel says so rather than stalling: a line above the composer explains it will load on your next message, or offers you another model if the worker crashed.

Talking to a model directly, LatchAI’s assistant is deliberately conversation-first: it is told to answer in the chat rather than writing a file to explain itself, to prefer read-only tools while exploring, and to reserve write_file, edit_file and run_shell for changes you actually asked for — ending with a one-line list of the paths it touched.

It also gets a compact index of your workflows — one line each, with the node shape and any cron schedule — so “what runs overnight?” is answerable without reading a single file.

The system prompt is rebuilt every turn, so a CLAUDE.md you just edited, a mount you just added, and a browser extension you just paired are all live on the next message.

Three things reach the model as context, and it’s worth knowing which is which.

The active tab. The composer shows a chip naming what you’re looking at, and each message carries a description of it. For a file tab that description includes the buffer itself — head-capped at 4,000 characters, and marked as dirty when what you’re looking at differs from disk, so the model never answers about the saved copy of a file you have unsaved edits in. A file over 1 MB, or a binary, attaches its header only. It does not repeat: a conversation about one file sends that file once and a one-line “unchanged” note after that, until the buffer actually changes.

Other tabs send bounded facts rather than contents: a workflow tab, its id and name; a database query tab, the connection, the SQL in the editor, and the shape of the last result — columns and row count, never rows; a table tab, the table’s columns and keys; a work item, its kind, lane, fields, description, and last few activity lines.

@ mentions. Typing @ in the composer autocompletes against the file tree; up to five mentioned files are fetched and inlined, capped at 12,000 characters in total.

Images. Paste one and it attaches as a thumbnail and is sent as a vision block — the OpenAI-compatible provider expands it to an image_url part, the Anthropic provider to a base64 image block. Click any image in the transcript to open the Lightbox: a full-window view with a fit1:1 toggle, dismissed with Esc, the close button, or a click on the backdrop.

The tab description travels in a context envelope marked as environment, not as something you typed, and is stripped back out of the transcript when a session is reloaded — so re-opening a chat doesn’t show you the plumbing, and neither does the markdown mirror.

Type / as the first character and a popover lists the commands, filtered as you type. / move, Tab completes, Enter runs the selected one, Esc closes. A slash command is intercepted and never sent to the model.

Command Does
/rename <title> Rename this chat. With no title, opens the rename editor in place
/new Start a new chat
/stop Stop the turn that is running (says so if nothing is)
/mode <ask|allow> Show or set when this chat asks before running a tool
/help List every slash command

The parse is deliberately narrow: a line is a command only when it starts with / and its first token names one of the five above. /usr/local/bin, /deploy and hey /new are prose and go to the model exactly as typed.

A conversation names itself after your opening line, which is a decent guess and a poor permanent name. Rename it three ways: right-click its tab and pick Rename…, type /rename a better name, or type /rename alone to put the caret in the label. Titles are 1–120 characters.

The name you choose sticks. A rename locks the title, so no later message quietly takes it back — and both copies move together, so the heading of the conversation’s markdown note changes with it.

Chat has the same tool surface agents do: file reads and edits, shell, semantic search, diagnostics, web fetch, skills, browser automation, and every connected MCP server. Calls stream as they resolve, and the set_plan tool drives a live plan strip above the conversation so a long multi-step turn is legible while it happens.

Each tool call is a row in the transcript — status, tool name, a summary of the arguments — that expands to the full arguments and result. Browser actions get a row of their own, 🌐 navigate example.com, with the page as it looked afterwards as a thumbnail you can open in the Lightbox. The full entries also land in the events panel (⌘J). A chat turn’s events are excluded from the Run Monitor — the panel you are already looking at is the better view of them.

A picker in the chat toolbar sets the session’s permission mode: 🔒 Ask permission (the default) or ⚡ Allow all tools; /mode sets the same thing from the composer.

In ask mode three classes of call raise a permission card in the transcript — arbitrary code (run_shell, and browser_evaluate on a given host), the delete-class built-ins, and any tool that isn’t ours, meaning an MCP server’s. The card shows the tool, a preview of the arguments, the rule key it would write, and four buttons: Allow once, Always allow <key>, Deny once, Always deny. “Always” rules persist in <latchHome>/permissions.json, and Settings ▸ Permissions edits them.

Flipping the mode applies to the running turn immediately: a prompt already on screen is released. The mode is per session, never a persisted rule.

When a model streams its reasoning — the reasoning field Qwen, DeepSeek, and vLLM-style servers emit — the chat shows it as a Thinking… block that expands while it streams and folds to Thought process once the reply begins; click to reopen it. It is display only: reasoning is never persisted or sent back to the model.

A small ring in the chat toolbar fills as the conversation approaches the point where LatchAI will compact it — amber past 80%, pulsing when compaction is imminent, dashed when the server reports no usage. Hover it for the real numbers. When compaction happens, a slim divider marks the spot in the transcript (context compacted — 18k → fresh window) and the ring empties.

Every code block in a reply carries an Apply button beside Copy, and it says where the code is going before you press it. The label reads Apply → app.ts; the tooltip carries the full path and how it was decided, in this order:

  1. the file named in the code fence — ```ts src/app.ts or ```ts:src/app.ts
  2. a path in the prose around the block that is a real file in this workspace
  3. the file tab you have open
  4. nobody said — the button reads Apply to…, which is a promise you’ll be asked

Apply merges the snippet into that file and opens a proposed diff tab: the buffer as it was on the left, as it would be on the right, with Accept and Reject. Nothing is written to disk either way. Accept puts the merged text into the editor buffer and marks it dirty, so you still save it yourself with ⌘S; Reject just closes the tab. If the file is open with unsaved edits, those edits are what the merge is computed against.

An Apply proposal: before and after side by side, with Accept and Reject

Apply from chat: the proposed diff, before and after, with Accept and Reject. Nothing is written to disk until you save the buffer yourself. The merge runs in two tiers. First a free, exact splice inside the daemon: it handles a whole-file rewrite, elision markers (// ... existing code ...), and fragments whose surrounding lines pin down exactly one place in the file — and it re-indents a snippet the model wrote at the wrong column. It declines rather than guesses: anything ambiguous falls through to the second tier, which asks a model to integrate the change and hand back the whole file. The diff labels which tier produced it, so merged by model is worth a second read that exact splice is not.

That model is the one in Settings ▸ Editor — the same slot tab-completion uses. If you haven’t configured one, Apply says so quietly beside the block instead of interrupting the conversation. The model tier also refuses files over 1,500 lines, because the answer is the whole file coming back.

The button appears in chat only — the markdown file preview and the Usage transcripts are untouched — and never while a reply is still streaming, because half a function is not a change.

Every chat turn is checkpointed. The ± Review changes affordance opens the diff for that turn in the review overlay, with a revert — the same machinery workflow runs use, so “what did the assistant actually do” has one answer everywhere in the product.

Reviewing a chat turn’s diff

A chat turn’s file changes, reviewable and revertible.

The button appears once the turn finishes and carries the checkpoint’s short sha. It shows only when the turn actually changed something: no files touched, no checkpoint, no button.

Open the chat while a dashboard tab is active and the turn is bound to that board. Two things follow. The assistant is given bounded context — the brief, every widget’s summary and displayed data, and an index of the run’s sources (labels and refs, never the payloads). And publish_dashboard resolves against that board, so “drop the token count and make the list full width” re-renders the live dashboard instead of describing a change to it.

dashboard_source fetches one archived payload on demand when a question needs the numbers behind a number rather than the summary.

Sessions persist twice, deliberately:

  • Canonical envelopes at <latchHome>/runs/chats/<id>.json — the exact messages including tool turns. These survive restarts and are what a resumed session replays.
  • Markdown mirrors in <latchHome>/workspace/chats/ — YAML frontmatter and timestamped turn headings. Point an Obsidian vault at that folder, or symlink it, and every conversation is indexed and linkable with no plugin.

The mirror is rewritten on every turn, named <date>-<slugified-title>-<id-suffix>.md. It looks like this:

---
latchai: chat
session: chat-1748...
created: 2026-07-28T09:14:02.113Z
updated: 2026-07-28T09:21:40.882Z
tags:
- latchai
- ai-chat
---
# Why is the webhook returning 404?
## You — 09:14:02
Why is the webhook returning 404?
> ⚙ tools: read_file, search_code
## LatchAI — 09:14:31
Because `hook-echo` has no `webhook_trigger` node…

The # heading is the conversation’s title, so renaming a chat renames the note’s heading too. Tool calls are recorded as that one > ⚙ tools: line; the full arguments and results stay in the JSON envelope, which is what keeps a vault note readable.

The button in the panel lists saved sessions newest-first with their message counts, and reloads any of them — or opens one in a tab. A reload restores the prose and the tool rows alike, so the model resumes with the complete history.

  • One turn at a time per session — but you can queue. The composer stays live while a turn works; ↳ Queue holds your next message and delivers it at the model’s next step, and a queued message can be edited or cancelled until then.
  • Turn budget. A chat turn stops after 80 tool-loop turns as a runaway backstop. Change it with chatMaxTurns in latchai.config.json (1–500). An agent’s own maxTurns applies to workflow nodes and delegation, not to chat.
  • Esc stops a turn. The turn ends at the next step boundary; what it already did stays and is reviewable.
  • The conversation itself is not run history. The chat.* events — messages, deltas, busy status — are excluded from <latchHome>/runs/*.jsonl on purpose. The turn’s work is logged: its tool calls, file writes and checkpoint land in a chat-<session>-<timestamp>.jsonl log like any other run, which is how the brain can connect a conversation to the files it touched.