Skip to content

Threads: several conversations per folder, subagents, project brief - #22

Merged
lszl84 merged 41 commits into
mainfrom
feat/threads
Oct 25, 2026
Merged

lszl84 merged 41 commits into
mainfrom
feat/threads

Conversation

@lszl84

@lszl84 lszl84 commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Several conversations per folder, running at the same time, plus subagents and a project brief. This is the 0.6.0 feature set.

Threads

  • A folder holds several threads that run at once; a new thread starts with a clean context.
  • Threads are switched in the left pane (new icon toolbar). The session dropdown still picks the folder.
  • Each thread keeps its own draft; ▶ Play stays per folder.
  • ● / ⚠ marks in the dropdown and the Threads list show threads that finished or failed while you weren't looking. A turn you stopped yourself doesn't count.
  • Agents sharing a checkout are told so, and when to use a git worktree.

Subagents

  • DeepSeek and Kilo get a task tool that runs a child thread in the background.
  • Claude Code's own subagents show up as child threads.
  • Children are indented under their parent with a live status line and can be opened to watch; they read and research, the parent edits.

Project brief

  • Written once, in the background, when the first new thread starts in a folder, by the selected model's provider (DeepSeek Flash, Sonnet through Claude Code, or the free model).
  • Stored in the app data dir (briefs/<id>.md), never in the repo.

History search

  • grit_history_search leaves out the calling thread, takes scope: "folder" and shows each hit's thread title.

Fixes found along the way

  • A timed-out bash command takes its whole process group down.
  • Run-config saves are locked and atomic.
  • The SSE parser handles CRLF streams, an unterminated last event and data: without a space.
  • Refresh in the file tree no longer saves the open file.

Compatibility

The on-disk formats stay readable by 0.5.x: session files keep their format, the index adds a threads key 0.5.x ignores, and memory.db adds a table next to the unchanged FTS5 turns table. Downgrading shows each folder's first thread with full history.

Testing

  • Smoke tests (scripts/smoke/) against a local fake model server: threads, thread pane, subagents, brief, history, Play.
  • A private macOS build of this branch (run 40208619338) was signed, notarized and checked by hand.

🤖 Generated with Claude Code

lszl84 and others added 30 commits October 19, 2026 19:07
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Relative paths in the file tools resolve against the cwd DispatchTool is
given, and bash chdir()s into it in the forked child (lpCurrentDirectory
on Windows), so tools no longer depend on the process cwd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ChatFrame's per-conversation state moves into a plain struct Thread held
in a std::deque; the existing functions take Thread&. Async callbacks
capture (thread id, gen) and drop stale results. Only the active thread
draws to the canvas; switching rebuilds it from history plus the
in-flight partial reply. One running turn per folder: a second thread in
the same folder waits ("Waiting for <thread> to finish…") and starts
when the first finishes. The process-wide ChdirToCwd is gone.

Storage keeps the 0.5.x layout: a folder's session is its first thread,
the index keeps one `sessions` entry per folder and adds `threads` and
`lastActiveThread`. Session files not in the index (after 0.5.x rewrote
it) are listed again from their headers. Nothing is written at startup.

The session dropdown lists the folder's threads (running ones marked),
then New thread and Open folder…. The control server gains listThreads,
newThread and selectThread; GRIT_MCP_PORT pins its port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…latest thread

A turn that fails while its thread isn't shown keeps its notice, and the
prompt the failure rolled back out of history, on the Thread. Switching
to the thread shows the notice and puts the prompt back into the input
(unless the user is typing there); until then the dropdown marks the
thread with "⚠ ". The no-API-key path now fails like the other errors,
rolling back the unsent message.

Opening a folder shows its most recently used thread instead of its
first one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plain Thread struct in ChatFrame, (id, gen) callbacks, per-thread cwd for
tools, one running turn per folder, no-migration storage with 0.5.x
compatibility, background failures kept until viewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ID_TREE_REFRESH and ID_EDITOR_SAVE were both wxID_HIGHEST + 21. The
handler bound last runs first, so the tree menu's Refresh ran Save:
it wrote the open file, or opened Save As when none was open. Give
Refresh its own id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_seconds

After a timeout or Escape the group got SIGTERM, but once bash itself
died the loop returned before the SIGKILL step, so a grandchild that
ignores SIGTERM lived on. SIGKILL the group after bash is reaped (only
then: after a normal exit, `nohup … &` processes keep running).

The model can pass timeout_seconds (default 30, clamped to 1..600) for
slow builds and tests; the description says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Threads run tools at once, and two run_project set calls at the same
time each loaded the file, added their entry and saved, so one entry
was lost (16 of 320 kept with 16 threads saving at once). Get, Set and
Forget now go through one mutex, and Save writes a temp file and
renames it, so a reader (also in another gritcode process) never sees
a half-written file. Load/Save become private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt threads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…without a space

The chat and summary streams split events on "\n\n" only, so a
CRLF-delimited stream yielded no events at all, a last event without a
closing blank line was dropped, and `data:` without a space was
ignored. Each chunk also rescanned the whole buffer.

One free function, TakeSseEvents, now does it per the SSE spec: lines
end in "\n", "\r\n" or "\r", a blank line ends an event, its data lines
are joined, one space after the colon is optional. Only the tail of the
previous buffer is rescanned. When the stream ends OK, what is left
counts as a last event; on errors the buffer still holds the raw body
for the error message. The Exa web-search response uses it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hildren

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Refresh fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (step 2)

DeepSeek/Kilo: top-level threads get a task tool; each call runs a
read-only child thread with a fresh context, concurrently with the rest
of the batch, and its final message (or failure) becomes the call's
result in call order. Claude Code: Agent/Task calls get a child thread
that assembles the subagent's events with the top-level code. Children
are view-only, listed under their parent and linked from its canvas; a
thread and its subagents count as one owner of the folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new thread starts without the history of the folder's other threads. A
short per-folder brief (what the project is, its layout and conventions, the
current work) and a list of the folder's other titled threads now go into
every thread's system prompt, next to AGENTS.md, and into Claude turns'
appended prompt.

The brief lives in the app data dir as briefs/<folder id>.md, keyed like
sessions, so nothing is written into the user's repo. "Project brief…" in
the thread dropdown opens a dialog to edit it; "Generate" streams a draft
from DeepSeek Flash (or the free model without a key), with failures shown
in the dialog. A new thread in a folder with other threads and no brief
shows a one-line hint linking to the dialog; nothing is generated unasked.

scripts/smoke/brief.py checks the injection; threads.py takes GRIT_MCP_PORT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Subagents keep their note when the system prompt is rebuilt (brief or
AGENTS.md change): SystemPrompt adds it for threads with a parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The smoke tests ran their turns on the shared Kilo free tier, which stalls
or drops one of two concurrent requests, so the concurrency checks failed
at random and the `task` tool had no repeatable test.

GRIT_FREE_MODEL_URL (a test/dev hook, read in RouteForIndex) overrides the
free model's endpoint. scripts/smoke/fake_model.py is a stdlib, threaded,
127.0.0.1-only OpenAI-compatible SSE server scripted by the prompt: bash
or arbitrary tool calls (task, write_file), a text reply after tool
results, HTTP 500, and CRLF / unterminated-last-event framing on request,
with a 1.1 s delay per reply.

threads.py and subagents.py start it in-process by default (--live keeps
the real Kilo behaviour). threads.py checks the step-3 SSE fixes end to
end; subagents.py's task-tool section is now real checks: two calls in a
batch run at once and report in call order, a failing child gives the
parent an error result, children can't write_file, and cancelling the
parent stops the child and kills its bash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
grit_history_search leaves out the calling thread, takes scope "folder"
and shows each hit's thread title (marking subagents). Titles and parents
go into a new plain threads table in memory.db; the FTS5 turns table keeps
the 0.5.x schema, so old versions and existing databases keep working
without a rebuild. --reindex records titles and parents too, and
--mcp-stdio routes its calls through DispatchTool (scope folder = its cwd).

Smoke: scripts/smoke/history.py (old-format DB, reindex, scope, exclusion).
threads.py and subagents.py now wait for their own port before the first
control call, so they can't reach a neighbouring gritcode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Smoke tests passed a base port and the client then tried the next five
ports too, which reached the user's own gritcode windows on 8765/8766.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An optional fixed-width pane left of the splitters lists the active
folder's threads, most recent first with subagents nested: a state mark
(running, waiting, failed until viewed), the title and a dim line with the
current step (tool, writing, thinking, waiting for <thread>), the failure,
or when it was last used. Clicking a row selects the thread, right-click
stops a running one, "New thread" sits on top. The dropdown and the pane
share one row list (FolderRows). The toggle is remembered in preferences
and grows the window (or shares a tiled one) like the other panes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calling thread stays searchable once compaction summarised part of
it away. Smoke scripts use the exact-port control client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread can work in its own git worktree (grit/<id>, under the data dir),
so it writes alongside the folder's other threads: one running turn per
work dir instead of per folder. Review shows the worktree's diff in the
canvas, Merge commits its uncommitted changes and merges the branch with
--no-ff (refused on a dirty checkout, aborted on conflicts), Discard asks
before losing commits or changes. Worktree and branch are optional session
fields. Smoke test: scripts/smoke/worktrees.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lszl84 and others added 11 commits October 20, 2026 00:26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session dropdown lists folders again (New Session…, then folders most
recent first, ● when one has a running thread). The separate thread pane and
its toolbar button are gone: the existing left pane gets a Session | Threads
switch, remembered in preferences, and the Threads view holds New thread,
New thread in worktree, Project brief… and the thread list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut for now

The left pane's Session | Threads switch becomes a bottom toolbar like the
main pane's: a session icon and a threads icon (the current view in the text
colour) and "+" for a new thread in the Threads view.

The project brief is no longer edited by hand: the first new thread in a
folder without one has it written in the background (DeepSeek Flash, the
free model without a key), and later new threads reuse it. The new thread
says it is being written and by which model, then shows the brief it starts
with; a failure shows the model's error and the next new thread retries.

Worktree threads are reverted (merge d48a624) until they are wanted again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DeepSeek users get DeepSeek Flash, free-model users the free model, and
Claude users Sonnet through their Claude Code CLI (print mode, no tools,
no saved session, material on stdin), so a folder's README and file names
never go to a provider the user didn't pick. The brief test checks the
Claude case with a stand-in CLI and that nothing reached the HTTP model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the one-running-turn-per-folder lock and its waiting state: a turn
starts as soon as it is sent, whatever else runs in the folder. Subagents
stay read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A top-level thread in a git repo gets rules in its system prompt: don't move
the shared checkout's branch or index, work in place by default, and take
branch work, or bigger work while another thread is busy there, into a git
worktree under the data dir. When a turn starts while other threads run in
the same checkout, the user message gets a note naming them; the model sees
it after the message and the canvas shows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The input's text and attached images stay with the thread you switch away
from and come back with it. Play keeps one run per folder, so Play in one
project no longer stops another's, and its blocks stay with the thread Play
was pressed in, put back in place when the canvas is rebuilt; they are still
display-only. Also: a DeepSeek key from GRIT_DEEPSEEK_API_KEY for headless
test instances, and the control API lists the model dropdown and can set
the input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder's mark is now ⚠ when a thread there failed unseen and ● when one is
working or finished since it was last viewed; the thread on screen never
counts, so the closed dropdown tells you about the folder's other threads.
Subagents report through their parent, a turn you stopped leaves nothing to
see. The Threads list marks the same threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lszl84
lszl84 merged commit c533ee5 into main Oct 25, 2026
5 checks passed
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.

1 participant