@growthub/cli 0.13.9 → 0.14.1

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 (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  39. package/package.json +1 -1
@@ -47,6 +47,14 @@ import {
47
47
  isHelperConfigured,
48
48
  WorkspaceHelperSetupModal,
49
49
  } from "../../components/WorkspaceHelperSetupModal.jsx";
50
+ import { SwarmRunCockpit, SwarmAgentTranscript } from "./SwarmRunCockpit.jsx";
51
+ import { SidecarExpandView } from "./SidecarExpandView.jsx";
52
+ import { parseSlashInput } from "./helper-commands.js";
53
+ import {
54
+ deriveHelperWidgetCausationState,
55
+ summarizeSwarmRunProposal,
56
+ SWARM_WORKFLOWS_OBJECT_ID,
57
+ } from "@/lib/workspace-swarm-proposal";
50
58
 
51
59
  // Generic "Tool Call Output" title matches the reference grammar — the
52
60
  // user already sees the prompt + assistant response in the chat above,
@@ -69,6 +77,7 @@ function ToolCallCard({ proposal, content, onOpenArtifact }) {
69
77
  rationale: proposal?.rationale,
70
78
  confidence: proposal?.confidence,
71
79
  };
80
+ const showJson = meta.payload != null || meta.affectedField || meta.rationale;
72
81
  return (
73
82
  <div className="dm-helper-toolcall" data-toolcall-type={proposal?.type}>
74
83
  <button
@@ -89,9 +98,11 @@ function ToolCallCard({ proposal, content, onOpenArtifact }) {
89
98
  {open && (
90
99
  <div className="dm-helper-toolcall-body">
91
100
  {content && <div className="dm-helper-toolcall-content">{content}</div>}
92
- <pre className="dm-helper-toolcall-json">
93
- {JSON.stringify(meta, null, 2)}
94
- </pre>
101
+ {showJson && (
102
+ <pre className="dm-helper-toolcall-json">
103
+ {JSON.stringify(meta, null, 2)}
104
+ </pre>
105
+ )}
95
106
  </div>
96
107
  )}
97
108
  {canNavigate && (
@@ -108,6 +119,55 @@ function ToolCallCard({ proposal, content, onOpenArtifact }) {
108
119
  );
109
120
  }
110
121
 
122
+ function formatRunDuration(ms) {
123
+ const value = Number(ms);
124
+ if (!Number.isFinite(value) || value < 0) return "";
125
+ const totalSeconds = Math.max(0, Math.round(value / 1000));
126
+ const minutes = Math.floor(totalSeconds / 60);
127
+ const seconds = totalSeconds % 60;
128
+ if (minutes <= 0) return `${seconds}s`;
129
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
130
+ }
131
+
132
+ function ProposalReviewCard({ proposal, checked, disabled, onCheckedChange, onKeyDown }) {
133
+ const [open, setOpen] = useState(false);
134
+ const summary = summarizePayload(proposal);
135
+ return (
136
+ <div className="dm-helper-toolcall" data-proposal-item="" tabIndex={0} onKeyDown={onKeyDown}>
137
+ <div className="dm-helper-toolcall-row dm-helper-proposal-card-row">
138
+ <input
139
+ type="checkbox"
140
+ checked={!!checked}
141
+ onChange={(e) => onCheckedChange(e.target.checked)}
142
+ disabled={disabled}
143
+ data-proposal-accept=""
144
+ aria-label={`Select ${proposal.type}`}
145
+ />
146
+ <button
147
+ type="button"
148
+ className="dm-helper-proposal-card-toggle"
149
+ onClick={() => setOpen((v) => !v)}
150
+ aria-expanded={open}
151
+ >
152
+ <span className="dm-helper-toolcall-title">{proposal.type}</span>
153
+ <span className="dm-helper-proposal-field">→ {proposal.affectedField}</span>
154
+ <ChevronDown
155
+ size={14}
156
+ className={`dm-helper-toolcall-chevron${open ? " is-open" : ""}`}
157
+ aria-hidden="true"
158
+ />
159
+ </button>
160
+ </div>
161
+ {open && (
162
+ <div className="dm-helper-toolcall-body">
163
+ {summary && <p className="dm-helper-proposal-payload" data-proposal-payload="">{summary}</p>}
164
+ {proposal.rationale && <p className="dm-helper-proposal-rationale">{proposal.rationale}</p>}
165
+ </div>
166
+ )}
167
+ </div>
168
+ );
169
+ }
170
+
111
171
  // Pair a system apply-receipt message with the actual proposal payload
112
172
  // it confirms. The applyResult (rehydrated from row.lastApplied at thread
113
173
  // load time) carries the typed payloads keyed in order — we walk the
@@ -139,8 +199,16 @@ function resolveSystemReceipt(message, applyResult) {
139
199
  // Resolve where the Open button should navigate based on the proposal
140
200
  // shape. Returns null when no navigation makes sense (e.g. explain.object).
141
201
  function resolveArtifactTarget(proposal) {
202
+ // Apply receipts in the swarm lane carry an explicit artifact target.
203
+ if (proposal?.artifact?.surface === "swarm-run") return proposal.artifact;
142
204
  const pl = proposal?.payload || {};
143
205
  switch (proposal?.type) {
206
+ case "swarm.run.propose":
207
+ case "swarm.workflow.save":
208
+ case "swarm.run.resume":
209
+ return pl.name
210
+ ? { surface: "swarm-run", objectId: SWARM_WORKFLOWS_OBJECT_ID, name: pl.name }
211
+ : null;
144
212
  case "dataModel.object.create":
145
213
  case "dataModel.object.update":
146
214
  return pl.label || pl.id ? { surface: "data-model", source: pl.label || pl.id } : null;
@@ -269,12 +337,16 @@ function summarizePayload(proposal) {
269
337
  return p.objectId ? `binding for: ${p.objectId}` : "";
270
338
  case "explain.object":
271
339
  return p.target ? `about: ${p.target}` : "informational";
340
+ case "swarm.run.propose":
341
+ case "swarm.workflow.save":
342
+ case "swarm.run.resume":
343
+ return summarizeSwarmRunProposal(proposal);
272
344
  default:
273
345
  return "";
274
346
  }
275
347
  }
276
348
 
277
- export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, initialPrompt, initialThread, onApplied, onOpenArtifact }) {
349
+ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, initialPrompt, initialThread, onApplied, onOpenArtifact, onOpenSwarmWorkflow }) {
278
350
  const [activeTab, setActiveTab] = useState("assistant");
279
351
  const [intent, setIntent] = useState(initialIntent || "create_object");
280
352
  const [prompt, setPrompt] = useState(initialPrompt || "");
@@ -300,6 +372,22 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
300
372
  const [moreOpen, setMoreOpen] = useState(false);
301
373
  const moreMenuRef = useRef(null);
302
374
 
375
+ // Sidecar internal view (SWARM_RUN_CONTRACT_V1) — the swarm cockpit lives
376
+ // INSIDE the helper sidecar, no new route. "chat" is the existing
377
+ // conversation surface; swarm views render the governed run cockpit;
378
+ // "tool-output" is the expanded transcript frame.
379
+ const [activeView, setActiveView] = useState("chat");
380
+ // Focused workflow row when opened from an apply receipt's Open button.
381
+ const [swarmFocus, setSwarmFocus] = useState(null);
382
+ // Expanded transcript agent (tool-output view) + full-width takeover flag.
383
+ const [expandedAgent, setExpandedAgent] = useState(null);
384
+ const [expandActive, setExpandActive] = useState(false);
385
+ const priorViewRef = useRef("swarm-list");
386
+ // Slash menu — active index; "dismissed" suppresses the menu until the
387
+ // prompt changes again (Esc behavior).
388
+ const [slashIndex, setSlashIndex] = useState(0);
389
+ const [slashDismissed, setSlashDismissed] = useState(false);
390
+
303
391
  // Setup tab state
304
392
  const [connectionStatus, setConnectionStatus] = useState(null);
305
393
  const [pingLoading, setPingLoading] = useState(false);
@@ -412,6 +500,12 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
412
500
  setThreadId(null);
413
501
  setMessages([]);
414
502
  setMoreOpen(false);
503
+ setActiveView("chat");
504
+ setSwarmFocus(null);
505
+ setExpandedAgent(null);
506
+ setExpandActive(false);
507
+ setSlashIndex(0);
508
+ setSlashDismissed(false);
415
509
  }
416
510
  }, [open]);
417
511
 
@@ -426,13 +520,27 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
426
520
  return () => document.removeEventListener("pointerdown", onPointerDown);
427
521
  }, [moreOpen]);
428
522
 
429
- // Escape key
523
+ // Escape key — expand mode collapses back to the prior sidecar view first;
524
+ // a second Escape (or Escape outside expand mode) closes the sidecar as
525
+ // before. Predictable, no scroll trap.
430
526
  useEffect(() => {
431
527
  if (!open) return undefined;
432
- const handler = (e) => { if (e.key === "Escape") onClose(); };
528
+ const handler = (e) => {
529
+ if (e.key !== "Escape") return;
530
+ if (expandActive) {
531
+ e.preventDefault();
532
+ setExpandActive(false);
533
+ if (activeView === "tool-output") {
534
+ setActiveView(priorViewRef.current || "swarm-list");
535
+ setExpandedAgent(null);
536
+ }
537
+ return;
538
+ }
539
+ onClose();
540
+ };
433
541
  window.addEventListener("keydown", handler);
434
542
  return () => window.removeEventListener("keydown", handler);
435
- }, [open, onClose]);
543
+ }, [open, onClose, expandActive, activeView]);
436
544
 
437
545
  // Cmd+Enter at the window level fires apply when there is a result with
438
546
  // accepted proposals. The textarea handler stops propagation when the
@@ -489,6 +597,11 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
489
597
 
490
598
  async function runQuery() {
491
599
  if (!prompt.trim() || streaming) return;
600
+ if (!helperWidgetState.ready) {
601
+ setQueryError(helperWidgetState.guidance);
602
+ setActiveTab("chat");
603
+ return;
604
+ }
492
605
  setResult(null);
493
606
  setQueryError("");
494
607
  setStreamBuffer("");
@@ -620,6 +733,10 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
620
733
 
621
734
  const sandboxRow = resolveSandboxEnvRow(workspaceConfig);
622
735
  const helperAgentConfigured = isHelperConfigured(workspaceConfig);
736
+ const helperWidgetState = useMemo(
737
+ () => deriveHelperWidgetCausationState(workspaceConfig),
738
+ [workspaceConfig]
739
+ );
623
740
  const liveModel = sandboxRow?.localModel || "";
624
741
  const liveEndpoint = sandboxRow?.localEndpoint || "";
625
742
  const liveAdapter = sandboxRow?.intelligenceAdapterMode || "ollama";
@@ -747,6 +864,11 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
747
864
  const acceptedCount = Object.values(accepted).filter(Boolean).length;
748
865
  const skippedCount = applyResult?.skipped?.length || 0;
749
866
  const hasProposals = result && (result.proposals || []).length > 0;
867
+ const visibleWarnings = (result?.warnings || []).filter((warning) => {
868
+ const text = String(warning || "");
869
+ return !/transcript does not include the actual registry row id or lastResponse payload/i.test(text)
870
+ && !/No credentials or env values should be stored/i.test(text);
871
+ });
750
872
 
751
873
  // Thread is "active" the moment the user has sent at least one message,
752
874
  // OR we have rehydrated a prior thread row. Pills only show on the
@@ -761,6 +883,106 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
761
883
  try { promptRef.current?.focus(); } catch {}
762
884
  };
763
885
 
886
+ // Slash menu state derives from the live prompt. The menu only engages
887
+ // when "/" is the first character (parseSlashInput), never mid-sentence.
888
+ const slash = slashDismissed ? { active: false, query: "", matches: [] } : parseSlashInput(prompt);
889
+ const slashActive = slash.active && slash.matches.length > 0 && !streaming && activeView === "chat";
890
+
891
+ // Selecting a command never mutates config: read-only commands switch the
892
+ // sidecar view or seed a prompt; mutating commands seed a governed
893
+ // proposal request (intent + template) that still travels query → review
894
+ // → apply. No command calls sandbox-run or PATCH directly.
895
+ const selectSlashCommand = (cmd) => {
896
+ if (!helperWidgetState.ready) {
897
+ setQueryError(helperWidgetState.guidance);
898
+ setActiveTab("chat");
899
+ return;
900
+ }
901
+ setSlashDismissed(false);
902
+ setSlashIndex(0);
903
+ if (cmd.view) {
904
+ setPrompt("");
905
+ setSwarmFocus(null);
906
+ setActiveView(cmd.view);
907
+ return;
908
+ }
909
+ if (cmd.intent) onPickIntent(cmd.intent);
910
+ setPrompt(cmd.promptTemplate ? `${cmd.promptTemplate} ` : "");
911
+ try { promptRef.current?.focus(); } catch {}
912
+ };
913
+
914
+ const handleSlashKeyDown = (e) => {
915
+ if (!slashActive) return false;
916
+ if (e.key === "ArrowDown") {
917
+ e.preventDefault();
918
+ setSlashIndex((i) => (i + 1) % slash.matches.length);
919
+ return true;
920
+ }
921
+ if (e.key === "ArrowUp") {
922
+ e.preventDefault();
923
+ setSlashIndex((i) => (i - 1 + slash.matches.length) % slash.matches.length);
924
+ return true;
925
+ }
926
+ if (e.key === "Enter" || e.key === "Tab") {
927
+ e.preventDefault();
928
+ e.stopPropagation();
929
+ selectSlashCommand(slash.matches[Math.min(slashIndex, slash.matches.length - 1)]);
930
+ return true;
931
+ }
932
+ if (e.key === "Escape") {
933
+ e.preventDefault();
934
+ e.stopPropagation();
935
+ setSlashDismissed(true);
936
+ return true;
937
+ }
938
+ return false;
939
+ };
940
+
941
+ // Refresh the page-level workspace config after a sandbox-run stamps run
942
+ // state onto a row — same onApplied channel the apply flow already uses.
943
+ const refreshWorkspaceConfig = async () => {
944
+ try {
945
+ const res = await fetch("/api/workspace");
946
+ const data = await res.json();
947
+ if (data?.workspaceConfig && onApplied) onApplied(data.workspaceConfig);
948
+ } catch {
949
+ // Non-fatal — the cockpit's own history polling still reflects the run.
950
+ }
951
+ };
952
+
953
+ // Open a swarm artifact (apply receipt → Open) inside the sidecar cockpit;
954
+ // every other artifact surface keeps routing through the page-level handler.
955
+ const handleOpenArtifact = (target, proposal) => {
956
+ if (target?.surface === "swarm-run") {
957
+ setSwarmFocus({ objectId: target.objectId, name: target.name });
958
+ setActiveView("swarm-detail");
959
+ return;
960
+ }
961
+ if (typeof onOpenArtifact === "function") onOpenArtifact(target, proposal);
962
+ };
963
+
964
+ const handleExpandTranscript = (agent) => {
965
+ priorViewRef.current = activeView === "tool-output" ? priorViewRef.current : activeView;
966
+ setExpandedAgent(agent);
967
+ setActiveView("tool-output");
968
+ setExpandActive(true);
969
+ };
970
+
971
+ const collapseExpand = () => {
972
+ setExpandActive(false);
973
+ setActiveView(priorViewRef.current || "swarm-list");
974
+ setExpandedAgent(null);
975
+ };
976
+
977
+ const inSwarmView = activeView === "swarm-list" || activeView === "swarm-detail" || activeView === "tool-output";
978
+ const canOpenSwarmWorkflow = Boolean(
979
+ inSwarmView
980
+ && activeTab === "assistant"
981
+ && swarmFocus?.objectId
982
+ && swarmFocus?.name
983
+ && typeof onOpenSwarmWorkflow === "function"
984
+ );
985
+
764
986
  if (!open) return null;
765
987
 
766
988
  return (
@@ -773,7 +995,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
773
995
  role="dialog"
774
996
  aria-label="Workspace helper"
775
997
  aria-modal="true"
776
- style={{ width: panelWidth }}
998
+ style={{ width: expandActive ? "80vw" : panelWidth }}
777
999
  >
778
1000
  {/* Drag handle */}
779
1001
  <div
@@ -787,22 +1009,50 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
787
1009
  {/* Header — title left; gear toggles Assistant ↔ Setup, then close. */}
788
1010
  <div className="dm-sidecar-header">
789
1011
  <div className="dm-sidecar-header-left">
1012
+ {inSwarmView && (
1013
+ <button
1014
+ type="button"
1015
+ className="dm-sidecar-icon-btn"
1016
+ onClick={() => {
1017
+ if (activeView === "tool-output") { collapseExpand(); return; }
1018
+ setActiveView("chat");
1019
+ setSwarmFocus(null);
1020
+ }}
1021
+ aria-label="Back to chat"
1022
+ title="Back to chat"
1023
+ data-swarm-back=""
1024
+ >
1025
+ <ArrowLeft size={14} />
1026
+ </button>
1027
+ )}
790
1028
  <span className="dm-sidecar-title" data-helper-title="">
791
- {threadActive
792
- ? deriveThreadDisplayTitle(initialThread, "Workspace Helper")
793
- : "Workspace Helper"}
1029
+ {inSwarmView
1030
+ ? "Background tasks"
1031
+ : threadActive
1032
+ ? deriveThreadDisplayTitle(initialThread, "Workspace Helper")
1033
+ : "Workspace Helper"}
794
1034
  </span>
795
1035
  </div>
796
1036
  <div className="dm-sidecar-header-right">
797
1037
  <button
798
1038
  type="button"
799
1039
  className="dm-sidecar-icon-btn"
800
- onClick={() => setActiveTab((current) => (current === "setup" ? "assistant" : "setup"))}
801
- aria-label={activeTab === "setup" ? "Back" : "Setup"}
802
- title={activeTab === "setup" ? "Back" : "Setup"}
1040
+ onClick={() => {
1041
+ if (canOpenSwarmWorkflow) {
1042
+ onOpenSwarmWorkflow(swarmFocus);
1043
+ onClose?.();
1044
+ return;
1045
+ }
1046
+ setActiveTab((current) => (current === "setup" ? "assistant" : "setup"));
1047
+ }}
1048
+ disabled={inSwarmView && activeTab === "assistant" && !canOpenSwarmWorkflow}
1049
+ aria-label={inSwarmView && activeTab === "assistant" ? "Open workflow canvas" : activeTab === "setup" ? "Back" : "Setup"}
1050
+ title={inSwarmView && activeTab === "assistant" ? "Open workflow canvas" : activeTab === "setup" ? "Back" : "Setup"}
803
1051
  data-tab={activeTab === "setup" ? "assistant" : "setup"}
804
1052
  >
805
- {activeTab === "setup" ? <ArrowLeft size={14} /> : <Settings size={14} />}
1053
+ {inSwarmView && activeTab === "assistant"
1054
+ ? <ArrowUpRight size={14} />
1055
+ : activeTab === "setup" ? <ArrowLeft size={14} /> : <Settings size={14} />}
806
1056
  </button>
807
1057
  <button
808
1058
  type="button"
@@ -816,11 +1066,31 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
816
1066
  </div>
817
1067
  </div>
818
1068
 
1069
+ {/* Swarm cockpit views — Background tasks list / detail / expanded
1070
+ transcript. Same sidecar shell, no route change; chat state stays
1071
+ mounted-adjacent and untouched while the user inspects runs. */}
1072
+ {activeTab === "assistant" && inSwarmView && (
1073
+ <div className="dm-sidecar-body dm-swarm-body" data-swarm-view={activeView}>
1074
+ {activeView === "tool-output" && expandedAgent ? (
1075
+ <SidecarExpandView title={expandedAgent.label || "Transcript"} onBack={collapseExpand}>
1076
+ <SwarmAgentTranscript agent={expandedAgent} />
1077
+ </SidecarExpandView>
1078
+ ) : (
1079
+ <SwarmRunCockpit
1080
+ workspaceConfig={workspaceConfig}
1081
+ focus={activeView === "swarm-detail" ? swarmFocus : null}
1082
+ onConfigRefresh={refreshWorkspaceConfig}
1083
+ onExpandTranscript={handleExpandTranscript}
1084
+ />
1085
+ )}
1086
+ </div>
1087
+ )}
1088
+
819
1089
  {/* Assistant tab — composer-at-bottom layout (Twenty Ask AI parity):
820
1090
  conversation/result area on top (flex:1), bottom-anchored composer
821
1091
  holds chip stack (empty state) → mode row (active thread) →
822
1092
  textarea with attach + mode + send-arrow action row. */}
823
- {activeTab === "assistant" && (
1093
+ {activeTab === "assistant" && !inSwarmView && (
824
1094
  <div className="dm-sidecar-body dm-helper-body">
825
1095
  <div className="dm-helper-conversation" ref={conversationRef}>
826
1096
  {/* Conversation — ChatGPT-grade multi-turn. User bubble
@@ -854,11 +1124,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
854
1124
  <ToolCallCard
855
1125
  proposal={receipt}
856
1126
  content={m.content || ""}
857
- onOpenArtifact={(p) => {
858
- if (typeof onOpenArtifact === "function") {
859
- onOpenArtifact(resolveArtifactTarget(p), p);
860
- }
861
- }}
1127
+ onOpenArtifact={(p) => handleOpenArtifact(resolveArtifactTarget(p), p)}
862
1128
  />
863
1129
  </div>
864
1130
  );
@@ -916,18 +1182,9 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
916
1182
  {/* Proposals */}
917
1183
  {result && (
918
1184
  <div className="dm-helper-result">
919
- <div className="dm-helper-summary">
920
- <span>{result.summary}</span>
921
- </div>
922
-
923
- {(result.warnings || []).length > 0 && (
924
- <div className="dm-helper-warnings">
925
- {result.warnings.map((w, i) => (
926
- <div key={i} className="dm-helper-warning">
927
- <AlertCircle size={12} />
928
- <span>{w}</span>
929
- </div>
930
- ))}
1185
+ {!threadActive && result.summary && (
1186
+ <div className="dm-helper-summary">
1187
+ <span>{result.summary}</span>
931
1188
  </div>
932
1189
  )}
933
1190
 
@@ -943,42 +1200,17 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
943
1200
  aria-label="Proposals"
944
1201
  >
945
1202
  {result.proposals.map((proposal, i) => {
946
- const summary = summarizePayload(proposal);
947
- const conf = typeof proposal.confidence === "number" ? Math.round(proposal.confidence * 100) : null;
948
1203
  return (
949
- <label
1204
+ <ProposalReviewCard
950
1205
  key={i}
951
- className={`dm-helper-proposal${accepted[i] ? " accepted" : ""}`}
952
- data-proposal-item=""
953
- tabIndex={0}
1206
+ proposal={proposal}
1207
+ checked={accepted[i]}
1208
+ disabled={applying}
1209
+ onCheckedChange={(checked) =>
1210
+ setAccepted((prev) => ({ ...prev, [i]: checked }))
1211
+ }
954
1212
  onKeyDown={(e) => handleProposalKeyDown(e, i)}
955
- >
956
- <input
957
- type="checkbox"
958
- checked={!!accepted[i]}
959
- onChange={(e) =>
960
- setAccepted((prev) => ({ ...prev, [i]: e.target.checked }))
961
- }
962
- disabled={applying}
963
- data-proposal-accept=""
964
- tabIndex={-1}
965
- />
966
- <div className="dm-helper-proposal-body">
967
- <div className="dm-helper-proposal-row">
968
- <span className="dm-helper-proposal-type">{proposal.type}</span>
969
- <span className="dm-helper-proposal-field">→ {proposal.affectedField}</span>
970
- {conf !== null && (
971
- <span className="dm-helper-proposal-confidence" data-proposal-confidence={conf}>
972
- {conf}%
973
- </span>
974
- )}
975
- </div>
976
- {summary && (
977
- <p className="dm-helper-proposal-payload" data-proposal-payload="">{summary}</p>
978
- )}
979
- <p className="dm-helper-proposal-rationale">{proposal.rationale}</p>
980
- </div>
981
- </label>
1213
+ />
982
1214
  );
983
1215
  })}
984
1216
  </div>
@@ -1055,13 +1287,10 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
1055
1287
  )}
1056
1288
 
1057
1289
  {result.receipts && (
1058
- <p className="dm-field-hint" style={{ marginTop: 8 }} data-helper-receipt="">
1059
- Run: {result.receipts.model} · confidence{" "}
1060
- {typeof result.receipts.confidence === "number"
1061
- ? `${Math.round(result.receipts.confidence * 100)}%`
1062
- : "n/a"}{" "}
1063
- · {result.receipts.latencyMs}ms
1064
- </p>
1290
+ <div className="dm-helper-run-meta" data-helper-receipt="">
1291
+ <span>{result.receipts.model || "run"}</span>
1292
+ <span>{formatRunDuration(result.receipts.latencyMs)}</span>
1293
+ </div>
1065
1294
  )}
1066
1295
  </div>
1067
1296
  )}
@@ -1134,20 +1363,53 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
1134
1363
  ) : null}
1135
1364
 
1136
1365
  <div className="dm-helper-composer-input">
1366
+ {!helperWidgetState.ready && (
1367
+ <div className="dm-helper-error" role="status">
1368
+ <span>{helperWidgetState.guidance}</span>
1369
+ </div>
1370
+ )}
1371
+ {slashActive && (
1372
+ <div className="dm-helper-pill-menu dm-helper-slash-menu" role="listbox" data-helper-slash-menu="">
1373
+ {slash.matches.map((cmd, i) => (
1374
+ <button
1375
+ key={cmd.name}
1376
+ type="button"
1377
+ className={`dm-helper-pill-menu-item${i === Math.min(slashIndex, slash.matches.length - 1) ? " active" : ""}`}
1378
+ role="option"
1379
+ aria-selected={i === slashIndex}
1380
+ data-helper-slash-item={cmd.name}
1381
+ disabled={!helperWidgetState.ready}
1382
+ title={!helperWidgetState.ready ? helperWidgetState.guidance : cmd.description || cmd.label}
1383
+ onClick={() => selectSlashCommand(cmd)}
1384
+ onMouseEnter={() => setSlashIndex(i)}
1385
+ >
1386
+ <span className="dm-helper-slash-name">{cmd.name}</span>
1387
+ <span className="dm-field-hint">{cmd.description || cmd.label}</span>
1388
+ </button>
1389
+ ))}
1390
+ </div>
1391
+ )}
1137
1392
  <textarea
1138
1393
  id="helper-prompt"
1139
1394
  ref={promptRef}
1140
1395
  className="dm-helper-composer-textarea"
1141
1396
  rows={threadActive ? 2 : 3}
1142
1397
  placeholder={threadActive
1143
- ? 'Continue the conversation…'
1144
- : 'Ask, search or make anything…'}
1398
+ ? 'Continue, or type / for commands…'
1399
+ : 'Type / for commands, or ask anything…'}
1145
1400
  value={prompt}
1146
- onChange={(e) => setPrompt(e.target.value)}
1401
+ onChange={(e) => {
1402
+ setPrompt(e.target.value);
1403
+ setSlashDismissed(false);
1404
+ setSlashIndex(0);
1405
+ }}
1147
1406
  disabled={streaming}
1148
1407
  data-helper-prompt=""
1149
1408
  aria-label="Helper prompt"
1150
1409
  onKeyDown={(e) => {
1410
+ // Slash menu intercepts navigation/selection keys while
1411
+ // open; Cmd+Enter submission below stays intact.
1412
+ if (handleSlashKeyDown(e)) return;
1151
1413
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1152
1414
  e.preventDefault();
1153
1415
  // Stop the window-level apply handler from firing on the
@@ -1172,7 +1434,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
1172
1434
  type="button"
1173
1435
  className="dm-helper-composer-send"
1174
1436
  onClick={runQuery}
1175
- disabled={streaming || !prompt.trim()}
1437
+ disabled={streaming || !prompt.trim() || !helperWidgetState.ready}
1176
1438
  data-helper-submit=""
1177
1439
  aria-label={streaming ? "Sending" : "Send (⌘+Enter)"}
1178
1440
  title={streaming ? "Sending…" : `Send · ${intentLabel(activeIntent)} (⌘+Enter)`}