Commit Graph
40 Commits
Author SHA1 Message Date
Kaede AkinoandClaude Opus 5 ffdd36aa1d Move session context off the cached prefix
Every cache these providers keep is a prefix cache, and the working
directory of a terminal is the most volatile value the app has. Putting
one inside the other meant a single `cd` between turns changed byte zero
of the request and missed the entire replayed conversation — on a BYOK
key, in an app whose whole purpose is moving around a filesystem.

Nothing sets `cache_control` yet, so Anthropic was unaffected, but
OpenAI and Google cache prefixes automatically: this was already
costing money, and it was a trap laid for the day explicit caching lands.

The context now rides the tail of the message list, immediately before
the turn it describes. History the model has already seen is immutable,
so `[system + history]` keeps hitting and the divergence is the newest
turn, which was never cacheable anyway. The system prompt goes back to
being byte-identical across every run, conversation, and user.

It is sent but not kept. `snapshot.messages` is the durable transcript,
and a block added there would be persisted, replayed, and — since
restore projects user rows into the visible list — eventually shown in
the user's own chat bubble. Leaving it out costs the next run one
uncached message at the tail and nothing before it.

Two consecutive user messages are already routine here: `.tool` rows
become `user` rows on all three adapters, so any parallel tool call
produces them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 22:45:58 +08:00
Kaede AkinoandClaude Opus 5 699b26b6d8 Ask titling for no reasoning, not a little
`clamp` walks upward first, so `clamp(.low)` on a model whose levels are
`[off, high]` — several Kimi builds — returns high. Across the bundled
catalog that is 70 models spending full reasoning effort to write six
words, with `off` sitting right there unused.

The upward bias is correct where `SecurityReviewClient` uses it: a review
that does not reason is worse than a slow one, so failing upward fails
safe. Titling is the opposite. It needs no reasoning, and a rejected
title just falls back to the opening line.

`clamp(.off)` is the one request the clamp cannot answer by walking up,
so it takes off where it exists and the lowest available level where it
does not: 847 models now send no reasoning field at all, and the models
left reasoning are the ten with no lower rung to stand on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 22:32:39 +08:00
Kaede AkinoandClaude Opus 5 d7029a0557 Make Context real, and name conversations by summary
Context was decoration. Three defects, all reported from use.

Session context followed the conversation. `ConversationModel` owned the
chips and the remote system info, and `refreshAssistantContext` stamped
the focused pane onto whichever chat the sidebar was showing — so sending
from the sidebar carried a host and directory into the Assistant tab,
which has no session to describe. It now lives on `WindowSidebarState`
as a `SessionContext`, and `ComposerView` takes it as a parameter
defaulting to `.none`. The Assistant tab passes nothing, so it cannot
show or send a session it does not have. Two bugs fell out with it:
leaving a Workspace clears the context instead of freezing the last
value, and opening a chat in the sidebar no longer marks it.

Nothing was sent. Only `RemoteSystemInfo` reached the model; host and
cwd reached nothing, and `ContextChip` had no payload at all — the
SSH-failure attachment was a label for a log pasted into the draft.
`SessionContext.modelContext` now renders one fenced data-only block
into the run's system prompt, chips carry their content, and
`appendUserTurn` folds attachments into the turn it returns. What was
uploaded is literally what the bubble shows.

Titles were the first message. `ConversationTitleService` mirrors
`CommandComposerService` — tool-free, strict JSON, validated — and
resolves `.chat` rather than adding a Titler role, per the argument
already written into `AIRole`. It runs once after the first reply and
persists to the `title` column that existed but was never read back, so
no migration. It reads `transcript`, not `messages`: a failed run writes
its reason to the visible list alone, and that is not a name.

`Pane.remoteSystemInfo` already withholds the probe when Shell
Integration is off, so the second gate in `AppModel` is gone — the value
sent is the value the chips were drawn from, and the two cannot
disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 22:26:19 +08:00
Kaede Akino 67f2be6540 feat: provide remote system context to AI 2026-08-25 21:59:25 +08:00
Kaede AkinoandClaude Opus 5 4ff030c73e Give a conversation a real transcript, and make Stop stop
Three defects in how a conversation drives and shows a run.

Stop never reached the runtime. `Task { await runtime?.cancel() }`
resolves the property when the task runs, which is after `stop()` has
already set it to nil, so the run carried on executing tools with nobody
listening — secrets undiscarded, SSH and SFTP connections uncleaned.

`messages` was replayed as the next run's prior context, but it is a
display projection: no tool calls, and no tool results at all, because
those are drawn as cards. The model lost everything it had already done,
and a tool-only turn came back as an assistant message with empty
content, which Anthropic and Google both answer with a 400. The
model-facing transcript is now kept beside the visible one and rebuilt
from the same events.

The runtime held the conversation through both its approval broker and
its interaction handler while the conversation held the runtime, so every
chat that had ever run an agent stayed alive for the session. All three
edges are weak now, and a conversation that is gone denies and cancels.

Alongside: messages and tool cards are merged into one ordered timeline
rather than drawn as two lists, a tool-only turn no longer leaves an
empty bubble, the transcript follows streaming text instead of only new
rows, and conversations live in one app-wide store so two windows share
instances rather than each building their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:44:02 +08:00
Kaede AkinoandClaude Opus 5 3cce29edaf Frame SSE ourselves instead of on AsyncLineSequence
`bytes.lines` discards empty lines, and a blank line is the only thing
that dispatches an SSE event. Every streamed chat response therefore
arrived as one undelimited blob: `parse` failed to decode it, the turn
produced no text and no tool calls, and the agent loop ended after a
single request. That is both "the assistant never says anything" and
"tool calls never continue".

Line splitting now lives beside the field accumulator, so the streaming
and buffered entry points frame identically — they did not before, which
is why the bug hid on the side without tests. CRLF, LF and bare CR all
terminate a line; a body that ends without a trailing blank line still
delivers its last event; and a single line is bounded so an endless one
cannot exhaust memory.

The legacy MCP HTTP+SSE transport reads through the same parser. Its
stream stays open, so the accumulator never flushed and the endpoint
handshake waited until it timed out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:43:57 +08:00
Kaede AkinoandClaude Opus 5 ee824ee021 Harden the agent runtime, provider client, and run history
A tool the runtime does not have produced an error result and no events
at all, so the transcript showed an empty assistant turn and nothing
else — "the model said nothing" rather than "the model called a tool we
do not have". It gets a card now.

Registering tools trapped on a duplicate name, and names partly come from
remote MCP servers: one server exposing both `list-hosts` and
`list.hosts` sanitizes to a collision and took the app down. First
registration wins, so a remote name cannot shadow a built-in, and MCP
names are disambiguated where they are minted.

Every event was handed to the repository, which meant an unstructured
task and a `ModelContext.save()` per streamed token, all on the main
actor — while recording nothing, because the store ignores message
events. Only run-scoped events reach storage now, and the store no longer
saves when it has written nothing.

Run history also matched stored messages by run id, so a conversation's
earlier turns — replayed into every later run — were re-inserted on each
save, colliding with the unique constraint. Matching by message id
instead leaves the rows that are already correct alone. Restoring now
orders by run before time, so two runs with close timestamps cannot
interleave.

Also: one shared URLSession rather than one per run that was never
invalidated, a bounded read of a failed response body, the write-only
`sawOutput` plumbing removed, an orphaned event stream finished rather
than left suspended, and `AgentRunPersisting` no longer ships default
implementations that could silently become the witness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:42:22 +08:00
Kaede Akino 177da8bc28 feat: add command composer 2026-08-25 21:08:40 +08:00
Kaede Akino a42f9c7bb0 refactor: separate macOS speech audio input 2026-08-25 20:47:11 +08:00
Kaede Akino 2a2e8033c1 Fix agent tool continuation and conversation events 2026-08-25 20:37:35 +08:00
Kaede Akino 59c29957c5 Remove inline suggestions 2026-08-25 20:25:27 +08:00
Kaede Akino 8c858d246b feat: add Termind agent runtime and inline suggestions 2026-08-25 20:06:06 +08:00
Kaede Akino 8e2112f44d feat: add Apple speech transcription to chat 2026-08-25 18:01:32 +08:00
Kaede Akino 3ad69b9ff9 Refine settings app subtitle 2026-08-25 17:42:10 +08:00
Kaede Akino 8a9c133c0e Add Codex auto-review model metadata 2026-08-25 17:34:16 +08:00
Kaede Akino 84eb26ad89 Add model row context actions 2026-08-25 17:22:54 +08:00
Kaede Akino c03c81d1d6 Fix Responses model probes 2026-08-25 17:10:26 +08:00
Kaede Akino 555680f4c7 Refine model details panel labels 2026-08-25 17:10:26 +08:00
Kaede Akino 42938a7d04 Add close control to model details 2026-08-25 17:10:26 +08:00
Kaede Akino b9afb18c0b Persist model providers and unify capability resolution 2026-08-25 17:10:25 +08:00
Kaede Akino bd09284247 Refactor Keychain storage and SSH key editing 2026-08-25 17:10:23 +08:00
Kaede Akino 37e1e0a9ec Add SFTP permission editor 2026-08-25 12:38:31 +08:00
Kaede Akino 339665e6fb fix: sync SFTP directory context 2026-08-25 12:28:40 +08:00
Kaede Akino 264a2dc8ae fix: restore workspace context in assistant 2026-08-25 12:26:02 +08:00
Kaede Akino c662532551 Refine Assistant sidebar navigation 2026-08-25 12:26:02 +08:00
Kaede Akino 7b920db5c5 Fix pinned tab alignment with sidebar 2026-08-25 12:26:01 +08:00
Kaede Akino a0441d8c97 feat: redesign chat composer controls 2026-08-25 12:25:53 +08:00
Kaede Akino 4998f12372 Fix SFTP table AttributeGraph crash 2026-08-25 07:52:10 +08:00
Kaede Akino 977dbb872e Sync pinned tabs with sidebar width 2026-08-25 00:55:44 +08:00
Kaede Akino 9f0f96eaea Add automatic SSH and SFTP reconnection 2026-08-24 23:55:21 +08:00
Kaede Akino 639dd9b5c6 Fix Assistant tab creating chats 2026-08-24 23:11:46 +08:00
Kaede Akino 48f76aa863 Add per-host command history sidebar 2026-08-24 23:07:15 +08:00
Kaede Akino 499dc6fefd Fix settings sidebar icon alignment 2026-08-24 22:32:38 +08:00
Kaede Akino 783c897f71 Keep host subeditors within detail column 2026-08-24 22:26:16 +08:00
Kaede Akino 1a698c30dc Make workspace tabs shrink adaptively 2026-08-24 22:15:21 +08:00
Kaede Akino 4fb25a6a48 Redesign settings navigation 2026-08-24 21:58:30 +08:00
Kaede Akino e9d5df8379 Refactor Vault workflows into split navigation 2026-08-24 21:58:29 +08:00
Kaede Akino 3c5951fedc Merge context panel into sidebar 2026-08-24 21:58:00 +08:00
Kaede Akino c6a8620f81 Fix SFTP name cell selection 2026-08-24 21:57:17 +08:00
Kaede Akino 3f4f382ac4 Initial commit 2026-08-24 18:20:20 +08:00