@growthub/cli 0.14.0 → 0.14.2
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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +179 -117
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +136 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- 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,
|
|
@@ -191,8 +199,16 @@ function resolveSystemReceipt(message, applyResult) {
|
|
|
191
199
|
// Resolve where the Open button should navigate based on the proposal
|
|
192
200
|
// shape. Returns null when no navigation makes sense (e.g. explain.object).
|
|
193
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;
|
|
194
204
|
const pl = proposal?.payload || {};
|
|
195
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;
|
|
196
212
|
case "dataModel.object.create":
|
|
197
213
|
case "dataModel.object.update":
|
|
198
214
|
return pl.label || pl.id ? { surface: "data-model", source: pl.label || pl.id } : null;
|
|
@@ -321,12 +337,16 @@ function summarizePayload(proposal) {
|
|
|
321
337
|
return p.objectId ? `binding for: ${p.objectId}` : "";
|
|
322
338
|
case "explain.object":
|
|
323
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);
|
|
324
344
|
default:
|
|
325
345
|
return "";
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
348
|
|
|
329
|
-
export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, initialPrompt, initialThread, onApplied, onOpenArtifact }) {
|
|
349
|
+
export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, initialPrompt, initialThread, onApplied, onOpenArtifact, onOpenSwarmWorkflow }) {
|
|
330
350
|
const [activeTab, setActiveTab] = useState("assistant");
|
|
331
351
|
const [intent, setIntent] = useState(initialIntent || "create_object");
|
|
332
352
|
const [prompt, setPrompt] = useState(initialPrompt || "");
|
|
@@ -352,6 +372,22 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
352
372
|
const [moreOpen, setMoreOpen] = useState(false);
|
|
353
373
|
const moreMenuRef = useRef(null);
|
|
354
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
|
+
|
|
355
391
|
// Setup tab state
|
|
356
392
|
const [connectionStatus, setConnectionStatus] = useState(null);
|
|
357
393
|
const [pingLoading, setPingLoading] = useState(false);
|
|
@@ -464,6 +500,12 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
464
500
|
setThreadId(null);
|
|
465
501
|
setMessages([]);
|
|
466
502
|
setMoreOpen(false);
|
|
503
|
+
setActiveView("chat");
|
|
504
|
+
setSwarmFocus(null);
|
|
505
|
+
setExpandedAgent(null);
|
|
506
|
+
setExpandActive(false);
|
|
507
|
+
setSlashIndex(0);
|
|
508
|
+
setSlashDismissed(false);
|
|
467
509
|
}
|
|
468
510
|
}, [open]);
|
|
469
511
|
|
|
@@ -478,13 +520,27 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
478
520
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
479
521
|
}, [moreOpen]);
|
|
480
522
|
|
|
481
|
-
// 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.
|
|
482
526
|
useEffect(() => {
|
|
483
527
|
if (!open) return undefined;
|
|
484
|
-
const handler = (e) => {
|
|
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
|
+
};
|
|
485
541
|
window.addEventListener("keydown", handler);
|
|
486
542
|
return () => window.removeEventListener("keydown", handler);
|
|
487
|
-
}, [open, onClose]);
|
|
543
|
+
}, [open, onClose, expandActive, activeView]);
|
|
488
544
|
|
|
489
545
|
// Cmd+Enter at the window level fires apply when there is a result with
|
|
490
546
|
// accepted proposals. The textarea handler stops propagation when the
|
|
@@ -541,6 +597,11 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
541
597
|
|
|
542
598
|
async function runQuery() {
|
|
543
599
|
if (!prompt.trim() || streaming) return;
|
|
600
|
+
if (!helperWidgetState.ready) {
|
|
601
|
+
setQueryError(helperWidgetState.guidance);
|
|
602
|
+
setActiveTab("chat");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
544
605
|
setResult(null);
|
|
545
606
|
setQueryError("");
|
|
546
607
|
setStreamBuffer("");
|
|
@@ -672,6 +733,10 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
672
733
|
|
|
673
734
|
const sandboxRow = resolveSandboxEnvRow(workspaceConfig);
|
|
674
735
|
const helperAgentConfigured = isHelperConfigured(workspaceConfig);
|
|
736
|
+
const helperWidgetState = useMemo(
|
|
737
|
+
() => deriveHelperWidgetCausationState(workspaceConfig),
|
|
738
|
+
[workspaceConfig]
|
|
739
|
+
);
|
|
675
740
|
const liveModel = sandboxRow?.localModel || "";
|
|
676
741
|
const liveEndpoint = sandboxRow?.localEndpoint || "";
|
|
677
742
|
const liveAdapter = sandboxRow?.intelligenceAdapterMode || "ollama";
|
|
@@ -818,6 +883,106 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
818
883
|
try { promptRef.current?.focus(); } catch {}
|
|
819
884
|
};
|
|
820
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
|
+
|
|
821
986
|
if (!open) return null;
|
|
822
987
|
|
|
823
988
|
return (
|
|
@@ -830,7 +995,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
830
995
|
role="dialog"
|
|
831
996
|
aria-label="Workspace helper"
|
|
832
997
|
aria-modal="true"
|
|
833
|
-
style={{ width: panelWidth }}
|
|
998
|
+
style={{ width: expandActive ? "80vw" : panelWidth }}
|
|
834
999
|
>
|
|
835
1000
|
{/* Drag handle */}
|
|
836
1001
|
<div
|
|
@@ -844,22 +1009,50 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
844
1009
|
{/* Header — title left; gear toggles Assistant ↔ Setup, then close. */}
|
|
845
1010
|
<div className="dm-sidecar-header">
|
|
846
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
|
+
)}
|
|
847
1028
|
<span className="dm-sidecar-title" data-helper-title="">
|
|
848
|
-
{
|
|
849
|
-
?
|
|
850
|
-
:
|
|
1029
|
+
{inSwarmView
|
|
1030
|
+
? "Background tasks"
|
|
1031
|
+
: threadActive
|
|
1032
|
+
? deriveThreadDisplayTitle(initialThread, "Workspace Helper")
|
|
1033
|
+
: "Workspace Helper"}
|
|
851
1034
|
</span>
|
|
852
1035
|
</div>
|
|
853
1036
|
<div className="dm-sidecar-header-right">
|
|
854
1037
|
<button
|
|
855
1038
|
type="button"
|
|
856
1039
|
className="dm-sidecar-icon-btn"
|
|
857
|
-
onClick={() =>
|
|
858
|
-
|
|
859
|
-
|
|
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"}
|
|
860
1051
|
data-tab={activeTab === "setup" ? "assistant" : "setup"}
|
|
861
1052
|
>
|
|
862
|
-
{activeTab === "
|
|
1053
|
+
{inSwarmView && activeTab === "assistant"
|
|
1054
|
+
? <ArrowUpRight size={14} />
|
|
1055
|
+
: activeTab === "setup" ? <ArrowLeft size={14} /> : <Settings size={14} />}
|
|
863
1056
|
</button>
|
|
864
1057
|
<button
|
|
865
1058
|
type="button"
|
|
@@ -873,11 +1066,31 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
873
1066
|
</div>
|
|
874
1067
|
</div>
|
|
875
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
|
+
|
|
876
1089
|
{/* Assistant tab — composer-at-bottom layout (Twenty Ask AI parity):
|
|
877
1090
|
conversation/result area on top (flex:1), bottom-anchored composer
|
|
878
1091
|
holds chip stack (empty state) → mode row (active thread) →
|
|
879
1092
|
textarea with attach + mode + send-arrow action row. */}
|
|
880
|
-
{activeTab === "assistant" && (
|
|
1093
|
+
{activeTab === "assistant" && !inSwarmView && (
|
|
881
1094
|
<div className="dm-sidecar-body dm-helper-body">
|
|
882
1095
|
<div className="dm-helper-conversation" ref={conversationRef}>
|
|
883
1096
|
{/* Conversation — ChatGPT-grade multi-turn. User bubble
|
|
@@ -911,11 +1124,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
911
1124
|
<ToolCallCard
|
|
912
1125
|
proposal={receipt}
|
|
913
1126
|
content={m.content || ""}
|
|
914
|
-
onOpenArtifact={(p) =>
|
|
915
|
-
if (typeof onOpenArtifact === "function") {
|
|
916
|
-
onOpenArtifact(resolveArtifactTarget(p), p);
|
|
917
|
-
}
|
|
918
|
-
}}
|
|
1127
|
+
onOpenArtifact={(p) => handleOpenArtifact(resolveArtifactTarget(p), p)}
|
|
919
1128
|
/>
|
|
920
1129
|
</div>
|
|
921
1130
|
);
|
|
@@ -1154,20 +1363,53 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
1154
1363
|
) : null}
|
|
1155
1364
|
|
|
1156
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
|
+
)}
|
|
1157
1392
|
<textarea
|
|
1158
1393
|
id="helper-prompt"
|
|
1159
1394
|
ref={promptRef}
|
|
1160
1395
|
className="dm-helper-composer-textarea"
|
|
1161
1396
|
rows={threadActive ? 2 : 3}
|
|
1162
1397
|
placeholder={threadActive
|
|
1163
|
-
? 'Continue
|
|
1164
|
-
: '
|
|
1398
|
+
? 'Continue, or type / for commands…'
|
|
1399
|
+
: 'Type / for commands, or ask anything…'}
|
|
1165
1400
|
value={prompt}
|
|
1166
|
-
onChange={(e) =>
|
|
1401
|
+
onChange={(e) => {
|
|
1402
|
+
setPrompt(e.target.value);
|
|
1403
|
+
setSlashDismissed(false);
|
|
1404
|
+
setSlashIndex(0);
|
|
1405
|
+
}}
|
|
1167
1406
|
disabled={streaming}
|
|
1168
1407
|
data-helper-prompt=""
|
|
1169
1408
|
aria-label="Helper prompt"
|
|
1170
1409
|
onKeyDown={(e) => {
|
|
1410
|
+
// Slash menu intercepts navigation/selection keys while
|
|
1411
|
+
// open; Cmd+Enter submission below stays intact.
|
|
1412
|
+
if (handleSlashKeyDown(e)) return;
|
|
1171
1413
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
1172
1414
|
e.preventDefault();
|
|
1173
1415
|
// Stop the window-level apply handler from firing on the
|
|
@@ -1192,7 +1434,7 @@ export function HelperSidecar({ open, onClose, workspaceConfig, initialIntent, i
|
|
|
1192
1434
|
type="button"
|
|
1193
1435
|
className="dm-helper-composer-send"
|
|
1194
1436
|
onClick={runQuery}
|
|
1195
|
-
disabled={streaming || !prompt.trim()}
|
|
1437
|
+
disabled={streaming || !prompt.trim() || !helperWidgetState.ready}
|
|
1196
1438
|
data-helper-submit=""
|
|
1197
1439
|
aria-label={streaming ? "Sending" : "Send (⌘+Enter)"}
|
|
1198
1440
|
title={streaming ? "Sending…" : `Send · ${intentLabel(activeIntent)} (⌘+Enter)`}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
ArrowDownToLine,
|
|
6
6
|
Bot,
|
|
@@ -53,6 +53,15 @@ const CONNECTOR_OPTIONS = [
|
|
|
53
53
|
{ id: "preview", label: "Preview output" }
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
+
const MIN_ZOOM = 0.45;
|
|
57
|
+
const MAX_ZOOM = 1.4;
|
|
58
|
+
const NODE_BLOCK_HEIGHT = 98;
|
|
59
|
+
const FIT_VIEW_PADDING = 128;
|
|
60
|
+
|
|
61
|
+
function clampZoom(value) {
|
|
62
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(value.toFixed(2))));
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
function nodeSubtitle(node) {
|
|
57
66
|
const config = node?.config || {};
|
|
58
67
|
if (node?.subtitle) return String(node.subtitle);
|
|
@@ -111,8 +120,61 @@ export function OrchestrationGraphCanvas({
|
|
|
111
120
|
const [internalSelected, setInternalSelected] = useState(null);
|
|
112
121
|
const [connectorPopover, setConnectorPopover] = useState(null);
|
|
113
122
|
const [zoom, setZoom] = useState(1);
|
|
123
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
124
|
+
const canvasRef = useRef(null);
|
|
125
|
+
const dragRef = useRef(null);
|
|
114
126
|
const activeId = selectedNodeId ?? internalSelected;
|
|
115
127
|
|
|
128
|
+
function edgeBetween(fromId, toId) {
|
|
129
|
+
return edges.find((e) => String(e.from) === fromId && String(e.to) === toId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const zoomBy = useCallback((delta) => {
|
|
133
|
+
setZoom((value) => clampZoom(value + delta));
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const fitView = useCallback(() => {
|
|
137
|
+
const rect = canvasRef.current?.getBoundingClientRect();
|
|
138
|
+
const availableHeight = Math.max(240, (rect?.height || 720) - FIT_VIEW_PADDING);
|
|
139
|
+
const graphHeight = Math.max(NODE_BLOCK_HEIGHT, nodes.length * NODE_BLOCK_HEIGHT);
|
|
140
|
+
const nextZoom = clampZoom(Math.min(1, availableHeight / graphHeight));
|
|
141
|
+
setZoom(nextZoom);
|
|
142
|
+
setPan({ x: 0, y: 0 });
|
|
143
|
+
}, [nodes.length]);
|
|
144
|
+
|
|
145
|
+
const handleWheel = useCallback((event) => {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
const direction = event.deltaY > 0 ? -0.08 : 0.08;
|
|
148
|
+
zoomBy(direction);
|
|
149
|
+
}, [zoomBy]);
|
|
150
|
+
|
|
151
|
+
const handlePointerDown = useCallback((event) => {
|
|
152
|
+
if (event.button !== 0) return;
|
|
153
|
+
if (event.target.closest("button, .dm-orchestration-node, .dm-orchestration-connector__popover")) return;
|
|
154
|
+
dragRef.current = {
|
|
155
|
+
pointerId: event.pointerId,
|
|
156
|
+
startX: event.clientX,
|
|
157
|
+
startY: event.clientY,
|
|
158
|
+
originX: pan.x,
|
|
159
|
+
originY: pan.y
|
|
160
|
+
};
|
|
161
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
162
|
+
}, [pan.x, pan.y]);
|
|
163
|
+
|
|
164
|
+
const handlePointerMove = useCallback((event) => {
|
|
165
|
+
const drag = dragRef.current;
|
|
166
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
setPan({
|
|
169
|
+
x: drag.originX + event.clientX - drag.startX,
|
|
170
|
+
y: drag.originY + event.clientY - drag.startY
|
|
171
|
+
});
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const endDrag = useCallback((event) => {
|
|
175
|
+
if (dragRef.current?.pointerId === event.pointerId) dragRef.current = null;
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
116
178
|
if (!nodes.length) {
|
|
117
179
|
return (
|
|
118
180
|
<div className="dm-orchestration-canvas dm-orchestration-canvas--empty">
|
|
@@ -121,12 +183,18 @@ export function OrchestrationGraphCanvas({
|
|
|
121
183
|
);
|
|
122
184
|
}
|
|
123
185
|
|
|
124
|
-
function edgeBetween(fromId, toId) {
|
|
125
|
-
return edges.find((e) => String(e.from) === fromId && String(e.to) === toId);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
186
|
return (
|
|
129
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
ref={canvasRef}
|
|
189
|
+
className="dm-orchestration-canvas"
|
|
190
|
+
aria-label="Orchestration graph field editor"
|
|
191
|
+
onWheel={handleWheel}
|
|
192
|
+
onPointerDown={handlePointerDown}
|
|
193
|
+
onPointerMove={handlePointerMove}
|
|
194
|
+
onPointerUp={endDrag}
|
|
195
|
+
onPointerCancel={endDrag}
|
|
196
|
+
onPointerLeave={endDrag}
|
|
197
|
+
>
|
|
130
198
|
<span className={`dm-orchestration-canvas__badge is-${String(statusLabel || "draft").toLowerCase()}`}>{statusLabel}</span>
|
|
131
199
|
<div className="dm-orchestration-floating-tools" aria-label="Canvas tools">
|
|
132
200
|
<button type="button" title="Add node" aria-label="Add node" onClick={() => onConnectorAction?.({ action: "add-step", from: String(nodes[nodes.length - 1]?.id || ""), to: "" })}>
|
|
@@ -135,17 +203,20 @@ export function OrchestrationGraphCanvas({
|
|
|
135
203
|
<button type="button" title="Tidy workflow" aria-label="Tidy workflow">
|
|
136
204
|
<Settings size={14} />
|
|
137
205
|
</button>
|
|
138
|
-
<button type="button" title="Zoom in" aria-label="Zoom in" onClick={() =>
|
|
206
|
+
<button type="button" title="Zoom in" aria-label="Zoom in" onClick={() => zoomBy(0.1)}>
|
|
139
207
|
<ZoomIn size={14} />
|
|
140
208
|
</button>
|
|
141
|
-
<button type="button" title="Zoom out" aria-label="Zoom out" onClick={() =>
|
|
209
|
+
<button type="button" title="Zoom out" aria-label="Zoom out" onClick={() => zoomBy(-0.1)}>
|
|
142
210
|
<ZoomOut size={14} />
|
|
143
211
|
</button>
|
|
144
|
-
<button type="button" title="
|
|
212
|
+
<button type="button" title="Fit view" aria-label="Fit view" onClick={fitView}>
|
|
145
213
|
<Maximize2 size={14} />
|
|
146
214
|
</button>
|
|
147
215
|
</div>
|
|
148
|
-
<div
|
|
216
|
+
<div
|
|
217
|
+
className="dm-orchestration-canvas__viewport"
|
|
218
|
+
style={{ transform: `translate3d(${pan.x}px, ${pan.y}px, 0) scale(${zoom})` }}
|
|
219
|
+
>
|
|
149
220
|
{nodes.map((node, index) => {
|
|
150
221
|
const id = String(node.id || "");
|
|
151
222
|
const isSelected = activeId === id;
|