diff --git a/apps/web/src/pages/chat-page.tsx b/apps/web/src/pages/chat-page.tsx index 47c8ed17f..a63caddcb 100644 --- a/apps/web/src/pages/chat-page.tsx +++ b/apps/web/src/pages/chat-page.tsx @@ -306,8 +306,12 @@ export function ChatPage({ // are global-only pages now, reached from the shell rail — no // per-workbench header button or `/run` command opens a scoped view // of either here. - onCreateRoutineInSpace={(inSpaceWorkbenchId) => - openRoutine({ routineId: null, workbenchId: inSpaceWorkbenchId }) + onCreateRoutineInSpace={(inSpaceWorkbenchId, preselectedAssetId) => + openRoutine({ + routineId: null, + workbenchId: inSpaceWorkbenchId, + ...(preselectedAssetId !== undefined ? { preselectedAssetId } : {}), + }) } onWorkbenchNotFound={reportWorkbenchNotFound} onGoToMissionControl={() => navigate(MISSION_CONTROL_PATH)} diff --git a/apps/web/src/shell/canvas-availability.tsx b/apps/web/src/shell/canvas-availability.tsx index f36889abd..f45ad4d07 100644 --- a/apps/web/src/shell/canvas-availability.tsx +++ b/apps/web/src/shell/canvas-availability.tsx @@ -64,6 +64,14 @@ export type RoutinePanelSubject = { * in which case the panel falls back to this workbench's own default * (Myra) workbench — never mints a new one. */ readonly workbenchId?: string; + /** Seeds the target picker's initial selection (CL-7356) — the + * conversation's own single agent participant's definition asset id, + * when the opener could resolve exactly one. Shown visibly in + * `DefinitionTargetPicker` and freely replaceable/clearable by the + * person; only their final explicit pick is ever sent to the backend. + * Omitted whenever the opener found zero or several candidates, or has + * no conversation to derive one from at all. */ + readonly preselectedAssetId?: string; }; /** Workbench's concrete instantiation of `@corbits/shell-layout`'s generic diff --git a/apps/web/src/shell/routine-panel.tsx b/apps/web/src/shell/routine-panel.tsx index e48785faa..66981e542 100644 --- a/apps/web/src/shell/routine-panel.tsx +++ b/apps/web/src/shell/routine-panel.tsx @@ -683,6 +683,9 @@ function RoutineEditorPanel({ tenantId={tenantId} value={targetAssetId} onChange={pickTarget} + {...(subject.preselectedAssetId !== undefined + ? { preselectedAssetId: subject.preselectedAssetId } + : {})} /> {needsTargetHint && targetAssetId === null ? (
diff --git a/apps/web/test/routine-panel.test.tsx b/apps/web/test/routine-panel.test.tsx
index baab1fbfb..f8a0c167a 100644
--- a/apps/web/test/routine-panel.test.tsx
+++ b/apps/web/test/routine-panel.test.tsx
@@ -526,6 +526,51 @@ describe("RoutinePanel", () => {
expect(select.value).toBe("");
});
+ // CL-7356: `/routine`'s (and the palette action's) optional
+ // preselection — computed upstream from the conversation's own agent
+ // participants — is carried on the subject and shown visibly here,
+ // never inferred by the panel itself.
+ test("a subject with no preselectedAssetId opens with nothing chosen (zero or several agent participants upstream)", async () => {
+ await renderPanel({ routineId: null, workbenchId: "ch_1" });
+ await settle();
+ const select = container.querySelector(
+ "#routine-panel-target",
+ ) as HTMLSelectElement;
+ expect(select.value).toBe("");
+ });
+
+ test("a subject with preselectedAssetId shows that target already chosen, visibly", async () => {
+ await renderPanel({
+ routineId: null,
+ workbenchId: "ch_1",
+ preselectedAssetId: "asset_myra",
+ });
+ await settle();
+ const select = container.querySelector(
+ "#routine-panel-target",
+ ) as HTMLSelectElement;
+ expect(select.value).toBe("asset_myra");
+ });
+
+ test("a preselected target is replaceable: picking a different one, then naming the routine, creates with the newly picked definitionAssetId", async () => {
+ await renderPanel({
+ routineId: null,
+ workbenchId: "ch_1",
+ preselectedAssetId: "asset_myra",
+ });
+ await settle();
+
+ selectTarget("asset_digest");
+ await settle();
+
+ const name = fieldByLabel("Name this routine") as HTMLInputElement;
+ fillAndBlur(name, "Morning digest");
+ await settle();
+
+ expect(createRoutineCalls).toHaveLength(1);
+ expect(createRoutineCalls[0]?.["definitionAssetId"]).toBe("asset_digest");
+ });
+
test("empty target list shows the empty state with a link to Agents settings, not a picker", async () => {
targets = [];
await renderPanel({ routineId: null, workbenchId: "ch_1" });
diff --git a/packages/chat-ui/src/chat-workspace.tsx b/packages/chat-ui/src/chat-workspace.tsx
index 95f264e2d..7d3e122d5 100644
--- a/packages/chat-ui/src/chat-workspace.tsx
+++ b/packages/chat-ui/src/chat-workspace.tsx
@@ -694,7 +694,10 @@ function ChatWorkspaceInner({
* CL-6099) are global-only pages now — reached from the shell rail, not
* a per-workbench header button or composer command.
*/
- readonly onCreateRoutineInSpace?: (workbenchId: string) => void;
+ readonly onCreateRoutineInSpace?: (
+ workbenchId: string,
+ preselectedAssetId?: string,
+ ) => void;
/** Fired when the routed workbench 404s — a deleted workbench, or a stale
* Recents entry that outlived it. The host owns Recents (this package
* never touches localStorage), so it's told rather than reaching out. */
@@ -1150,6 +1153,22 @@ function ChatWorkspaceInner({
: Promise.resolve([]),
enabled: activeWorkbenchId !== null,
});
+ // `/routine`'s and "New routine in this space"'s optional preselection
+ // (CL-7356): exactly one agent participant hands its definition asset id
+ // straight to the routine panel's picker, visibly and replaceably — zero
+ // or several participants leave the picker with nothing chosen, same as
+ // opening it from `/routines` (CL-7357). Not a guarantee: this reads
+ // `workbenchAgentsQuery`'s current data, which can still be loading (or
+ // mid-refetch after a participant just joined/left) the moment `/routine`
+ // fires — a person who types it before the query resolves gets no
+ // preselection even with exactly one agent, silently. In the common case
+ // the query is already warm (`failedTurnRecovery` below reads the same
+ // data), so this is rarely hit in practice; it's a soft nicety, not
+ // something a caller should rely on always firing.
+ const singleWorkbenchAgentDefinitionAssetId: string | undefined =
+ workbenchAgentsQuery.data?.length === 1
+ ? workbenchAgentsQuery.data[0]?.definitionAssetId
+ : undefined;
const failedTurnRecovery = useMemo((): FailedTurnRecovery => {
const definitionIdByAddress: Record