Merge PR #6990: fix(collab-web): make host state authoritative for guest liveness (@metaphorics)
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the guest rendering idle while the host agent is running (spinner vanishing mid-turn after a reconnect dropped `agent_start`): host `state` frames are now authoritative for liveness in both directions, and an idle report clears tool cards pinned by a dropped `tool_execution_end` ([#6990](https://github.com/can1357/oh-my-pi/pull/6990) by [@metaphorics](https://github.com/metaphorics)).
|
||||
- Fixed an issue where the agent would stop silently without a message by ensuring terminal auto-retry failures are properly surfaced as error notices.
|
||||
|
||||
## [17.1.0] - 2026-07-24
|
||||
|
||||
@@ -117,7 +117,7 @@ export function Composer({ client, snapshot }: ComposerProps): ReactNode {
|
||||
const readOnly = snapshot.readOnly;
|
||||
const uiRequest = snapshot.uiRequest;
|
||||
const canPrompt = live && !readOnly;
|
||||
const busy = snapshot.working || (snapshot.state?.isStreaming ?? false);
|
||||
const busy = snapshot.working;
|
||||
const queued = snapshot.state?.queuedMessageCount ?? 0;
|
||||
const canSend = canPrompt && text.trim().length > 0;
|
||||
|
||||
|
||||
@@ -334,8 +334,15 @@ export class GuestClient {
|
||||
break;
|
||||
case "state":
|
||||
this.#state = frame.state;
|
||||
// Host state is authoritative for liveness in both directions: the
|
||||
// payload is built at fire time, so `isStreaming` is never stale.
|
||||
// This covers a connected guest that misses the discrete `agent_start`
|
||||
// without receiving a new `welcome` (for example, mid-stream).
|
||||
this.#working = frame.state.isStreaming;
|
||||
if (!frame.state.isStreaming) {
|
||||
this.#working = false;
|
||||
// Host idle implies no tool can be running, so clear any card
|
||||
// pinned by a dropped `tool_execution_end` off this signal.
|
||||
this.#activeTools = new Map();
|
||||
if (this.#streamDone) {
|
||||
this.#stream = null;
|
||||
this.#streamDone = false;
|
||||
|
||||
@@ -192,6 +192,34 @@ describe("GuestClient frame apply", () => {
|
||||
client.applyFrameForTest({ t: "state", state: { ...STATE, isStreaming: false } });
|
||||
expect(client.getSnapshot().working).toBe(false);
|
||||
});
|
||||
it("a state frame recovers a stuck-idle guest when agent_start was dropped", () => {
|
||||
// The host begins streaming mid-turn, but the matching `agent_start`
|
||||
// never arrived (e.g. dropped on a reconnect). Before the fix nothing
|
||||
// set `working` true except `agent_start`, so the guest stayed idle.
|
||||
const client = liveClient();
|
||||
expect(client.getSnapshot().working).toBe(false);
|
||||
client.applyFrameForTest({ t: "state", state: { ...STATE, isStreaming: true } });
|
||||
expect(client.getSnapshot().working).toBe(true);
|
||||
});
|
||||
|
||||
it("an idle state frame clears a pinned tool card when tool_execution_end was dropped", () => {
|
||||
// Host reports idle, but the matching `tool_execution_end` was dropped,
|
||||
// leaving a stuck tool card. The authoritative idle signal must clear it.
|
||||
const client = liveClient();
|
||||
client.applyFrameForTest({
|
||||
t: "event",
|
||||
event: {
|
||||
type: "tool_execution_start",
|
||||
toolCallId: "tc1",
|
||||
toolName: "bash",
|
||||
args: { command: "ls" },
|
||||
intent: "Listing",
|
||||
},
|
||||
});
|
||||
expect(client.getSnapshot().activeTools.size).toBe(1);
|
||||
client.applyFrameForTest({ t: "state", state: { ...STATE, isStreaming: false } });
|
||||
expect(client.getSnapshot().activeTools.size).toBe(0);
|
||||
});
|
||||
|
||||
it("bus progress frames update the progress map", () => {
|
||||
const client = liveClient();
|
||||
|
||||
Reference in New Issue
Block a user