@buoy-gg/scenarios 7.0.76 → 7.0.77
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/README.md +6 -4
- package/lib/commonjs/components/ScenarioDetailScreen.js +1 -1
- package/lib/commonjs/components/ScenarioRow.js +1 -1
- package/lib/commonjs/components/ScenariosBody.js +1 -1
- package/lib/commonjs/store/scenariosStore.js +1 -1
- package/lib/commonjs/sync/scenariosSyncAdapter.js +1 -1
- package/lib/commonjs/team/teamClient.js +1 -0
- package/lib/module/components/ScenarioDetailScreen.js +1 -1
- package/lib/module/components/ScenarioRow.js +1 -1
- package/lib/module/components/ScenariosBody.js +1 -1
- package/lib/module/store/scenariosStore.js +1 -1
- package/lib/module/sync/scenariosSyncAdapter.js +1 -1
- package/lib/module/team/teamClient.js +1 -0
- package/lib/typescript/components/ScenarioDetailScreen.d.ts +9 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/store/scenariosStore.d.ts +59 -1
- package/lib/typescript/sync/scenariosSyncAdapter.d.ts +86 -0
- package/lib/typescript/team/teamClient.d.ts +77 -0
- package/lib/typescript/types.d.ts +22 -1
- package/lib/web/index.mjs +2 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -10,13 +10,15 @@ npm install @buoy-gg/core @buoy-gg/scenarios @buoy-gg/network @buoy-gg/route-eve
|
|
|
10
10
|
|
|
11
11
|
## Account setup
|
|
12
12
|
|
|
13
|
-
Start in a development build
|
|
13
|
+
Start in a development build. You need a Free or Pro Buoy account. Sign in from your app's folder:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npx --package=@buoy-gg/core buoy login
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Your browser opens buoy.gg. Click Allow. The CLI then writes a dev token. It works in dev builds for 30 days.
|
|
20
|
+
|
|
21
|
+
On Expo, the token goes in `.env.development.local`. Buoy reads it by itself in dev builds. You can also pass it in yourself:
|
|
20
22
|
|
|
21
23
|
```tsx
|
|
22
24
|
import { Buoy } from "@buoy-gg/core";
|
|
@@ -24,7 +26,7 @@ import { Buoy } from "@buoy-gg/core";
|
|
|
24
26
|
Buoy.init({ licenseKey: process.env.EXPO_PUBLIC_BUOY_KEY });
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
On React Native CLI, the token goes in `.env.local` as `BUOY_KEY`. React Native does not load that file by itself. Pass the token in with your app's own env setup. Keep the menu inside the providers used by your app. Restart the development server after installing packages. See the [Quick Start](https://buoy.gg/buoy/latest/docs/quick-start) for the complete root component.
|
|
28
30
|
|
|
29
31
|
## Define a scenario
|
|
30
32
|
|
|
@@ -70,7 +72,7 @@ Remote drafts require acceptance on the device before they can run. The runner r
|
|
|
70
72
|
|
|
71
73
|
## Desktop and MCP
|
|
72
74
|
|
|
73
|
-
React Native connections require `@buoy-gg/external-sync`. Follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop), sign in to Desktop separately, and confirm the selected device. [MCP](https://buoy.gg/buoy/latest/docs/mcp)
|
|
75
|
+
React Native connections require `@buoy-gg/external-sync`. Follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop), sign in to Desktop separately, and confirm the selected device. [MCP](https://buoy.gg/buoy/latest/docs/mcp) uses the sign-in from `buoy login` on your computer. Free has the basic MCP. Reading this tool from the MCP needs Pro.
|
|
74
76
|
|
|
75
77
|
Device data can travel over your LAN to the configured broker. Account validation makes network requests. See [Telemetry](https://buoy.gg/buoy/latest/docs/telemetry) for data-flow details.
|
|
76
78
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenarioDetailScreen=ScenarioDetailScreen;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _core=require("@buoy-gg/core");var _catalog=require("../catalog");var _folders=require("../folders");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _ScenarioRow=require("./ScenarioRow");var _jsxRuntime=require("react/jsx-runtime");const DURABILITY_TEXT={durable:"survives restart",session:"until restart",transient:"until refetch \u26A0","one-shot":"one-shot"};function tone(e){if(e==="durable")return"durable";if(e==="transient")return"transient";return"neutral"}function ScenarioDetailScreen({scenario:e,onRun:
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenarioDetailScreen=ScenarioDetailScreen;exports.sourceText=sourceText;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _core=require("@buoy-gg/core");var _catalog=require("../catalog");var _folders=require("../folders");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _ScenarioRow=require("./ScenarioRow");var _jsxRuntime=require("react/jsx-runtime");const DURABILITY_TEXT={durable:"survives restart",session:"until restart",transient:"until refetch \u26A0","one-shot":"one-shot"};function tone(e){if(e==="durable")return"durable";if(e==="transient")return"transient";return"neutral"}function sourceText(e){if(e.source==="code")return"shipped with this app";if(e.source==="team")return`shared with your team by ${e.team?.sharedBy.label??"a teammate"}`;if(e.source==="draft")return"a draft, waiting for review";return"saved on this device"}function ScenarioDetailScreen({scenario:e,onRun:o,onDelete:s,onDuplicate:t,onExported:l,onMoveToFolder:n,onShare:c,onRemoveFromTeam:d,teamBusy:i,onBack:p}){(0,_core.useToolHeader)({title:(0,_ScenarioRow.displayName)(e.name),subtitle:sourceText(e),onBack:p,searchable:false},[e.id]);const a=e.source==="device";const u=(0,_folders.normalizeFolder)(e.folder);return(0,_jsxRuntime.jsxs)(_reactNative.ScrollView,{style:styles.fill,contentContainerStyle:styles.content,testID:"scenario-detail-scroll",children:[e.description?(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:e.description}):null,(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Layers,count:e.steps.length,children:"Steps"}),(0,_jsxRuntime.jsx)(_primitives.Panel,{children:e.steps.map((r,x)=>{const h=(0,_catalog.stepDurability)(r);return(0,_jsxRuntime.jsxs)(_react.Fragment,{children:[x>0?(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}):null,(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.step,children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.stepIndex,children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.stepIndexText,children:x+1})}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.stepBody,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.stepLabel,children:(0,_catalog.describeStep)(r)}),(0,_jsxRuntime.jsxs)(_reactNative.Text,{style:styles.stepTool,children:[r.tool,".",r.action,(0,_catalog.isAdvancedStep)(r)?" \xB7 advanced":""]})]}),(0,_jsxRuntime.jsx)(_primitives.DurabilityChip,{label:DURABILITY_TEXT[h],tone:tone(h)})]})]},r.id)})}),e.expectedOutcome?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.CheckCircle,children:"Expected outcome"}),(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.expected,children:e.expectedOutcome})})]}):null,(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.FileText,children:"Details"}),(0,_jsxRuntime.jsxs)(_primitives.Panel,{children:[(0,_jsxRuntime.jsx)(Detail,{label:"Folder",value:u??"No folder",muted:!u,onPress:a?n:void 0,testID:"scenario-detail-folder"}),(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Source",value:sourceText(e),testID:"scenario-detail-source"}),(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Version",value:`v${e.version}`}),e.author?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Author",value:e.author})]}):null,e.authoredAgainst?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Authored against",value:e.authoredAgainst})]}):null,(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Runs",value:String(e.usage?.runs??0)}),e.tags?.length?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.PanelSeparator,{}),(0,_jsxRuntime.jsx)(Detail,{label:"Tags",value:e.tags.join(", ")})]}):null]}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.footer,children:[(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:"Run",variant:"primary",onPress:o,icon:(0,_jsxRuntime.jsx)(_sharedUi.Play,{size:12,color:_scnTheme.scn.color.accent}),style:styles.grow,testID:"scenario-detail-run"}),(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:"Export",variant:"ghost",onPress:()=>{void(0,_sharedUi.copyToClipboard)(JSON.stringify(e,null,2));l?.()},icon:(0,_jsxRuntime.jsx)(_sharedUi.Copy,{size:12,color:_scnTheme.scn.color.textSecondary}),style:styles.grow,testID:"scenario-detail-export"})]}),a&&c?(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.footer,children:(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:i?"Sharing\u2026":"Share with team",variant:"ghost",onPress:c,disabled:i,icon:(0,_jsxRuntime.jsx)(_sharedUi.Upload,{size:12,color:_scnTheme.scn.color.textSecondary}),style:styles.grow,testID:"scenario-detail-share"})}):null,(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.footer,children:[a?(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:"Delete",variant:"danger",onPress:s,icon:(0,_jsxRuntime.jsx)(_sharedUi.Trash,{size:12,color:_scnTheme.scn.color.danger}),style:styles.grow,testID:"scenario-detail-delete"}):(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:"Make an editable copy",variant:"ghost",onPress:t,style:styles.grow,testID:"scenario-detail-duplicate"}),e.source==="team"&&d?(0,_jsxRuntime.jsx)(_sharedUi.ToolButton,{label:i?"Removing\u2026":"Remove from team",variant:"danger",onPress:d,disabled:i,icon:(0,_jsxRuntime.jsx)(_sharedUi.Users,{size:12,color:_scnTheme.scn.color.danger}),style:styles.grow,testID:"scenario-detail-remove-team"}):null]}),e.source==="code"?(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:"Shipped scenarios are read-only on the device \u2014 they change in a pull request, with the code they bend."}):null,e.source==="team"?(0,_jsxRuntime.jsx)(_primitives.Footnote,{testID:"scenario-detail-team-note",children:"Your whole team sees this scenario in this app. It can't be changed here. Make an editable copy to change it on this device."}):null,a&&c?(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:"Share with team puts this scenario in Scenarios for everyone on your team, in this app."}):null]})}function Detail({label:e,value:o,muted:s,onPress:t,testID:l}){const n=(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.detailLabel,children:e}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:[styles.detailValue,s&&styles.detailValueMuted,t&&styles.detailValueAction],numberOfLines:2,children:o}),t?(0,_jsxRuntime.jsx)(_sharedUi.ChevronRight,{size:13,color:_scnTheme.scn.color.textTertiary}):null]});if(!t){return(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.detail,testID:l,children:n})}return(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{style:styles.detail,onPress:t,activeOpacity:.7,accessibilityRole:"button",accessibilityLabel:`${e}: ${o}. Change it.`,testID:l,children:n})}const styles=_reactNative.StyleSheet.create({fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:32},step:{flexDirection:"row",alignItems:"flex-start",gap:9,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:10},stepIndex:{width:18,height:18,borderRadius:_scnTheme.scn.radius.badge,backgroundColor:_scnTheme.scn.color.fill,alignItems:"center",justifyContent:"center",marginTop:1},stepIndexText:{color:_scnTheme.scn.color.textTertiary,fontSize:10,fontWeight:"700",fontFamily:_scnTheme.SCN_MONO},stepBody:{flex:1,minWidth:0},stepLabel:{color:_scnTheme.scn.color.text,fontSize:_scnTheme.scn.font.control},stepTool:{color:_scnTheme.scn.color.textTertiary,fontSize:_scnTheme.scn.font.mono,fontFamily:_scnTheme.SCN_MONO,marginTop:2},expected:{color:_scnTheme.scn.color.textSecondary,fontSize:_scnTheme.scn.font.control,lineHeight:18,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:12},detail:{flexDirection:"row",alignItems:"center",justifyContent:"space-between",gap:8,minHeight:40,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:10},detailLabel:{color:_scnTheme.scn.color.textTertiary,fontSize:_scnTheme.scn.font.control},detailValue:{flex:1,textAlign:"right",color:_scnTheme.scn.color.textSecondary,fontSize:_scnTheme.scn.font.control,fontFamily:_scnTheme.SCN_MONO},detailValueMuted:{color:_scnTheme.scn.color.textTertiary},detailValueAction:{color:_scnTheme.scn.color.accent},footer:{flexDirection:"row",gap:8,paddingHorizontal:_scnTheme.scn.gutter,paddingTop:12},grow:{flex:1}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenarioRow=void 0;exports.displayName=displayName;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _folders=require("../folders");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _jsxRuntime=require("react/jsx-runtime");function displayName(e){return e.replace(/\{\{\s*(\w+)\s*\}\}/g,"\u27E8$1\u27E9")}function metaLine(e){const t=[`v${e.version}`];if(e.author)t.push(e.author);const s=e.usage?.runs??0;if(s)t.push(`${s}\xD7`);if(e.unsavedToRepo)t.push("unsaved to repo");if(e.source==="draft")t.push("inert until you accept");return t.join(" \xB7 ")}function origin(e){if(e.source==="code")return{label:"Shipped",color:_scnTheme.scn.color.accent};if(e.source==="draft")return{label:"Draft",color:_scnTheme.scn.color.sim};return{label:"Saved",color:_scnTheme.scn.color.info}}function ScenarioRowInner({scenario:e,currentBuild:t,disabled:s,showFolder:i,onOpen:n,onRun:u,testID:c}){const r=c??`scenario-row-${e.id}`;const l=origin(e);const a=(0,_folders.normalizeFolder)(e.folder);const d=e.source==="draft";const o=d?{label:"Review",color:_scnTheme.scn.color.sim}:{label:"Run",color:_scnTheme.scn.color.accent};const p=!!e.authoredAgainst&&!!t&&e.authoredAgainst!==t&&e.verifiedAt?.build!==t;return(0,_jsxRuntime.jsx)(_sharedUi.CompactRow,{statusDotColor:l.color,statusLabel:l.label,statusSublabel:`${e.steps.length} step${e.steps.length===1?"":"s"}`,primaryText:displayName(e.name),primaryTestID:`${r}-name`,secondaryText:metaLine(e),secondaryTestID:`${r}-meta`,secondaryAccessory:(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[i&&a?(0,_jsxRuntime.jsx)(_primitives.DurabilityChip,{label:a,tone:"neutral",testID:`${r}-folder`}):null,p?(0,_jsxRuntime.jsx)(_primitives.DurabilityChip,{label:`stale \xB7 ${e.authoredAgainst}`,tone:"transient",testID:`${r}-stale`}):null]}),trailingAction:(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:u,disabled:s,style:s?styles.disabled:null,hitSlop:HIT_SLOP,accessibilityRole:"button",accessibilityLabel:`${o.label} ${displayName(e.name)}`,testID:`${r}-run`,children:(0,_jsxRuntime.jsx)(_reactNative.View,{children:(0,_jsxRuntime.jsx)(_primitives.PillLabel,{label:o.label,color:o.color})})}),showChevron:true,onPress:n,testID:r,accessibilityLabel:`${displayName(e.name)}. ${l.label}, ${e.steps.length} step${e.steps.length===1?"":"s"}, ${metaLine(e)}. Open details.`})}const ScenarioRow=exports.ScenarioRow=(0,_react.memo)(ScenarioRowInner);const HIT_SLOP={top:8,bottom:8,left:8,right:8};const styles=_reactNative.StyleSheet.create({disabled:{opacity:.4}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenarioRow=void 0;exports.displayName=displayName;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _folders=require("../folders");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _jsxRuntime=require("react/jsx-runtime");function displayName(e){return e.replace(/\{\{\s*(\w+)\s*\}\}/g,"\u27E8$1\u27E9")}function metaLine(e){const t=[`v${e.version}`];if(e.source==="team"&&e.team)t.push(`shared by ${e.team.sharedBy.label}`);else if(e.author)t.push(e.author);const s=e.usage?.runs??0;if(s)t.push(`${s}\xD7`);if(e.unsavedToRepo)t.push("unsaved to repo");if(e.source==="draft")t.push("inert until you accept");return t.join(" \xB7 ")}function origin(e){if(e.source==="code")return{label:"Shipped",color:_scnTheme.scn.color.accent};if(e.source==="draft")return{label:"Draft",color:_scnTheme.scn.color.sim};if(e.source==="team")return{label:"Team",color:_scnTheme.scn.color.accent};return{label:"Saved",color:_scnTheme.scn.color.info}}function ScenarioRowInner({scenario:e,currentBuild:t,disabled:s,showFolder:i,onOpen:n,onRun:u,testID:c}){const r=c??`scenario-row-${e.id}`;const l=origin(e);const a=(0,_folders.normalizeFolder)(e.folder);const d=e.source==="draft";const o=d?{label:"Review",color:_scnTheme.scn.color.sim}:{label:"Run",color:_scnTheme.scn.color.accent};const p=!!e.authoredAgainst&&!!t&&e.authoredAgainst!==t&&e.verifiedAt?.build!==t;return(0,_jsxRuntime.jsx)(_sharedUi.CompactRow,{statusDotColor:l.color,statusLabel:l.label,statusSublabel:`${e.steps.length} step${e.steps.length===1?"":"s"}`,primaryText:displayName(e.name),primaryTestID:`${r}-name`,secondaryText:metaLine(e),secondaryTestID:`${r}-meta`,secondaryAccessory:(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[i&&a?(0,_jsxRuntime.jsx)(_primitives.DurabilityChip,{label:a,tone:"neutral",testID:`${r}-folder`}):null,p?(0,_jsxRuntime.jsx)(_primitives.DurabilityChip,{label:`stale \xB7 ${e.authoredAgainst}`,tone:"transient",testID:`${r}-stale`}):null]}),trailingAction:(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:u,disabled:s,style:s?styles.disabled:null,hitSlop:HIT_SLOP,accessibilityRole:"button",accessibilityLabel:`${o.label} ${displayName(e.name)}`,testID:`${r}-run`,children:(0,_jsxRuntime.jsx)(_reactNative.View,{children:(0,_jsxRuntime.jsx)(_primitives.PillLabel,{label:o.label,color:o.color})})}),showChevron:true,onPress:n,testID:r,accessibilityLabel:`${displayName(e.name)}. ${l.label}, ${e.steps.length} step${e.steps.length===1?"":"s"}, ${metaLine(e)}. Open details.`})}const ScenarioRow=exports.ScenarioRow=(0,_react.memo)(ScenarioRowInner);const HIT_SLOP={top:8,bottom:8,left:8,right:8};const styles=_reactNative.StyleSheet.create({disabled:{opacity:.4}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenariosBody=ScenariosBody;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _core=require("@buoy-gg/core");var _interpolate=require("../interpolate");var _scenariosStore=require("../store/scenariosStore");var _useScenarios=require("../hooks/useScenarios");var _recorderStore=require("../record/recorderStore");var _condense=require("../record/condense");var _MacroReviewScreen=require("./MacroReviewScreen");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _ActiveCard=require("./ActiveCard");var _DeactivateReportCard=require("./DeactivateReportCard");var _PreviewScreen=require("./PreviewScreen");var _RunProgressScreen=require("./RunProgressScreen");var _ScenarioDetailScreen=require("./ScenarioDetailScreen");var _ScenarioRow=require("./ScenarioRow");var _ScenarioFolderBar=require("./ScenarioFolderBar");var _FolderPickerScreen=require("./FolderPickerScreen");var _jsxRuntime=require("react/jsx-runtime");function macroId(r){const l=r.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40);return`macro-${l||"recording"}-${Date.now().toString(36)}`}function ScenariosBody({currentBuild:r}){const l=(0,_useScenarios.useScenariosState)();const S=(0,_useScenarios.useScenariosRunState)();const c=(0,_useScenarios.useRecording)();const o=(0,_core.useToolHost)();const f=(0,_sharedUi.useSafeAreaInsets)({minBottom:16});const y=(0,_sharedUi.useLimit)("scenarios.saved");const[d,a]=(0,_react.useState)({name:"list"});const[g,u]=(0,_react.useState)(false);const i=(0,_react.useCallback)(()=>a({name:"list"}),[]);const x=(0,_react.useCallback)(e=>t=>{return _scenariosStore.scenariosStore.checkBeforeRun(e,t)},[]);const m=(0,_react.useCallback)(async(e,t)=>{u(true);a({name:"run"});try{const n=await _scenariosStore.scenariosStore.run(e,t);if(n.ok){o.showToast(`Scenario active \u2014 ${n.effects.length} change${n.effects.length===1?"":"s"}`,"success");_scenariosStore.scenariosStore.clearRunState();a({name:"list"});o.minimize()}}catch(n){o.showToast(n instanceof Error?n.message:String(n),"error");a({name:"list"})}finally{u(false)}},[o]);const p=(0,_react.useCallback)(async()=>{u(true);try{await _scenariosStore.scenariosStore.deactivate()}finally{u(false)}},[]);const b=(0,_react.useCallback)(async()=>{await _recorderStore.recorderStore.start();const{status:e,reason:t}=_recorderStore.recorderStore.getState();if(e!=="recording"){o.showToast(t??"Recording isn't available in this app.","error");return}o.minimize()},[o]);const h=(0,_react.useCallback)(async(e,t)=>{u(true);try{const n=(0,_condense.macroToSteps)(t);const w=Date.now();await _scenariosStore.scenariosStore.saveScenario({id:macroId(e),name:e,version:1,source:"device",author:"recorded here",vars:[],steps:n,authoredAgainst:r,unsavedToRepo:true,createdAt:w,updatedAt:w,usage:{runs:0}});await _recorderStore.recorderStore.cancel();o.showToast(`Saved \u201C${e}\u201D`,"success");i()}catch(n){o.showToast(n instanceof Error?n.message:String(n),"error")}finally{u(false)}},[r,i,o]);if(c.status==="review"&&c.draft){return(0,_jsxRuntime.jsx)(_MacroReviewScreen.MacroReviewScreen,{draft:c.draft,busy:g,onChange:e=>_recorderStore.recorderStore.setDraft(e),onSave:h,onDiscard:()=>{_reactNative.Alert.alert("Discard this recording?","The steps you just recorded will be thrown away.",[{text:"Keep",style:"cancel"},{text:"Discard",style:"destructive",onPress:()=>{void _recorderStore.recorderStore.cancel();i()}}])}})}if(d.name==="run"&&S){return(0,_jsxRuntime.jsx)(_RunProgressScreen.RunProgressScreen,{runState:S,busy:g,onUndoPartial:async()=>{u(true);try{await _scenariosStore.scenariosStore.undoPartialRun();o.showToast("Applied steps undone","success")}finally{u(false);i()}},onKeepPartial:()=>{_scenariosStore.scenariosStore.clearRunState();i()},onRetry:()=>{const e=S.scenarioId;const t=_scenariosStore.scenariosStore.find(e);if(t){void m(e,_scenariosStore.scenariosStore.lastValuesFor(e))}},onDone:()=>{_scenariosStore.scenariosStore.clearRunState();i()},onBack:()=>{_scenariosStore.scenariosStore.clearRunState();i()}})}if(d.name==="preview"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...v()});const t={...(0,_interpolate.resolveVarValues)(e.vars),..._scenariosStore.scenariosStore.lastValuesFor(e.id)};return(0,_jsxRuntime.jsx)(_PreviewScreen.PreviewScreen,{scenario:e,initialValues:t,check:x(e),currentBuild:r,mode:d.mode,onRun:n=>{_scenariosStore.scenariosStore.rememberVarValues(e.id,n);void m(e.id,n)},onAcceptDraft:async()=>{await _scenariosStore.scenariosStore.acceptDraft(e.id);o.showToast("Accepted \u2014 it's now in your library","success");i()},onDiscardDraft:async()=>{await _scenariosStore.scenariosStore.discardDraft(e.id);o.showToast("Draft discarded","info");i()},onBack:i})}if(d.name==="detail"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...v()});return(0,_jsxRuntime.jsx)(_ScenarioDetailScreen.ScenarioDetailScreen,{scenario:e,onRun:()=>a({name:"preview",scenarioId:e.id,mode:"run"}),onDelete:()=>{_reactNative.Alert.alert("Delete scenario?",`\u201C${(0,_ScenarioRow.displayName)(e.name)}\u201D will be removed from this device.`,[{text:"Cancel",style:"cancel"},{text:"Delete",style:"destructive",onPress:()=>{void _scenariosStore.scenariosStore.deleteScenario(e.id);i()}}])},onDuplicate:async()=>{const t={...e,id:`${e.id}-copy-${Date.now().toString(36)}`,name:`${e.name} (copy)`,source:"device",version:1,unsavedToRepo:true};await _scenariosStore.scenariosStore.saveScenario(t);o.showToast("Copied to this device","success");i()},onExported:()=>o.showToast("Scenario JSON copied","success"),onMoveToFolder:()=>a({name:"folder",scenarioId:e.id}),onBack:i})}if(d.name==="folder"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...v()});return(0,_jsxRuntime.jsx)(_FolderPickerScreen.FolderPickerScreen,{scenario:e,folders:_scenariosStore.scenariosStore.folders(),onPick:async t=>{try{await _scenariosStore.scenariosStore.setFolder(e.id,t);o.showToast(t?`Filed under ${t}`:"Removed from its folder","success")}catch(n){o.showToast(n instanceof Error?n.message:String(n),"error")}a({name:"detail",scenarioId:e.id})},onBack:()=>a({name:"detail",scenarioId:e.id})})}function v(){return{state:l,insets:f,host:o,busy:g,savedLimit:y,currentBuild:r,onOpen:e=>a({name:"detail",scenarioId:e}),onRun:e=>a({name:"preview",scenarioId:e,mode:"run"}),onReviewDraft:e=>a({name:"preview",scenarioId:e,mode:"review-draft"}),onDeactivate:p,onRerun:()=>{const e=l.active;if(e)void m(e.scenarioId,e.varValues)},onStartRecording:b,canRecord:_recorderStore.recorderStore.canRecord(),recordUnavailable:c.status==="unavailable"?c.reason:null}}return(0,_jsxRuntime.jsx)(ScenariosList,{...v()})}function emptyText(r,l,S){if(r)return"No matches.";if(l===void 0)return"Nothing unfiled here.";if(l!==null)return`Nothing in ${l}.`;return S}function ScenariosList({state:r,insets:l,host:S,busy:c,savedLimit:o,currentBuild:f,onOpen:y,onRun:d,onReviewDraft:a,onDeactivate:g,onRerun:u,onStartRecording:i,canRecord:x,recordUnavailable:m}){const[p,b]=(0,_react.useState)(null);const h=S.search.trim().toLowerCase();const v=s=>!h||s.name.toLowerCase().includes(h)||(s.folder??"").toLowerCase().includes(h)||(s.tags??[]).some(_=>_.toLowerCase().includes(h))||(s.author??"").toLowerCase().includes(h);const e={code:r.code.filter(v),device:r.device.filter(v),drafts:r.drafts.filter(v)};const t=_scenariosStore.scenariosStore.folders();const n=[...e.code,...e.device,...e.drafts];const w=s=>(0,_ScenarioFolderBar.folderMatches)(s,p);const R=e.code.filter(w);const D=e.device.filter(w);const j=e.drafts.filter(w);const k=r.device.length>=o;const F=_scenariosStore.scenariosStore.isLongRunning();const T=p===null&&t.length>0;const C=(0,_react.useCallback)(()=>({shipped:r.code,saved:r.device,drafts:r.drafts,active:r.active??null}),[r.code,r.device,r.drafts,r.active]);const I=(0,_react.useMemo)(()=>(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:C,buttonStyle:styles.headerButton,testID:"scenarios-copy-library",accessibilityLabel:"Copy the scenario library as JSON"}),[C]);(0,_core.useToolHeader)({actions:I},[I]);return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.fill,children:[t.length?(0,_jsxRuntime.jsx)(_ScenarioFolderBar.ScenarioFolderBar,{folders:t,active:p,onChange:b,scenarios:n}):null,(0,_jsxRuntime.jsxs)(_reactNative.ScrollView,{style:styles.fill,contentContainerStyle:[styles.content,{paddingBottom:l.bottom}],keyboardShouldPersistTaps:"handled",testID:"scenarios-scroll",children:[x?(0,_jsxRuntime.jsxs)(_reactNative.TouchableOpacity,{style:styles.record,onPress:i,disabled:c,activeOpacity:.75,accessibilityRole:"button",accessibilityLabel:"Record what you do next as a macro",testID:"scenarios-record",children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.recordDot}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.recordText,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.recordTitle,children:"Record a macro"}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.recordHint,children:"Do the flow once. Buoy writes down the steps."})]})]}):null,m?(0,_jsxRuntime.jsx)(_primitives.Footnote,{tone:"warning",testID:"scenarios-record-unavailable",children:m}):null,r.lastDeactivateReport?(0,_jsxRuntime.jsx)(_DeactivateReportCard.DeactivateReportCard,{report:r.lastDeactivateReport,onDismiss:()=>_scenariosStore.scenariosStore.dismissDeactivateReport()}):null,r.active?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Zap,color:_scnTheme.scn.color.sim,testID:"scenarios-active-label",children:"Active now"}),(0,_jsxRuntime.jsx)(_ActiveCard.ActiveCard,{active:r.active,build:f,busy:c,onDeactivate:g,onRerun:u,onCopied:()=>S.showToast("Repro info copied \u2014 paste into the ticket","success")}),F?(0,_jsxRuntime.jsx)(_primitives.Footnote,{tone:"warning",testID:"scenarios-long-running",children:"This scenario has been active for a while. If you have moved on to something else, deactivate it before you test anything real."}):null]}):null,j.length?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.FileText,color:_scnTheme.scn.color.sim,count:j.length,testID:"scenarios-drafts-label",children:"Awaiting review"}),j.map(s=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:s,currentBuild:f,disabled:c,showFolder:T,onOpen:()=>a(s.id),onRun:()=>a(s.id),testID:`scenario-draft-${s.id}`},s.id)),(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:"A scenario sent from chat or the desktop never runs on its own. You review what it will do, then accept it."})]}):null,(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Box,count:R.length,testID:"scenarios-code-label",children:"Shipped with this app"}),R.length?R.map(s=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:s,currentBuild:f,disabled:c,showFolder:T,onOpen:()=>y(s.id),onRun:()=>d(s.id)},s.id)):(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.empty,testID:"scenarios-code-empty",children:emptyText(h,p,"None yet \u2014 your developers can ship a QA menu with the app using defineScenario().")})}),(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Smartphone,color:_scnTheme.scn.color.info,count:D.length,testID:"scenarios-device-label",children:"Saved on this device"}),D.length?D.map(s=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:s,currentBuild:f,disabled:c,showFolder:T,onOpen:()=>y(s.id),onRun:()=>d(s.id)},s.id)):(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.empty,testID:"scenarios-device-empty",children:emptyText(h,p,"Nothing saved here yet. Ask your AI assistant to save the state you just set up as a scenario.")})}),k?(0,_jsxRuntime.jsxs)(_primitives.Footnote,{tone:"warning",testID:"scenarios-limit",children:["Your plan keeps ",o," saved scenario",o===1?"":"s"," on a device. Scenarios shipped in code don't count toward it."]}):null,(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:t.length?"Scenarios only run in development builds. Tap a row to see exactly what it will do before you run it.":"Scenarios only run in development builds. Open one and tap its Folder row to group it by flow \u2014 Checkout, Login \u2014 and a folder bar appears up here."})]})]})}const styles=_reactNative.StyleSheet.create({fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:24},record:{flexDirection:"row",alignItems:"center",gap:10,marginHorizontal:_scnTheme.scn.gutter,marginTop:8,marginBottom:4,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:12,borderRadius:_scnTheme.scn.radius.card,borderWidth:1,borderColor:_scnTheme.scn.color.simBorder,backgroundColor:_scnTheme.scn.color.simSoft},recordDot:{width:10,height:10,borderRadius:5,backgroundColor:_scnTheme.scn.color.sim},recordText:{flex:1,minWidth:0},recordTitle:{color:_scnTheme.scn.color.sim,fontSize:_scnTheme.scn.font.row,fontWeight:"700"},recordHint:{color:_scnTheme.scn.color.textSecondary,fontSize:_scnTheme.scn.font.micro,marginTop:2},headerButton:{width:32,height:32,borderRadius:8,backgroundColor:_scnTheme.scn.color.chip,borderWidth:1,borderColor:_scnTheme.scn.color.border,alignItems:"center",justifyContent:"center"},empty:{color:_scnTheme.scn.color.textTertiary,fontSize:_scnTheme.scn.font.control,lineHeight:18,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:14}});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ScenariosBody=ScenariosBody;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _core=require("@buoy-gg/core");var _interpolate=require("../interpolate");var _scenariosStore=require("../store/scenariosStore");var _useScenarios=require("../hooks/useScenarios");var _recorderStore=require("../record/recorderStore");var _condense=require("../record/condense");var _MacroReviewScreen=require("./MacroReviewScreen");var _scnTheme=require("../ui/scnTheme");var _primitives=require("../ui/primitives");var _ActiveCard=require("./ActiveCard");var _DeactivateReportCard=require("./DeactivateReportCard");var _PreviewScreen=require("./PreviewScreen");var _RunProgressScreen=require("./RunProgressScreen");var _ScenarioDetailScreen=require("./ScenarioDetailScreen");var _ScenarioRow=require("./ScenarioRow");var _ScenarioFolderBar=require("./ScenarioFolderBar");var _FolderPickerScreen=require("./FolderPickerScreen");var _jsxRuntime=require("react/jsx-runtime");function macroId(r){const a=r.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40);return`macro-${a||"recording"}-${Date.now().toString(36)}`}function ScenariosBody({currentBuild:r}){const a=(0,_useScenarios.useScenariosState)();const h=(0,_useScenarios.useScenariosRunState)();const l=(0,_useScenarios.useRecording)();const s=(0,_core.useToolHost)();const S=(0,_sharedUi.useSafeAreaInsets)({minBottom:16});const p=(0,_sharedUi.useLimit)("scenarios.saved");const[d,c]=(0,_react.useState)({name:"list"});const[y,u]=(0,_react.useState)(false);const[f,g]=(0,_react.useState)(false);(0,_react.useEffect)(()=>{void _scenariosStore.scenariosStore.refreshTeam();return(0,_sharedUi.subscribeAccountAccess)(()=>{void _scenariosStore.scenariosStore.onAccountChange()})},[]);const i=(0,_react.useCallback)(()=>c({name:"list"}),[]);const v=(0,_react.useCallback)(e=>o=>{return _scenariosStore.scenariosStore.checkBeforeRun(e,o)},[]);const b=(0,_react.useCallback)(async(e,o)=>{u(true);c({name:"run"});try{const n=await _scenariosStore.scenariosStore.run(e,o);if(n.ok){s.showToast(`Scenario active \u2014 ${n.effects.length} change${n.effects.length===1?"":"s"}`,"success");_scenariosStore.scenariosStore.clearRunState();c({name:"list"});s.minimize()}}catch(n){s.showToast(n instanceof Error?n.message:String(n),"error");c({name:"list"})}finally{u(false)}},[s]);const m=(0,_react.useCallback)(async()=>{u(true);try{await _scenariosStore.scenariosStore.deactivate()}finally{u(false)}},[]);const x=(0,_react.useCallback)(async()=>{await _recorderStore.recorderStore.start();const{status:e,reason:o}=_recorderStore.recorderStore.getState();if(e!=="recording"){s.showToast(o??"Recording isn't available in this app.","error");return}s.minimize()},[s]);const D=(0,_react.useCallback)(async(e,o)=>{u(true);try{const n=(0,_condense.macroToSteps)(o);const R=Date.now();await _scenariosStore.scenariosStore.saveScenario({id:macroId(e),name:e,version:1,source:"device",author:"recorded here",vars:[],steps:n,authoredAgainst:r,unsavedToRepo:true,createdAt:R,updatedAt:R,usage:{runs:0}});await _recorderStore.recorderStore.cancel();s.showToast(`Saved \u201C${e}\u201D`,"success");i()}catch(n){s.showToast(n instanceof Error?n.message:String(n),"error")}finally{u(false)}},[r,i,s]);if(l.status==="review"&&l.draft){return(0,_jsxRuntime.jsx)(_MacroReviewScreen.MacroReviewScreen,{draft:l.draft,busy:y,onChange:e=>_recorderStore.recorderStore.setDraft(e),onSave:D,onDiscard:()=>{_reactNative.Alert.alert("Discard this recording?","The steps you just recorded will be thrown away.",[{text:"Keep",style:"cancel"},{text:"Discard",style:"destructive",onPress:()=>{void _recorderStore.recorderStore.cancel();i()}}])}})}if(d.name==="run"&&h){return(0,_jsxRuntime.jsx)(_RunProgressScreen.RunProgressScreen,{runState:h,busy:y,onUndoPartial:async()=>{u(true);try{await _scenariosStore.scenariosStore.undoPartialRun();s.showToast("Applied steps undone","success")}finally{u(false);i()}},onKeepPartial:()=>{_scenariosStore.scenariosStore.clearRunState();i()},onRetry:()=>{const e=h.scenarioId;const o=_scenariosStore.scenariosStore.find(e);if(o){void b(e,_scenariosStore.scenariosStore.lastValuesFor(e))}},onDone:()=>{_scenariosStore.scenariosStore.clearRunState();i()},onBack:()=>{_scenariosStore.scenariosStore.clearRunState();i()}})}if(d.name==="preview"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...w()});const o={...(0,_interpolate.resolveVarValues)(e.vars),..._scenariosStore.scenariosStore.lastValuesFor(e.id)};return(0,_jsxRuntime.jsx)(_PreviewScreen.PreviewScreen,{scenario:e,initialValues:o,check:v(e),currentBuild:r,mode:d.mode,onRun:n=>{_scenariosStore.scenariosStore.rememberVarValues(e.id,n);void b(e.id,n)},onAcceptDraft:async()=>{await _scenariosStore.scenariosStore.acceptDraft(e.id);s.showToast("Accepted \u2014 it's now in your library","success");i()},onDiscardDraft:async()=>{await _scenariosStore.scenariosStore.discardDraft(e.id);s.showToast("Draft discarded","info");i()},onBack:i})}if(d.name==="detail"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...w()});return(0,_jsxRuntime.jsx)(_ScenarioDetailScreen.ScenarioDetailScreen,{scenario:e,onRun:()=>c({name:"preview",scenarioId:e.id,mode:"run"}),onDelete:()=>{_reactNative.Alert.alert("Delete scenario?",`\u201C${(0,_ScenarioRow.displayName)(e.name)}\u201D will be removed from this device.`,[{text:"Cancel",style:"cancel"},{text:"Delete",style:"destructive",onPress:()=>{void _scenariosStore.scenariosStore.deleteScenario(e.id);i()}}])},onDuplicate:async()=>{const o={...e,id:`${e.id}-copy-${Date.now().toString(36)}`,name:`${e.name} (copy)`,source:"device",version:1,unsavedToRepo:true,team:void 0};await _scenariosStore.scenariosStore.saveScenario(o);s.showToast("Copied to this device","success");i()},onExported:()=>s.showToast("Scenario JSON copied","success"),onMoveToFolder:()=>c({name:"folder",scenarioId:e.id}),teamBusy:f,onShare:e.source==="device"&&!_scenariosStore.scenariosStore.isMirror()&&(a.teamInfo.status==="ready"||a.teamInfo.status==="error")?()=>{_reactNative.Alert.alert("Share with your team?",`Everyone on your team will see \u201C${(0,_ScenarioRow.displayName)(e.name)}\u201D in Scenarios in this app.`,[{text:"Cancel",style:"cancel"},{text:"Share",onPress:async()=>{g(true);try{await _scenariosStore.scenariosStore.shareWithTeam(e.id);s.showToast("Shared with your team","success")}catch(o){s.showToast(o instanceof Error?o.message:"Could not share it.","error")}finally{g(false)}}}])}:void 0,onRemoveFromTeam:_scenariosStore.scenariosStore.canRemoveFromTeam(e)?()=>{_reactNative.Alert.alert("Remove from team?",`\u201C${(0,_ScenarioRow.displayName)(e.name)}\u201D leaves Scenarios for everyone on your team.`,[{text:"Cancel",style:"cancel"},{text:"Remove",style:"destructive",onPress:async()=>{g(true);try{await _scenariosStore.scenariosStore.removeFromTeam(e.id);s.showToast("Removed from your team","success");i()}catch(o){s.showToast(o instanceof Error?o.message:"Could not remove it.","error")}finally{g(false)}}}])}:void 0,onBack:i})}if(d.name==="folder"){const e=_scenariosStore.scenariosStore.find(d.scenarioId);if(!e)return(0,_jsxRuntime.jsx)(ScenariosList,{...w()});return(0,_jsxRuntime.jsx)(_FolderPickerScreen.FolderPickerScreen,{scenario:e,folders:_scenariosStore.scenariosStore.folders(),onPick:async o=>{try{await _scenariosStore.scenariosStore.setFolder(e.id,o);s.showToast(o?`Filed under ${o}`:"Removed from its folder","success")}catch(n){s.showToast(n instanceof Error?n.message:String(n),"error")}c({name:"detail",scenarioId:e.id})},onBack:()=>c({name:"detail",scenarioId:e.id})})}function w(){return{state:a,insets:S,host:s,busy:y,savedLimit:p,currentBuild:r,onOpen:e=>c({name:"detail",scenarioId:e}),onRun:e=>c({name:"preview",scenarioId:e,mode:"run"}),onReviewDraft:e=>c({name:"preview",scenarioId:e,mode:"review-draft"}),onDeactivate:m,onRerun:()=>{const e=a.active;if(e)void b(e.scenarioId,e.varValues)},onStartRecording:x,canRecord:_recorderStore.recorderStore.canRecord(),recordUnavailable:l.status==="unavailable"?l.reason:null}}return(0,_jsxRuntime.jsx)(ScenariosList,{...w()})}function emptyText(r,a,h){if(r)return"No matches.";if(a===void 0)return"Nothing unfiled here.";if(a!==null)return`Nothing in ${a}.`;return h}function ScenariosList({state:r,insets:a,host:h,busy:l,savedLimit:s,currentBuild:S,onOpen:p,onRun:d,onReviewDraft:c,onDeactivate:y,onRerun:u,onStartRecording:f,canRecord:g,recordUnavailable:i}){const[v,b]=(0,_react.useState)(null);const m=h.search.trim().toLowerCase();const x=t=>!m||t.name.toLowerCase().includes(m)||(t.folder??"").toLowerCase().includes(m)||(t.tags??[]).some(_=>_.toLowerCase().includes(m))||(t.author??"").toLowerCase().includes(m);const D=new Set(r.code.map(t=>t.id));const w=r.team.filter(t=>!D.has(t.id));const e=new Set(w.map(t=>t.id));const o={code:r.code.filter(x),team:w.filter(x),device:r.device.filter(t=>!e.has(t.id)).filter(x),drafts:r.drafts.filter(x)};const n=_scenariosStore.scenariosStore.folders();const R=[...o.code,...o.team,...o.device,...o.drafts];const T=t=>(0,_ScenarioFolderBar.folderMatches)(t,v);const I=o.code.filter(T);const A=o.team.filter(T);const C=o.device.filter(T);const k=o.drafts.filter(T);const P=r.device.length>=s;const q=_scenariosStore.scenariosStore.isLongRunning();const j=v===null&&n.length>0;const F=(0,_react.useCallback)(()=>({shipped:r.code,saved:r.device,drafts:r.drafts,active:r.active??null}),[r.code,r.device,r.drafts,r.active]);const B=(0,_react.useMemo)(()=>(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:F,buttonStyle:styles.headerButton,testID:"scenarios-copy-library",accessibilityLabel:"Copy the scenario library as JSON"}),[F]);(0,_core.useToolHeader)({actions:B},[B]);return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.fill,children:[n.length?(0,_jsxRuntime.jsx)(_ScenarioFolderBar.ScenarioFolderBar,{folders:n,active:v,onChange:b,scenarios:R}):null,(0,_jsxRuntime.jsxs)(_reactNative.ScrollView,{style:styles.fill,contentContainerStyle:[styles.content,{paddingBottom:a.bottom}],keyboardShouldPersistTaps:"handled",testID:"scenarios-scroll",children:[g?(0,_jsxRuntime.jsxs)(_reactNative.TouchableOpacity,{style:styles.record,onPress:f,disabled:l,activeOpacity:.75,accessibilityRole:"button",accessibilityLabel:"Record what you do next as a macro",testID:"scenarios-record",children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.recordDot}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.recordText,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.recordTitle,children:"Record a macro"}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.recordHint,children:"Do the flow once. Buoy writes down the steps."})]})]}):null,i?(0,_jsxRuntime.jsx)(_primitives.Footnote,{tone:"warning",testID:"scenarios-record-unavailable",children:i}):null,r.lastDeactivateReport?(0,_jsxRuntime.jsx)(_DeactivateReportCard.DeactivateReportCard,{report:r.lastDeactivateReport,onDismiss:()=>_scenariosStore.scenariosStore.dismissDeactivateReport()}):null,r.active?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Zap,color:_scnTheme.scn.color.sim,testID:"scenarios-active-label",children:"Active now"}),(0,_jsxRuntime.jsx)(_ActiveCard.ActiveCard,{active:r.active,build:S,busy:l,onDeactivate:y,onRerun:u,onCopied:()=>h.showToast("Repro info copied \u2014 paste into the ticket","success")}),q?(0,_jsxRuntime.jsx)(_primitives.Footnote,{tone:"warning",testID:"scenarios-long-running",children:"This scenario has been active for a while. If you have moved on to something else, deactivate it before you test anything real."}):null]}):null,k.length?(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.FileText,color:_scnTheme.scn.color.sim,count:k.length,testID:"scenarios-drafts-label",children:"Awaiting review"}),k.map(t=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:t,currentBuild:S,disabled:l,showFolder:j,onOpen:()=>c(t.id),onRun:()=>c(t.id),testID:`scenario-draft-${t.id}`},t.id)),(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:"A scenario sent from chat or the desktop never runs on its own. You review what it will do, then accept it."})]}):null,(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Box,count:I.length,testID:"scenarios-code-label",children:"Shipped with this app"}),I.length?I.map(t=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:t,currentBuild:S,disabled:l,showFolder:j,onOpen:()=>p(t.id),onRun:()=>d(t.id)},t.id)):(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.empty,testID:"scenarios-code-empty",children:emptyText(m,v,"None yet \u2014 your developers can ship a QA menu with the app using defineScenario().")})}),(0,_jsxRuntime.jsx)(TeamSection,{info:r.teamInfo,scenarios:A,query:m,folder:v,currentBuild:S,busy:l,showFolder:j,onOpen:p,onRun:d}),(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Smartphone,color:_scnTheme.scn.color.info,count:C.length,testID:"scenarios-device-label",children:"Saved on this device"}),C.length?C.map(t=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:t,currentBuild:S,disabled:l,showFolder:j,onOpen:()=>p(t.id),onRun:()=>d(t.id)},t.id)):(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.empty,testID:"scenarios-device-empty",children:emptyText(m,v,"Nothing saved here yet. Ask your AI assistant to save the state you just set up as a scenario.")})}),P?(0,_jsxRuntime.jsxs)(_primitives.Footnote,{tone:"warning",testID:"scenarios-limit",children:["Your plan keeps ",s," saved scenario",s===1?"":"s"," on a device. Scenarios shipped in code don't count toward it."]}):null,(0,_jsxRuntime.jsx)(_primitives.Footnote,{children:n.length?"Scenarios only run in development builds. Tap a row to see exactly what it will do before you run it.":"Scenarios only run in development builds. Open one and tap its Folder row to group it by flow \u2014 Checkout, Login \u2014 and a folder bar appears up here."})]})]})}function TeamSection({info:r,scenarios:a,query:h,folder:l,currentBuild:s,busy:S,showFolder:p,onOpen:d,onRun:c}){const y=a.length>0||r.status==="ready"||r.status==="error"||r.status==="off"&&r.reason!=="no-team";if(!y)return null;const u=r.status==="loading"?"Checking buoy.gg\u2026":r.status==="error"||r.status==="off"?r.message:null;return(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_primitives.SectionLabel,{icon:_sharedUi.Users,color:_scnTheme.scn.color.accent,count:a.length,testID:"scenarios-team-label",children:"Shared with your team"}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.teamBar,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:[styles.teamStatus,(r.status==="error"||r.status==="off")&&styles.teamStatusWarn],numberOfLines:3,testID:"scenarios-team-status",children:u??"Everyone on your team sees these in this app."}),!_scenariosStore.scenariosStore.isMirror()&&(r.status!=="off"||r.reason!=="no-app")?(0,_jsxRuntime.jsxs)(_reactNative.TouchableOpacity,{style:styles.teamRefresh,onPress:()=>void _scenariosStore.scenariosStore.refreshTeam({force:true}),disabled:r.status==="loading",accessibilityRole:"button",accessibilityLabel:"Refresh the team's scenarios",testID:"scenarios-team-refresh",children:[(0,_jsxRuntime.jsx)(_sharedUi.RefreshCw,{size:12,color:_scnTheme.scn.color.textSecondary}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.teamRefreshText,children:"Refresh"})]}):null]}),a.length?a.map(f=>(0,_jsxRuntime.jsx)(_ScenarioRow.ScenarioRow,{scenario:f,currentBuild:s,disabled:S,showFolder:p,onOpen:()=>d(f.id),onRun:()=>c(f.id),testID:`scenario-team-${f.id}`},f.id)):r.status==="ready"?(0,_jsxRuntime.jsx)(_primitives.Panel,{children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.empty,testID:"scenarios-team-empty",children:emptyText(h,l,"Nothing shared yet. Open a scenario saved on this device and tap Share with team.")})}):null]})}const styles=_reactNative.StyleSheet.create({teamBar:{flexDirection:"row",alignItems:"center",gap:8,paddingHorizontal:_scnTheme.scn.textInset,marginBottom:6},teamStatus:{flex:1,color:_scnTheme.scn.color.textTertiary,fontSize:_scnTheme.scn.font.footnote},teamStatusWarn:{color:_scnTheme.scn.color.sim},teamRefresh:{flexDirection:"row",alignItems:"center",gap:4,paddingVertical:4,paddingHorizontal:8,borderRadius:_scnTheme.scn.radius.pill,borderWidth:1,borderColor:_scnTheme.scn.color.border,backgroundColor:_scnTheme.scn.color.chip},teamRefreshText:{color:_scnTheme.scn.color.textSecondary,fontSize:_scnTheme.scn.font.control},fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:24},record:{flexDirection:"row",alignItems:"center",gap:10,marginHorizontal:_scnTheme.scn.gutter,marginTop:8,marginBottom:4,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:12,borderRadius:_scnTheme.scn.radius.card,borderWidth:1,borderColor:_scnTheme.scn.color.simBorder,backgroundColor:_scnTheme.scn.color.simSoft},recordDot:{width:10,height:10,borderRadius:5,backgroundColor:_scnTheme.scn.color.sim},recordText:{flex:1,minWidth:0},recordTitle:{color:_scnTheme.scn.color.sim,fontSize:_scnTheme.scn.font.row,fontWeight:"700"},recordHint:{color:_scnTheme.scn.color.textSecondary,fontSize:_scnTheme.scn.font.micro,marginTop:2},headerButton:{width:32,height:32,borderRadius:8,backgroundColor:_scnTheme.scn.color.chip,borderWidth:1,borderColor:_scnTheme.scn.color.border,alignItems:"center",justifyContent:"center"},empty:{color:_scnTheme.scn.color.textTertiary,fontSize:_scnTheme.scn.font.control,lineHeight:18,paddingHorizontal:_scnTheme.scn.textInset,paddingVertical:14}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.MAX_DEVICE_SCENARIOS=void 0;exports.installedToolIds=installedToolIds;exports.replaceScenariosFromSnapshot=replaceScenariosFromSnapshot;exports.scenariosStore=void 0;exports.setRemoteScenariosHandler=setRemoteScenariosHandler;var _sharedUi=require("@buoy-gg/shared-ui");var _engine=require("../engine");var _catalog=require("../catalog");var _defineScenario=require("../defineScenario");var _folders=require("../folders");var _interpolate=require("../interpolate");const LIBRARY_KEY=_sharedUi.devToolsStorageKeys.scenarios.library();const ACTIVE_KEY=_sharedUi.devToolsStorageKeys.scenarios.active();const MAX_DEVICE_SCENARIOS=exports.MAX_DEVICE_SCENARIOS=100;let remoteHandler=null;function setRemoteScenariosHandler(d){remoteHandler=d}function isRunnableInThisBuild(){return!(typeof __DEV__!=="undefined"&&!__DEV__)}class ScenariosStore{state={code:[],device:[],drafts:[],active:null,runState:null,lastDeactivateReport:null,hasLoaded:false,lastVarValues:{}};listeners=new Set;loadPromise=null;expiryTimer=null;runController=null;getState=()=>this.state;subscribe=t=>{this.listeners.add(t);void this.ensureLoaded();return()=>{this.listeners.delete(t)}};setState(t){this.state={...this.state,...t};this.listeners.forEach(e=>{try{e()}catch{}})}allScenarios(){const t=new Set(this.state.code.map(e=>e.id));return[...this.state.code,...this.state.device.filter(e=>!t.has(e.id))]}find(t){return this.allScenarios().find(e=>e.id===t)??this.state.drafts.find(e=>e.id===t)}folders(){return(0,_folders.listFolders)([...this.state.code,...this.state.device,...this.state.drafts])}registerCodeScenarios(t){const e=new Map(this.state.code.map(r=>[r.id,r]));for(const r of t)e.set(r.id,{...r,source:"code"});this.setState({code:[...e.values()]})}ensureLoaded(){if(remoteHandler)return Promise.resolve();if(!this.loadPromise){this.loadPromise=this.load()}return this.loadPromise}async load(){let t=[];let e=[];let r={};try{const a=await _sharedUi.persistentStorage.getItem(LIBRARY_KEY);if(a){const i=JSON.parse(a);t=Array.isArray(i.device)?i.device:[];e=Array.isArray(i.drafts)?i.drafts:[];r=i.lastVarValues&&typeof i.lastVarValues==="object"?i.lastVarValues:{}}}catch{}let s=null;try{const a=await _sharedUi.persistentStorage.getItem(ACTIVE_KEY);if(a)s=JSON.parse(a)}catch{s=null}this.setState({device:t,drafts:e,lastVarValues:r,active:s,hasLoaded:true});if(s)await this.reconcileActive()}async persistLibrary(){if(remoteHandler)return;const t={version:1,device:this.state.device,drafts:this.state.drafts,lastVarValues:this.state.lastVarValues};try{await _sharedUi.persistentStorage.setItem(LIBRARY_KEY,JSON.stringify(t))}catch{}}async persistActive(){if(remoteHandler)return;try{if(this.state.active){await _sharedUi.persistentStorage.setItem(ACTIVE_KEY,JSON.stringify(this.state.active))}else{await _sharedUi.persistentStorage.removeItem(ACTIVE_KEY)}}catch{}}async reconcileActive(){const t=this.state.active;if(!t)return;const e=t.effects.filter(r=>r.kind==="override-rule");if(!e.length)return;if(!(0,_sharedUi.listToolIds)().includes("network"))return;try{const r=await(0,_sharedUi.dispatchToolAction)("network","listOverrideRules",{});const s=new Set((r?.rules??[]).map(i=>i.id));const a=t.effects.filter(i=>i.kind!=="override-rule"||s.has(i.ruleId));if(a.length===t.effects.length)return;if(!a.length){this.setState({active:null})}else{this.setState({active:{...t,effects:a}})}await this.persistActive()}catch{}}checkBeforeRun(t,e,r=[]){const s=this.state.active;const a=[...r];if(!isRunnableInThisBuild()){a.push("Scenarios cannot run in a production build.")}const i=(0,_interpolate.resolveVarValues)(t.vars,e);return(0,_engine.preflight)(t,i,{availableTools:(0,_sharedUi.listToolIds)(),availableActions:(0,_sharedUi.listToolActions)(),activeScenario:s?{scenarioId:s.scenarioId,name:s.name,conflictKeys:this.activeConflictKeys()}:null,extraErrors:a})}holdObservers(t){const e=[];const r=(0,_sharedUi.getToolAdapters)();for(const s of t){const a=r[s]?.subscribe;if(!a)continue;try{e.push(a(()=>{}))}catch{}}return e}activeConflictKeys(){const t=this.state.active;if(!t)return[];const e=this.find(t.scenarioId);if(!e)return[];const r=new Set;for(const s of e.steps){for(const a of(0,_catalog.stepConflictKeys)(s,(0,_interpolate.interpolate)(s.params,t.varValues))){r.add(a)}}return[...r]}async run(t,e={},r={}){(0,_sharedUi.assertAccountAccess)();if(remoteHandler){return await remoteHandler("run",{id:t,params:e})}await this.ensureLoaded();(0,_sharedUi.assertAccountAccess)();const s=this.find(t);if(!s)throw new Error(`No scenario "${t}"`);if(s.source==="draft"){throw new Error("Drafts must be accepted before they can run.")}if(!isRunnableInThisBuild()){throw new Error("Scenarios cannot run in a production build.")}if(!await(0,_sharedUi.waitForToolRegistry)()){throw new Error("Buoy tools are still starting up \u2014 try again in a moment.")}const a=(0,_interpolate.resolveVarValues)(s.vars,e);if(this.state.active&&this.state.active.scenarioId!==t){await this.deactivate()}else if(this.state.active){const o=this.state.active;await(0,_engine.revertEffects)(o.effects.filter(l=>l.kind==="override-rule"),_sharedUi.dispatchToolAction);this.setState({active:{...o,effects:o.effects.filter(l=>l.kind!=="override-rule")}})}(0,_sharedUi.assertAccountAccess)();this.setState({runState:{scenarioId:t,name:(0,_interpolate.interpolateString)(s.name,a),steps:[],current:0,done:false}});const i=this.holdObservers(["route-events"]);let c;this.runController?.abort();const n=new AbortController;this.runController=n;try{c=await(0,_engine.runScenario)(s,a,_sharedUi.dispatchToolAction,{signal:n.signal,onStep:o=>{const l=this.state.runState;if(l)this.setState({runState:{...l,current:o}})}})}finally{if(this.runController===n)this.runController=null;i.forEach(o=>{try{o()}catch{}})}this.setState({runState:{scenarioId:t,name:(0,_interpolate.interpolateString)(s.name,a),steps:c.steps,current:-1,done:true,report:c}});if(c.ok&&(0,_sharedUi.hasAccountAccess)()){const o={scenarioId:t,name:(0,_interpolate.interpolateString)(s.name,a),version:s.version,source:s.source,runAt:Date.now(),varValues:a,effects:c.effects,expiresAt:r.expiresInMs?Date.now()+r.expiresInMs:void 0};this.setState({active:o,lastDeactivateReport:null,lastVarValues:{...this.state.lastVarValues,[t]:a}});this.bumpUsage(t);this.armExpiry();await this.persistActive();await this.persistLibrary()}return c}async undoPartialRun(){const t=this.state.runState?.report;if(!t){return{removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]}}const e=await(0,_engine.revertEffects)(t.effects,_sharedUi.dispatchToolAction);const r={...e,undoStepsRan:0};this.setState({runState:null,lastDeactivateReport:r});return r}clearRunState(){this.setState({runState:null})}bumpUsage(t){const e=r=>r.map(s=>s.id===t?{...s,usage:{runs:(s.usage?.runs??0)+1,lastRunAt:Date.now()}}:s);this.setState({code:e(this.state.code),device:e(this.state.device)})}async deactivate(){if(remoteHandler){return await remoteHandler("deactivate",{})}const t=this.state.active;if(!t){const n={removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]};return n}const e=await(0,_engine.revertEffects)(t.effects,_sharedUi.dispatchToolAction);const r=[...e.errors];let s=0;const a=this.find(t.scenarioId);const i=a?.undoSteps??[];if(i.length){const n=await(0,_engine.runScenario)({...a,steps:i},t.varValues,_sharedUi.dispatchToolAction);s=n.steps.filter(o=>o.ok).length;for(const o of n.steps){if(!o.ok&&o.error)r.push(o.error)}}const c={removedRules:e.removedRules,stoppedImpersonation:e.stoppedImpersonation,clearedTransients:e.clearedTransients,unrestoredStorageKeys:s?[]:e.unrestoredStorageKeys,unrestoredState:s?[]:e.unrestoredState,undoStepsRan:s,errors:r};this.clearExpiry();this.setState({active:null,lastDeactivateReport:c});await this.persistActive();return c}async disposeAccountSession(t){this.runController?.abort();this.runController=null;if((0,_sharedUi.hasAccountAccess)())return;this.clearExpiry();const e=this.state.active;if(!e||remoteHandler)return;const r=await(0,_engine.revertEffects)(e.effects.filter(s=>s.kind==="override-rule"),t);this.setState({active:null});await this.persistActive();if(r.errors.length)throw new Error(r.errors.join("; "))}dismissDeactivateReport(){this.setState({lastDeactivateReport:null})}armExpiry(){this.clearExpiry();const t=this.state.active?.expiresAt;if(!t)return;const e=Math.max(0,t-Date.now());this.expiryTimer=setTimeout(()=>{void this.deactivate()},e)}clearExpiry(){if(this.expiryTimer){clearTimeout(this.expiryTimer);this.expiryTimer=null}}isLongRunning(t=8*60*60*1e3){const e=this.state.active;return!!e&&Date.now()-e.runAt>t}async saveScenario(t,e={}){await this.ensureLoaded();const r=Date.now();const s=e.asDraft??false;const a=s?this.state.drafts:this.state.device;const i=a.findIndex(o=>o.id===t.id);const c={...t,steps:(0,_defineScenario.withStepIds)(t.steps,"s"),undoSteps:t.undoSteps?(0,_defineScenario.withStepIds)(t.undoSteps,"u"):void 0,folder:(0,_folders.resolveFolder)(t.folder,this.folders()),source:s?"draft":"device",version:i>=0?(a[i].version??1)+1:t.version||1,createdAt:i>=0?a[i].createdAt:r,updatedAt:r,usage:i>=0?a[i].usage:{runs:0}};if(!s&&i<0&&this.state.device.length>=MAX_DEVICE_SCENARIOS){throw new Error(`This device already holds ${MAX_DEVICE_SCENARIOS} scenarios \u2014 delete one first.`)}const n=[...a];if(i>=0)n[i]=c;else n.unshift(c);this.setState(s?{drafts:n}:{device:n});await this.persistLibrary();return c}async acceptDraft(t){await this.ensureLoaded();const e=this.state.drafts.find(s=>s.id===t);if(!e)throw new Error(`No draft "${t}"`);const r={...e,source:"device",unsavedToRepo:true,updatedAt:Date.now()};this.setState({drafts:this.state.drafts.filter(s=>s.id!==t),device:[r,...this.state.device]});await this.persistLibrary();return r}async discardDraft(t){await this.ensureLoaded();this.setState({drafts:this.state.drafts.filter(e=>e.id!==t)});await this.persistLibrary()}async setFolder(t,e){await this.ensureLoaded();if(remoteHandler){const n=await remoteHandler("setFolder",{id:t,folder:e});return n.scenario}if(this.state.code.some(n=>n.id===t)){throw new Error("Scenarios shipped with the app are filed in code \u2014 change `folder` in defineScenario().")}const r=(0,_folders.resolveFolder)(e,this.folders());let s;const a=n=>n.map(o=>{if(o.id!==t)return o;s={...o,folder:r,updatedAt:Date.now()};return s});const i=a(this.state.device);const c=s?this.state.drafts:a(this.state.drafts);if(!s)throw new Error(`No scenario "${t}"`);this.setState({device:i,drafts:c});await this.persistLibrary();return s}async deleteScenario(t){await this.ensureLoaded();if(this.state.active?.scenarioId===t)await this.deactivate();this.setState({device:this.state.device.filter(e=>e.id!==t)});await this.persistLibrary()}rememberVarValues(t,e){this.setState({lastVarValues:{...this.state.lastVarValues,[t]:e}});void this.persistLibrary()}lastValuesFor(t){return this.state.lastVarValues[t]??{}}replaceFromSnapshot(t){this.setState({code:t.code??[],device:t.device??[],drafts:t.drafts??[],active:t.active??null,runState:t.runState??null,hasLoaded:true})}}const scenariosStore=exports.scenariosStore=new ScenariosStore;function replaceScenariosFromSnapshot(d){scenariosStore.replaceFromSnapshot(d)}function installedToolIds(){return Object.keys((0,_sharedUi.getToolAdapters)())}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.MAX_DEVICE_SCENARIOS=void 0;exports.installedToolIds=installedToolIds;exports.replaceScenariosFromSnapshot=replaceScenariosFromSnapshot;exports.scenariosStore=void 0;exports.setRemoteScenariosHandler=setRemoteScenariosHandler;exports.setTeamClientDepsForTests=setTeamClientDepsForTests;exports.teamScenarioFromRow=teamScenarioFromRow;var _sharedUi=require("@buoy-gg/shared-ui");var _engine=require("../engine");var _catalog=require("../catalog");var _defineScenario=require("../defineScenario");var _folders=require("../folders");var _interpolate=require("../interpolate");var _teamClient=require("../team/teamClient");const LIBRARY_KEY=_sharedUi.devToolsStorageKeys.scenarios.library();const ACTIVE_KEY=_sharedUi.devToolsStorageKeys.scenarios.active();const TEAM_KEY=_sharedUi.devToolsStorageKeys.scenarios.team();const TEAM_PULL_FRESH_MS=3e4;let teamDeps=null;function setTeamClientDepsForTests(l){teamDeps=l}function teamClient(){return teamDeps??(0,_teamClient.defaultTeamClientDeps)()}function teamScenarioFromRow(l){return{...l.scenario,id:l.id,source:"team",author:l.sharedBy.label,version:l.version,createdAt:l.scenario.createdAt??l.sharedAt,updatedAt:l.updatedAt,usage:{runs:0},team:{sharedBy:l.sharedBy,sharedAt:l.sharedAt,updatedAt:l.updatedAt,app:l.app}}}const MAX_DEVICE_SCENARIOS=exports.MAX_DEVICE_SCENARIOS=100;let remoteHandler=null;function setRemoteScenariosHandler(l){remoteHandler=l}function isRunnableInThisBuild(){return!(typeof __DEV__!=="undefined"&&!__DEV__)}class ScenariosStore{state={code:[],device:[],drafts:[],team:[],teamInfo:{status:"idle",you:null,admin:false},active:null,runState:null,lastDeactivateReport:null,hasLoaded:false,lastVarValues:{}};listeners=new Set;loadPromise=null;expiryTimer=null;runController=null;getState=()=>this.state;subscribe=e=>{this.listeners.add(e);void this.ensureLoaded();return()=>{this.listeners.delete(e)}};setState(e){this.state={...this.state,...e};this.listeners.forEach(t=>{try{t()}catch{}})}allScenarios(){const e=new Set(this.state.code.map(s=>s.id));const t=this.state.team.filter(s=>!e.has(s.id));const a=new Set([...e,...t.map(s=>s.id)]);return[...this.state.code,...t,...this.state.device.filter(s=>!a.has(s.id))]}find(e){return this.allScenarios().find(t=>t.id===e)??this.state.drafts.find(t=>t.id===e)}folders(){return(0,_folders.listFolders)([...this.state.code,...this.state.team,...this.state.device,...this.state.drafts])}registerCodeScenarios(e){const t=new Map(this.state.code.map(a=>[a.id,a]));for(const a of e)t.set(a.id,{...a,source:"code"});this.setState({code:[...t.values()]})}ensureLoaded(){if(remoteHandler)return Promise.resolve();if(!this.loadPromise){this.loadPromise=this.load()}return this.loadPromise}async load(){let e=[];let t=[];let a={};try{const o=await _sharedUi.persistentStorage.getItem(LIBRARY_KEY);if(o){const i=JSON.parse(o);e=Array.isArray(i.device)?i.device:[];t=Array.isArray(i.drafts)?i.drafts:[];a=i.lastVarValues&&typeof i.lastVarValues==="object"?i.lastVarValues:{}}}catch{}let s=null;try{const o=await _sharedUi.persistentStorage.getItem(ACTIVE_KEY);if(o)s=JSON.parse(o)}catch{s=null}let r=[];let n=this.state.teamInfo;try{const o=await _sharedUi.persistentStorage.getItem(TEAM_KEY);const i=o?JSON.parse(o):null;const c=(0,_teamClient.teamAppId)(teamClient());if(i&&Array.isArray(i.rows)&&(!c||i.app===c)){this.teamRows=i.rows;r=i.rows.map(teamScenarioFromRow);n={status:"idle",you:i.you||null,admin:i.admin,pulledAt:i.pulledAt}}}catch{}this.setState({device:e,drafts:t,team:r,teamInfo:n,lastVarValues:a,active:s,hasLoaded:true});if(s)await this.reconcileActive()}async persistLibrary(){if(remoteHandler)return;const e={version:1,device:this.state.device,drafts:this.state.drafts,lastVarValues:this.state.lastVarValues};try{await _sharedUi.persistentStorage.setItem(LIBRARY_KEY,JSON.stringify(e))}catch{}}async persistActive(){if(remoteHandler)return;try{if(this.state.active){await _sharedUi.persistentStorage.setItem(ACTIVE_KEY,JSON.stringify(this.state.active))}else{await _sharedUi.persistentStorage.removeItem(ACTIVE_KEY)}}catch{}}async persistTeam(e,t){if(remoteHandler)return;try{if(!e||!t){await _sharedUi.persistentStorage.removeItem(TEAM_KEY);return}const{you:a,admin:s,pulledAt:r}=this.state.teamInfo;const n={version:1,app:t,you:a??"",admin:s,pulledAt:r??Date.now(),rows:e};await _sharedUi.persistentStorage.setItem(TEAM_KEY,JSON.stringify(n))}catch{}}teamRows=[];teamPull=null;teamCredentialKey=null;credentialKey(e){const t=e.credential();return t?`${t.team}:${t.token}`:""}onAccountChange(){if(remoteHandler)return Promise.resolve();if(this.credentialKey(teamClient())===this.teamCredentialKey)return Promise.resolve();return this.refreshTeam({force:true})}refreshTeam(e={}){if(remoteHandler)return Promise.resolve();const{pulledAt:t,status:a}=this.state.teamInfo;if(!e.force&&a==="ready"&&t&&Date.now()-t<TEAM_PULL_FRESH_MS){return Promise.resolve()}if(!this.teamPull){this.teamPull=this.pullTeam().finally(()=>{this.teamPull=null})}return this.teamPull}async pullTeam(){await this.ensureLoaded();const e=teamClient();this.teamCredentialKey=this.credentialKey(e);this.setState({teamInfo:{...this.state.teamInfo,status:"loading",message:void 0}});const t=await(0,_teamClient.pullTeamScenarios)(e);if(!t.ok){if(t.code==="network"){this.setState({teamInfo:{...this.state.teamInfo,status:"error",message:t.message}});return}this.teamRows=[];this.setState({team:[],teamInfo:{status:"off",reason:t.code,message:t.message,you:null,admin:false}});await this.persistTeam(null,null);return}this.teamRows=t.value.scenarios;this.setState({team:t.value.scenarios.map(teamScenarioFromRow),teamInfo:{status:"ready",you:t.value.you,admin:t.value.admin,pulledAt:Date.now()}});await this.persistTeam(this.teamRows,(0,_teamClient.teamAppId)(e))}async shareWithTeam(e){await this.ensureLoaded();if(remoteHandler)throw new Error("Share from the app on the device.");const t=this.state.device.find(o=>o.id===e);if(!t)throw new Error("Only a scenario saved on this device can be shared.");const a=teamClient();const s=await(0,_teamClient.shareTeamScenario)(t,a);if(!s.ok)throw new Error(s.message);const r=s.value;this.teamRows=[r,...this.teamRows.filter(o=>o.id!==r.id)];const n=teamScenarioFromRow(r);this.setState({team:[n,...this.state.team.filter(o=>o.id!==r.id)],teamInfo:{...this.state.teamInfo,status:"ready",message:void 0,you:this.state.teamInfo.you??r.sharedBy.id,pulledAt:this.state.teamInfo.pulledAt??Date.now()}});await this.persistTeam(this.teamRows,(0,_teamClient.teamAppId)(a));return n}isMirror(){return remoteHandler!==null}canRemoveFromTeam(e){if(remoteHandler||e.source!=="team"||!e.team)return false;const{you:t,admin:a}=this.state.teamInfo;return a||!!t&&e.team.sharedBy.id===t}async removeFromTeam(e){await this.ensureLoaded();if(remoteHandler)throw new Error("Remove it from the app on the device.");if(!this.state.team.some(s=>s.id===e))throw new Error("That scenario is not on the team's list.");const t=teamClient();const a=await(0,_teamClient.removeTeamScenario)(e,t);if(!a.ok)throw new Error(a.message);this.teamRows=this.teamRows.filter(s=>s.id!==e);this.setState({team:this.state.team.filter(s=>s.id!==e)});await this.persistTeam(this.teamRows,(0,_teamClient.teamAppId)(t))}async reconcileActive(){const e=this.state.active;if(!e)return;const t=e.effects.filter(a=>a.kind==="override-rule");if(!t.length)return;if(!(0,_sharedUi.listToolIds)().includes("network"))return;try{const a=await(0,_sharedUi.dispatchToolAction)("network","listOverrideRules",{});const s=new Set((a?.rules??[]).map(n=>n.id));const r=e.effects.filter(n=>n.kind!=="override-rule"||s.has(n.ruleId));if(r.length===e.effects.length)return;if(!r.length){this.setState({active:null})}else{this.setState({active:{...e,effects:r}})}await this.persistActive()}catch{}}checkBeforeRun(e,t,a=[]){const s=this.state.active;const r=[...a];if(!isRunnableInThisBuild()){r.push("Scenarios cannot run in a production build.")}const n=(0,_interpolate.resolveVarValues)(e.vars,t);return(0,_engine.preflight)(e,n,{availableTools:(0,_sharedUi.listToolIds)(),availableActions:(0,_sharedUi.listToolActions)(),activeScenario:s?{scenarioId:s.scenarioId,name:s.name,conflictKeys:this.activeConflictKeys()}:null,extraErrors:r})}holdObservers(e){const t=[];const a=(0,_sharedUi.getToolAdapters)();for(const s of e){const r=a[s]?.subscribe;if(!r)continue;try{t.push(r(()=>{}))}catch{}}return t}activeConflictKeys(){const e=this.state.active;if(!e)return[];const t=this.find(e.scenarioId);if(!t)return[];const a=new Set;for(const s of t.steps){for(const r of(0,_catalog.stepConflictKeys)(s,(0,_interpolate.interpolate)(s.params,e.varValues))){a.add(r)}}return[...a]}async run(e,t={},a={}){(0,_sharedUi.assertAccountAccess)();if(remoteHandler){return await remoteHandler("run",{id:e,params:t})}await this.ensureLoaded();(0,_sharedUi.assertAccountAccess)();const s=this.find(e);if(!s)throw new Error(`No scenario "${e}"`);if(s.source==="draft"){throw new Error("Drafts must be accepted before they can run.")}if(!isRunnableInThisBuild()){throw new Error("Scenarios cannot run in a production build.")}if(!await(0,_sharedUi.waitForToolRegistry)()){throw new Error("Buoy tools are still starting up \u2014 try again in a moment.")}const r=(0,_interpolate.resolveVarValues)(s.vars,t);if(this.state.active&&this.state.active.scenarioId!==e){await this.deactivate()}else if(this.state.active){const c=this.state.active;await(0,_engine.revertEffects)(c.effects.filter(d=>d.kind==="override-rule"),_sharedUi.dispatchToolAction);this.setState({active:{...c,effects:c.effects.filter(d=>d.kind!=="override-rule")}})}(0,_sharedUi.assertAccountAccess)();this.setState({runState:{scenarioId:e,name:(0,_interpolate.interpolateString)(s.name,r),steps:[],current:0,done:false}});const n=this.holdObservers(["route-events"]);let o;this.runController?.abort();const i=new AbortController;this.runController=i;try{o=await(0,_engine.runScenario)(s,r,_sharedUi.dispatchToolAction,{signal:i.signal,onStep:c=>{const d=this.state.runState;if(d)this.setState({runState:{...d,current:c}})}})}finally{if(this.runController===i)this.runController=null;n.forEach(c=>{try{c()}catch{}})}this.setState({runState:{scenarioId:e,name:(0,_interpolate.interpolateString)(s.name,r),steps:o.steps,current:-1,done:true,report:o}});if(o.ok&&(0,_sharedUi.hasAccountAccess)()){const c={scenarioId:e,name:(0,_interpolate.interpolateString)(s.name,r),version:s.version,source:s.source,runAt:Date.now(),varValues:r,effects:o.effects,expiresAt:a.expiresInMs?Date.now()+a.expiresInMs:void 0,undoSteps:s.undoSteps};this.setState({active:c,lastDeactivateReport:null,lastVarValues:{...this.state.lastVarValues,[e]:r}});this.bumpUsage(e);this.armExpiry();await this.persistActive();await this.persistLibrary()}return o}async undoPartialRun(){const e=this.state.runState?.report;if(!e){return{removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]}}const t=await(0,_engine.revertEffects)(e.effects,_sharedUi.dispatchToolAction);const a={...t,undoStepsRan:0};this.setState({runState:null,lastDeactivateReport:a});return a}clearRunState(){this.setState({runState:null})}bumpUsage(e){const t=a=>a.map(s=>s.id===e?{...s,usage:{runs:(s.usage?.runs??0)+1,lastRunAt:Date.now()}}:s);this.setState({code:t(this.state.code),team:t(this.state.team),device:t(this.state.device)})}async deactivate(){if(remoteHandler){return await remoteHandler("deactivate",{})}const e=this.state.active;if(!e){const i={removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]};return i}const t=await(0,_engine.revertEffects)(e.effects,_sharedUi.dispatchToolAction);const a=[...t.errors];let s=0;const r=this.find(e.scenarioId);const n=e.undoSteps??r?.undoSteps??[];if(n.length){const i=r??{id:e.scenarioId,name:e.name,version:e.version,source:e.source,vars:[],steps:[],createdAt:e.runAt,updatedAt:e.runAt,usage:{runs:0}};const c=await(0,_engine.runScenario)({...i,steps:n},e.varValues,_sharedUi.dispatchToolAction);s=c.steps.filter(d=>d.ok).length;for(const d of c.steps){if(!d.ok&&d.error)a.push(d.error)}}const o={removedRules:t.removedRules,stoppedImpersonation:t.stoppedImpersonation,clearedTransients:t.clearedTransients,unrestoredStorageKeys:s?[]:t.unrestoredStorageKeys,unrestoredState:s?[]:t.unrestoredState,undoStepsRan:s,errors:a};this.clearExpiry();this.setState({active:null,lastDeactivateReport:o});await this.persistActive();return o}async disposeAccountSession(e){this.runController?.abort();this.runController=null;if((0,_sharedUi.hasAccountAccess)())return;this.clearExpiry();const t=this.state.active;if(!t||remoteHandler)return;const a=await(0,_engine.revertEffects)(t.effects.filter(s=>s.kind==="override-rule"),e);this.setState({active:null});await this.persistActive();if(a.errors.length)throw new Error(a.errors.join("; "))}dismissDeactivateReport(){this.setState({lastDeactivateReport:null})}armExpiry(){this.clearExpiry();const e=this.state.active?.expiresAt;if(!e)return;const t=Math.max(0,e-Date.now());this.expiryTimer=setTimeout(()=>{void this.deactivate()},t)}clearExpiry(){if(this.expiryTimer){clearTimeout(this.expiryTimer);this.expiryTimer=null}}isLongRunning(e=8*60*60*1e3){const t=this.state.active;return!!t&&Date.now()-t.runAt>e}async saveScenario(e,t={}){await this.ensureLoaded();const a=Date.now();const s=t.asDraft??false;const r=s?this.state.drafts:this.state.device;const n=r.findIndex(c=>c.id===e.id);const o={...e,steps:(0,_defineScenario.withStepIds)(e.steps,"s"),undoSteps:e.undoSteps?(0,_defineScenario.withStepIds)(e.undoSteps,"u"):void 0,folder:(0,_folders.resolveFolder)(e.folder,this.folders()),source:s?"draft":"device",version:n>=0?(r[n].version??1)+1:e.version||1,createdAt:n>=0?r[n].createdAt:a,updatedAt:a,usage:n>=0?r[n].usage:{runs:0}};if(!s&&n<0&&this.state.device.length>=MAX_DEVICE_SCENARIOS){throw new Error(`This device already holds ${MAX_DEVICE_SCENARIOS} scenarios \u2014 delete one first.`)}const i=[...r];if(n>=0)i[n]=o;else i.unshift(o);this.setState(s?{drafts:i}:{device:i});await this.persistLibrary();return o}async acceptDraft(e){await this.ensureLoaded();const t=this.state.drafts.find(s=>s.id===e);if(!t)throw new Error(`No draft "${e}"`);const a={...t,source:"device",unsavedToRepo:true,updatedAt:Date.now()};this.setState({drafts:this.state.drafts.filter(s=>s.id!==e),device:[a,...this.state.device]});await this.persistLibrary();return a}async discardDraft(e){await this.ensureLoaded();this.setState({drafts:this.state.drafts.filter(t=>t.id!==e)});await this.persistLibrary()}async setFolder(e,t){await this.ensureLoaded();if(remoteHandler){const i=await remoteHandler("setFolder",{id:e,folder:t});return i.scenario}if(this.state.code.some(i=>i.id===e)){throw new Error("Scenarios shipped with the app are filed in code \u2014 change `folder` in defineScenario().")}if(this.state.team.some(i=>i.id===e)&&!this.state.device.some(i=>i.id===e)){throw new Error("A team scenario keeps the folder its sharer gave it. Make an editable copy to file it here.")}const a=(0,_folders.resolveFolder)(t,this.folders());let s;const r=i=>i.map(c=>{if(c.id!==e)return c;s={...c,folder:a,updatedAt:Date.now()};return s});const n=r(this.state.device);const o=s?this.state.drafts:r(this.state.drafts);if(!s)throw new Error(`No scenario "${e}"`);this.setState({device:n,drafts:o});await this.persistLibrary();return s}async deleteScenario(e){await this.ensureLoaded();if(this.state.active?.scenarioId===e)await this.deactivate();this.setState({device:this.state.device.filter(t=>t.id!==e)});await this.persistLibrary()}rememberVarValues(e,t){this.setState({lastVarValues:{...this.state.lastVarValues,[e]:t}});void this.persistLibrary()}lastValuesFor(e){return this.state.lastVarValues[e]??{}}replaceFromSnapshot(e){this.setState({code:e.code??[],device:e.device??[],drafts:e.drafts??[],team:e.team??[],teamInfo:e.teamInfo??{status:"idle",you:null,admin:false},active:e.active??null,runState:e.runState??null,hasLoaded:true})}}const scenariosStore=exports.scenariosStore=new ScenariosStore;function replaceScenariosFromSnapshot(l){scenariosStore.replaceFromSnapshot(l)}function installedToolIds(){return Object.keys((0,_sharedUi.getToolAdapters)())}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.scenariosSyncAdapter=void 0;var _catalog=require("../catalog");var _interpolate=require("../interpolate");var _scenariosStore=require("../store/scenariosStore");const MAX_STEP_BYTES=64*1024;const MAX_SCENARIO_BYTES=256*1024;function readString(e,r){const t=e?.[r];return typeof t==="string"&&t.trim()?t.trim():void 0}function readValues(e){const r=e?.params;if(!r||typeof r!=="object")return{};const t={};for(const[o,s]of Object.entries(r)){if(typeof s==="string"||typeof s==="number"||typeof s==="boolean"){t[o]=s}}return t}const WIRE_STEP_LIMIT=8*1024;function summarize(e){return{id:e.id,name:e.name,description:e.description,version:e.version,source:e.source,author:e.author,folder:e.folder,tags:e.tags,vars:e.vars,stepCount:e.steps.length,expectedOutcome:e.expectedOutcome,authoredAgainst:e.authoredAgainst,verifiedAt:e.verifiedAt,unsavedToRepo:e.unsavedToRepo,usage:e.usage,createdAt:e.createdAt,updatedAt:e.updatedAt,steps:e.steps.map(r=>{const t=JSON.stringify(r.params??{}).length;return t>WIRE_STEP_LIMIT?{...r,params:void 0,paramsOmitted:true}:r}),stepLines:e.steps.map(r=>({label:(0,_catalog.describeStep)(r),durability:(0,_catalog.stepDurability)(r)}))}}function validateIncoming(e){const r=[];if(!e.id||typeof e.id!=="string"){r.push("A scenario needs a string `id`.")}if(!e.name||typeof e.name!=="string"){r.push("A scenario needs a `name`.")}if(!Array.isArray(e.steps)||!e.steps.length){r.push("A scenario needs at least one step.")}else{e.steps.forEach((o,s)=>{if(!o||typeof o!=="object"){r.push(`Step ${s+1} is not an object.`);return}if(!o.tool||!o.action){r.push(`Step ${s+1} needs both \`tool\` and \`action\`.`)}const n=JSON.stringify(o.params??{}).length;if(n>MAX_STEP_BYTES){r.push(`Step ${s+1} is ${Math.round(n/1024)}KB \u2014 over the 64KB limit. Use a smaller response body.`)}})}if(e.vars&&!Array.isArray(e.vars)){r.push("`vars` must be an array.")}const t=JSON.stringify(e).length;if(t>MAX_SCENARIO_BYTES){r.push(`The scenario is ${Math.round(t/1024)}KB \u2014 over the 256KB limit.`)}return r}function snapshot(){const e=_scenariosStore.scenariosStore.getState();return{code:e.code.map(summarize),device:e.device.map(summarize),drafts:e.drafts.map(summarize),active:e.active,runState:e.runState,hasLoaded:e.hasLoaded,folders:_scenariosStore.scenariosStore.folders()}}const scenariosSyncAdapter=exports.scenariosSyncAdapter={dispose:e=>_scenariosStore.scenariosStore.disposeAccountSession(e),version:1,getSnapshot:()=>snapshot(),subscribe:e=>_scenariosStore.scenariosStore.subscribe(e),actions:{listScenarios:()=>snapshot(),getScenario:e=>{const r=readString(e,"id");if(!r)throw new Error("getScenario requires an `id`");const t=_scenariosStore.scenariosStore.find(r);if(!t)throw new Error(`No scenario "${r}"`);return{scenario:t}},save:async e=>{const r=e??{};const t=r.scenario;if(!t)return{ok:false,error:"save requires a `scenario`."};const o=validateIncoming(t);if(o.length)return{ok:false,errors:o};const s=Date.now();const n={id:t.id,name:t.name,description:t.description,version:t.version??1,source:"draft",author:t.author??"remote",vars:t.vars??[],steps:t.steps,undoSteps:t.undoSteps,folder:t.folder,tags:t.tags,expectedOutcome:t.expectedOutcome,authoredAgainst:t.authoredAgainst,unsavedToRepo:true,createdAt:s,updatedAt:s,usage:{runs:0}};const
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.scenariosSyncAdapter=void 0;var _catalog=require("../catalog");var _interpolate=require("../interpolate");var _scenariosStore=require("../store/scenariosStore");const MAX_STEP_BYTES=64*1024;const MAX_SCENARIO_BYTES=256*1024;function readString(e,r){const t=e?.[r];return typeof t==="string"&&t.trim()?t.trim():void 0}function readValues(e){const r=e?.params;if(!r||typeof r!=="object")return{};const t={};for(const[o,s]of Object.entries(r)){if(typeof s==="string"||typeof s==="number"||typeof s==="boolean"){t[o]=s}}return t}const WIRE_STEP_LIMIT=8*1024;function summarize(e){return{id:e.id,name:e.name,description:e.description,version:e.version,source:e.source,author:e.author,folder:e.folder,tags:e.tags,vars:e.vars,stepCount:e.steps.length,expectedOutcome:e.expectedOutcome,authoredAgainst:e.authoredAgainst,verifiedAt:e.verifiedAt,unsavedToRepo:e.unsavedToRepo,usage:e.usage,createdAt:e.createdAt,updatedAt:e.updatedAt,team:e.team,steps:e.steps.map(r=>{const t=JSON.stringify(r.params??{}).length;return t>WIRE_STEP_LIMIT?{...r,params:void 0,paramsOmitted:true}:r}),stepLines:e.steps.map(r=>({label:(0,_catalog.describeStep)(r),durability:(0,_catalog.stepDurability)(r)}))}}function validateIncoming(e){const r=[];if(!e.id||typeof e.id!=="string"){r.push("A scenario needs a string `id`.")}if(!e.name||typeof e.name!=="string"){r.push("A scenario needs a `name`.")}if(!Array.isArray(e.steps)||!e.steps.length){r.push("A scenario needs at least one step.")}else{e.steps.forEach((o,s)=>{if(!o||typeof o!=="object"){r.push(`Step ${s+1} is not an object.`);return}if(!o.tool||!o.action){r.push(`Step ${s+1} needs both \`tool\` and \`action\`.`)}const n=JSON.stringify(o.params??{}).length;if(n>MAX_STEP_BYTES){r.push(`Step ${s+1} is ${Math.round(n/1024)}KB \u2014 over the 64KB limit. Use a smaller response body.`)}})}if(e.vars&&!Array.isArray(e.vars)){r.push("`vars` must be an array.")}const t=JSON.stringify(e).length;if(t>MAX_SCENARIO_BYTES){r.push(`The scenario is ${Math.round(t/1024)}KB \u2014 over the 256KB limit.`)}return r}function snapshot(){const e=_scenariosStore.scenariosStore.getState();return{code:e.code.map(summarize),device:e.device.map(summarize),drafts:e.drafts.map(summarize),team:e.team.map(summarize),teamInfo:e.teamInfo,active:e.active,runState:e.runState,hasLoaded:e.hasLoaded,folders:_scenariosStore.scenariosStore.folders()}}const scenariosSyncAdapter=exports.scenariosSyncAdapter={dispose:e=>_scenariosStore.scenariosStore.disposeAccountSession(e),version:1,getSnapshot:()=>snapshot(),subscribe:e=>_scenariosStore.scenariosStore.subscribe(e),actions:{listScenarios:()=>snapshot(),getScenario:e=>{const r=readString(e,"id");if(!r)throw new Error("getScenario requires an `id`");const t=_scenariosStore.scenariosStore.find(r);if(!t)throw new Error(`No scenario "${r}"`);return{scenario:t}},save:async e=>{const r=e??{};const t=r.scenario;if(!t)return{ok:false,error:"save requires a `scenario`."};const o=validateIncoming(t);if(o.length)return{ok:false,errors:o};const s=Date.now();const n={id:t.id,name:t.name,description:t.description,version:t.version??1,source:"draft",author:t.author??"remote",vars:t.vars??[],steps:t.steps,undoSteps:t.undoSteps,folder:t.folder,tags:t.tags,expectedOutcome:t.expectedOutcome,authoredAgainst:t.authoredAgainst,unsavedToRepo:true,createdAt:s,updatedAt:s,usage:{runs:0}};const a=await _scenariosStore.scenariosStore.saveScenario(n,{asDraft:true});return{ok:true,savedAsDraft:true,id:a.id,message:"Saved as a draft. Open Scenarios on the device to review and accept it."}},acceptDraft:async e=>{const r=readString(e,"id");if(!r)throw new Error("acceptDraft requires an `id`");const t=await _scenariosStore.scenariosStore.acceptDraft(r);return{ok:true,id:t.id}},discardDraft:async e=>{const r=readString(e,"id");if(!r)throw new Error("discardDraft requires an `id`");await _scenariosStore.scenariosStore.discardDraft(r);return{ok:true,id:r}},preview:e=>{const r=readString(e,"id");if(!r)throw new Error("preview requires an `id`");const t=_scenariosStore.scenariosStore.find(r);if(!t)throw new Error(`No scenario "${r}"`);const o=(0,_interpolate.resolveVarValues)(t.vars,readValues(e));const s=_scenariosStore.scenariosStore.checkBeforeRun(t,o);return{willDo:t.steps.map(n=>({label:(0,_catalog.describeStep)(n,(0,_interpolate.interpolate)(n.params??{},o)),durability:(0,_catalog.stepDurability)(n)})),values:o,ok:s.ok,errors:s.errors,warnings:s.warnings,conflict:s.conflict,expectedOutcome:t.expectedOutcome}},run:async e=>{const r=readString(e,"id");if(!r)throw new Error("run requires an `id`");const t=readValues(e);const o=_scenariosStore.scenariosStore.find(r);if(!o)throw new Error(`No scenario "${r}"`);const s=_scenariosStore.scenariosStore.checkBeforeRun(o,t);if(!s.ok){return{ok:false,preflightErrors:s.errors,conflict:s.conflict}}return await _scenariosStore.scenariosStore.run(r,t)},deactivate:async()=>{const e=await _scenariosStore.scenariosStore.deactivate();return{ok:true,report:e}},delete:async e=>{const r=readString(e,"id");if(!r)throw new Error("delete requires an `id`");await _scenariosStore.scenariosStore.deleteScenario(r);return{ok:true,id:r}},listFolders:()=>({folders:_scenariosStore.scenariosStore.folders()}),setFolder:async e=>{const r=readString(e,"id");if(!r)throw new Error("setFolder requires an `id`");const t=readString(e,"folder");const o=await _scenariosStore.scenariosStore.setFolder(r,t);return{ok:true,id:o.id,folder:o.folder??null}},getActive:()=>({active:_scenariosStore.scenariosStore.getState().active}),export:e=>{const r=readString(e,"id");if(!r)throw new Error("export requires an `id`");const t=_scenariosStore.scenariosStore.find(r);if(!t)throw new Error(`No scenario "${r}"`);return{json:JSON.stringify(t,null,2)}}}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NO_TEAM_MESSAGE=exports.NO_APP_MESSAGE=void 0;exports.defaultTeamClientDeps=defaultTeamClientDeps;exports.forSharing=forSharing;exports.pullTeamScenarios=pullTeamScenarios;exports.removeTeamScenario=removeTeamScenario;exports.shareTeamScenario=shareTeamScenario;exports.teamAppId=teamAppId;var _sharedUi=require("@buoy-gg/shared-ui");const CALL_TIMEOUT_MS=15e3;const NO_TEAM_MESSAGE=exports.NO_TEAM_MESSAGE="Sign in with a Business team account to share scenarios with your team.";const NO_APP_MESSAGE=exports.NO_APP_MESSAGE="Buoy can't tell which app this is. Pass signIn={{ appId }} to FloatingDevTools to name it.";function defaultTeamClientDeps(){return{credential:_sharedUi.getTeamCredential,appId:_sharedUi.getBuoyAppId,fetch:(o,e)=>fetch(o,e)}}function teamAppId(o){return o.credential()?.appId??o.appId()}async function call(o,e,t){const r=t.credential();if(!r)return{ok:false,code:"no-team",message:NO_TEAM_MESSAGE};const c=r.appId??t.appId();if(!c)return{ok:false,code:"no-app",message:NO_APP_MESSAGE};const n=typeof AbortController!=="undefined"?new AbortController:void 0;const l=setTimeout(()=>n?.abort(),CALL_TIMEOUT_MS);let s;let a;try{const u=await t.fetch(`${r.origin.replace(/\/$/,"")}${o}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:r.token,app:c,...e}),signal:n?.signal});s=u.status;a=await u.json().catch(()=>null)}catch{return{ok:false,code:"network",message:"Buoy could not reach buoy.gg. Check the connection and try again."}}finally{clearTimeout(l)}if(s===200&&a&&typeof a==="object")return{ok:true,value:a};const i=a?.error_description;if(s>=500||s===429){return{ok:false,code:"network",message:typeof i==="string"?i:"buoy.gg is busy. Try again in a minute."}}return{ok:false,code:"refused",message:typeof i==="string"?i:"buoy.gg said no to that."}}function isRow(o){const e=o;return!!e&&typeof e.id==="string"&&!!e.scenario&&typeof e.scenario==="object"&&Array.isArray(e.scenario.steps)}async function pullTeamScenarios(o=defaultTeamClientDeps()){const e=await call("/api/scenarios/team/pull",{},o);if(!e.ok)return e;const t=e.value;return{ok:true,value:{scenarios:Array.isArray(t.scenarios)?t.scenarios.filter(isRow):[],you:typeof t.you==="string"?t.you:"",admin:t.admin===true}}}function forSharing(o){const{usage:e,source:t,verifiedAt:r,team:c,...n}=o;return n}async function shareTeamScenario(o,e=defaultTeamClientDeps()){const t=await call("/api/scenarios/team/share",{scenario:forSharing(o)},e);if(!t.ok)return t;return isRow(t.value.scenario)?{ok:true,value:t.value.scenario}:{ok:false,code:"refused",message:"buoy.gg sent back a scenario this app could not read."}}async function removeTeamScenario(o,e=defaultTeamClientDeps()){const t=await call("/api/scenarios/team/remove",{id:o},e);return t.ok?{ok:true,value:true}:t}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{Fragment as
|
|
1
|
+
"use strict";import{Fragment as A}from"react";import{ScrollView as F,StyleSheet as L,Text as s,TouchableOpacity as O,View as i}from"react-native";import{CheckCircle as P,ChevronRight as j,Copy as H,FileText as R,Layers as k,Play as M,ToolButton as u,Trash as N,Upload as _,Users as $,copyToClipboard as B}from"@buoy-gg/shared-ui";import{useToolHeader as E}from"@buoy-gg/core";import{describeStep as U,isAdvancedStep as W,stepDurability as Y}from"../catalog";import{normalizeFolder as q}from"../folders";import{scn as l,SCN_MONO as S}from"../ui/scnTheme";import{DurabilityChip as J,Footnote as b,Panel as I,PanelSeparator as n,SectionLabel as T}from"../ui/primitives";import{displayName as X}from"./ScenarioRow";import{jsx as e,jsxs as r,Fragment as h}from"react/jsx-runtime";const G={durable:"survives restart",session:"until restart",transient:"until refetch \u26A0","one-shot":"one-shot"};function K(t){if(t==="durable")return"durable";if(t==="transient")return"transient";return"neutral"}export function sourceText(t){if(t.source==="code")return"shipped with this app";if(t.source==="team")return`shared with your team by ${t.team?.sharedBy.label??"a teammate"}`;if(t.source==="draft")return"a draft, waiting for review";return"saved on this device"}export function ScenarioDetailScreen({scenario:t,onRun:p,onDelete:x,onDuplicate:c,onExported:f,onMoveToFolder:g,onShare:v,onRemoveFromTeam:w,teamBusy:y,onBack:V}){E({title:X(t.name),subtitle:sourceText(t),onBack:V,searchable:false},[t.id]);const m=t.source==="device";const D=q(t.folder);return r(F,{style:o.fill,contentContainerStyle:o.content,testID:"scenario-detail-scroll",children:[t.description?e(b,{children:t.description}):null,e(T,{icon:k,count:t.steps.length,children:"Steps"}),e(I,{children:t.steps.map((d,z)=>{const C=Y(d);return r(A,{children:[z>0?e(n,{}):null,r(i,{style:o.step,children:[e(i,{style:o.stepIndex,children:e(s,{style:o.stepIndexText,children:z+1})}),r(i,{style:o.stepBody,children:[e(s,{style:o.stepLabel,children:U(d)}),r(s,{style:o.stepTool,children:[d.tool,".",d.action,W(d)?" \xB7 advanced":""]})]}),e(J,{label:G[C],tone:K(C)})]})]},d.id)})}),t.expectedOutcome?r(h,{children:[e(T,{icon:P,children:"Expected outcome"}),e(I,{children:e(s,{style:o.expected,children:t.expectedOutcome})})]}):null,e(T,{icon:R,children:"Details"}),r(I,{children:[e(a,{label:"Folder",value:D??"No folder",muted:!D,onPress:m?g:void 0,testID:"scenario-detail-folder"}),e(n,{}),e(a,{label:"Source",value:sourceText(t),testID:"scenario-detail-source"}),e(n,{}),e(a,{label:"Version",value:`v${t.version}`}),t.author?r(h,{children:[e(n,{}),e(a,{label:"Author",value:t.author})]}):null,t.authoredAgainst?r(h,{children:[e(n,{}),e(a,{label:"Authored against",value:t.authoredAgainst})]}):null,e(n,{}),e(a,{label:"Runs",value:String(t.usage?.runs??0)}),t.tags?.length?r(h,{children:[e(n,{}),e(a,{label:"Tags",value:t.tags.join(", ")})]}):null]}),r(i,{style:o.footer,children:[e(u,{label:"Run",variant:"primary",onPress:p,icon:e(M,{size:12,color:l.color.accent}),style:o.grow,testID:"scenario-detail-run"}),e(u,{label:"Export",variant:"ghost",onPress:()=>{void B(JSON.stringify(t,null,2));f?.()},icon:e(H,{size:12,color:l.color.textSecondary}),style:o.grow,testID:"scenario-detail-export"})]}),m&&v?e(i,{style:o.footer,children:e(u,{label:y?"Sharing\u2026":"Share with team",variant:"ghost",onPress:v,disabled:y,icon:e(_,{size:12,color:l.color.textSecondary}),style:o.grow,testID:"scenario-detail-share"})}):null,r(i,{style:o.footer,children:[m?e(u,{label:"Delete",variant:"danger",onPress:x,icon:e(N,{size:12,color:l.color.danger}),style:o.grow,testID:"scenario-detail-delete"}):e(u,{label:"Make an editable copy",variant:"ghost",onPress:c,style:o.grow,testID:"scenario-detail-duplicate"}),t.source==="team"&&w?e(u,{label:y?"Removing\u2026":"Remove from team",variant:"danger",onPress:w,disabled:y,icon:e($,{size:12,color:l.color.danger}),style:o.grow,testID:"scenario-detail-remove-team"}):null]}),t.source==="code"?e(b,{children:"Shipped scenarios are read-only on the device \u2014 they change in a pull request, with the code they bend."}):null,t.source==="team"?e(b,{testID:"scenario-detail-team-note",children:"Your whole team sees this scenario in this app. It can't be changed here. Make an editable copy to change it on this device."}):null,m&&v?e(b,{children:"Share with team puts this scenario in Scenarios for everyone on your team, in this app."}):null]})}function a({label:t,value:p,muted:x,onPress:c,testID:f}){const g=r(h,{children:[e(s,{style:o.detailLabel,children:t}),e(s,{style:[o.detailValue,x&&o.detailValueMuted,c&&o.detailValueAction],numberOfLines:2,children:p}),c?e(j,{size:13,color:l.color.textTertiary}):null]});if(!c){return e(i,{style:o.detail,testID:f,children:g})}return e(O,{style:o.detail,onPress:c,activeOpacity:.7,accessibilityRole:"button",accessibilityLabel:`${t}: ${p}. Change it.`,testID:f,children:g})}const o=L.create({fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:32},step:{flexDirection:"row",alignItems:"flex-start",gap:9,paddingHorizontal:l.textInset,paddingVertical:10},stepIndex:{width:18,height:18,borderRadius:l.radius.badge,backgroundColor:l.color.fill,alignItems:"center",justifyContent:"center",marginTop:1},stepIndexText:{color:l.color.textTertiary,fontSize:10,fontWeight:"700",fontFamily:S},stepBody:{flex:1,minWidth:0},stepLabel:{color:l.color.text,fontSize:l.font.control},stepTool:{color:l.color.textTertiary,fontSize:l.font.mono,fontFamily:S,marginTop:2},expected:{color:l.color.textSecondary,fontSize:l.font.control,lineHeight:18,paddingHorizontal:l.textInset,paddingVertical:12},detail:{flexDirection:"row",alignItems:"center",justifyContent:"space-between",gap:8,minHeight:40,paddingHorizontal:l.textInset,paddingVertical:10},detailLabel:{color:l.color.textTertiary,fontSize:l.font.control},detailValue:{flex:1,textAlign:"right",color:l.color.textSecondary,fontSize:l.font.control,fontFamily:S},detailValueMuted:{color:l.color.textTertiary},detailValueAction:{color:l.color.accent},footer:{flexDirection:"row",gap:8,paddingHorizontal:l.gutter,paddingTop:12},grow:{flex:1}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{memo as $}from"react";import{StyleSheet as y,TouchableOpacity as g,View as D}from"react-native";import{CompactRow as S}from"@buoy-gg/shared-ui";import{normalizeFolder as v}from"../folders";import{scn as
|
|
1
|
+
"use strict";import{memo as $}from"react";import{StyleSheet as y,TouchableOpacity as g,View as D}from"react-native";import{CompactRow as S}from"@buoy-gg/shared-ui";import{normalizeFolder as v}from"../folders";import{scn as l}from"../ui/scnTheme";import{DurabilityChip as u,PillLabel as w}from"../ui/primitives";import{jsx as s,Fragment as x,jsxs as I}from"react/jsx-runtime";export function displayName(t){return t.replace(/\{\{\s*(\w+)\s*\}\}/g,"\u27E8$1\u27E9")}function c(t){const e=[`v${t.version}`];if(t.source==="team"&&t.team)e.push(`shared by ${t.team.sharedBy.label}`);else if(t.author)e.push(t.author);const r=t.usage?.runs??0;if(r)e.push(`${r}\xD7`);if(t.unsavedToRepo)e.push("unsaved to repo");if(t.source==="draft")e.push("inert until you accept");return e.join(" \xB7 ")}function T(t){if(t.source==="code")return{label:"Shipped",color:l.color.accent};if(t.source==="draft")return{label:"Draft",color:l.color.sim};if(t.source==="team")return{label:"Team",color:l.color.accent};return{label:"Saved",color:l.color.info}}function R({scenario:t,currentBuild:e,disabled:r,showFolder:m,onOpen:p,onRun:b,testID:d}){const o=d??`scenario-row-${t.id}`;const a=T(t);const i=v(t.folder);const f=t.source==="draft";const n=f?{label:"Review",color:l.color.sim}:{label:"Run",color:l.color.accent};const h=!!t.authoredAgainst&&!!e&&t.authoredAgainst!==e&&t.verifiedAt?.build!==e;return s(S,{statusDotColor:a.color,statusLabel:a.label,statusSublabel:`${t.steps.length} step${t.steps.length===1?"":"s"}`,primaryText:displayName(t.name),primaryTestID:`${o}-name`,secondaryText:c(t),secondaryTestID:`${o}-meta`,secondaryAccessory:I(x,{children:[m&&i?s(u,{label:i,tone:"neutral",testID:`${o}-folder`}):null,h?s(u,{label:`stale \xB7 ${t.authoredAgainst}`,tone:"transient",testID:`${o}-stale`}):null]}),trailingAction:s(g,{onPress:b,disabled:r,style:r?L.disabled:null,hitSlop:A,accessibilityRole:"button",accessibilityLabel:`${n.label} ${displayName(t.name)}`,testID:`${o}-run`,children:s(D,{children:s(w,{label:n.label,color:n.color})})}),showChevron:true,onPress:p,testID:o,accessibilityLabel:`${displayName(t.name)}. ${a.label}, ${t.steps.length} step${t.steps.length===1?"":"s"}, ${c(t)}. Open details.`})}export const ScenarioRow=$(R);const A={top:8,bottom:8,left:8,right:8};const L=y.create({disabled:{opacity:.4}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{useCallback as S,useMemo as U,useState as H}from"react";import{Alert as W,ScrollView as Z,StyleSheet as G,Text as F,TouchableOpacity as X,View as N}from"react-native";import{Box as ee,FileText as oe,Smartphone as te,ToolbarCopyButton as re,Zap as ne,useLimit as ie,useSafeAreaInsets as ae}from"@buoy-gg/shared-ui";import{useToolHeader as se,useToolHost as ce}from"@buoy-gg/core";import{resolveVarValues as de}from"../interpolate";import{scenariosStore as r}from"../store/scenariosStore";import{useRecording as le,useScenariosRunState as ue,useScenariosState as me}from"../hooks/useScenarios";import{recorderStore as I}from"../record/recorderStore";import{macroToSteps as he}from"../record/condense";import{MacroReviewScreen as fe}from"./MacroReviewScreen";import{scn as c}from"../ui/scnTheme";import{Footnote as C,Panel as _,SectionLabel as A}from"../ui/primitives";import{ActiveCard as pe}from"./ActiveCard";import{DeactivateReportCard as ve}from"./DeactivateReportCard";import{PreviewScreen as ye}from"./PreviewScreen";import{RunProgressScreen as ge}from"./RunProgressScreen";import{ScenarioDetailScreen as we}from"./ScenarioDetailScreen";import{ScenarioRow as j,displayName as Se}from"./ScenarioRow";import{ScenarioFolderBar as be,folderMatches as De}from"./ScenarioFolderBar";import{FolderPickerScreen as Re}from"./FolderPickerScreen";import{jsx as o,jsxs as b,Fragment as J}from"react/jsx-runtime";function Te(t){const f=t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40);return`macro-${f||"recording"}-${Date.now().toString(36)}`}export function ScenariosBody({currentBuild:t}){const f=me();const y=ue();const u=le();const n=ce();const D=ae({minBottom:16});const k=ie("scenarios.saved");const[m,l]=H({name:"list"});const[x,p]=H(false);const d=S(()=>l({name:"list"}),[]);const L=S(e=>i=>{return r.checkBeforeRun(e,i)},[]);const R=S(async(e,i)=>{p(true);l({name:"run"});try{const a=await r.run(e,i);if(a.ok){n.showToast(`Scenario active \u2014 ${a.effects.length} change${a.effects.length===1?"":"s"}`,"success");r.clearRunState();l({name:"list"});n.minimize()}}catch(a){n.showToast(a instanceof Error?a.message:String(a),"error");l({name:"list"})}finally{p(false)}},[n]);const w=S(async()=>{p(true);try{await r.deactivate()}finally{p(false)}},[]);const P=S(async()=>{await I.start();const{status:e,reason:i}=I.getState();if(e!=="recording"){n.showToast(i??"Recording isn't available in this app.","error");return}n.minimize()},[n]);const v=S(async(e,i)=>{p(true);try{const a=he(i);const T=Date.now();await r.saveScenario({id:Te(e),name:e,version:1,source:"device",author:"recorded here",vars:[],steps:a,authoredAgainst:t,unsavedToRepo:true,createdAt:T,updatedAt:T,usage:{runs:0}});await I.cancel();n.showToast(`Saved \u201C${e}\u201D`,"success");d()}catch(a){n.showToast(a instanceof Error?a.message:String(a),"error")}finally{p(false)}},[t,d,n]);if(u.status==="review"&&u.draft){return o(fe,{draft:u.draft,busy:x,onChange:e=>I.setDraft(e),onSave:v,onDiscard:()=>{W.alert("Discard this recording?","The steps you just recorded will be thrown away.",[{text:"Keep",style:"cancel"},{text:"Discard",style:"destructive",onPress:()=>{void I.cancel();d()}}])}})}if(m.name==="run"&&y){return o(ge,{runState:y,busy:x,onUndoPartial:async()=>{p(true);try{await r.undoPartialRun();n.showToast("Applied steps undone","success")}finally{p(false);d()}},onKeepPartial:()=>{r.clearRunState();d()},onRetry:()=>{const e=y.scenarioId;const i=r.find(e);if(i){void R(e,r.lastValuesFor(e))}},onDone:()=>{r.clearRunState();d()},onBack:()=>{r.clearRunState();d()}})}if(m.name==="preview"){const e=r.find(m.scenarioId);if(!e)return o(B,{...g()});const i={...de(e.vars),...r.lastValuesFor(e.id)};return o(ye,{scenario:e,initialValues:i,check:L(e),currentBuild:t,mode:m.mode,onRun:a=>{r.rememberVarValues(e.id,a);void R(e.id,a)},onAcceptDraft:async()=>{await r.acceptDraft(e.id);n.showToast("Accepted \u2014 it's now in your library","success");d()},onDiscardDraft:async()=>{await r.discardDraft(e.id);n.showToast("Draft discarded","info");d()},onBack:d})}if(m.name==="detail"){const e=r.find(m.scenarioId);if(!e)return o(B,{...g()});return o(we,{scenario:e,onRun:()=>l({name:"preview",scenarioId:e.id,mode:"run"}),onDelete:()=>{W.alert("Delete scenario?",`\u201C${Se(e.name)}\u201D will be removed from this device.`,[{text:"Cancel",style:"cancel"},{text:"Delete",style:"destructive",onPress:()=>{void r.deleteScenario(e.id);d()}}])},onDuplicate:async()=>{const i={...e,id:`${e.id}-copy-${Date.now().toString(36)}`,name:`${e.name} (copy)`,source:"device",version:1,unsavedToRepo:true};await r.saveScenario(i);n.showToast("Copied to this device","success");d()},onExported:()=>n.showToast("Scenario JSON copied","success"),onMoveToFolder:()=>l({name:"folder",scenarioId:e.id}),onBack:d})}if(m.name==="folder"){const e=r.find(m.scenarioId);if(!e)return o(B,{...g()});return o(Re,{scenario:e,folders:r.folders(),onPick:async i=>{try{await r.setFolder(e.id,i);n.showToast(i?`Filed under ${i}`:"Removed from its folder","success")}catch(a){n.showToast(a instanceof Error?a.message:String(a),"error")}l({name:"detail",scenarioId:e.id})},onBack:()=>l({name:"detail",scenarioId:e.id})})}function g(){return{state:f,insets:D,host:n,busy:x,savedLimit:k,currentBuild:t,onOpen:e=>l({name:"detail",scenarioId:e}),onRun:e=>l({name:"preview",scenarioId:e,mode:"run"}),onReviewDraft:e=>l({name:"preview",scenarioId:e,mode:"review-draft"}),onDeactivate:w,onRerun:()=>{const e=f.active;if(e)void R(e.scenarioId,e.varValues)},onStartRecording:P,canRecord:I.canRecord(),recordUnavailable:u.status==="unavailable"?u.reason:null}}return o(B,{...g()})}function K(t,f,y){if(t)return"No matches.";if(f===void 0)return"Nothing unfiled here.";if(f!==null)return`Nothing in ${f}.`;return y}function B({state:t,insets:f,host:y,busy:u,savedLimit:n,currentBuild:D,onOpen:k,onRun:m,onReviewDraft:l,onDeactivate:x,onRerun:p,onStartRecording:d,canRecord:L,recordUnavailable:R}){const[w,P]=H(null);const v=y.search.trim().toLowerCase();const g=s=>!v||s.name.toLowerCase().includes(v)||(s.folder??"").toLowerCase().includes(v)||(s.tags??[]).some(Q=>Q.toLowerCase().includes(v))||(s.author??"").toLowerCase().includes(v);const e={code:t.code.filter(g),device:t.device.filter(g),drafts:t.drafts.filter(g)};const i=r.folders();const a=[...e.code,...e.device,...e.drafts];const T=s=>De(s,w);const $=e.code.filter(T);const V=e.device.filter(T);const z=e.drafts.filter(T);const Y=t.device.length>=n;const q=r.isLongRunning();const O=w===null&&i.length>0;const M=S(()=>({shipped:t.code,saved:t.device,drafts:t.drafts,active:t.active??null}),[t.code,t.device,t.drafts,t.active]);const E=U(()=>o(re,{value:M,buttonStyle:h.headerButton,testID:"scenarios-copy-library",accessibilityLabel:"Copy the scenario library as JSON"}),[M]);se({actions:E},[E]);return b(N,{style:h.fill,children:[i.length?o(be,{folders:i,active:w,onChange:P,scenarios:a}):null,b(Z,{style:h.fill,contentContainerStyle:[h.content,{paddingBottom:f.bottom}],keyboardShouldPersistTaps:"handled",testID:"scenarios-scroll",children:[L?b(X,{style:h.record,onPress:d,disabled:u,activeOpacity:.75,accessibilityRole:"button",accessibilityLabel:"Record what you do next as a macro",testID:"scenarios-record",children:[o(N,{style:h.recordDot}),b(N,{style:h.recordText,children:[o(F,{style:h.recordTitle,children:"Record a macro"}),o(F,{style:h.recordHint,children:"Do the flow once. Buoy writes down the steps."})]})]}):null,R?o(C,{tone:"warning",testID:"scenarios-record-unavailable",children:R}):null,t.lastDeactivateReport?o(ve,{report:t.lastDeactivateReport,onDismiss:()=>r.dismissDeactivateReport()}):null,t.active?b(J,{children:[o(A,{icon:ne,color:c.color.sim,testID:"scenarios-active-label",children:"Active now"}),o(pe,{active:t.active,build:D,busy:u,onDeactivate:x,onRerun:p,onCopied:()=>y.showToast("Repro info copied \u2014 paste into the ticket","success")}),q?o(C,{tone:"warning",testID:"scenarios-long-running",children:"This scenario has been active for a while. If you have moved on to something else, deactivate it before you test anything real."}):null]}):null,z.length?b(J,{children:[o(A,{icon:oe,color:c.color.sim,count:z.length,testID:"scenarios-drafts-label",children:"Awaiting review"}),z.map(s=>o(j,{scenario:s,currentBuild:D,disabled:u,showFolder:O,onOpen:()=>l(s.id),onRun:()=>l(s.id),testID:`scenario-draft-${s.id}`},s.id)),o(C,{children:"A scenario sent from chat or the desktop never runs on its own. You review what it will do, then accept it."})]}):null,o(A,{icon:ee,count:$.length,testID:"scenarios-code-label",children:"Shipped with this app"}),$.length?$.map(s=>o(j,{scenario:s,currentBuild:D,disabled:u,showFolder:O,onOpen:()=>k(s.id),onRun:()=>m(s.id)},s.id)):o(_,{children:o(F,{style:h.empty,testID:"scenarios-code-empty",children:K(v,w,"None yet \u2014 your developers can ship a QA menu with the app using defineScenario().")})}),o(A,{icon:te,color:c.color.info,count:V.length,testID:"scenarios-device-label",children:"Saved on this device"}),V.length?V.map(s=>o(j,{scenario:s,currentBuild:D,disabled:u,showFolder:O,onOpen:()=>k(s.id),onRun:()=>m(s.id)},s.id)):o(_,{children:o(F,{style:h.empty,testID:"scenarios-device-empty",children:K(v,w,"Nothing saved here yet. Ask your AI assistant to save the state you just set up as a scenario.")})}),Y?b(C,{tone:"warning",testID:"scenarios-limit",children:["Your plan keeps ",n," saved scenario",n===1?"":"s"," on a device. Scenarios shipped in code don't count toward it."]}):null,o(C,{children:i.length?"Scenarios only run in development builds. Tap a row to see exactly what it will do before you run it.":"Scenarios only run in development builds. Open one and tap its Folder row to group it by flow \u2014 Checkout, Login \u2014 and a folder bar appears up here."})]})]})}const h=G.create({fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:24},record:{flexDirection:"row",alignItems:"center",gap:10,marginHorizontal:c.gutter,marginTop:8,marginBottom:4,paddingHorizontal:c.textInset,paddingVertical:12,borderRadius:c.radius.card,borderWidth:1,borderColor:c.color.simBorder,backgroundColor:c.color.simSoft},recordDot:{width:10,height:10,borderRadius:5,backgroundColor:c.color.sim},recordText:{flex:1,minWidth:0},recordTitle:{color:c.color.sim,fontSize:c.font.row,fontWeight:"700"},recordHint:{color:c.color.textSecondary,fontSize:c.font.micro,marginTop:2},headerButton:{width:32,height:32,borderRadius:8,backgroundColor:c.color.chip,borderWidth:1,borderColor:c.color.border,alignItems:"center",justifyContent:"center"},empty:{color:c.color.textTertiary,fontSize:c.font.control,lineHeight:18,paddingHorizontal:c.textInset,paddingVertical:14}});
|
|
1
|
+
"use strict";import{useCallback as I,useEffect as te,useMemo as re,useState as V}from"react";import{Alert as O,ScrollView as ne,StyleSheet as ae,Text as x,TouchableOpacity as Z,View as H}from"react-native";import{Box as se,FileText as ie,RefreshCw as ce,Smartphone as le,ToolbarCopyButton as de,Users as ue,subscribeAccountAccess as me,Zap as he,useLimit as fe,useSafeAreaInsets as pe}from"@buoy-gg/shared-ui";import{useToolHeader as ye,useToolHost as ve}from"@buoy-gg/core";import{resolveVarValues as ge}from"../interpolate";import{scenariosStore as r}from"../store/scenariosStore";import{useRecording as we,useScenariosRunState as Se,useScenariosState as be}from"../hooks/useScenarios";import{recorderStore as B}from"../record/recorderStore";import{macroToSteps as Re}from"../record/condense";import{MacroReviewScreen as De}from"./MacroReviewScreen";import{scn as s}from"../ui/scnTheme";import{Footnote as A,Panel as q,SectionLabel as L}from"../ui/primitives";import{ActiveCard as Te}from"./ActiveCard";import{DeactivateReportCard as Ie}from"./DeactivateReportCard";import{PreviewScreen as xe}from"./PreviewScreen";import{RunProgressScreen as Ce}from"./RunProgressScreen";import{ScenarioDetailScreen as ke}from"./ScenarioDetailScreen";import{ScenarioRow as E,displayName as J}from"./ScenarioRow";import{ScenarioFolderBar as Be,folderMatches as Fe}from"./ScenarioFolderBar";import{FolderPickerScreen as Ae}from"./FolderPickerScreen";import{jsx as t,jsxs as g,Fragment as K}from"react/jsx-runtime";function Le(o){const l=o.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,40);return`macro-${l||"recording"}-${Date.now().toString(36)}`}export function ScenariosBody({currentBuild:o}){const l=be();const y=Se();const h=we();const i=ve();const w=pe({minBottom:16});const R=fe("scenarios.saved");const[f,m]=V({name:"list"});const[D,p]=V(false);const[S,C]=V(false);te(()=>{void r.refreshTeam();return me(()=>{void r.onAccountChange()})},[]);const d=I(()=>m({name:"list"}),[]);const b=I(e=>n=>{return r.checkBeforeRun(e,n)},[]);const F=I(async(e,n)=>{p(true);m({name:"run"});try{const c=await r.run(e,n);if(c.ok){i.showToast(`Scenario active \u2014 ${c.effects.length} change${c.effects.length===1?"":"s"}`,"success");r.clearRunState();m({name:"list"});i.minimize()}}catch(c){i.showToast(c instanceof Error?c.message:String(c),"error");m({name:"list"})}finally{p(false)}},[i]);const v=I(async()=>{p(true);try{await r.deactivate()}finally{p(false)}},[]);const k=I(async()=>{await B.start();const{status:e,reason:n}=B.getState();if(e!=="recording"){i.showToast(n??"Recording isn't available in this app.","error");return}i.minimize()},[i]);const W=I(async(e,n)=>{p(true);try{const c=Re(n);const P=Date.now();await r.saveScenario({id:Le(e),name:e,version:1,source:"device",author:"recorded here",vars:[],steps:c,authoredAgainst:o,unsavedToRepo:true,createdAt:P,updatedAt:P,usage:{runs:0}});await B.cancel();i.showToast(`Saved \u201C${e}\u201D`,"success");d()}catch(c){i.showToast(c instanceof Error?c.message:String(c),"error")}finally{p(false)}},[o,d,i]);if(h.status==="review"&&h.draft){return t(De,{draft:h.draft,busy:D,onChange:e=>B.setDraft(e),onSave:W,onDiscard:()=>{O.alert("Discard this recording?","The steps you just recorded will be thrown away.",[{text:"Keep",style:"cancel"},{text:"Discard",style:"destructive",onPress:()=>{void B.cancel();d()}}])}})}if(f.name==="run"&&y){return t(Ce,{runState:y,busy:D,onUndoPartial:async()=>{p(true);try{await r.undoPartialRun();i.showToast("Applied steps undone","success")}finally{p(false);d()}},onKeepPartial:()=>{r.clearRunState();d()},onRetry:()=>{const e=y.scenarioId;const n=r.find(e);if(n){void F(e,r.lastValuesFor(e))}},onDone:()=>{r.clearRunState();d()},onBack:()=>{r.clearRunState();d()}})}if(f.name==="preview"){const e=r.find(f.scenarioId);if(!e)return t(N,{...T()});const n={...ge(e.vars),...r.lastValuesFor(e.id)};return t(xe,{scenario:e,initialValues:n,check:b(e),currentBuild:o,mode:f.mode,onRun:c=>{r.rememberVarValues(e.id,c);void F(e.id,c)},onAcceptDraft:async()=>{await r.acceptDraft(e.id);i.showToast("Accepted \u2014 it's now in your library","success");d()},onDiscardDraft:async()=>{await r.discardDraft(e.id);i.showToast("Draft discarded","info");d()},onBack:d})}if(f.name==="detail"){const e=r.find(f.scenarioId);if(!e)return t(N,{...T()});return t(ke,{scenario:e,onRun:()=>m({name:"preview",scenarioId:e.id,mode:"run"}),onDelete:()=>{O.alert("Delete scenario?",`\u201C${J(e.name)}\u201D will be removed from this device.`,[{text:"Cancel",style:"cancel"},{text:"Delete",style:"destructive",onPress:()=>{void r.deleteScenario(e.id);d()}}])},onDuplicate:async()=>{const n={...e,id:`${e.id}-copy-${Date.now().toString(36)}`,name:`${e.name} (copy)`,source:"device",version:1,unsavedToRepo:true,team:void 0};await r.saveScenario(n);i.showToast("Copied to this device","success");d()},onExported:()=>i.showToast("Scenario JSON copied","success"),onMoveToFolder:()=>m({name:"folder",scenarioId:e.id}),teamBusy:S,onShare:e.source==="device"&&!r.isMirror()&&(l.teamInfo.status==="ready"||l.teamInfo.status==="error")?()=>{O.alert("Share with your team?",`Everyone on your team will see \u201C${J(e.name)}\u201D in Scenarios in this app.`,[{text:"Cancel",style:"cancel"},{text:"Share",onPress:async()=>{C(true);try{await r.shareWithTeam(e.id);i.showToast("Shared with your team","success")}catch(n){i.showToast(n instanceof Error?n.message:"Could not share it.","error")}finally{C(false)}}}])}:void 0,onRemoveFromTeam:r.canRemoveFromTeam(e)?()=>{O.alert("Remove from team?",`\u201C${J(e.name)}\u201D leaves Scenarios for everyone on your team.`,[{text:"Cancel",style:"cancel"},{text:"Remove",style:"destructive",onPress:async()=>{C(true);try{await r.removeFromTeam(e.id);i.showToast("Removed from your team","success");d()}catch(n){i.showToast(n instanceof Error?n.message:"Could not remove it.","error")}finally{C(false)}}}])}:void 0,onBack:d})}if(f.name==="folder"){const e=r.find(f.scenarioId);if(!e)return t(N,{...T()});return t(Ae,{scenario:e,folders:r.folders(),onPick:async n=>{try{await r.setFolder(e.id,n);i.showToast(n?`Filed under ${n}`:"Removed from its folder","success")}catch(c){i.showToast(c instanceof Error?c.message:String(c),"error")}m({name:"detail",scenarioId:e.id})},onBack:()=>m({name:"detail",scenarioId:e.id})})}function T(){return{state:l,insets:w,host:i,busy:D,savedLimit:R,currentBuild:o,onOpen:e=>m({name:"detail",scenarioId:e}),onRun:e=>m({name:"preview",scenarioId:e,mode:"run"}),onReviewDraft:e=>m({name:"preview",scenarioId:e,mode:"review-draft"}),onDeactivate:v,onRerun:()=>{const e=l.active;if(e)void F(e.scenarioId,e.varValues)},onStartRecording:k,canRecord:B.canRecord(),recordUnavailable:h.status==="unavailable"?h.reason:null}}return t(N,{...T()})}function U(o,l,y){if(o)return"No matches.";if(l===void 0)return"Nothing unfiled here.";if(l!==null)return`Nothing in ${l}.`;return y}function N({state:o,insets:l,host:y,busy:h,savedLimit:i,currentBuild:w,onOpen:R,onRun:f,onReviewDraft:m,onDeactivate:D,onRerun:p,onStartRecording:S,canRecord:C,recordUnavailable:d}){const[b,F]=V(null);const v=y.search.trim().toLowerCase();const k=a=>!v||a.name.toLowerCase().includes(v)||(a.folder??"").toLowerCase().includes(v)||(a.tags??[]).some(oe=>oe.toLowerCase().includes(v))||(a.author??"").toLowerCase().includes(v);const W=new Set(o.code.map(a=>a.id));const T=o.team.filter(a=>!W.has(a.id));const e=new Set(T.map(a=>a.id));const n={code:o.code.filter(k),team:T.filter(k),device:o.device.filter(a=>!e.has(a.id)).filter(k),drafts:o.drafts.filter(k)};const c=r.folders();const P=[...n.code,...n.team,...n.device,...n.drafts];const $=a=>Fe(a,b);const j=n.code.filter($);const G=n.team.filter($);const M=n.device.filter($);const _=n.drafts.filter($);const X=o.device.length>=i;const ee=r.isLongRunning();const z=b===null&&c.length>0;const Y=I(()=>({shipped:o.code,saved:o.device,drafts:o.drafts,active:o.active??null}),[o.code,o.device,o.drafts,o.active]);const Q=re(()=>t(de,{value:Y,buttonStyle:u.headerButton,testID:"scenarios-copy-library",accessibilityLabel:"Copy the scenario library as JSON"}),[Y]);ye({actions:Q},[Q]);return g(H,{style:u.fill,children:[c.length?t(Be,{folders:c,active:b,onChange:F,scenarios:P}):null,g(ne,{style:u.fill,contentContainerStyle:[u.content,{paddingBottom:l.bottom}],keyboardShouldPersistTaps:"handled",testID:"scenarios-scroll",children:[C?g(Z,{style:u.record,onPress:S,disabled:h,activeOpacity:.75,accessibilityRole:"button",accessibilityLabel:"Record what you do next as a macro",testID:"scenarios-record",children:[t(H,{style:u.recordDot}),g(H,{style:u.recordText,children:[t(x,{style:u.recordTitle,children:"Record a macro"}),t(x,{style:u.recordHint,children:"Do the flow once. Buoy writes down the steps."})]})]}):null,d?t(A,{tone:"warning",testID:"scenarios-record-unavailable",children:d}):null,o.lastDeactivateReport?t(Ie,{report:o.lastDeactivateReport,onDismiss:()=>r.dismissDeactivateReport()}):null,o.active?g(K,{children:[t(L,{icon:he,color:s.color.sim,testID:"scenarios-active-label",children:"Active now"}),t(Te,{active:o.active,build:w,busy:h,onDeactivate:D,onRerun:p,onCopied:()=>y.showToast("Repro info copied \u2014 paste into the ticket","success")}),ee?t(A,{tone:"warning",testID:"scenarios-long-running",children:"This scenario has been active for a while. If you have moved on to something else, deactivate it before you test anything real."}):null]}):null,_.length?g(K,{children:[t(L,{icon:ie,color:s.color.sim,count:_.length,testID:"scenarios-drafts-label",children:"Awaiting review"}),_.map(a=>t(E,{scenario:a,currentBuild:w,disabled:h,showFolder:z,onOpen:()=>m(a.id),onRun:()=>m(a.id),testID:`scenario-draft-${a.id}`},a.id)),t(A,{children:"A scenario sent from chat or the desktop never runs on its own. You review what it will do, then accept it."})]}):null,t(L,{icon:se,count:j.length,testID:"scenarios-code-label",children:"Shipped with this app"}),j.length?j.map(a=>t(E,{scenario:a,currentBuild:w,disabled:h,showFolder:z,onOpen:()=>R(a.id),onRun:()=>f(a.id)},a.id)):t(q,{children:t(x,{style:u.empty,testID:"scenarios-code-empty",children:U(v,b,"None yet \u2014 your developers can ship a QA menu with the app using defineScenario().")})}),t(Pe,{info:o.teamInfo,scenarios:G,query:v,folder:b,currentBuild:w,busy:h,showFolder:z,onOpen:R,onRun:f}),t(L,{icon:le,color:s.color.info,count:M.length,testID:"scenarios-device-label",children:"Saved on this device"}),M.length?M.map(a=>t(E,{scenario:a,currentBuild:w,disabled:h,showFolder:z,onOpen:()=>R(a.id),onRun:()=>f(a.id)},a.id)):t(q,{children:t(x,{style:u.empty,testID:"scenarios-device-empty",children:U(v,b,"Nothing saved here yet. Ask your AI assistant to save the state you just set up as a scenario.")})}),X?g(A,{tone:"warning",testID:"scenarios-limit",children:["Your plan keeps ",i," saved scenario",i===1?"":"s"," on a device. Scenarios shipped in code don't count toward it."]}):null,t(A,{children:c.length?"Scenarios only run in development builds. Tap a row to see exactly what it will do before you run it.":"Scenarios only run in development builds. Open one and tap its Folder row to group it by flow \u2014 Checkout, Login \u2014 and a folder bar appears up here."})]})]})}function Pe({info:o,scenarios:l,query:y,folder:h,currentBuild:i,busy:w,showFolder:R,onOpen:f,onRun:m}){const D=l.length>0||o.status==="ready"||o.status==="error"||o.status==="off"&&o.reason!=="no-team";if(!D)return null;const p=o.status==="loading"?"Checking buoy.gg\u2026":o.status==="error"||o.status==="off"?o.message:null;return g(K,{children:[t(L,{icon:ue,color:s.color.accent,count:l.length,testID:"scenarios-team-label",children:"Shared with your team"}),g(H,{style:u.teamBar,children:[t(x,{style:[u.teamStatus,(o.status==="error"||o.status==="off")&&u.teamStatusWarn],numberOfLines:3,testID:"scenarios-team-status",children:p??"Everyone on your team sees these in this app."}),!r.isMirror()&&(o.status!=="off"||o.reason!=="no-app")?g(Z,{style:u.teamRefresh,onPress:()=>void r.refreshTeam({force:true}),disabled:o.status==="loading",accessibilityRole:"button",accessibilityLabel:"Refresh the team's scenarios",testID:"scenarios-team-refresh",children:[t(ce,{size:12,color:s.color.textSecondary}),t(x,{style:u.teamRefreshText,children:"Refresh"})]}):null]}),l.length?l.map(S=>t(E,{scenario:S,currentBuild:i,disabled:w,showFolder:R,onOpen:()=>f(S.id),onRun:()=>m(S.id),testID:`scenario-team-${S.id}`},S.id)):o.status==="ready"?t(q,{children:t(x,{style:u.empty,testID:"scenarios-team-empty",children:U(y,h,"Nothing shared yet. Open a scenario saved on this device and tap Share with team.")})}):null]})}const u=ae.create({teamBar:{flexDirection:"row",alignItems:"center",gap:8,paddingHorizontal:s.textInset,marginBottom:6},teamStatus:{flex:1,color:s.color.textTertiary,fontSize:s.font.footnote},teamStatusWarn:{color:s.color.sim},teamRefresh:{flexDirection:"row",alignItems:"center",gap:4,paddingVertical:4,paddingHorizontal:8,borderRadius:s.radius.pill,borderWidth:1,borderColor:s.color.border,backgroundColor:s.color.chip},teamRefreshText:{color:s.color.textSecondary,fontSize:s.font.control},fill:{flex:1,backgroundColor:"transparent"},content:{paddingBottom:24},record:{flexDirection:"row",alignItems:"center",gap:10,marginHorizontal:s.gutter,marginTop:8,marginBottom:4,paddingHorizontal:s.textInset,paddingVertical:12,borderRadius:s.radius.card,borderWidth:1,borderColor:s.color.simBorder,backgroundColor:s.color.simSoft},recordDot:{width:10,height:10,borderRadius:5,backgroundColor:s.color.sim},recordText:{flex:1,minWidth:0},recordTitle:{color:s.color.sim,fontSize:s.font.row,fontWeight:"700"},recordHint:{color:s.color.textSecondary,fontSize:s.font.micro,marginTop:2},headerButton:{width:32,height:32,borderRadius:8,backgroundColor:s.color.chip,borderWidth:1,borderColor:s.color.border,alignItems:"center",justifyContent:"center"},empty:{color:s.color.textTertiary,fontSize:s.font.control,lineHeight:18,paddingHorizontal:s.textInset,paddingVertical:14}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{devToolsStorageKeys as y,persistentStorage as h,dispatchToolAction as u,assertAccountAccess as v,hasAccountAccess as m,getToolAdapters as g,listToolActions as D,listToolIds as A,waitForToolRegistry as I}from"@buoy-gg/shared-ui";import{preflight as L,revertEffects as p,runScenario as V}from"../engine";import{stepConflictKeys as C}from"../catalog";import{withStepIds as b}from"../defineScenario";import{listFolders as k,resolveFolder as E}from"../folders";import{interpolate as _,interpolateString as S,resolveVarValues as R}from"../interpolate";const x=y.scenarios.library();const w=y.scenarios.active();export const MAX_DEVICE_SCENARIOS=100;let l=null;export function setRemoteScenariosHandler(f){l=f}function T(){return!(typeof __DEV__!=="undefined"&&!__DEV__)}class K{state={code:[],device:[],drafts:[],active:null,runState:null,lastDeactivateReport:null,hasLoaded:false,lastVarValues:{}};listeners=new Set;loadPromise=null;expiryTimer=null;runController=null;getState=()=>this.state;subscribe=t=>{this.listeners.add(t);void this.ensureLoaded();return()=>{this.listeners.delete(t)}};setState(t){this.state={...this.state,...t};this.listeners.forEach(e=>{try{e()}catch{}})}allScenarios(){const t=new Set(this.state.code.map(e=>e.id));return[...this.state.code,...this.state.device.filter(e=>!t.has(e.id))]}find(t){return this.allScenarios().find(e=>e.id===t)??this.state.drafts.find(e=>e.id===t)}folders(){return k([...this.state.code,...this.state.device,...this.state.drafts])}registerCodeScenarios(t){const e=new Map(this.state.code.map(r=>[r.id,r]));for(const r of t)e.set(r.id,{...r,source:"code"});this.setState({code:[...e.values()]})}ensureLoaded(){if(l)return Promise.resolve();if(!this.loadPromise){this.loadPromise=this.load()}return this.loadPromise}async load(){let t=[];let e=[];let r={};try{const a=await h.getItem(x);if(a){const i=JSON.parse(a);t=Array.isArray(i.device)?i.device:[];e=Array.isArray(i.drafts)?i.drafts:[];r=i.lastVarValues&&typeof i.lastVarValues==="object"?i.lastVarValues:{}}}catch{}let s=null;try{const a=await h.getItem(w);if(a)s=JSON.parse(a)}catch{s=null}this.setState({device:t,drafts:e,lastVarValues:r,active:s,hasLoaded:true});if(s)await this.reconcileActive()}async persistLibrary(){if(l)return;const t={version:1,device:this.state.device,drafts:this.state.drafts,lastVarValues:this.state.lastVarValues};try{await h.setItem(x,JSON.stringify(t))}catch{}}async persistActive(){if(l)return;try{if(this.state.active){await h.setItem(w,JSON.stringify(this.state.active))}else{await h.removeItem(w)}}catch{}}async reconcileActive(){const t=this.state.active;if(!t)return;const e=t.effects.filter(r=>r.kind==="override-rule");if(!e.length)return;if(!A().includes("network"))return;try{const r=await u("network","listOverrideRules",{});const s=new Set((r?.rules??[]).map(i=>i.id));const a=t.effects.filter(i=>i.kind!=="override-rule"||s.has(i.ruleId));if(a.length===t.effects.length)return;if(!a.length){this.setState({active:null})}else{this.setState({active:{...t,effects:a}})}await this.persistActive()}catch{}}checkBeforeRun(t,e,r=[]){const s=this.state.active;const a=[...r];if(!T()){a.push("Scenarios cannot run in a production build.")}const i=R(t.vars,e);return L(t,i,{availableTools:A(),availableActions:D(),activeScenario:s?{scenarioId:s.scenarioId,name:s.name,conflictKeys:this.activeConflictKeys()}:null,extraErrors:a})}holdObservers(t){const e=[];const r=g();for(const s of t){const a=r[s]?.subscribe;if(!a)continue;try{e.push(a(()=>{}))}catch{}}return e}activeConflictKeys(){const t=this.state.active;if(!t)return[];const e=this.find(t.scenarioId);if(!e)return[];const r=new Set;for(const s of e.steps){for(const a of C(s,_(s.params,t.varValues))){r.add(a)}}return[...r]}async run(t,e={},r={}){v();if(l){return await l("run",{id:t,params:e})}await this.ensureLoaded();v();const s=this.find(t);if(!s)throw new Error(`No scenario "${t}"`);if(s.source==="draft"){throw new Error("Drafts must be accepted before they can run.")}if(!T()){throw new Error("Scenarios cannot run in a production build.")}if(!await I()){throw new Error("Buoy tools are still starting up \u2014 try again in a moment.")}const a=R(s.vars,e);if(this.state.active&&this.state.active.scenarioId!==t){await this.deactivate()}else if(this.state.active){const n=this.state.active;await p(n.effects.filter(d=>d.kind==="override-rule"),u);this.setState({active:{...n,effects:n.effects.filter(d=>d.kind!=="override-rule")}})}v();this.setState({runState:{scenarioId:t,name:S(s.name,a),steps:[],current:0,done:false}});const i=this.holdObservers(["route-events"]);let c;this.runController?.abort();const o=new AbortController;this.runController=o;try{c=await V(s,a,u,{signal:o.signal,onStep:n=>{const d=this.state.runState;if(d)this.setState({runState:{...d,current:n}})}})}finally{if(this.runController===o)this.runController=null;i.forEach(n=>{try{n()}catch{}})}this.setState({runState:{scenarioId:t,name:S(s.name,a),steps:c.steps,current:-1,done:true,report:c}});if(c.ok&&m()){const n={scenarioId:t,name:S(s.name,a),version:s.version,source:s.source,runAt:Date.now(),varValues:a,effects:c.effects,expiresAt:r.expiresInMs?Date.now()+r.expiresInMs:void 0};this.setState({active:n,lastDeactivateReport:null,lastVarValues:{...this.state.lastVarValues,[t]:a}});this.bumpUsage(t);this.armExpiry();await this.persistActive();await this.persistLibrary()}return c}async undoPartialRun(){const t=this.state.runState?.report;if(!t){return{removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]}}const e=await p(t.effects,u);const r={...e,undoStepsRan:0};this.setState({runState:null,lastDeactivateReport:r});return r}clearRunState(){this.setState({runState:null})}bumpUsage(t){const e=r=>r.map(s=>s.id===t?{...s,usage:{runs:(s.usage?.runs??0)+1,lastRunAt:Date.now()}}:s);this.setState({code:e(this.state.code),device:e(this.state.device)})}async deactivate(){if(l){return await l("deactivate",{})}const t=this.state.active;if(!t){const o={removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]};return o}const e=await p(t.effects,u);const r=[...e.errors];let s=0;const a=this.find(t.scenarioId);const i=a?.undoSteps??[];if(i.length){const o=await V({...a,steps:i},t.varValues,u);s=o.steps.filter(n=>n.ok).length;for(const n of o.steps){if(!n.ok&&n.error)r.push(n.error)}}const c={removedRules:e.removedRules,stoppedImpersonation:e.stoppedImpersonation,clearedTransients:e.clearedTransients,unrestoredStorageKeys:s?[]:e.unrestoredStorageKeys,unrestoredState:s?[]:e.unrestoredState,undoStepsRan:s,errors:r};this.clearExpiry();this.setState({active:null,lastDeactivateReport:c});await this.persistActive();return c}async disposeAccountSession(t){this.runController?.abort();this.runController=null;if(m())return;this.clearExpiry();const e=this.state.active;if(!e||l)return;const r=await p(e.effects.filter(s=>s.kind==="override-rule"),t);this.setState({active:null});await this.persistActive();if(r.errors.length)throw new Error(r.errors.join("; "))}dismissDeactivateReport(){this.setState({lastDeactivateReport:null})}armExpiry(){this.clearExpiry();const t=this.state.active?.expiresAt;if(!t)return;const e=Math.max(0,t-Date.now());this.expiryTimer=setTimeout(()=>{void this.deactivate()},e)}clearExpiry(){if(this.expiryTimer){clearTimeout(this.expiryTimer);this.expiryTimer=null}}isLongRunning(t=8*60*60*1e3){const e=this.state.active;return!!e&&Date.now()-e.runAt>t}async saveScenario(t,e={}){await this.ensureLoaded();const r=Date.now();const s=e.asDraft??false;const a=s?this.state.drafts:this.state.device;const i=a.findIndex(n=>n.id===t.id);const c={...t,steps:b(t.steps,"s"),undoSteps:t.undoSteps?b(t.undoSteps,"u"):void 0,folder:E(t.folder,this.folders()),source:s?"draft":"device",version:i>=0?(a[i].version??1)+1:t.version||1,createdAt:i>=0?a[i].createdAt:r,updatedAt:r,usage:i>=0?a[i].usage:{runs:0}};if(!s&&i<0&&this.state.device.length>=MAX_DEVICE_SCENARIOS){throw new Error(`This device already holds ${MAX_DEVICE_SCENARIOS} scenarios \u2014 delete one first.`)}const o=[...a];if(i>=0)o[i]=c;else o.unshift(c);this.setState(s?{drafts:o}:{device:o});await this.persistLibrary();return c}async acceptDraft(t){await this.ensureLoaded();const e=this.state.drafts.find(s=>s.id===t);if(!e)throw new Error(`No draft "${t}"`);const r={...e,source:"device",unsavedToRepo:true,updatedAt:Date.now()};this.setState({drafts:this.state.drafts.filter(s=>s.id!==t),device:[r,...this.state.device]});await this.persistLibrary();return r}async discardDraft(t){await this.ensureLoaded();this.setState({drafts:this.state.drafts.filter(e=>e.id!==t)});await this.persistLibrary()}async setFolder(t,e){await this.ensureLoaded();if(l){const o=await l("setFolder",{id:t,folder:e});return o.scenario}if(this.state.code.some(o=>o.id===t)){throw new Error("Scenarios shipped with the app are filed in code \u2014 change `folder` in defineScenario().")}const r=E(e,this.folders());let s;const a=o=>o.map(n=>{if(n.id!==t)return n;s={...n,folder:r,updatedAt:Date.now()};return s});const i=a(this.state.device);const c=s?this.state.drafts:a(this.state.drafts);if(!s)throw new Error(`No scenario "${t}"`);this.setState({device:i,drafts:c});await this.persistLibrary();return s}async deleteScenario(t){await this.ensureLoaded();if(this.state.active?.scenarioId===t)await this.deactivate();this.setState({device:this.state.device.filter(e=>e.id!==t)});await this.persistLibrary()}rememberVarValues(t,e){this.setState({lastVarValues:{...this.state.lastVarValues,[t]:e}});void this.persistLibrary()}lastValuesFor(t){return this.state.lastVarValues[t]??{}}replaceFromSnapshot(t){this.setState({code:t.code??[],device:t.device??[],drafts:t.drafts??[],active:t.active??null,runState:t.runState??null,hasLoaded:true})}}export const scenariosStore=new K;export function replaceScenariosFromSnapshot(f){scenariosStore.replaceFromSnapshot(f)}export function installedToolIds(){return Object.keys(g())}
|
|
1
|
+
"use strict";import{devToolsStorageKeys as w,persistentStorage as h,dispatchToolAction as f,assertAccountAccess as y,hasAccountAccess as I,getToolAdapters as T,listToolActions as C,listToolIds as R,waitForToolRegistry as K}from"@buoy-gg/shared-ui";import{preflight as _,revertEffects as p,runScenario as E}from"../engine";import{stepConflictKeys as P}from"../catalog";import{withStepIds as b}from"../defineScenario";import{listFolders as F,resolveFolder as V}from"../folders";import{interpolate as O,interpolateString as S,resolveVarValues as D}from"../interpolate";import{defaultTeamClientDeps as M,pullTeamScenarios as N,removeTeamScenario as B,shareTeamScenario as J,teamAppId as v}from"../team/teamClient";const x=w.scenarios.library();const A=w.scenarios.active();const g=w.scenarios.team();const $=3e4;let L=null;export function setTeamClientDepsForTests(l){L=l}function m(){return L??M()}export function teamScenarioFromRow(l){return{...l.scenario,id:l.id,source:"team",author:l.sharedBy.label,version:l.version,createdAt:l.scenario.createdAt??l.sharedAt,updatedAt:l.updatedAt,usage:{runs:0},team:{sharedBy:l.sharedBy,sharedAt:l.sharedAt,updatedAt:l.updatedAt,app:l.app}}}export const MAX_DEVICE_SCENARIOS=100;let d=null;export function setRemoteScenariosHandler(l){d=l}function k(){return!(typeof __DEV__!=="undefined"&&!__DEV__)}class Y{state={code:[],device:[],drafts:[],team:[],teamInfo:{status:"idle",you:null,admin:false},active:null,runState:null,lastDeactivateReport:null,hasLoaded:false,lastVarValues:{}};listeners=new Set;loadPromise=null;expiryTimer=null;runController=null;getState=()=>this.state;subscribe=t=>{this.listeners.add(t);void this.ensureLoaded();return()=>{this.listeners.delete(t)}};setState(t){this.state={...this.state,...t};this.listeners.forEach(e=>{try{e()}catch{}})}allScenarios(){const t=new Set(this.state.code.map(s=>s.id));const e=this.state.team.filter(s=>!t.has(s.id));const a=new Set([...t,...e.map(s=>s.id)]);return[...this.state.code,...e,...this.state.device.filter(s=>!a.has(s.id))]}find(t){return this.allScenarios().find(e=>e.id===t)??this.state.drafts.find(e=>e.id===t)}folders(){return F([...this.state.code,...this.state.team,...this.state.device,...this.state.drafts])}registerCodeScenarios(t){const e=new Map(this.state.code.map(a=>[a.id,a]));for(const a of t)e.set(a.id,{...a,source:"code"});this.setState({code:[...e.values()]})}ensureLoaded(){if(d)return Promise.resolve();if(!this.loadPromise){this.loadPromise=this.load()}return this.loadPromise}async load(){let t=[];let e=[];let a={};try{const o=await h.getItem(x);if(o){const i=JSON.parse(o);t=Array.isArray(i.device)?i.device:[];e=Array.isArray(i.drafts)?i.drafts:[];a=i.lastVarValues&&typeof i.lastVarValues==="object"?i.lastVarValues:{}}}catch{}let s=null;try{const o=await h.getItem(A);if(o)s=JSON.parse(o)}catch{s=null}let r=[];let n=this.state.teamInfo;try{const o=await h.getItem(g);const i=o?JSON.parse(o):null;const c=v(m());if(i&&Array.isArray(i.rows)&&(!c||i.app===c)){this.teamRows=i.rows;r=i.rows.map(teamScenarioFromRow);n={status:"idle",you:i.you||null,admin:i.admin,pulledAt:i.pulledAt}}}catch{}this.setState({device:t,drafts:e,team:r,teamInfo:n,lastVarValues:a,active:s,hasLoaded:true});if(s)await this.reconcileActive()}async persistLibrary(){if(d)return;const t={version:1,device:this.state.device,drafts:this.state.drafts,lastVarValues:this.state.lastVarValues};try{await h.setItem(x,JSON.stringify(t))}catch{}}async persistActive(){if(d)return;try{if(this.state.active){await h.setItem(A,JSON.stringify(this.state.active))}else{await h.removeItem(A)}}catch{}}async persistTeam(t,e){if(d)return;try{if(!t||!e){await h.removeItem(g);return}const{you:a,admin:s,pulledAt:r}=this.state.teamInfo;const n={version:1,app:e,you:a??"",admin:s,pulledAt:r??Date.now(),rows:t};await h.setItem(g,JSON.stringify(n))}catch{}}teamRows=[];teamPull=null;teamCredentialKey=null;credentialKey(t){const e=t.credential();return e?`${e.team}:${e.token}`:""}onAccountChange(){if(d)return Promise.resolve();if(this.credentialKey(m())===this.teamCredentialKey)return Promise.resolve();return this.refreshTeam({force:true})}refreshTeam(t={}){if(d)return Promise.resolve();const{pulledAt:e,status:a}=this.state.teamInfo;if(!t.force&&a==="ready"&&e&&Date.now()-e<$){return Promise.resolve()}if(!this.teamPull){this.teamPull=this.pullTeam().finally(()=>{this.teamPull=null})}return this.teamPull}async pullTeam(){await this.ensureLoaded();const t=m();this.teamCredentialKey=this.credentialKey(t);this.setState({teamInfo:{...this.state.teamInfo,status:"loading",message:void 0}});const e=await N(t);if(!e.ok){if(e.code==="network"){this.setState({teamInfo:{...this.state.teamInfo,status:"error",message:e.message}});return}this.teamRows=[];this.setState({team:[],teamInfo:{status:"off",reason:e.code,message:e.message,you:null,admin:false}});await this.persistTeam(null,null);return}this.teamRows=e.value.scenarios;this.setState({team:e.value.scenarios.map(teamScenarioFromRow),teamInfo:{status:"ready",you:e.value.you,admin:e.value.admin,pulledAt:Date.now()}});await this.persistTeam(this.teamRows,v(t))}async shareWithTeam(t){await this.ensureLoaded();if(d)throw new Error("Share from the app on the device.");const e=this.state.device.find(o=>o.id===t);if(!e)throw new Error("Only a scenario saved on this device can be shared.");const a=m();const s=await J(e,a);if(!s.ok)throw new Error(s.message);const r=s.value;this.teamRows=[r,...this.teamRows.filter(o=>o.id!==r.id)];const n=teamScenarioFromRow(r);this.setState({team:[n,...this.state.team.filter(o=>o.id!==r.id)],teamInfo:{...this.state.teamInfo,status:"ready",message:void 0,you:this.state.teamInfo.you??r.sharedBy.id,pulledAt:this.state.teamInfo.pulledAt??Date.now()}});await this.persistTeam(this.teamRows,v(a));return n}isMirror(){return d!==null}canRemoveFromTeam(t){if(d||t.source!=="team"||!t.team)return false;const{you:e,admin:a}=this.state.teamInfo;return a||!!e&&t.team.sharedBy.id===e}async removeFromTeam(t){await this.ensureLoaded();if(d)throw new Error("Remove it from the app on the device.");if(!this.state.team.some(s=>s.id===t))throw new Error("That scenario is not on the team's list.");const e=m();const a=await B(t,e);if(!a.ok)throw new Error(a.message);this.teamRows=this.teamRows.filter(s=>s.id!==t);this.setState({team:this.state.team.filter(s=>s.id!==t)});await this.persistTeam(this.teamRows,v(e))}async reconcileActive(){const t=this.state.active;if(!t)return;const e=t.effects.filter(a=>a.kind==="override-rule");if(!e.length)return;if(!R().includes("network"))return;try{const a=await f("network","listOverrideRules",{});const s=new Set((a?.rules??[]).map(n=>n.id));const r=t.effects.filter(n=>n.kind!=="override-rule"||s.has(n.ruleId));if(r.length===t.effects.length)return;if(!r.length){this.setState({active:null})}else{this.setState({active:{...t,effects:r}})}await this.persistActive()}catch{}}checkBeforeRun(t,e,a=[]){const s=this.state.active;const r=[...a];if(!k()){r.push("Scenarios cannot run in a production build.")}const n=D(t.vars,e);return _(t,n,{availableTools:R(),availableActions:C(),activeScenario:s?{scenarioId:s.scenarioId,name:s.name,conflictKeys:this.activeConflictKeys()}:null,extraErrors:r})}holdObservers(t){const e=[];const a=T();for(const s of t){const r=a[s]?.subscribe;if(!r)continue;try{e.push(r(()=>{}))}catch{}}return e}activeConflictKeys(){const t=this.state.active;if(!t)return[];const e=this.find(t.scenarioId);if(!e)return[];const a=new Set;for(const s of e.steps){for(const r of P(s,O(s.params,t.varValues))){a.add(r)}}return[...a]}async run(t,e={},a={}){y();if(d){return await d("run",{id:t,params:e})}await this.ensureLoaded();y();const s=this.find(t);if(!s)throw new Error(`No scenario "${t}"`);if(s.source==="draft"){throw new Error("Drafts must be accepted before they can run.")}if(!k()){throw new Error("Scenarios cannot run in a production build.")}if(!await K()){throw new Error("Buoy tools are still starting up \u2014 try again in a moment.")}const r=D(s.vars,e);if(this.state.active&&this.state.active.scenarioId!==t){await this.deactivate()}else if(this.state.active){const c=this.state.active;await p(c.effects.filter(u=>u.kind==="override-rule"),f);this.setState({active:{...c,effects:c.effects.filter(u=>u.kind!=="override-rule")}})}y();this.setState({runState:{scenarioId:t,name:S(s.name,r),steps:[],current:0,done:false}});const n=this.holdObservers(["route-events"]);let o;this.runController?.abort();const i=new AbortController;this.runController=i;try{o=await E(s,r,f,{signal:i.signal,onStep:c=>{const u=this.state.runState;if(u)this.setState({runState:{...u,current:c}})}})}finally{if(this.runController===i)this.runController=null;n.forEach(c=>{try{c()}catch{}})}this.setState({runState:{scenarioId:t,name:S(s.name,r),steps:o.steps,current:-1,done:true,report:o}});if(o.ok&&I()){const c={scenarioId:t,name:S(s.name,r),version:s.version,source:s.source,runAt:Date.now(),varValues:r,effects:o.effects,expiresAt:a.expiresInMs?Date.now()+a.expiresInMs:void 0,undoSteps:s.undoSteps};this.setState({active:c,lastDeactivateReport:null,lastVarValues:{...this.state.lastVarValues,[t]:r}});this.bumpUsage(t);this.armExpiry();await this.persistActive();await this.persistLibrary()}return o}async undoPartialRun(){const t=this.state.runState?.report;if(!t){return{removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]}}const e=await p(t.effects,f);const a={...e,undoStepsRan:0};this.setState({runState:null,lastDeactivateReport:a});return a}clearRunState(){this.setState({runState:null})}bumpUsage(t){const e=a=>a.map(s=>s.id===t?{...s,usage:{runs:(s.usage?.runs??0)+1,lastRunAt:Date.now()}}:s);this.setState({code:e(this.state.code),team:e(this.state.team),device:e(this.state.device)})}async deactivate(){if(d){return await d("deactivate",{})}const t=this.state.active;if(!t){const i={removedRules:0,stoppedImpersonation:false,clearedTransients:0,unrestoredStorageKeys:[],unrestoredState:[],undoStepsRan:0,errors:[]};return i}const e=await p(t.effects,f);const a=[...e.errors];let s=0;const r=this.find(t.scenarioId);const n=t.undoSteps??r?.undoSteps??[];if(n.length){const i=r??{id:t.scenarioId,name:t.name,version:t.version,source:t.source,vars:[],steps:[],createdAt:t.runAt,updatedAt:t.runAt,usage:{runs:0}};const c=await E({...i,steps:n},t.varValues,f);s=c.steps.filter(u=>u.ok).length;for(const u of c.steps){if(!u.ok&&u.error)a.push(u.error)}}const o={removedRules:e.removedRules,stoppedImpersonation:e.stoppedImpersonation,clearedTransients:e.clearedTransients,unrestoredStorageKeys:s?[]:e.unrestoredStorageKeys,unrestoredState:s?[]:e.unrestoredState,undoStepsRan:s,errors:a};this.clearExpiry();this.setState({active:null,lastDeactivateReport:o});await this.persistActive();return o}async disposeAccountSession(t){this.runController?.abort();this.runController=null;if(I())return;this.clearExpiry();const e=this.state.active;if(!e||d)return;const a=await p(e.effects.filter(s=>s.kind==="override-rule"),t);this.setState({active:null});await this.persistActive();if(a.errors.length)throw new Error(a.errors.join("; "))}dismissDeactivateReport(){this.setState({lastDeactivateReport:null})}armExpiry(){this.clearExpiry();const t=this.state.active?.expiresAt;if(!t)return;const e=Math.max(0,t-Date.now());this.expiryTimer=setTimeout(()=>{void this.deactivate()},e)}clearExpiry(){if(this.expiryTimer){clearTimeout(this.expiryTimer);this.expiryTimer=null}}isLongRunning(t=8*60*60*1e3){const e=this.state.active;return!!e&&Date.now()-e.runAt>t}async saveScenario(t,e={}){await this.ensureLoaded();const a=Date.now();const s=e.asDraft??false;const r=s?this.state.drafts:this.state.device;const n=r.findIndex(c=>c.id===t.id);const o={...t,steps:b(t.steps,"s"),undoSteps:t.undoSteps?b(t.undoSteps,"u"):void 0,folder:V(t.folder,this.folders()),source:s?"draft":"device",version:n>=0?(r[n].version??1)+1:t.version||1,createdAt:n>=0?r[n].createdAt:a,updatedAt:a,usage:n>=0?r[n].usage:{runs:0}};if(!s&&n<0&&this.state.device.length>=MAX_DEVICE_SCENARIOS){throw new Error(`This device already holds ${MAX_DEVICE_SCENARIOS} scenarios \u2014 delete one first.`)}const i=[...r];if(n>=0)i[n]=o;else i.unshift(o);this.setState(s?{drafts:i}:{device:i});await this.persistLibrary();return o}async acceptDraft(t){await this.ensureLoaded();const e=this.state.drafts.find(s=>s.id===t);if(!e)throw new Error(`No draft "${t}"`);const a={...e,source:"device",unsavedToRepo:true,updatedAt:Date.now()};this.setState({drafts:this.state.drafts.filter(s=>s.id!==t),device:[a,...this.state.device]});await this.persistLibrary();return a}async discardDraft(t){await this.ensureLoaded();this.setState({drafts:this.state.drafts.filter(e=>e.id!==t)});await this.persistLibrary()}async setFolder(t,e){await this.ensureLoaded();if(d){const i=await d("setFolder",{id:t,folder:e});return i.scenario}if(this.state.code.some(i=>i.id===t)){throw new Error("Scenarios shipped with the app are filed in code \u2014 change `folder` in defineScenario().")}if(this.state.team.some(i=>i.id===t)&&!this.state.device.some(i=>i.id===t)){throw new Error("A team scenario keeps the folder its sharer gave it. Make an editable copy to file it here.")}const a=V(e,this.folders());let s;const r=i=>i.map(c=>{if(c.id!==t)return c;s={...c,folder:a,updatedAt:Date.now()};return s});const n=r(this.state.device);const o=s?this.state.drafts:r(this.state.drafts);if(!s)throw new Error(`No scenario "${t}"`);this.setState({device:n,drafts:o});await this.persistLibrary();return s}async deleteScenario(t){await this.ensureLoaded();if(this.state.active?.scenarioId===t)await this.deactivate();this.setState({device:this.state.device.filter(e=>e.id!==t)});await this.persistLibrary()}rememberVarValues(t,e){this.setState({lastVarValues:{...this.state.lastVarValues,[t]:e}});void this.persistLibrary()}lastValuesFor(t){return this.state.lastVarValues[t]??{}}replaceFromSnapshot(t){this.setState({code:t.code??[],device:t.device??[],drafts:t.drafts??[],team:t.team??[],teamInfo:t.teamInfo??{status:"idle",you:null,admin:false},active:t.active??null,runState:t.runState??null,hasLoaded:true})}}export const scenariosStore=new Y;export function replaceScenariosFromSnapshot(l){scenariosStore.replaceFromSnapshot(l)}export function installedToolIds(){return Object.keys(T())}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{describeStep as d,stepDurability as u}from"../catalog";import{interpolate as h,resolveVarValues as
|
|
1
|
+
"use strict";import{describeStep as d,stepDurability as u}from"../catalog";import{interpolate as h,resolveVarValues as m}from"../interpolate";import{scenariosStore as a}from"../store/scenariosStore";const v=64*1024;const w=256*1024;function i(e,r){const t=e?.[r];return typeof t==="string"&&t.trim()?t.trim():void 0}function f(e){const r=e?.params;if(!r||typeof r!=="object")return{};const t={};for(const[o,n]of Object.entries(r)){if(typeof n==="string"||typeof n==="number"||typeof n==="boolean"){t[o]=n}}return t}const g=8*1024;function c(e){return{id:e.id,name:e.name,description:e.description,version:e.version,source:e.source,author:e.author,folder:e.folder,tags:e.tags,vars:e.vars,stepCount:e.steps.length,expectedOutcome:e.expectedOutcome,authoredAgainst:e.authoredAgainst,verifiedAt:e.verifiedAt,unsavedToRepo:e.unsavedToRepo,usage:e.usage,createdAt:e.createdAt,updatedAt:e.updatedAt,team:e.team,steps:e.steps.map(r=>{const t=JSON.stringify(r.params??{}).length;return t>g?{...r,params:void 0,paramsOmitted:true}:r}),stepLines:e.steps.map(r=>({label:d(r),durability:u(r)}))}}function S(e){const r=[];if(!e.id||typeof e.id!=="string"){r.push("A scenario needs a string `id`.")}if(!e.name||typeof e.name!=="string"){r.push("A scenario needs a `name`.")}if(!Array.isArray(e.steps)||!e.steps.length){r.push("A scenario needs at least one step.")}else{e.steps.forEach((o,n)=>{if(!o||typeof o!=="object"){r.push(`Step ${n+1} is not an object.`);return}if(!o.tool||!o.action){r.push(`Step ${n+1} needs both \`tool\` and \`action\`.`)}const s=JSON.stringify(o.params??{}).length;if(s>v){r.push(`Step ${n+1} is ${Math.round(s/1024)}KB \u2014 over the 64KB limit. Use a smaller response body.`)}})}if(e.vars&&!Array.isArray(e.vars)){r.push("`vars` must be an array.")}const t=JSON.stringify(e).length;if(t>w){r.push(`The scenario is ${Math.round(t/1024)}KB \u2014 over the 256KB limit.`)}return r}function p(){const e=a.getState();return{code:e.code.map(c),device:e.device.map(c),drafts:e.drafts.map(c),team:e.team.map(c),teamInfo:e.teamInfo,active:e.active,runState:e.runState,hasLoaded:e.hasLoaded,folders:a.folders()}}export const scenariosSyncAdapter={dispose:e=>a.disposeAccountSession(e),version:1,getSnapshot:()=>p(),subscribe:e=>a.subscribe(e),actions:{listScenarios:()=>p(),getScenario:e=>{const r=i(e,"id");if(!r)throw new Error("getScenario requires an `id`");const t=a.find(r);if(!t)throw new Error(`No scenario "${r}"`);return{scenario:t}},save:async e=>{const r=e??{};const t=r.scenario;if(!t)return{ok:false,error:"save requires a `scenario`."};const o=S(t);if(o.length)return{ok:false,errors:o};const n=Date.now();const s={id:t.id,name:t.name,description:t.description,version:t.version??1,source:"draft",author:t.author??"remote",vars:t.vars??[],steps:t.steps,undoSteps:t.undoSteps,folder:t.folder,tags:t.tags,expectedOutcome:t.expectedOutcome,authoredAgainst:t.authoredAgainst,unsavedToRepo:true,createdAt:n,updatedAt:n,usage:{runs:0}};const l=await a.saveScenario(s,{asDraft:true});return{ok:true,savedAsDraft:true,id:l.id,message:"Saved as a draft. Open Scenarios on the device to review and accept it."}},acceptDraft:async e=>{const r=i(e,"id");if(!r)throw new Error("acceptDraft requires an `id`");const t=await a.acceptDraft(r);return{ok:true,id:t.id}},discardDraft:async e=>{const r=i(e,"id");if(!r)throw new Error("discardDraft requires an `id`");await a.discardDraft(r);return{ok:true,id:r}},preview:e=>{const r=i(e,"id");if(!r)throw new Error("preview requires an `id`");const t=a.find(r);if(!t)throw new Error(`No scenario "${r}"`);const o=m(t.vars,f(e));const n=a.checkBeforeRun(t,o);return{willDo:t.steps.map(s=>({label:d(s,h(s.params??{},o)),durability:u(s)})),values:o,ok:n.ok,errors:n.errors,warnings:n.warnings,conflict:n.conflict,expectedOutcome:t.expectedOutcome}},run:async e=>{const r=i(e,"id");if(!r)throw new Error("run requires an `id`");const t=f(e);const o=a.find(r);if(!o)throw new Error(`No scenario "${r}"`);const n=a.checkBeforeRun(o,t);if(!n.ok){return{ok:false,preflightErrors:n.errors,conflict:n.conflict}}return await a.run(r,t)},deactivate:async()=>{const e=await a.deactivate();return{ok:true,report:e}},delete:async e=>{const r=i(e,"id");if(!r)throw new Error("delete requires an `id`");await a.deleteScenario(r);return{ok:true,id:r}},listFolders:()=>({folders:a.folders()}),setFolder:async e=>{const r=i(e,"id");if(!r)throw new Error("setFolder requires an `id`");const t=i(e,"folder");const o=await a.setFolder(r,t);return{ok:true,id:o.id,folder:o.folder??null}},getActive:()=>({active:a.getState().active}),export:e=>{const r=i(e,"id");if(!r)throw new Error("export requires an `id`");const t=a.find(r);if(!t)throw new Error(`No scenario "${r}"`);return{json:JSON.stringify(t,null,2)}}}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";import{getBuoyAppId as d,getTeamCredential as y}from"@buoy-gg/shared-ui";const g=15e3;export const NO_TEAM_MESSAGE="Sign in with a Business team account to share scenarios with your team.";export const NO_APP_MESSAGE="Buoy can't tell which app this is. Pass signIn={{ appId }} to FloatingDevTools to name it.";export function defaultTeamClientDeps(){return{credential:y,appId:d,fetch:(o,e)=>fetch(o,e)}}export function teamAppId(o){return o.credential()?.appId??o.appId()}async function u(o,e,t){const a=t.credential();if(!a)return{ok:false,code:"no-team",message:NO_TEAM_MESSAGE};const c=a.appId??t.appId();if(!c)return{ok:false,code:"no-app",message:NO_APP_MESSAGE};const n=typeof AbortController!=="undefined"?new AbortController:void 0;const f=setTimeout(()=>n?.abort(),g);let s;let r;try{const l=await t.fetch(`${a.origin.replace(/\/$/,"")}${o}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:a.token,app:c,...e}),signal:n?.signal});s=l.status;r=await l.json().catch(()=>null)}catch{return{ok:false,code:"network",message:"Buoy could not reach buoy.gg. Check the connection and try again."}}finally{clearTimeout(f)}if(s===200&&r&&typeof r==="object")return{ok:true,value:r};const i=r?.error_description;if(s>=500||s===429){return{ok:false,code:"network",message:typeof i==="string"?i:"buoy.gg is busy. Try again in a minute."}}return{ok:false,code:"refused",message:typeof i==="string"?i:"buoy.gg said no to that."}}function p(o){const e=o;return!!e&&typeof e.id==="string"&&!!e.scenario&&typeof e.scenario==="object"&&Array.isArray(e.scenario.steps)}export async function pullTeamScenarios(o=defaultTeamClientDeps()){const e=await u("/api/scenarios/team/pull",{},o);if(!e.ok)return e;const t=e.value;return{ok:true,value:{scenarios:Array.isArray(t.scenarios)?t.scenarios.filter(p):[],you:typeof t.you==="string"?t.you:"",admin:t.admin===true}}}export function forSharing(o){const{usage:e,source:t,verifiedAt:a,team:c,...n}=o;return n}export async function shareTeamScenario(o,e=defaultTeamClientDeps()){const t=await u("/api/scenarios/team/share",{scenario:forSharing(o)},e);if(!t.ok)return t;return p(t.value.scenario)?{ok:true,value:t.value.scenario}:{ok:false,code:"refused",message:"buoy.gg sent back a scenario this app could not read."}}export async function removeTeamScenario(o,e=defaultTeamClientDeps()){const t=await u("/api/scenarios/team/remove",{id:o},e);return t.ok?{ok:true,value:true}:t}
|
|
@@ -14,7 +14,15 @@ export interface ScenarioDetailScreenProps {
|
|
|
14
14
|
onExported?: () => void;
|
|
15
15
|
/** Opens the folder picker. Absent for scenarios that can't be re-filed. */
|
|
16
16
|
onMoveToFolder?: () => void;
|
|
17
|
+
/** Shares a device scenario with the team. Absent when this person isn't on a team. */
|
|
18
|
+
onShare?: () => void;
|
|
19
|
+
/** Takes a team scenario off the team's list. Present for its sharer and for admins. */
|
|
20
|
+
onRemoveFromTeam?: () => void;
|
|
21
|
+
/** A share or removal is on its way to buoy.gg. */
|
|
22
|
+
teamBusy?: boolean;
|
|
17
23
|
onBack: () => void;
|
|
18
24
|
}
|
|
19
|
-
|
|
25
|
+
/** Where a scenario came from, in words. */
|
|
26
|
+
export declare function sourceText(scenario: Scenario): string;
|
|
27
|
+
export declare function ScenarioDetailScreen({ scenario, onRun, onDelete, onDuplicate, onExported, onMoveToFolder, onShare, onRemoveFromTeam, teamBusy, onBack, }: ScenarioDetailScreenProps): import("react").JSX.Element;
|
|
20
28
|
//# sourceMappingURL=ScenarioDetailScreen.d.ts.map
|
|
@@ -21,7 +21,7 @@ export { condense, macroToSteps, describeTarget, selectorFor, humanizeTestID, ti
|
|
|
21
21
|
export type { MacroLane, MacroStep, RecordedAction, RecordedTouch, RecordingState, RecorderStatus, TouchTargetInfo, } from "./record/types";
|
|
22
22
|
export { scenariosSyncAdapter } from "./sync/scenariosSyncAdapter";
|
|
23
23
|
export { ScenariosToolView } from "./components/ScenariosToolView";
|
|
24
|
-
export { scenariosStore, setRemoteScenariosHandler, replaceScenariosFromSnapshot, MAX_DEVICE_SCENARIOS, type ScenariosState, type RunState, } from "./store/scenariosStore";
|
|
24
|
+
export { scenariosStore, setRemoteScenariosHandler, replaceScenariosFromSnapshot, MAX_DEVICE_SCENARIOS, type ScenariosState, type RunState, type TeamInfo, } from "./store/scenariosStore";
|
|
25
25
|
export { useScenariosState, useActiveScenario, useScenariosRunState, } from "./hooks/useScenarios";
|
|
26
26
|
export { defineScenario, type ScenarioDefinition } from "./defineScenario";
|
|
27
27
|
export { runScenario, revertEffects, preflight, STEP_TIMEOUT_MS, SCENARIO_TIMEOUT_MS, type DispatchToolAction, type PreflightResult, type PreflightContext, } from "./engine";
|