Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lat.md/chat-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ So `ensureClient` distinguishes two failures: a **genuinely absent** dashboard (

On `message.complete` the desktop reconciles the text streamed via `message.delta` with the turn's `final_response`, because a last-turn-only final would otherwise clobber text streamed before a tool call (#746).

[[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#completeAssistantWithFinalText]] rewrites the last assistant bubble through [[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#mergeStreamedWithFinal]], which compares whitespace-insensitively and: uses the final text when it already contains the streamed text; keeps the streamed text when it contains the final (preserving pre-tool-call content); stitches a re-streamed boundary by dropping the duplicated word-aligned seam (rejecting coincidental mid-word overlaps); replaces a garbled re-stream with the final text when the two converge on a substantial common suffix (a corrupted-prefix delta β€” e.g. a mangled CJK stream β€” that ends the same sentence as the clean final, rather than the disjoint pre-tool-call + answer pair); and otherwise concatenates the two with a blank-line separator so segments never run together. On the remote/SSH path deltas are not rendered (`renderAssistantDeltas: false`), so the bubble starts empty and the final text is used verbatim.
[[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#completeAssistantWithFinalText]] rewrites the last assistant bubble through [[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#mergeStreamedWithFinal]], which compares whitespace-insensitively and: uses the final text when it already contains the streamed text; keeps the streamed text when it contains the final (preserving pre-tool-call content); replaces a **lossy chunk-dropped stream** with the final text when the streamed content is a subsequence of the final with substantial coverage (β‰₯12 chars and β‰₯30% of the final β€” the upstream tagging alternate chunks as `reasoning` leaves the content stream a garbled subset like "! What are we working on?" for "Hey! What are we working on today?"; concatenating stacked the partial above the clean answer in one bubble); stitches a re-streamed boundary by dropping the duplicated word-aligned seam (rejecting coincidental mid-word overlaps); replaces a garbled re-stream with the final text when the two converge on a substantial common suffix (a corrupted-prefix delta β€” e.g. a mangled CJK stream β€” that ends the same sentence as the clean final, rather than the disjoint pre-tool-call + answer pair); and otherwise concatenates the two with a blank-line separator so segments never run together. On the remote/SSH path deltas are not rendered (`renderAssistantDeltas: false`), so the bubble starts empty and the final text is used verbatim.

## Streaming source-of-truth ref

Expand All @@ -61,6 +61,26 @@ Streamed reasoning and tool calls are folded into compact, collapsible transcrip

[[src/renderer/src/screens/Chat/HistoryRow.tsx#ReasoningRow]] renders the `Thought` / `Thinking…` row and [[src/renderer/src/screens/Chat/HistoryRow.tsx#ToolActivityGroup]] folds a contiguous run of tool calls/results into one row titled by [[src/renderer/src/screens/Chat/HistoryRow.tsx#toolActivityGroupTitle]]. Each row is collapsed by default and borderless (Codex-style): dim at rest, it brightens and reveals an expand chevron beside the title on hover/focus, and clicking toggles the body open. While the turn is still streaming the leading icon is a `Grid` loader (purple for reasoning, blue for tools); once finished it shows the brain/tool glyph.

### Reasoning reconciliation

The live reasoning stream is best-effort β€” dropped delta chunks leave the streamed row garbled β€” while state.db holds the canonical text. The DB refresh must collapse the two, or the user sees both stacked in one Thought block.

The observed symptom: a Thought block showing "moon-k3 … ous" (lossy live preview) above "moonshotai/kimi-k3 … nous" (canonical DB row) for the same thought.

Because the garbled text can't match the DB row's text-based reconciliation key, [[src/renderer/src/screens/Chat/sessionHistory.ts#reconcileStreamedWithDb]] ends with [[src/renderer/src/screens/Chat/sessionHistory.ts#dropLossyStreamedReasoning]]: a streamed reasoning row is dropped when a DB reasoning row (`db-r-…`) in the **same turn** contains its normalized text as a **subsequence** (and is strictly longer). A dropped-chunks preview is by construction a subsequence of the canonical text, which cleanly separates "same thought, chunks missing" from a genuinely distinct live segment.

#### Lossy live preview collapses into the DB row

A streamed reasoning row whose garbled text is a subsequence of the same turn's DB reasoning row disappears from the merge; only the canonical DB text renders.

#### Distinct live segments survive

A second live reasoning segment that is not a lossy duplicate of any DB row in the turn (multi-segment thinking around tool calls) is kept alongside the reconciled first segment.

#### Turn-scoped matching

The subsequence check never crosses turns: a live preview in turn 2 is kept even when its text happens to be a subsequence of turn 1's canonical reasoning, so repeated questions can't cross-cancel live rows.

## Bubble hover timestamp

Each user/assistant bubble reveals a relative "time ago" label on row hover, so the transcript stays uncluttered at rest but is still scrutable when a user wants to know _when_ something was said.
Expand Down
24 changes: 24 additions & 0 deletions lat.md/hermes-account-login.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ account" card that opens it; once signed in it renders an identity card β€”
avatar (or letter fallback), name/email, a "Connected" status line β€” with a
Sign out action.

## Auto-provisioned inference key and credits

Signing in should yield model access without hand-copying keys: the desktop auto-issues a Hermes One Inference gateway key from the account and shows the account's AI-credit balance on the Providers card.

[[src/main/hermesone-provision.ts]] is the convenience layer. `ensureHermesOneApiKey` checks the profile's `.env` for `HERMESONE_API_KEY`; when missing, it POSTs the backend's `/api/credits/keys` (bearer device-login token, key named `Hermes Desktop (<hostname>)` so the console list shows its origin) and persists the one-time raw `hs-live-…` key via `setEnvValue`. Setting that env var **is** what "adds the provider": the Hermes One card ([[provider-setup]]) and the active-model picker both key off it, so no store writes are needed. `fetchHermesOneCredits` GETs `/api/credits/balance` for the USD-denominated balance.

It runs from two places, both idempotent: the `hermes-account-login` IPC handler right after a successful device login, and the Providers screen whenever it loads with a signed-in account (covering users who signed in before this feature). Both are **local-mode only** β€” the key lands in the local profile `.env`, which remote/SSH chat doesn't read, and provisioning there would strand an orphan backend key per visit. The account card renders a credits chip (`$X.XX credits`, Coins icon) next to Connected/Sync-on, backed by the `hermesone-credits` IPC; a `created` result makes the screen re-read env so the Hermes One card appears immediately.

### Issues a key only when missing

An existing `HERMESONE_API_KEY` is always kept β€” the backend shows a raw key exactly once, so re-issuing would orphan the old one. No backend call happens at all in that case.

### Provisions and persists a fresh key

With a signed-in account and no local key, one authenticated POST issues the key and it is written to the target profile's `.env` under `HERMESONE_API_KEY`.

### Single-flight provisioning

Concurrent ensure calls (post-login hook + Providers screen mount) coalesce into a single backend key issue, preventing orphan keys.

### Credits for the account card

The balance endpoint is called with the bearer token and returns the numeric USD credit balance; signed-out or malformed responses yield `null` so the chip simply hides.

## Tests

Unit tests cover the two pieces that can break silently.
Expand Down
Loading
Loading