Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 9 additions & 8 deletions apps/web/src/pages/workspace/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
} from "./tab-follow.ts";
import {
awaitingCommit,
awaitingReply,
committedSteps,
emptyLiveRun,
foldLiveRun,
Expand Down Expand Up @@ -998,19 +999,19 @@ export function StageWorkspace({
}
};
const held = waitingOnPerson(waits, openApprovalIds, runId, liveRun.running);
// A message sent here waits on the coordinator from the moment it leaves, before the stream
// reports its run: until a reply follows it or a run ends after it.
const awaitingReply =
awaitingSince !== null &&
turns.at(-1)?.role === "person" &&
(liveRun.endedAt === null || liveRun.endedAt < awaitingSince) &&
const awaiting =
awaitingReply(
awaitingSince,
turns.at(-1)?.role,
liveRun.endedAt,
parkedOnApproval(waits, openApprovalIds, runId),
) &&
!archived &&
startFault === null &&
modelNeeded === null;
// The mark stays for the whole turn: between model calls and during long tool work the live
// reply shows nothing moving.
const thinking =
!sendFailed && (sending || awaitingReply || (working && !held));
const thinking = !sendFailed && (sending || awaiting || (working && !held));
// Each concept's option is its own tab beside the others, never an ordinary document.
const options = artifacts.filter((item) => item.kind === DESIGN_OPTION);
const paneDeliverables = documentsForPane(
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/pages/workspace/live-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,23 @@ export function waitingOnPerson(
)
);
}

/**
* A message sent here waits on the coordinator from the moment it leaves, before the stream
* reports its run: until a reply follows it, a run ends after it, or its run is held on an open
* approval. An approval card alone brings no reply text while its run stays open. A standing
* question does not end the wait: an answer to it is sent before the stream reports the new run.
*/
export function awaitingReply(
sentAt: number | null,
lastTurnRole: ConversationTurn["role"] | undefined,
runEndedAt: number | null,
heldOnApproval: boolean,
): boolean {
return (
sentAt !== null &&
lastTurnRole === "person" &&
(runEndedAt === null || runEndedAt < sentAt) &&
!heldOnApproval
);
}
18 changes: 18 additions & 0 deletions apps/web/src/pages/workspace/transcript.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { wording } from "../../config.ts";
import { DocumentDecision } from "./document-decision.tsx";
import {
awaitingCommit,
awaitingReply,
emptyLiveRun,
foldLiveRun,
liveTurnStreaming,
Expand Down Expand Up @@ -171,6 +172,23 @@ describe("conversation transcript", () => {
);
});

test("a sent message waits on the coordinator until its approval card opens, and through an answered question", () => {
const approval = { runId: "run-c", approvalId: "appr-1" };
const question = { runId: "run-c", park: { seq: 4 } };
const heldOnCard = parkedOnApproval(
[approval],
new Set(["appr-1"]),
"run-c",
);
expect(awaitingReply(10, "person", null, false)).toBe(true);
expect(awaitingReply(10, "person", null, heldOnCard)).toBe(false);
expect(awaitingReply(10, "assistant", null, false)).toBe(false);
expect(awaitingReply(10, "person", 12, false)).toBe(false);
// The answer is sent while the question still stands and before the stream reports the run.
const answered = parkedOnApproval([question], new Set(), "run-c");
expect(awaitingReply(20, "person", 12, answered)).toBe(true);
});

test("an inference call that finishes with tool work settles even while the message run stays open on a park", () => {
const started = foldLiveRun(
emptyLiveRun,
Expand Down