Skip to content

Commit c391854

Browse files
committed
sync(bfmono): fix(gambit): validate explicit deck path and stabilize stop test (+19 more) (bfmono@9bdc33456)
This PR is an automated gambitmono sync of bfmono Gambit packages. - Source: `packages/gambit/` - Core: `packages/gambit/packages/gambit-core/` - bfmono rev: 9bdc33456 Changes: - 71a94654b fix(gambit): validate explicit deck path and stabilize stop test - 77fa7f139 refactor(gambit-simulator-ui): replace placeholders with reusable callout - d90d57c08 feat(gambit-simulator-ui): send scenario run errors to workbench chat - dc7d93a08 refactor(gambit): Fix left drawer - bdad96a86 chore(repo): pin jsx-runtime imports to react 19.2.4 - 0c787f96d chore(gambit): cut 0.8.5-rc.5 and pin React to 19.2.4 - 0fdcdf864 fix(gambit): unblock npm dnt build for 0.8.5-rc.4 - 48cd964d4 feat(gambit-core): restore built-in exec via host adapter - b48ad8ec2 chore(gambit): cut 0.8.5-rc.3 - 701935f94 ci(gambit-core): gate npm compatibility in core CI - 6957cc580 docs(gambit-core): document worker sandbox host contract - 2eb1ce7c6 test(gambit-core): add unsupported worker sandbox coverage - 825145eea fix(gambit-core): partition worker sandbox by runtime host - 1801a5380 refactor(gambit-core): remove built-in exec tool from runtime - 9db408859 fix(gambit-release): support nested core path and cut 0.8.5-rc.2 - 5e286d3a9 fix(gambit): clean codex smoke temp artifacts under ignored tmp - 2ce4b7ee8 fix(gambit): resolve local jsr package in release-binaries compile - e0c706e16 feat(gambit-simulator): show deck labels in build file selector - 02e3a5133 fix(gambit-simulator): keep test run metadata timestamp stable on selection - d5d4c4790 fix(gambit): make publish config resilient when init package scaffold is absent Do not edit this repo directly; make changes in bfmono and re-run the sync.
1 parent 5b71e7e commit c391854

13 files changed

Lines changed: 865 additions & 118 deletions

simulator-ui/src/BuildPage.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import PageShell from "./gds/PageShell.tsx";
1010
import PageGrid from "./gds/PageGrid.tsx";
1111
import Panel from "./gds/Panel.tsx";
1212
import Listbox, { type ListboxOption } from "./gds/Listbox.tsx";
13+
import Callout from "./gds/Callout.tsx";
1314
import { useWorkspaceBuild } from "./WorkspaceContext.tsx";
1415

1516
type BuildFileEntry = {
@@ -335,11 +336,11 @@ export default function BuildPage(props: {
335336
style={{ minHeight: 0 }}
336337
>
337338
{workspaceOnboardingEnabled && (
338-
<div className="placeholder emphasis">
339+
<Callout variant="emphasis">
339340
Workspace scaffold created. Use the Build chat to refine
340341
<code>PROMPT.md</code>,{" "}
341342
<code>INTENT.md</code>, and the default scenario/grader decks.
342-
</div>
343+
</Callout>
343344
)}
344345
{fileListError && <div className="error">{fileListError}</div>}
345346
<div className="build-files-preview">
@@ -367,28 +368,28 @@ export default function BuildPage(props: {
367368
</div>
368369
<div className="build-files-preview-body">
369370
{!selectedPath && (
370-
<div className="placeholder">
371+
<Callout>
371372
Select a file to preview its contents.
372-
</div>
373+
</Callout>
373374
)}
374375
{selectedPath && filePreview.status === "loading" && (
375-
<div className="placeholder">Loading preview…</div>
376+
<Callout>Loading preview…</Callout>
376377
)}
377378
{selectedPath && filePreview.status === "too-large" && (
378-
<div className="placeholder">
379+
<Callout>
379380
File is too large to preview
380381
{filePreview.size
381382
? ` (${formatBytes(filePreview.size)}).`
382383
: "."}
383-
</div>
384+
</Callout>
384385
)}
385386
{selectedPath && filePreview.status === "binary" && (
386-
<div className="placeholder">
387+
<Callout>
387388
Cannot preview binary data
388389
{filePreview.size
389390
? ` (${formatBytes(filePreview.size)}).`
390391
: "."}
391-
</div>
392+
</Callout>
392393
)}
393394
{selectedPath && filePreview.status === "error" && (
394395
<div className="error">{filePreview.message}</div>

simulator-ui/src/Chat.test.tsx

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,18 @@ const {
5959
BuildChatRows,
6060
ChatView,
6161
bucketBuildChatDisplay,
62+
decodeWorkbenchMessageWithErrorContext,
6263
deriveBuildChatActivityState,
64+
encodeWorkbenchMessageWithErrorContext,
6365
formatElapsedDuration,
6466
} = await import("./Chat.tsx");
6567
const { WorkspaceProvider } = await import("./WorkspaceContext.tsx");
6668
const { globalStyles } = await import("./styles.ts");
6769
type BuildDisplayMessage = import("./utils.ts").BuildDisplayMessage;
6870
type WorkspaceSocketMessage = import("./utils.ts").WorkspaceSocketMessage;
6971
type BuildChatViewState = import("./Chat.tsx").BuildChatViewState;
72+
type WorkbenchScenarioErrorChip =
73+
import("./Chat.tsx").WorkbenchScenarioErrorChip;
7074

7175
type ToolCallSummary = import("./utils.ts").ToolCallSummary;
7276

@@ -161,6 +165,34 @@ function makeChatState(
161165
};
162166
}
163167

168+
Deno.test("Workbench error context envelope encodes and decodes with optional body", () => {
169+
const context = {
170+
source: "scenario_run_error" as const,
171+
workspaceId: "ws-1",
172+
runId: "run-1",
173+
capturedAt: "2026-02-19T00:00:00.000Z",
174+
error: "Scenario run failed",
175+
};
176+
const encodedWithBody = encodeWorkbenchMessageWithErrorContext(
177+
"Please fix this",
178+
context,
179+
);
180+
const decodedWithBody = decodeWorkbenchMessageWithErrorContext(
181+
encodedWithBody,
182+
);
183+
assert(decodedWithBody);
184+
assertEquals(decodedWithBody.context, context);
185+
assertEquals(decodedWithBody.body, "Please fix this");
186+
187+
const encodedChipOnly = encodeWorkbenchMessageWithErrorContext("", context);
188+
const decodedChipOnly = decodeWorkbenchMessageWithErrorContext(
189+
encodedChipOnly,
190+
);
191+
assert(decodedChipOnly);
192+
assertEquals(decodedChipOnly.context, context);
193+
assertEquals(decodedChipOnly.body, "");
194+
});
195+
164196
Deno.test("bucketBuildChatDisplay collapses adjacent non-message rows into one activity block", () => {
165197
const display: BuildDisplayMessage[] = [
166198
{ kind: "message", role: "user", content: "start" },
@@ -769,6 +801,176 @@ Deno.test("reduced-motion fallback disables shimmer while status and timer remai
769801
}
770802
});
771803

804+
Deno.test("BuildChatRows renders Error chip for encoded user messages", async () => {
805+
const context = {
806+
source: "scenario_run_error" as const,
807+
workspaceId: "ws-1",
808+
runId: "run-1",
809+
capturedAt: "2026-02-19T00:00:00.000Z",
810+
error: "Scenario failed",
811+
};
812+
const display: BuildDisplayMessage[] = [
813+
{
814+
kind: "message",
815+
role: "user",
816+
content: encodeWorkbenchMessageWithErrorContext("", context),
817+
},
818+
{
819+
kind: "message",
820+
role: "user",
821+
content: encodeWorkbenchMessageWithErrorContext(
822+
"Please debug this",
823+
context,
824+
),
825+
},
826+
];
827+
828+
let renderer: TestRenderer.ReactTestRenderer | null = null;
829+
try {
830+
await act(async () => {
831+
renderer = TestRenderer.create(<BuildChatRows display={display} />);
832+
});
833+
assert(renderer);
834+
835+
const chips = renderer.root.findAll((node: ReactTestInstance) =>
836+
node.props["data-testid"] === "workbench-transcript-error-chip"
837+
);
838+
assertEquals(chips.length, 2);
839+
840+
const bubbleTexts = renderer.root.findAll((node: ReactTestInstance) =>
841+
node.props.className === "bubble-text" &&
842+
typeof node.props.dangerouslySetInnerHTML?.__html === "string"
843+
);
844+
assertEquals(bubbleTexts.length, 1);
845+
assert(
846+
String(bubbleTexts[0].props.dangerouslySetInnerHTML.__html).includes(
847+
"Please debug this",
848+
),
849+
);
850+
} finally {
851+
if (renderer) {
852+
await act(async () => {
853+
renderer?.unmount();
854+
});
855+
}
856+
}
857+
});
858+
859+
Deno.test("ChatView chip toggle controls whether outbound messages include error context", async () => {
860+
let sentMessage = "";
861+
const baseState = makeChatState({
862+
chatDraft: "Fix the scenario bot",
863+
sendMessage: async (message: string) => {
864+
sentMessage = message;
865+
},
866+
});
867+
const initialChip: WorkbenchScenarioErrorChip = {
868+
source: "scenario_run_error",
869+
workspaceId: "ws-1",
870+
runId: "run-1",
871+
capturedAt: "2026-02-19T00:00:00.000Z",
872+
error: "Scenario failed",
873+
enabled: true,
874+
};
875+
876+
function Harness() {
877+
const [chip, setChip] = React.useState<WorkbenchScenarioErrorChip | null>(
878+
initialChip,
879+
);
880+
return (
881+
<ChatView
882+
state={baseState}
883+
scenarioErrorChip={chip}
884+
onScenarioErrorChipChange={setChip}
885+
/>
886+
);
887+
}
888+
889+
let renderer: TestRenderer.ReactTestRenderer | null = null;
890+
try {
891+
await act(async () => {
892+
renderer = TestRenderer.create(<Harness />);
893+
});
894+
assert(renderer);
895+
896+
const chipNodes = renderer.root.findAll((node: ReactTestInstance) =>
897+
node.props["data-testid"] === "workbench-error-chip"
898+
);
899+
assertEquals(chipNodes.length, 1);
900+
901+
const toggle = renderer.root.find((node: ReactTestInstance) =>
902+
node.props["data-testid"] === "workbench-error-chip-toggle"
903+
);
904+
await act(async () => {
905+
toggle.props.onChange({ target: { checked: false } });
906+
});
907+
908+
const sendButton = renderer.root.find((node: ReactTestInstance) =>
909+
node.props["data-testid"] === "build-send"
910+
);
911+
await act(async () => {
912+
await sendButton.props.onClick();
913+
});
914+
assertEquals(sentMessage, "Fix the scenario bot");
915+
} finally {
916+
if (renderer) {
917+
await act(async () => {
918+
renderer?.unmount();
919+
});
920+
}
921+
}
922+
});
923+
924+
Deno.test("ChatView sends chip-only message when Error chip is on and draft is empty", async () => {
925+
let sentMessage = "";
926+
const baseState = makeChatState({
927+
chatDraft: "",
928+
sendMessage: async (message: string) => {
929+
sentMessage = message;
930+
},
931+
});
932+
const chip: WorkbenchScenarioErrorChip = {
933+
source: "scenario_run_error",
934+
workspaceId: "ws-1",
935+
runId: "run-1",
936+
capturedAt: "2026-02-19T00:00:00.000Z",
937+
error: "Scenario failed",
938+
enabled: true,
939+
};
940+
941+
let renderer: TestRenderer.ReactTestRenderer | null = null;
942+
try {
943+
await act(async () => {
944+
renderer = TestRenderer.create(
945+
<ChatView state={baseState} scenarioErrorChip={chip} />,
946+
);
947+
});
948+
assert(renderer);
949+
950+
const startButtons = renderer.root.findAll((node: ReactTestInstance) =>
951+
node.props["data-testid"] === "build-start"
952+
);
953+
assertEquals(startButtons.length, 0);
954+
const sendButton = renderer.root.find((node: ReactTestInstance) =>
955+
node.props["data-testid"] === "build-send"
956+
);
957+
await act(async () => {
958+
await sendButton.props.onClick();
959+
});
960+
961+
const decoded = decodeWorkbenchMessageWithErrorContext(sentMessage);
962+
assert(decoded);
963+
assertEquals(decoded.context.error, "Scenario failed");
964+
assertEquals(decoded.body, "");
965+
} finally {
966+
if (renderer) {
967+
await act(async () => {
968+
renderer?.unmount();
969+
});
970+
}
971+
}
972+
});
973+
772974
Deno.test("formatElapsedDuration renders mm:ss", () => {
773975
assertEquals(formatElapsedDuration(0), "00:00");
774976
assertEquals(formatElapsedDuration(61), "01:01");

0 commit comments

Comments
 (0)