fix(desktop-sdk): address adversarial review — type honesty, real tile coverage, docs

Independent second-pass review found three gaps:

- focusedUsage is null | UsageStats, not Partial — ClientSessionState.usage
  is the full type (app/types.ts) and its only write site seeds the four
  required fields before merging (gateway-event.ts). The earlier Partial
  annotation traced the wrong type (SessionRuntimeInfo, an RPC payload).
  Comment now names the genuinely optional fields instead.
- The tile-focus contract test never seeded $sessionTiles/$sessionStates,
  so it proved focusedStoredSessionId follows a tile but could not
  distinguish focusedSessionId/focusedUsage working from broken. Seed a
  bound runtime with distinct usage and assert both readout atoms move.
- The two public host.state references (website docs + bundled skill
  reference) enumerated the old six atoms; plugin authors would never
  discover the new ones. Both lists updated.

tsc/eslint/vitest green (4/4).
This commit is contained in:
lepetitprince716-prog
2026-08-06 15:49:30 -04:00
committed by Teknium
parent a445a81279
commit 5fa092b481
4 changed files with 38 additions and 9 deletions

View File

@@ -59,7 +59,7 @@ describe('host.state focused-session atoms', () => {
})
it('follows the interacted tile while the primary-only atom stays put', async () => {
const { host, session } = await setup()
const { host, session, states } = await setup()
const tree = await import('@/components/pane-shell/tree/store')
const model = await import('@/components/pane-shell/tree/model')
const { registry } = await import('@/contrib/registry')
@@ -85,9 +85,30 @@ describe('host.state focused-session atoms', () => {
const primaryBefore = session.$activeSessionId.get()
const primarySelection = session.$selectedStoredSessionId.get()
// Bind the tile to a live runtime with its own usage, so the readout
// atoms (focusedSessionId / focusedUsage) — not just the navigation id —
// are proven to follow the tile.
const tileUsage = {
calls: 3,
input: 1200,
output: 300,
total: 1500,
context_used: 42000,
context_max: 200000,
context_percent: 21,
cost_usd: 0.0123
}
states.$sessionTiles.set([{ storedSessionId: 'tile-a', runtimeId: 'runtime-tile-a' }])
states.$sessionStates.set({
'runtime-tile-a': { storedSessionId: 'tile-a', usage: tileUsage } as never
})
// Focusing the tile zone moves the focused atoms onto the tile's session…
tree.noteActiveTreeGroup('grp-side')
expect(host.state.focusedStoredSessionId.get()).toBe('tile-a')
expect(host.state.focusedSessionId.get()).toBe('runtime-tile-a')
expect(host.state.focusedUsage.get()).toBe(tileUsage)
// …while the primary-only atom a plugin used to rely on does not move.
expect(host.state.activeSessionId.get()).toBe(primaryBefore)

View File

@@ -146,9 +146,9 @@ export const host = {
/** Live usage snapshot of the focused session (`context_used` /
* `context_max` / `context_percent`, token counts, `cost_usd`) —
* streamed by the backend, no RPC needed. Null while unresolved.
* Partial: the backend streams whichever fields changed, so every
* field read needs a fallback. */
focusedUsage: readonlyAtom<null | Partial<UsageStats>>($focusedUsage),
* The UsageStats-optional fields (context_*, cost_usd) arrive as the
* backend reports them, so read them with a fallback. */
focusedUsage: readonlyAtom<null | UsageStats>($focusedUsage),
/** Gateway socket state: 'idle' | 'connecting' | 'open' | …. Not turn-busy. */
gateway: readonlyAtom<string>($gatewayState),
/** Current main model slug. */

View File

@@ -56,11 +56,16 @@ The ONLY import surface is `@hermes/plugin-sdk` (plus `react` /
- `host.state.*` — readonly reactive atoms: `activeSessionId`, `busy`,
`awaitingResponse`, `busyBySession`, `cwd`, `gateway` (socket state, not
turn-busy), `model`, `profile`, `viewport`.
`busy` is true while the focused chat is working after a send (thinking
and streaming). `awaitingResponse` is true until the first assistant
payload. `busyBySession` maps runtime session id → mid-turn, for rosters
that watch every session. Read with `.get()` in handlers,
turn-busy), `model`, `profile`, `viewport`, plus the tile-aware focused
session atoms: `focusedSessionId` (runtime id — key for `session.*` RPC),
`focusedStoredSessionId` (durable id — navigation / list matching), and
`focusedUsage` (live streamed `UsageStats` of the focused session, no RPC
needed). `busy` is true while the focused chat is working after a send
(thinking and streaming). `awaitingResponse` is true until the first
assistant payload. `busyBySession` maps runtime session id → mid-turn,
for rosters that watch every session. Prefer the focused atoms for any
readout that should follow the user between tiles. Read with `.get()`
in handlers,
`useValue(atom)` in components.
- `host.request(method, params)` — gateway JSON-RPC (sessions, config,
skills, cron — everything the app uses).

View File

@@ -378,6 +378,9 @@ host.state.activeSessionId // ReadableAtom<string | null>
host.state.awaitingResponse // ReadableAtom<boolean> true until the first assistant payload
host.state.busy // ReadableAtom<boolean> focused chat is working after a send
host.state.busyBySession // ReadableAtom<Record<string, boolean>> runtime id → mid-turn
host.state.focusedSessionId // ReadableAtom<string | null> (runtime id of the FOCUSED session — tile-aware; prefer for session.* RPC)
host.state.focusedStoredSessionId // ReadableAtom<string | null> (durable id — navigation / session-list matching)
host.state.focusedUsage // ReadableAtom<UsageStats | null> (live streamed usage of the focused session, no RPC needed)
host.state.cwd // ReadableAtom<string>
host.state.gateway // ReadableAtom<string> socket state ('idle' | 'connecting' | 'open' | …)
host.state.model // ReadableAtom<string>