@elizaos/app-core 2.0.0-alpha.397 → 2.0.0-alpha.398

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/apps/app-steward/src/services/steward-credentials.d.ts.map +1 -1
  2. package/apps/app-steward/src/services/steward-credentials.js +11 -1
  3. package/i18n/locales/en.json +2 -0
  4. package/package.json +5 -5
  5. package/packages/agent/src/triggers/action.d.ts.map +1 -1
  6. package/packages/agent/src/triggers/action.js +14 -2
  7. package/packages/app-core/src/api/automations-compat-routes.d.ts.map +1 -1
  8. package/packages/app-core/src/api/automations-compat-routes.js +18 -6
  9. package/packages/app-core/src/api/client-n8n.js +7 -1
  10. package/packages/app-core/src/api/n8n-routes.js +75 -0
  11. package/packages/app-core/src/components/pages/AutomationsView.d.ts.map +1 -1
  12. package/packages/app-core/src/components/pages/AutomationsView.js +117 -4
  13. package/packages/app-core/src/components/pages/N8nWorkflowsPanel.js +6 -1
  14. package/packages/app-core/src/components/pages/WorkflowGraphViewer.d.ts.map +1 -1
  15. package/packages/app-core/src/components/pages/WorkflowGraphViewer.js +73 -5
  16. package/packages/app-core/src/components/pages/page-scoped-conversations.js +1 -1
  17. package/packages/app-core/src/components/workspace/AppWorkspaceChrome.d.ts.map +1 -1
  18. package/packages/app-core/src/components/workspace/AppWorkspaceChrome.js +17 -4
  19. package/packages/app-core/src/i18n/locales/en.json +2 -0
  20. package/packages/app-core/src/runtime/eliza.d.ts.map +1 -1
  21. package/packages/app-core/src/runtime/eliza.js +37 -1
  22. package/packages/shared/src/index.d.ts +0 -2
  23. package/packages/shared/src/index.d.ts.map +1 -1
  24. package/packages/shared/src/index.js +6 -5
  25. package/packages/typescript/src/prompts.d.ts +2 -2
  26. package/packages/typescript/src/prompts.d.ts.map +1 -1
  27. package/packages/typescript/src/prompts.js +1 -0
  28. package/packages/typescript/src/services/message.d.ts.map +1 -1
  29. package/packages/typescript/src/services/message.js +22 -1
  30. package/packages/shared/src/eliza-core-roles.d.ts +0 -70
  31. package/packages/shared/src/eliza-core-roles.d.ts.map +0 -1
  32. package/packages/shared/src/eliza-core-roles.js +0 -541
@@ -1 +1 @@
1
- {"version":3,"file":"steward-credentials.d.ts","sourceRoot":"","sources":["../../../../../../../apps/app-steward/src/services/steward-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,2BAA2B,GAAG,IAAI,CAe3E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,2BAA2B,GACvC,IAAI,CAYN;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,2BAA2B,GAAG,IAAI,CA6BpC"}
1
+ {"version":3,"file":"steward-credentials.d.ts","sourceRoot":"","sources":["../../../../../../../apps/app-steward/src/services/steward-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,2BAA2B,GAAG,IAAI,CAe3E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,2BAA2B,GACvC,IAAI,CAYN;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,2BAA2B,GAAG,IAAI,CA6BpC"}
@@ -6,8 +6,18 @@
6
6
  * Environment variables always override file values.
7
7
  */
8
8
  import fs from "node:fs";
9
+ import { homedir } from "node:os";
9
10
  import path from "node:path";
10
- import { resolveStateDir } from "@elizaos/core";
11
+ // Inlined to avoid pulling the @elizaos/core source barrel into consumers
12
+ // that only need state-dir resolution (e.g. the Electrobun bun bundle, which
13
+ // would otherwise transitively bundle plugin-sql, transformers, and onnxruntime).
14
+ // Mirrors the canonical implementation in @elizaos/core's
15
+ // src/utils/state-dir.ts: MILADY_STATE_DIR > ELIZA_STATE_DIR > ~/.milady.
16
+ function resolveStateDir() {
17
+ return (process.env.MILADY_STATE_DIR?.trim() ||
18
+ process.env.ELIZA_STATE_DIR?.trim() ||
19
+ path.join(homedir(), ".milady"));
20
+ }
11
21
  const CREDENTIALS_FILENAME = "steward-credentials.json";
12
22
  function resolveCredentialsPath() {
13
23
  return path.join(resolveStateDir(), CREDENTIALS_FILENAME);
@@ -135,6 +135,8 @@
135
135
  "automations.n8n.deleteConfirmMessage": "Permanently delete this workflow. This cannot be undone.",
136
136
  "automations.n8n.deleteConfirmWorkflow": "Delete \"{{name}}\"? This cannot be undone.",
137
137
  "automations.n8n.deleteFailed": "Failed to delete workflow.",
138
+ "automations.n8n.detailEmptyBody": "Select a workflow from the sidebar to view its details, or switch to the dashboard to create a new one.",
139
+ "automations.n8n.detailEmptyHeading": "No workflow selected",
138
140
  "automations.n8n.deleteWorkflow": "Delete workflow",
139
141
  "automations.n8n.errorDeleteWorkflow": "Failed to delete workflow: {{message}}",
140
142
  "automations.n8n.errorLoadStatus": "Failed to load n8n status: {{message}}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elizaos/app-core",
3
- "version": "2.0.0-alpha.397",
3
+ "version": "2.0.0-alpha.398",
4
4
  "description": "Shared application core for elizaOS white-label agent apps.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -464,7 +464,7 @@
464
464
  "@capacitor/preferences": "^8.0.1",
465
465
  "@capacitor/push-notifications": "^8.0.0",
466
466
  "@clack/prompts": "^1.0.0",
467
- "@elizaos/agent": "^2.0.0-alpha.397",
467
+ "@elizaos/agent": "^2.0.0-alpha.398",
468
468
  "@elizaos/app-companion": "^0.0.0",
469
469
  "@elizaos/app-elizamaker": "^0.0.0",
470
470
  "@elizaos/app-lifeops": "^0.0.0",
@@ -473,12 +473,12 @@
473
473
  "@elizaos/app-task-coordinator": "^0.0.0",
474
474
  "@elizaos/app-training": "^0.0.1",
475
475
  "@elizaos/app-vincent": "^0.0.0",
476
- "@elizaos/core": "^2.0.0-alpha.397",
476
+ "@elizaos/core": "^2.0.0-alpha.398",
477
477
  "@elizaos/plugin-browser-bridge": "^0.1.0",
478
478
  "@elizaos/plugin-sql": "^2.0.0-alpha.19",
479
479
  "@elizaos/plugin-wechat": "^0.1.0",
480
- "@elizaos/shared": "^2.0.0-alpha.397",
481
- "@elizaos/ui": "^2.0.0-alpha.397",
480
+ "@elizaos/shared": "^2.0.0-alpha.398",
481
+ "@elizaos/ui": "^2.0.0-alpha.398",
482
482
  "@node-rs/argon2": "^2.0.2",
483
483
  "@radix-ui/react-checkbox": "^1.3.3",
484
484
  "@radix-ui/react-dialog": "^1.1.15",
@@ -1 +1 @@
1
- {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../../../../../agent/src/triggers/action.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AA6HvB,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO5D;AAED,eAAO,MAAM,uBAAuB,EAAE,MAiQrC,CAAC"}
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../../../../../agent/src/triggers/action.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AAgIvB,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAW5D;AAED,eAAO,MAAM,uBAAuB,EAAE,MAyQrC,CAAC"}
@@ -82,15 +82,28 @@ function scheduleText(summary) {
82
82
  }
83
83
  return `on cron ${summary.cronExpression ?? "* * * * *"}`;
84
84
  }
85
+ const EVERY_N_UNIT_PATTERN = /\bevery\s+\d+\s*(second|minute|hour|day|week|month)s?\b/i;
85
86
  export function looksLikeTriggerIntent(text) {
86
87
  const trimmed = text.trim();
87
88
  if (!trimmed) {
88
89
  return false;
89
90
  }
90
- return findKeywordTermMatch(trimmed, TRIGGER_INTENT_TERMS) !== undefined;
91
+ if (findKeywordTermMatch(trimmed, TRIGGER_INTENT_TERMS) !== undefined) {
92
+ return true;
93
+ }
94
+ return EVERY_N_UNIT_PATTERN.test(trimmed);
91
95
  }
92
96
  export const createTriggerTaskAction = {
93
97
  name: CREATE_TRIGGER_TASK_ACTION,
98
+ // SET_REMINDER is deliberately absent: it collides with LIFE's same simile
99
+ // (life.ts:2499) and LIFE owns the user-facing reminder/alarm concept.
100
+ // CREATE_TRIGGER_TASK is for programmatic scheduled jobs (cron, interval,
101
+ // agent-owned heartbeats), not LifeOps reminders. When both actions share
102
+ // a simile, runtime.processActions (runtime.ts:2400-2440) resolves it via
103
+ // first-match iteration order, so the collision was dispatching user
104
+ // reminder intents to whichever action registered first — a race the
105
+ // planner's validate-time gating cannot override (processActions does not
106
+ // re-validate at dispatch, per runtime.ts:2473).
94
107
  similes: [
95
108
  "CREATE_TRIGGER",
96
109
  "SCHEDULE_TRIGGER",
@@ -99,7 +112,6 @@ export const createTriggerTaskAction = {
99
112
  "SCHEDULE_HEARTBEAT",
100
113
  "CREATE_AUTOMATION",
101
114
  "SCHEDULE_AUTOMATION",
102
- "SET_REMINDER",
103
115
  "CREATE_CRON",
104
116
  "CREATE_RECURRING",
105
117
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"automations-compat-routes.d.ts","sourceRoot":"","sources":["../../../../../src/api/automations-compat-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAoClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AA6yBhE,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,OAAO,CAAC,CAiClB"}
1
+ {"version":3,"file":"automations-compat-routes.d.ts","sourceRoot":"","sources":["../../../../../src/api/automations-compat-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAoClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAyzBhE,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,OAAO,CAAC,CAiClB"}
@@ -366,12 +366,24 @@ async function buildAutomationListResponse(req, res, state) {
366
366
  }));
367
367
  }
368
368
  }
369
- for (const [workflowId, room] of workflowRooms.entries()) {
370
- if (!workflowItemsById.has(workflowId)) {
371
- workflowItemsById.set(workflowId, buildWorkflowItem(undefined, room, {
372
- workflowId,
373
- workflowName: room.metadata.workflowName,
374
- }));
369
+ // Only synthesize workflow items from rooms when n8n itself is offline
370
+ // (`workflowFetchError` set) — in that case the room is the most-recent
371
+ // ground truth we have and should be surfaced. When n8n IS online and
372
+ // returned a list, any workflowId in `workflowRooms` that isn't in the
373
+ // current n8n list is an ORPHAN: the workflow was deleted but the chat
374
+ // room/conversation wasn't cleaned up. Surfacing those creates ghost
375
+ // rows the user can't dismiss. Skip them; the UI's deleteWorkflow path
376
+ // also deletes the conversation now, so future deletions won't leak
377
+ // rooms.
378
+ const n8nOffline = workflowFetchError !== null;
379
+ if (n8nOffline) {
380
+ for (const [workflowId, room] of workflowRooms.entries()) {
381
+ if (!workflowItemsById.has(workflowId)) {
382
+ workflowItemsById.set(workflowId, buildWorkflowItem(undefined, room, {
383
+ workflowId,
384
+ workflowName: room.metadata.workflowName,
385
+ }));
386
+ }
375
387
  }
376
388
  }
377
389
  const coordinatorTriggerItems = triggerItems
@@ -32,10 +32,16 @@ ElizaClient.prototype.updateN8nWorkflow = async function (id, request) {
32
32
  });
33
33
  };
34
34
  ElizaClient.prototype.generateN8nWorkflow = async function (request) {
35
+ // LLM-driven workflow generation runs keyword extraction, node search,
36
+ // generation, multiple correction passes, and feasibility assessment
37
+ // sequentially — easily 30-90s on a cold cache. The 10s default fetch
38
+ // timeout is far too aggressive and surfaces as
39
+ // "Request timed out after 10000ms" in the Automations UI even when
40
+ // the backend would have succeeded a few seconds later.
35
41
  return this.fetch("/api/n8n/workflows/generate", {
36
42
  method: "POST",
37
43
  body: JSON.stringify(request),
38
- });
44
+ }, { timeoutMs: 120_000 });
39
45
  };
40
46
  ElizaClient.prototype.activateN8nWorkflow = async function (id) {
41
47
  return this.fetch(`/api/n8n/workflows/${encodeURIComponent(id)}/activate`, {
@@ -888,6 +888,81 @@ async function handleGenerateWorkflow(ctx, sidecar, native) {
888
888
  }
889
889
  const name = readOptionalString(body, "name");
890
890
  const workflowId = readOptionalString(body, "workflowId");
891
+ // Prefer the plugin's `deployWorkflow` pipeline so generated workflows go
892
+ // through credential resolution server-side. Without this, the LLM emits
893
+ // `credentials: { gmailOAuth2: { id: "{{CREDENTIAL_ID}}" } }` and the
894
+ // placeholder reaches n8n raw — n8n then crashes on
895
+ // `Cannot read properties of undefined (reading 'execute')` whenever the
896
+ // user tries to activate or run the workflow because no real credential id
897
+ // exists for the lookup. The fallback proxy chain only runs when the
898
+ // plugin's workflow service isn't registered (test/CI shapes).
899
+ const service = ctx.runtime?.getService?.("n8n_workflow");
900
+ if (typeof service?.generateWorkflowDraft === "function" &&
901
+ typeof service?.deployWorkflow === "function" &&
902
+ typeof service?.getWorkflow === "function") {
903
+ // Two-phase try blocks. The OUTER try wraps only operations that have NOT
904
+ // yet committed a side effect to n8n — generateWorkflowDraft (LLM call,
905
+ // pure) and deployWorkflow (the write). Failures here can safely fall
906
+ // through to the legacy proxy path because no workflow has been written
907
+ // yet. Once deployWorkflow returns successfully, the workflow IS in n8n
908
+ // and any further failure (e.g. the follow-up getWorkflow read) must NOT
909
+ // re-enter the legacy path — that path would re-invoke the LLM and POST
910
+ // a new workflow OR PUT-overwrite the just-deployed one with unresolved
911
+ // `{{CREDENTIAL_ID}}` placeholders.
912
+ let deployed;
913
+ try {
914
+ const draft = await service.generateWorkflowDraft(prompt);
915
+ if (name?.trim()) {
916
+ draft.name = name.trim();
917
+ }
918
+ if (workflowId) {
919
+ draft.id = workflowId;
920
+ }
921
+ // The plugin's deployWorkflow → getUserTagName → getEntityById(userId)
922
+ // requires a real UUID; passing "local" makes the entity lookup throw
923
+ // "Failed query: ... where entities.id in ($1)" because pg rejects the
924
+ // non-UUID string. Use the runtime's agentId — it's always a valid
925
+ // entity in Milady local and gives a stable per-agent tag for n8n
926
+ // credential mapping.
927
+ const userId = resolveAgentId(ctx);
928
+ deployed = await service.deployWorkflow(draft, userId);
929
+ }
930
+ catch (err) {
931
+ logger.warn({
932
+ err: err instanceof Error ? err.message : String(err),
933
+ }, "[n8n-routes] generate/deploy path failed before commit; falling back to direct proxy");
934
+ // Pre-deploy failure — deploy never committed. Legacy proxy fallback
935
+ // is safe here.
936
+ }
937
+ if (deployed) {
938
+ if (deployed.missingCredentials.length > 0) {
939
+ sendJson(ctx, 200, {
940
+ ...deployed,
941
+ warning: "missing credentials",
942
+ });
943
+ return true;
944
+ }
945
+ // Deploy succeeded — the workflow is committed in n8n. Try to enrich
946
+ // the response with the full workflow body, but on failure return the
947
+ // deploy summary instead of falling through (see comment above on why
948
+ // the legacy path would corrupt the just-deployed workflow).
949
+ let full;
950
+ try {
951
+ full = await service.getWorkflow(deployed.id);
952
+ }
953
+ catch (readErr) {
954
+ logger.warn({
955
+ err: readErr instanceof Error ? readErr.message : String(readErr),
956
+ deployedId: deployed.id,
957
+ }, "[n8n-routes] follow-up getWorkflow failed after successful deploy; returning deploy summary");
958
+ }
959
+ sendJson(ctx, 200, full ?? deployed);
960
+ return true;
961
+ }
962
+ }
963
+ // Legacy proxy fallback — used when the workflow service isn't available
964
+ // (e.g. plugin disabled in tests). Generated workflows from this path
965
+ // ship to n8n with `{{CREDENTIAL_ID}}` placeholders unresolved.
891
966
  const payload = await generateWorkflowPayload(ctx, prompt, name);
892
967
  if (workflowId) {
893
968
  return writeWorkflow(ctx, "PUT", `/${encodeURIComponent(workflowId)}`, payload, sidecar, native);
@@ -1 +1 @@
1
- {"version":3,"file":"AutomationsView.d.ts","sourceRoot":"","sources":["../../../../../../src/components/pages/AutomationsView.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAsrKH,wBAAgB,eAAe,4CAO9B;AAED,wBAAgB,uBAAuB,4CAmBtC"}
1
+ {"version":3,"file":"AutomationsView.d.ts","sourceRoot":"","sources":["../../../../../../src/components/pages/AutomationsView.tsx"],"names":[],"mappings":"AAAA;;GAEG;AA+wKH,wBAAgB,eAAe,4CAO9B;AAED,wBAAgB,uBAAuB,4CAoDtC"}
@@ -534,6 +534,15 @@ function useAutomationsViewController() {
534
534
  useEffect(() => {
535
535
  if (!selectedItemId)
536
536
  return;
537
+ // Exempt in-flight workflow drafts — they're not in allItems until
538
+ // generateWorkflowFromPrompt finishes and refreshAutomations
539
+ // surfaces the real workflow. Without this exemption the draft
540
+ // selection gets cleared mid-generation, the auto-select effect
541
+ // below picks lastSelectedIdRef (typically a task like Heartbeat),
542
+ // and the user lands somewhere unexpected — defeating the
543
+ // WorkflowGenerationProgress UI we just added on the draft pane.
544
+ if (selectedItemId.startsWith("workflow-draft:"))
545
+ return;
537
546
  if (!allItems.some((item) => item.id === selectedItemId)) {
538
547
  setSelectedItemId(null);
539
548
  setSelectedItemKind(null);
@@ -774,7 +783,42 @@ function useAutomationsViewController() {
774
783
  if (editorOpen || editingId || editingTaskId)
775
784
  return null;
776
785
  if (selectedItemId) {
777
- return allItems.find((item) => item.id === selectedItemId) ?? null;
786
+ const found = allItems.find((item) => item.id === selectedItemId);
787
+ if (found)
788
+ return found;
789
+ // In-flight workflow draft (createWorkflowDraft set selectedItemId
790
+ // BEFORE refreshAutomations surfaced the real workflow). Synthesize
791
+ // a minimal draft item so AutomationDraftPane renders with the
792
+ // WorkflowGenerationProgress card during the LLM call.
793
+ //
794
+ // The `room` field is left null here. `activeWorkflowConversation`
795
+ // — the live conversation tied to this draft — lives in
796
+ // AutomationsLayout (a different scope from this controller hook),
797
+ // so we cannot read it from here without a larger refactor.
798
+ // Instead, handleDeleteDraft reads activeWorkflowConversation
799
+ // directly and falls back to it when item.room is missing on a
800
+ // synthesized draft (see the handler below).
801
+ if (selectedItemId.startsWith("workflow-draft:")) {
802
+ const draftId = selectedItemId.slice("workflow-draft:".length);
803
+ const synthesized = {
804
+ id: selectedItemId,
805
+ type: "automation_draft",
806
+ source: "workflow_draft",
807
+ title: "New workflow",
808
+ description: "",
809
+ status: "draft",
810
+ enabled: false,
811
+ system: false,
812
+ isDraft: true,
813
+ hasBackingWorkflow: false,
814
+ updatedAt: null,
815
+ draftId,
816
+ schedules: [],
817
+ room: null,
818
+ };
819
+ return synthesized;
820
+ }
821
+ return null;
778
822
  }
779
823
  return allItems[0] ?? null;
780
824
  }, [allItems, editingId, editingTaskId, editorOpen, selectedItemId]);
@@ -2216,6 +2260,33 @@ function AutomationsLayout() {
2216
2260
  try {
2217
2261
  await client.deleteN8nWorkflow(item.workflowId);
2218
2262
  await Promise.all(item.schedules.map((schedule) => client.deleteTrigger(schedule.id)));
2263
+ // Also delete the chat conversation/room that backed this workflow.
2264
+ // Without this, the `/api/automations` aggregator keeps surfacing the
2265
+ // workflow row as a ghost entry because rooms with workflowId
2266
+ // metadata are listed even when the underlying n8n workflow is gone.
2267
+ const conversationId = item.room?.conversationId;
2268
+ if (conversationId) {
2269
+ try {
2270
+ await client.deleteConversation(conversationId);
2271
+ }
2272
+ catch (roomErr) {
2273
+ // Non-fatal: the workflow itself is deleted; surface the room
2274
+ // failure to the user but don't roll back.
2275
+ setPageNotice(`Workflow deleted, but its chat room could not be removed: ${roomErr instanceof Error ? roomErr.message : String(roomErr)}`);
2276
+ }
2277
+ }
2278
+ if (selectedItemId === item.id) {
2279
+ setSelectedItemId(null);
2280
+ setSelectedItemKind(null);
2281
+ }
2282
+ // Mirror handleDeleteDraft: also clear the active workflow
2283
+ // conversation if it's the one tied to the deleted workflow.
2284
+ // Without this, refreshAutomationsWithDraftBinding keeps receiving
2285
+ // a stale conversation reference on subsequent refreshes.
2286
+ if (conversationId &&
2287
+ activeWorkflowConversation?.id === conversationId) {
2288
+ setActiveWorkflowConversation(null);
2289
+ }
2219
2290
  await ctx.refreshAutomations();
2220
2291
  }
2221
2292
  catch (error) {
@@ -2228,7 +2299,15 @@ function AutomationsLayout() {
2228
2299
  finally {
2229
2300
  setWorkflowBusyId(null);
2230
2301
  }
2231
- }, [ctx, t]);
2302
+ }, [
2303
+ activeWorkflowConversation?.id,
2304
+ ctx,
2305
+ selectedItemId,
2306
+ setActiveWorkflowConversation,
2307
+ setSelectedItemId,
2308
+ setSelectedItemKind,
2309
+ t,
2310
+ ]);
2232
2311
  const handleDuplicateWorkflow = useCallback(async (item) => {
2233
2312
  if (!item.workflowId) {
2234
2313
  return;
@@ -2247,7 +2326,13 @@ function AutomationsLayout() {
2247
2326
  }
2248
2327
  }, [ctx, selectWorkflowById]);
2249
2328
  const handleDeleteDraft = useCallback(async (item) => {
2250
- const conversationId = item.room?.conversationId;
2329
+ // Synthesized in-flight drafts (workflow-draft:* ids surfaced
2330
+ // before refreshAutomations completes) have no `room` because the
2331
+ // controller's resolvedSelectedItem useMemo can't reach
2332
+ // activeWorkflowConversation from its scope. Fall back to the
2333
+ // live activeWorkflowConversation when item.room is missing —
2334
+ // that's the same conversation the synthesized item represents.
2335
+ const conversationId = item.room?.conversationId ?? activeWorkflowConversation?.id ?? null;
2251
2336
  if (!conversationId) {
2252
2337
  setPageNotice("This draft is missing its automation room.");
2253
2338
  return;
@@ -2377,5 +2462,33 @@ export function AutomationsView() {
2377
2462
  }
2378
2463
  export function AutomationsDesktopShell() {
2379
2464
  const controller = useAutomationsViewController();
2380
- return (_jsx(AutomationsViewContext.Provider, { value: controller, children: _jsx(AppWorkspaceChrome, { testId: "automations-workspace", chat: _jsx(AutomationsSidebarChat, { activeItem: controller.resolvedSelectedItem }), main: _jsx("div", { className: "flex flex-col flex-1 min-h-0 min-w-0 overflow-hidden", children: _jsx(AutomationsLayout, {}) }) }) }));
2465
+ // Collapse the right-rail chat dock when no workflow / draft is
2466
+ // selected. The Automations Overview page already has a centered hero
2467
+ // compose ("Describe a task or workflow…") that's the canonical create
2468
+ // surface; the bottom-right dock + hero showed two inputs at once and
2469
+ // confused users. When a workflow or draft IS selected, the rail (and
2470
+ // its PageScopedChatPane) opens so the user can edit/refine.
2471
+ //
2472
+ // Stay in CONTROLLED mode at all times. An earlier revision flipped
2473
+ // between controlled (when nothing selected) and uncontrolled (when
2474
+ // something selected); that left AppWorkspaceChrome's internal
2475
+ // `internalCollapsed` stuck at `true` after the controlled phase, so
2476
+ // the rail never opened on the first selection. Always-controlled
2477
+ // avoids that transition entirely.
2478
+ const hasScopedItem = controller.resolvedSelectedItem != null;
2479
+ const [userCollapsedWhenSelected, setUserCollapsedWhenSelected] = useState(false);
2480
+ // When the user clears the selection, also reset the toggle so the next
2481
+ // selection starts with the rail open by default.
2482
+ useEffect(() => {
2483
+ if (!hasScopedItem)
2484
+ setUserCollapsedWhenSelected(false);
2485
+ }, [hasScopedItem]);
2486
+ const chatCollapsed = hasScopedItem ? userCollapsedWhenSelected : true;
2487
+ const handleToggleChat = useCallback((next) => {
2488
+ // No-op when nothing is selected — the rail is force-closed in that
2489
+ // state and the toggle button is hidden, so this should never fire.
2490
+ if (hasScopedItem)
2491
+ setUserCollapsedWhenSelected(next);
2492
+ }, [hasScopedItem]);
2493
+ return (_jsx(AutomationsViewContext.Provider, { value: controller, children: _jsx(AppWorkspaceChrome, { testId: "automations-workspace", chat: _jsx(AutomationsSidebarChat, { activeItem: controller.resolvedSelectedItem }), chatCollapsed: chatCollapsed, onToggleChat: handleToggleChat, hideCollapseButton: !hasScopedItem, main: _jsx("div", { className: "flex flex-col flex-1 min-h-0 min-w-0 overflow-hidden", children: _jsx(AutomationsLayout, {}) }) }) }));
2381
2494
  }
@@ -106,7 +106,12 @@ function WorkflowDetailPane({ workflow, conversationTitle, conversationMetadata,
106
106
  setChatCollapsed(false);
107
107
  }, [workflow?.id]);
108
108
  if (!workflow) {
109
- return (_jsx("div", { className: "flex flex-col gap-4 p-4 h-full", children: _jsx(AutomationRoomChatPane, { assistantLabel: t("automations.chat.assistantLabel"), collapsed: false, metadata: conversationMetadata, onToggleCollapse: () => { }, composerRef: composerRef, onConversationResolved: onConversationResolved, onAutomationMutated: onWorkflowMutated, placeholder: t("automations.chat.placeholder"), systemAddendum: WORKFLOW_SYSTEM_ADDENDUM, title: conversationTitle }) }));
109
+ // Empty-state chat pane removed: the hero compose on the AutomationsView
110
+ // dashboard tab is the canonical workflow-create surface. The
111
+ // per-workflow chat pane below renders only after a workflow is selected,
112
+ // where the planner-routed conversational flow is the right tool for
113
+ // edit/refine.
114
+ return (_jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-2 p-8 text-center text-sm text-muted", children: [_jsx(Workflow, { className: "h-6 w-6 text-muted/40" }), _jsx("div", { className: "text-txt-strong font-semibold", children: t("automations.n8n.detailEmptyHeading") }), _jsx("div", { className: "text-xs text-muted/80 max-w-xs", children: t("automations.n8n.detailEmptyBody") })] }));
110
115
  }
111
116
  const nodes = workflow.nodes ?? [];
112
117
  const nodeCount = workflow.nodeCount ?? nodes.length;
@@ -1 +1 @@
1
- {"version":3,"file":"WorkflowGraphViewer.d.ts","sourceRoot":"","sources":["../../../../../../src/components/pages/WorkflowGraphViewer.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAEV,iBAAiB,EACjB,WAAW,EAEZ,MAAM,6BAA6B,CAAC;AA6hBrC,UAAU,wBAAwB;IAChC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,OAAe,EACf,YAAoB,EACpB,qBAAgD,EAChD,kBAAqE,EACrE,WAAW,EACX,kBAAkB,EAClB,MAAM,GACP,EAAE,wBAAwB,2CAwQ1B"}
1
+ {"version":3,"file":"WorkflowGraphViewer.d.ts","sourceRoot":"","sources":["../../../../../../src/components/pages/WorkflowGraphViewer.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAEV,iBAAiB,EACjB,WAAW,EAEZ,MAAM,6BAA6B,CAAC;AAwrBrC,UAAU,wBAAwB;IAChC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACnC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,OAAe,EACf,YAAoB,EACpB,qBAAgD,EAChD,kBAAqE,EACrE,WAAW,EACX,kBAAkB,EAClB,MAAM,GACP,EAAE,wBAAwB,2CA+P1B"}
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Spinner, StatusBadge, } from "@elizaos/ui";
3
3
  import { Background, Controls, MiniMap, ReactFlow, } from "@xyflow/react";
4
4
  import { ExternalLink, Maximize2, X } from "lucide-react";
@@ -170,6 +170,77 @@ function graphChrome(uiTheme) {
170
170
  overlayChipText: "#60a5fa",
171
171
  };
172
172
  }
173
+ // ── Generation progress overlay ───────────────────────────────────────────────
174
+ /**
175
+ * Stage messages for `WorkflowGenerationProgress`. The plugin's workflow
176
+ * generation today is a single request/response, so the client cannot yet
177
+ * observe the actual stage in real time. We cycle through plausible labels
178
+ * on a fixed timer based on observed median latencies of each phase:
179
+ * 1. extractKeywords (fast — runtime-context provider + keyword LLM call)
180
+ * 2. searchNodes + credential filter + fetchRuntimeContext
181
+ * 3. generateWorkflow (LLM, slowest)
182
+ * 4. validateAndRepair + injectMissingCredentialBlocks
183
+ * 5. deployWorkflow + resolveCredentials + activate
184
+ *
185
+ * When the plugin grows a server-sent-events streaming endpoint, the timer
186
+ * can be replaced with real per-stage progress events.
187
+ */
188
+ const WORKFLOW_GENERATION_STAGES = [
189
+ {
190
+ label: "Understanding your prompt",
191
+ hint: "Extracting keywords + matching providers",
192
+ startsAt: 0,
193
+ },
194
+ {
195
+ label: "Finding the right nodes",
196
+ hint: "Searching catalog + checking credentials",
197
+ startsAt: 3,
198
+ },
199
+ {
200
+ label: "Generating workflow",
201
+ hint: "Asking the LLM with runtime facts",
202
+ startsAt: 6,
203
+ },
204
+ {
205
+ label: "Validating + repairing",
206
+ hint: "Clamping versions + auto-fixing references",
207
+ startsAt: 18,
208
+ },
209
+ {
210
+ label: "Deploying to n8n",
211
+ hint: "Minting credentials + activating",
212
+ startsAt: 24,
213
+ },
214
+ {
215
+ label: "Almost done",
216
+ hint: "Wrapping up — this is taking a bit longer than usual",
217
+ startsAt: 35,
218
+ },
219
+ ];
220
+ function WorkflowGenerationProgress({ chrome, }) {
221
+ const [elapsed, setElapsed] = useState(0);
222
+ useEffect(() => {
223
+ const start = Date.now();
224
+ const id = setInterval(() => {
225
+ setElapsed(Math.floor((Date.now() - start) / 1000));
226
+ }, 500);
227
+ return () => clearInterval(id);
228
+ }, []);
229
+ const currentIndex = WORKFLOW_GENERATION_STAGES.reduce((acc, stage, idx) => (elapsed >= stage.startsAt ? idx : acc), 0);
230
+ return (_jsx("div", { className: "w-full max-w-md rounded-xl border px-5 py-4 text-sm shadow-lg", style: {
231
+ background: chrome.overlayChipBg,
232
+ color: chrome.overlayChipText,
233
+ borderColor: chrome.overlayChipText,
234
+ }, children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(Spinner, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [_jsxs("div", { children: [_jsx("div", { className: "font-semibold", children: "Building your workflow\u2026" }), _jsx("div", { className: "text-xs opacity-70", children: "Generations usually take 10\u201330 seconds." })] }), _jsx("ol", { className: "space-y-1.5", children: WORKFLOW_GENERATION_STAGES.map((stage, idx) => {
235
+ const isDone = idx < currentIndex;
236
+ const isActive = idx === currentIndex;
237
+ return (_jsxs("li", { className: `flex items-start gap-2 text-xs transition-opacity ${isDone || isActive ? "opacity-100" : "opacity-40"}`, children: [_jsx("span", { className: `mt-0.5 inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border ${isDone
238
+ ? "border-current bg-current/15"
239
+ : isActive
240
+ ? "border-current bg-current/15"
241
+ : "border-current/40"}`, "aria-hidden": true, children: isDone ? (_jsx("svg", { viewBox: "0 0 12 12", className: "h-2.5 w-2.5", fill: "none", stroke: "currentColor", strokeWidth: "2", role: "img", "aria-label": "completed", children: _jsx("path", { d: "M2.5 6.5l2.5 2.5 4.5-5", strokeLinecap: "round", strokeLinejoin: "round" }) })) : isActive ? (_jsx("span", { className: "h-1.5 w-1.5 animate-pulse rounded-full bg-current", "aria-hidden": true })) : null }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: `font-medium ${isActive ? "" : "opacity-70"}`, children: stage.label }), (isDone || isActive) && (_jsxs("span", { className: "ml-1.5 opacity-60", children: ["\u2014 ", stage.hint] }))] })] }, stage.label));
242
+ }) })] })] }) }));
243
+ }
173
244
  // ── Node detail drawer ────────────────────────────────────────────────────────
174
245
  const PARAM_TRUNCATE_LENGTH = 200;
175
246
  function ParamValue({ value }) {
@@ -317,10 +388,7 @@ export function WorkflowGraphViewer({ workflow, loading = false, isGenerating =
317
388
  ? "animate-pulse ring-2 ring-blue-500/50"
318
389
  : "ring-1 ring-border/30";
319
390
  const chrome = graphChrome(uiTheme);
320
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: containerRef, role: "img", "aria-label": ariaLabel, className: `relative overflow-hidden rounded-lg ${borderClass}`, style: { height: 420, background: chrome.canvasBg }, children: [loading && !hasNodes && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Spinner, { className: "h-6 w-6 text-muted" }) })), !loading && !hasNodes && !isGenerating && (_jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("p", { className: `text-sm font-medium ${chrome.emptyTitleClass}`, children: "Blank workflow" }), _jsx("p", { className: `max-w-sm text-xs ${chrome.emptyHelpClass}`, children: emptyStateHelpText }), onEmptyStateAction && (_jsx("button", { type: "button", className: "mt-1 rounded-md border border-border/40 bg-bg/40 px-3 py-1.5 text-xs text-txt hover:bg-bg/70 transition-colors", onClick: onEmptyStateAction, children: emptyStateActionLabel }))] })), isGenerating && (_jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center backdrop-blur-[1px]", style: { background: chrome.overlayBg }, children: _jsxs("div", { className: "flex items-center gap-2 rounded-full border border-blue-500/30 px-4 py-2 text-sm", style: {
321
- background: chrome.overlayChipBg,
322
- color: chrome.overlayChipText,
323
- }, children: [_jsx(Spinner, { className: "h-4 w-4" }), "Building workflow..."] }) })), !loading && (_jsx("div", { role: "presentation", className: "h-full w-full", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsxs(ReactFlow, { nodes: nodes, edges: isGenerating ? generatingEdges(edges) : edges, nodesDraggable: !isGenerating, nodesConnectable: false, edgesReconnectable: false, onNodeClick: handleNodeClick, fitView: true, fitViewOptions: { padding: 0.2, maxZoom: 1.2 }, proOptions: { hideAttribution: true }, "aria-label": ariaLabel, children: [_jsx(Background, { color: chrome.dots, gap: 20, size: 1 }), _jsx(Controls, { showInteractive: false }), hasNodes && (_jsx(MiniMap, { nodeColor: (n) => {
391
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: containerRef, role: "img", "aria-label": ariaLabel, className: `relative overflow-hidden rounded-lg ${borderClass}`, style: { height: 420, background: chrome.canvasBg }, children: [loading && !hasNodes && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Spinner, { className: "h-6 w-6 text-muted" }) })), !loading && !hasNodes && !isGenerating && (_jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("p", { className: `text-sm font-medium ${chrome.emptyTitleClass}`, children: "Blank workflow" }), _jsx("p", { className: `max-w-sm text-xs ${chrome.emptyHelpClass}`, children: emptyStateHelpText }), onEmptyStateAction && (_jsx("button", { type: "button", className: "mt-1 rounded-md border border-border/40 bg-bg/40 px-3 py-1.5 text-xs text-txt hover:bg-bg/70 transition-colors", onClick: onEmptyStateAction, children: emptyStateActionLabel }))] })), isGenerating && (_jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center backdrop-blur-[1px]", style: { background: chrome.overlayBg }, children: _jsx(WorkflowGenerationProgress, { chrome: chrome }) })), !loading && (_jsx("div", { role: "presentation", className: "h-full w-full", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsxs(ReactFlow, { nodes: nodes, edges: isGenerating ? generatingEdges(edges) : edges, nodesDraggable: !isGenerating, nodesConnectable: false, edgesReconnectable: false, onNodeClick: handleNodeClick, fitView: true, fitViewOptions: { padding: 0.2, maxZoom: 1.2 }, proOptions: { hideAttribution: true }, "aria-label": ariaLabel, children: [_jsx(Background, { color: chrome.dots, gap: 20, size: 1 }), _jsx(Controls, { showInteractive: false }), hasNodes && (_jsx(MiniMap, { nodeColor: (n) => {
324
392
  const colors = n.data
325
393
  ?.colors;
326
394
  return colors?.border ?? "#475569";
@@ -32,7 +32,7 @@ export const PAGE_SCOPE_COPY = {
32
32
  "page-automations": {
33
33
  title: "Automations",
34
34
  body: "Use me to create or inspect a task or n8n workflow. Tell me the trigger, timing, and result.",
35
- systemAddendum: "You are answering inside the Automations view. The user can create tasks and n8n workflows, attach either one to a schedule or event, configure wake mode, max runs, and enabled state, browse templates, inspect existing automations, and troubleshoot failed runs. Treat tasks as simple prompt-driven automations and workflows as multi-step n8n pipelines. Recommend the smaller task shape unless the user clearly needs a multi-step pipeline. Use createTriggerTaskAction and manageTasksAction when the request is concrete. Reference live tasks and workflows in context by display name. Never fabricate automation names.",
35
+ systemAddendum: "You are answering inside the Automations view. The user can create tasks and n8n workflows, attach either one to a schedule or event, configure wake mode, max runs, and enabled state, browse templates, inspect existing automations, and troubleshoot failed runs. Treat tasks as simple prompt-driven automations and workflows as multi-step n8n pipelines. Recommend the smaller task shape unless the user clearly needs a multi-step pipeline. When the user describes a concrete automation, dispatch it via the planner's <actions> field using <action><name>CREATE_TRIGGER_TASK</name></action> for scheduled or event tasks, or <action><name>MANAGE_TASKS</name></action> for task list operations. Reference live tasks and workflows in context by display name. Never fabricate automation names.",
36
36
  },
37
37
  "page-apps": {
38
38
  title: "Apps chat",
@@ -1 +1 @@
1
- {"version":3,"file":"AppWorkspaceChrome.d.ts","sourceRoot":"","sources":["../../../../../../src/components/workspace/AppWorkspaceChrome.tsx"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,GAAG,EACR,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAEvE,eAAO,MAAM,qCAAqC,wCACX,CAAC;AACxC,eAAO,MAAM,2CAA2C,oCACrB,CAAC;AAOpC,UAAU,kCAAkC;IAC1C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACrB;AAKD,wBAAgB,yBAAyB,IAAI,kCAAkC,GAAG,IAAI,CAErF;AAED,UAAU,mCAAmC;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,MAAsC,GACvC,EAAE,mCAAmC,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAgB1D;AA0HD,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kCAAkC;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,IAAI,CAC5B,uBAAuB,EACvB,OAAO,GAAG,eAAe,CAC1B,CAAC;IACF;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,uBAAuB,EACvB,aAAa,EAAE,iBAAiB,EAChC,YAAY,EACZ,oBAA4B,EAC5B,kBAA0B,EAC1B,YAAoB,EACpB,MAA+B,GAChC,EAAE,uBAAuB,GAAG,GAAG,CAAC,OAAO,CAgRvC"}
1
+ {"version":3,"file":"AppWorkspaceChrome.d.ts","sourceRoot":"","sources":["../../../../../../src/components/workspace/AppWorkspaceChrome.tsx"],"names":[],"mappings":"AAYA,OAAO,EAEL,KAAK,GAAG,EACR,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAEvE,eAAO,MAAM,qCAAqC,wCACX,CAAC;AACxC,eAAO,MAAM,2CAA2C,oCACrB,CAAC;AAOpC,UAAU,kCAAkC;IAC1C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACrB;AAKD,wBAAgB,yBAAyB,IAAI,kCAAkC,GAAG,IAAI,CAErF;AAED,UAAU,mCAAmC;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,MAAsC,GACvC,EAAE,mCAAmC,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAgB1D;AA0HD,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kCAAkC;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,IAAI,CAC5B,uBAAuB,EACvB,OAAO,GAAG,eAAe,CAC1B,CAAC;IACF;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,uBAAuB,EACvB,aAAa,EAAE,iBAAiB,EAChC,YAAY,EACZ,oBAA4B,EAC5B,kBAA0B,EAC1B,YAAoB,EACpB,MAA+B,GAChC,EAAE,uBAAuB,GAAG,GAAG,CAAC,OAAO,CA6RvC"}
@@ -79,14 +79,27 @@ export function AppWorkspaceChrome({ nav, main, chat, chatScope, pageScopedChatP
79
79
  const collapsed = isControlled
80
80
  ? (chatCollapsedProp ?? false)
81
81
  : internalCollapsed;
82
+ // Controlled mode is the source of truth on every viewport — including
83
+ // mobile. When the page passes chatCollapsed=true (e.g. forcing a
84
+ // single canonical compose surface in an empty state), the mobile
85
+ // pane-switcher must respect that and not flip back to open via local
86
+ // mobileChatOpen state.
82
87
  const effectiveCollapsed = chatDisabled
83
88
  ? true
84
- : isMobileViewport
85
- ? !mobileChatOpen
86
- : collapsed;
89
+ : isControlled
90
+ ? collapsed
91
+ : isMobileViewport
92
+ ? !mobileChatOpen
93
+ : collapsed;
87
94
  const handleToggle = useCallback((next) => {
88
95
  if (isMobileViewport) {
89
96
  mobileSidebarControl?.setOpen(false);
97
+ if (isControlled) {
98
+ // Page is the source of truth on mobile too — defer to it
99
+ // instead of touching local mobileChatOpen state.
100
+ onToggleChat?.(next);
101
+ return;
102
+ }
90
103
  setMobileChatOpen(chatDisabled ? false : !next);
91
104
  return;
92
105
  }
@@ -200,7 +213,7 @@ export function AppWorkspaceChrome({ nav, main, chat, chatScope, pageScopedChatP
200
213
  const chatContent = chat ??
201
214
  (chatScope ? (_jsx(PageScopedChatPane, { ...pageScopedChatPaneProps, scope: chatScope })) : (_jsx(ChatView, { variant: "default" })));
202
215
  return (_jsx(WorkspaceMobileSidebarControlsContext.Provider, { value: mobileSidebarControlsValue, children: _jsx(AppWorkspaceChatChromeContext.Provider, { value: chatChromeContextValue, children: _jsxs("div", { className: `flex min-h-0 min-w-0 w-full flex-1 bg-bg pb-[var(--eliza-mobile-nav-offset,0px)] ${isMobileViewport ? "flex-col" : ""}`, "data-testid": testId, children: [isMobileViewport &&
203
- (!chatDisabled || mobileSidebarControl !== null) ? (_jsx(MobileWorkspacePaneSwitcher, { chatAvailable: !chatDisabled, chatOpen: !effectiveCollapsed, sidebar: mobileSidebarControl, onChat: () => handleToggle(false), onMain: handleOpenMobileMain, onSidebar: handleOpenMobileSidebar })) : null, _jsxs("div", { className: `relative min-h-0 min-w-0 flex-1 flex-col overflow-hidden ${isMobileViewport && !effectiveCollapsed ? "hidden" : "flex"}`, children: [nav, _jsx("div", { className: "relative flex min-h-0 flex-1 flex-col overflow-hidden", children: main })] }), chatDisabled ? null : effectiveCollapsed ? (_jsx("aside", { className: "w-0 min-w-0 shrink-0", "data-testid": `${testId}-chat-sidebar`, "data-collapsed": true, children: isMobileViewport ? null : (_jsx(AppWorkspaceChatDockToggleButton, { collapsed: true, testId: `${testId}-chat-expand` })) })) : (_jsxs(_Fragment, { children: [isMobileViewport ? (_jsx("aside", { className: "flex min-h-0 min-w-0 w-full flex-1 flex-col overflow-hidden bg-bg", "data-testid": `${testId}-chat-sidebar`, children: _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: chatContent }) })) : null, !isMobileViewport ? (_jsxs("aside", { className: "relative flex shrink-0 flex-col overflow-hidden bg-bg", style: {
216
+ (!chatDisabled || mobileSidebarControl !== null) ? (_jsx(MobileWorkspacePaneSwitcher, { chatAvailable: !chatDisabled, chatOpen: !effectiveCollapsed, sidebar: mobileSidebarControl, onChat: () => handleToggle(false), onMain: handleOpenMobileMain, onSidebar: handleOpenMobileSidebar })) : null, _jsxs("div", { className: `relative min-h-0 min-w-0 flex-1 flex-col overflow-hidden ${isMobileViewport && !effectiveCollapsed ? "hidden" : "flex"}`, children: [nav, _jsx("div", { className: "relative flex min-h-0 flex-1 flex-col overflow-hidden", children: main })] }), chatDisabled ? null : effectiveCollapsed ? (_jsx("aside", { className: "w-0 min-w-0 shrink-0", "data-testid": `${testId}-chat-sidebar`, "data-collapsed": true, children: !isMobileViewport && !hideCollapseButton ? (_jsx(AppWorkspaceChatDockToggleButton, { collapsed: true, testId: `${testId}-chat-expand` })) : null })) : (_jsxs(_Fragment, { children: [isMobileViewport ? (_jsx("aside", { className: "flex min-h-0 min-w-0 w-full flex-1 flex-col overflow-hidden bg-bg", "data-testid": `${testId}-chat-sidebar`, children: _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: chatContent }) })) : null, !isMobileViewport ? (_jsxs("aside", { className: "relative flex shrink-0 flex-col overflow-hidden bg-bg", style: {
204
217
  width: `${chatWidth}px`,
205
218
  minWidth: `${chatWidth}px`,
206
219
  }, "data-testid": `${testId}-chat-sidebar`, children: [_jsx("hr", { "aria-label": "Resize chat", "aria-orientation": "vertical", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": 50, tabIndex: 0, "data-testid": `${testId}-chat-resize-handle`, onPointerDown: handleResizePointerDown, className: "absolute inset-y-0 left-0 z-20 m-0 h-full w-3 -translate-x-1/2 cursor-col-resize touch-none select-none border-0 bg-transparent transition-colors hover:bg-accent/20" }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: chatContent })] })) : null, !isMobileViewport && !hideCollapseButton ? (_jsx(AppWorkspaceChatDockToggleButton, { collapsed: false, testId: `${testId}-chat-collapse` })) : null] }))] }) }) }));