@tangle-network/agent-app 0.46.8 → 0.46.10

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.
@@ -340,9 +340,76 @@ export declare function sandboxToolPath(options: SandboxToolPathOptions & {
340
340
  }): string;
341
341
  /** Build file mounts for sandbox tools based on provided options and tool configurations */
342
342
  export declare function buildSandboxToolFileMounts(options: BuildSandboxToolFileMountsOptions): AgentProfileFileMount[];
343
- /** Build a shell script that sets up and exports the sandbox tool binary directory in user profiles */
343
+ /**
344
+ * Build the `SANDBOX_TOOL_BIN_DIRS` entry that puts these apps' tool bin dirs on
345
+ * a sandbox PATH. Declare it in the same box env as the mounts from
346
+ * {@link buildSandboxToolFileMounts}: the mounts place the executables, this
347
+ * places the directory that makes a bare tool name resolve.
348
+ *
349
+ * Every sandbox PATH builder appends these directories at the TAIL, after the
350
+ * Nix, npm, and image entries, so a tool never shadows a platform or system
351
+ * binary of the same name. That placement is identical for a non-interactive
352
+ * exec, a sidecar-spawned CLI, and an SSH shell, which is why this variable and
353
+ * not a shell rc file is the mechanism: an exec reads no rc file, and the SSH
354
+ * shell environment assigns PATH absolutely.
355
+ *
356
+ * Entries come from {@link sandboxToolBinDir}, so the value can never name a
357
+ * directory the mounts did not use — a caller that hand-writes the literal
358
+ * instead drifts silently the moment a base dir changes. Order is preserved,
359
+ * and two apps that resolve to one directory collapse to the FIRST position it
360
+ * appeared in.
361
+ *
362
+ * A `:` in a resolved bin dir throws here. The character separates entries both
363
+ * in this variable and in PATH itself, so such a directory is unrepresentable
364
+ * rather than merely awkward: joining it would split one absolute path into a
365
+ * leading path and a relative remainder, and the runtime rejects a relative
366
+ * segment by failing the whole box. Throwing names the offending directory
367
+ * instead.
368
+ *
369
+ * The consuming half lives in agent-dev-container: `resolveSandboxToolBinDirs`
370
+ * parses this value and every PATH builder appends it at the tail — sidecar
371
+ * exec and PTY through `packages/shared/src/cache-env.ts`, an SSH session
372
+ * through `packages/shared/src/ssh-bootstrap.ts`, and a spawned CLI through
373
+ * `packages/sdk-cli-runner/src/cli-process.ts`, each with its own tests. This
374
+ * package has no sandbox runtime, so tail placement is pinned there, not here.
375
+ */
376
+ export declare function buildSandboxToolBinDirsEnv(apps: readonly SandboxToolPathOptions[]): {
377
+ SANDBOX_TOOL_BIN_DIRS: string;
378
+ };
379
+ /**
380
+ * Build a shell script that creates the sandbox tool binary directory.
381
+ * {@link buildSandboxToolBinDirsEnv}, not this script, puts that directory on a
382
+ * sandbox PATH.
383
+ *
384
+ * The `mkdir -p` makes the directory exist before a tool mount lands in it.
385
+ */
386
+ export declare function buildSandboxToolBinDirScript(options: SandboxToolPathOptions): string;
387
+ /** Build a shell script that creates the sandbox tool binary directory.
388
+ *
389
+ * @deprecated Renamed to {@link buildSandboxToolBinDirScript}, and the script
390
+ * no longer appends the bin dir to PATH in `~/.profile`, `~/.bashrc` and
391
+ * `~/.zshrc`. Those edits never reached the contexts that run a tool: an exec
392
+ * and a spawned CLI read no rc file, and the SSH shell assigns PATH
393
+ * absolutely, which discards an appended entry. Put the directory on PATH with
394
+ * {@link buildSandboxToolBinDirsEnv} instead. This name forwards to the new
395
+ * one so an existing import keeps resolving; removal is a major.
396
+ */
344
397
  export declare function buildSandboxToolPathSetupScript(options: SandboxToolPathOptions): string;
345
- /** Resolve the sandbox environment PATH setup by executing the configuration script with given options */
398
+ /**
399
+ * Create the sandbox tool binary directory in a running box.
400
+ * {@link buildSandboxToolBinDirsEnv}, not this call, puts it on a sandbox PATH.
401
+ */
402
+ export declare function ensureSandboxToolBinDir(box: SandboxInstance, options: SandboxToolPathOptions): Promise<Outcome<void>>;
403
+ /** Create the sandbox tool binary directory in a running box.
404
+ *
405
+ * @deprecated Renamed to {@link ensureSandboxToolBinDir}, and the call no
406
+ * longer appends the bin dir to PATH in `~/.profile`, `~/.bashrc` and
407
+ * `~/.zshrc`. Those edits never reached the contexts that run a tool: an exec
408
+ * and a spawned CLI read no rc file, and the SSH shell assigns PATH
409
+ * absolutely, which discards an appended entry. Put the directory on PATH with
410
+ * {@link buildSandboxToolBinDirsEnv} instead. This name forwards to the new
411
+ * one so an existing import keeps resolving; removal is a major.
412
+ */
346
413
  export declare function runSandboxToolPathSetup(box: SandboxInstance, options: SandboxToolPathOptions): Promise<Outcome<void>>;
347
414
  /** Split profile files into inline deferred files and a lean profile without them */
348
415
  export declare function splitDeferredProfileFiles(profile: AgentProfile): {
@@ -28,6 +28,8 @@ import {
28
28
  attachReasoningEffort,
29
29
  buildAppToolMcpServers,
30
30
  buildProductEgressPolicy,
31
+ buildSandboxToolBinDirScript,
32
+ buildSandboxToolBinDirsEnv,
31
33
  buildSandboxToolFileMounts,
32
34
  buildSandboxToolPathSetupScript,
33
35
  classifySeveredStream,
@@ -41,6 +43,7 @@ import {
41
43
  deleteSecret,
42
44
  detectInteractiveQuestion,
43
45
  driveSandboxTurn,
46
+ ensureSandboxToolBinDir,
44
47
  ensureWorkspaceSandbox,
45
48
  flattenHistory,
46
49
  formatSandboxProvisioningSupportDetails,
@@ -88,7 +91,7 @@ import {
88
91
  workspaceSandboxRecoveryMessage,
89
92
  workspaceSandboxRecoveryRecommendedActions,
90
93
  writeProfileFilesToBox
91
- } from "../chunk-MA2RIIIH.js";
94
+ } from "../chunk-3HBH5GFN.js";
92
95
  import "../chunk-TH7L265V.js";
93
96
  import "../chunk-LWSJK546.js";
94
97
  import "../chunk-P5PIAQVS.js";
@@ -127,6 +130,8 @@ export {
127
130
  attachReasoningEffort,
128
131
  buildAppToolMcpServers,
129
132
  buildProductEgressPolicy,
133
+ buildSandboxToolBinDirScript,
134
+ buildSandboxToolBinDirsEnv,
130
135
  buildSandboxToolFileMounts,
131
136
  buildSandboxToolPathSetupScript,
132
137
  classifySeveredStream,
@@ -140,6 +145,7 @@ export {
140
145
  deleteSecret,
141
146
  detectInteractiveQuestion,
142
147
  driveSandboxTurn,
148
+ ensureSandboxToolBinDir,
143
149
  ensureWorkspaceSandbox,
144
150
  flattenHistory,
145
151
  formatSandboxProvisioningSupportDetails,
@@ -0,0 +1,12 @@
1
+ import type { ModelOptionValue } from '../studio';
2
+ import type { ComposerType } from './studio-composer';
3
+ export interface PersistedComposerSelections {
4
+ v: 1;
5
+ type: ComposerType;
6
+ selectedModels: Partial<Record<ComposerType, string>>;
7
+ /** Option values are model-specific: two models in one lane may publish
8
+ * different enums for the same parameter. */
9
+ optionsByModel: Record<string, Record<string, ModelOptionValue>>;
10
+ }
11
+ export declare function loadComposerSelections(workspaceId: string): PersistedComposerSelections | null;
12
+ export declare function saveComposerSelections(workspaceId: string, snapshot: PersistedComposerSelections): void;
@@ -685,6 +685,58 @@ function optionValueLabel(param, value) {
685
685
  return text.charAt(0).toUpperCase() + text.slice(1);
686
686
  }
687
687
 
688
+ // src/studio-react/composer-persistence.ts
689
+ var COMPOSER_TYPES = ["image", "video", "speech"];
690
+ function storageKey(workspaceId) {
691
+ return `studio-composer:${workspaceId}`;
692
+ }
693
+ function isPlainObject(value) {
694
+ if (value === null || typeof value !== "object") return false;
695
+ const prototype = Object.getPrototypeOf(value);
696
+ return prototype === Object.prototype || prototype === null;
697
+ }
698
+ function isComposerType(value) {
699
+ return COMPOSER_TYPES.some((type) => type === value);
700
+ }
701
+ function isOptionValue(value) {
702
+ return typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
703
+ }
704
+ function parseSelections(value) {
705
+ if (!isPlainObject(value) || value.v !== 1 || !isComposerType(value.type)) return null;
706
+ if (!isPlainObject(value.selectedModels) || !isPlainObject(value.optionsByModel)) return null;
707
+ const selectedModels = {};
708
+ for (const type of COMPOSER_TYPES) {
709
+ const modelId = value.selectedModels[type];
710
+ if (typeof modelId === "string") selectedModels[type] = modelId;
711
+ }
712
+ const optionsByModel = {};
713
+ for (const [modelId, rawOptions] of Object.entries(value.optionsByModel)) {
714
+ if (!isPlainObject(rawOptions)) continue;
715
+ const options = {};
716
+ for (const [param, optionValue] of Object.entries(rawOptions)) {
717
+ if (isOptionValue(optionValue)) options[param] = optionValue;
718
+ }
719
+ optionsByModel[modelId] = options;
720
+ }
721
+ return { v: 1, type: value.type, selectedModels, optionsByModel };
722
+ }
723
+ function loadComposerSelections(workspaceId) {
724
+ if (typeof window === "undefined") return null;
725
+ try {
726
+ const raw = window.localStorage.getItem(storageKey(workspaceId));
727
+ return raw === null ? null : parseSelections(JSON.parse(raw));
728
+ } catch {
729
+ return null;
730
+ }
731
+ }
732
+ function saveComposerSelections(workspaceId, snapshot) {
733
+ if (typeof window === "undefined") return;
734
+ try {
735
+ window.localStorage.setItem(storageKey(workspaceId), JSON.stringify(snapshot));
736
+ } catch {
737
+ }
738
+ }
739
+
688
740
  // src/studio-react/studio-composer.tsx
689
741
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
690
742
  var SEGMENTS = [
@@ -760,8 +812,26 @@ function StudioComposer({
760
812
  const [referenceImageUrl, setReferenceImageUrl] = useState3(null);
761
813
  const [isSubmitting, setIsSubmitting] = useState3(false);
762
814
  const [error, setError] = useState3(null);
815
+ const [hydratedWorkspaceId, setHydratedWorkspaceId] = useState3(null);
763
816
  const submitLockRef = useRef2(false);
764
817
  const bandRef = useRef2(null);
818
+ const hydratedRef = useRef2(false);
819
+ const persistedOptionsRef = useRef2({});
820
+ useEffect3(() => {
821
+ hydratedRef.current = false;
822
+ setHydratedWorkspaceId(null);
823
+ if (!workspaceId) return;
824
+ const persisted = loadComposerSelections(workspaceId);
825
+ if (persisted) {
826
+ setType(persisted.type);
827
+ setSelectedModels(persisted.selectedModels);
828
+ persistedOptionsRef.current = persisted.optionsByModel;
829
+ } else {
830
+ persistedOptionsRef.current = {};
831
+ }
832
+ hydratedRef.current = true;
833
+ setHydratedWorkspaceId(workspaceId);
834
+ }, [workspaceId]);
765
835
  useEffect3(() => {
766
836
  if (!workspaceId) return;
767
837
  let cancelled = false;
@@ -814,13 +884,35 @@ function StudioComposer({
814
884
  );
815
885
  useEffect3(() => {
816
886
  setOptionValues((current) => {
817
- const next = reconcileOptionValues(options, current[type], {
887
+ const seed = { ...persistedOptionsRef.current[modelId], ...current[type] };
888
+ const next = reconcileOptionValues(options, seed, {
818
889
  allowCustomSize: supportsCustomImageSize(modelId)
819
890
  });
820
891
  const unchanged = Object.keys(next).length === Object.keys(current[type]).length && Object.entries(next).every(([key, value]) => current[type][key] === value);
821
892
  return unchanged ? current : { ...current, [type]: next };
822
893
  });
823
894
  }, [modelId, options, type]);
895
+ useEffect3(() => {
896
+ if (!workspaceId || !hydratedRef.current || hydratedWorkspaceId !== workspaceId) return;
897
+ if (!catalog && modelId && persistedOptionsRef.current[modelId] !== void 0 && !options) return;
898
+ const seed = { ...persistedOptionsRef.current[modelId], ...optionValues[type] };
899
+ const reconciled = reconcileOptionValues(options, seed, {
900
+ allowCustomSize: supportsCustomImageSize(modelId)
901
+ });
902
+ const reconciledReady = Object.keys(reconciled).length === Object.keys(optionValues[type]).length && Object.entries(reconciled).every(([key, value]) => optionValues[type][key] === value);
903
+ if (!reconciledReady) return;
904
+ const optionsByModel = {
905
+ ...persistedOptionsRef.current,
906
+ ...modelId ? { [modelId]: optionValues[type] } : {}
907
+ };
908
+ saveComposerSelections(workspaceId, {
909
+ v: 1,
910
+ type,
911
+ selectedModels,
912
+ optionsByModel
913
+ });
914
+ persistedOptionsRef.current = optionsByModel;
915
+ }, [catalog, hydratedWorkspaceId, modelId, optionValues, options, selectedModels, type, workspaceId]);
824
916
  const values = optionValues[type];
825
917
  const params = visibleParams(type, options);
826
918
  const audioMeta = type === "video" ? options?.audio : void 0;