From 21f2b45650912d2520a21ad7c7bfa6ddb43bc8ce Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 10 Oct 2025 19:29:11 -0700 Subject: [PATCH 01/23] Add snapshots tab and arrowDownUp icon --- app/components/Icon.tsx | 1 + app/screens/StateScreen.tsx | 119 ++++++++++++++++++++++---------- assets/icons/arrowDownUp.png | Bin 0 -> 275 bytes assets/icons/arrowDownUp@2x.png | Bin 0 -> 362 bytes assets/icons/arrowDownUp@3x.png | Bin 0 -> 360 bytes 5 files changed, 83 insertions(+), 37 deletions(-) create mode 100644 assets/icons/arrowDownUp.png create mode 100644 assets/icons/arrowDownUp@2x.png create mode 100644 assets/icons/arrowDownUp@3x.png diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx index 13b5077..a96ddb8 100644 --- a/app/components/Icon.tsx +++ b/app/components/Icon.tsx @@ -66,6 +66,7 @@ export function Icon(props: IconProps) { } export const iconRegistry = { + arrowDownUp: require("../../assets/icons/arrowDownUp.png"), chevronsLeftRightEllipsis: require("../../assets/icons/chevronsLeftRightEllipsis.png"), circleGauge: require("../../assets/icons/circleGauge.png"), clipboard: require("../../assets/icons/clipboard.png"), diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 9786299..00fcf6e 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -8,9 +8,13 @@ import { Divider } from "../components/Divider" import { useKeyboardEvents } from "../utils/system" import type { StateSubscription } from "app/types" import { Icon } from "../components/Icon" +import { Tab } from "../components/Tab" + +type StateTab = "Subscriptions" | "Snapshots" export function StateScreen() { const [showAddSubscription, setShowAddSubscription] = useState(false) + const [activeStateTab, setActiveStateTab] = useGlobal("activeStateTab", "Subscriptions") const [stateSubscriptionsByClientId, setStateSubscriptionsByClientId] = useGlobal<{ [clientId: string]: StateSubscription[] @@ -52,49 +56,84 @@ export function StateScreen() { State - - setShowAddSubscription(true)}> - Add Subscription - - { - setStateSubscriptionsByClientId((prev) => ({ - ...prev, - [activeTab]: [], - })) - sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) - setActiveTab("") - }} - > - Clear State - - + {activeStateTab === "Subscriptions" ? ( + + setShowAddSubscription(true)}> + Add Subscription + + { + setStateSubscriptionsByClientId((prev) => ({ + ...prev, + [activeTab]: [], + })) + sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) + setActiveTab("") + }} + > + Clear State + + + ) : ( + + { + // TODO: Implement copy all snapshots to clipboard + console.log("Copy all snapshots to clipboard") + }} + > + Copy All Snapshots + + { + // TODO: Implement add snapshot + console.log("Add snapshot") + }} + > + Add Snapshot + + + )} + + + + - {clientStateSubscriptions.length > 0 ? ( + {activeStateTab === "Subscriptions" ? ( <> - {clientStateSubscriptions.map((subscription, index) => ( - - - {subscription.path ? subscription.path : "Full State"} - - - - + {clientStateSubscriptions.length > 0 ? ( + <> + {clientStateSubscriptions.map((subscription, index) => ( + + + {subscription.path ? subscription.path : "Full State"} + + + + + + removeSubscription(subscription.path)}> + + + + {index < clientStateSubscriptions.length - 1 && ( + + )} - removeSubscription(subscription.path)}> - - - - {index < clientStateSubscriptions.length - 1 && ( - - )} - - ))} + ))} + + ) : ( + State is empty + )} ) : ( - State is empty + + Snapshots will be displayed here + )} @@ -203,6 +242,12 @@ const $title = themed(({ colors, spacing, typography }) => ({ marginTop: spacing.xl, })) +const $tabsContainer = themed(({ spacing }) => ({ + flexDirection: "row", + marginTop: spacing.lg, + marginBottom: spacing.md, +})) + const $stateItemContainer = themed(({ spacing }) => ({ marginTop: spacing.xl, })) diff --git a/assets/icons/arrowDownUp.png b/assets/icons/arrowDownUp.png new file mode 100644 index 0000000000000000000000000000000000000000..460a1b4bdd7d183b5e657ec58d86635d5a7cb5c9 GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;w-2k5uS0GJ1IJiK=4yc;HB*-tA z;VU1@KfyB>KVJE8=zt>k!@#9AK&duQ7sn8Z%gG10dm3*Xm?3${^6-HJXRM45FdRuM zo-i|Re;u2$xWKHgJsUtF=IQF^ Jvd$@?2>=q@f_(r0 literal 0 HcmV?d00001 diff --git a/assets/icons/arrowDownUp@3x.png b/assets/icons/arrowDownUp@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..13adf3d344e1b3f530b1645626b5329054a2e140 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI76a#!hT!A!xkbBYNAW#KYNswPK z1Kaj@{hQA6Jh-cPjVBZ+`o`16F~sBe+o>Bl4;u)$KIZM|WDl5aHh1?HIYsW2W}eBH z$^`RHz34k2ZnN4XPi4YK_x^U?<-5J3-PgSS8+dH{-m(QZw<(p%|NhpZ7Ts+!D^e*X zt@Xvs$CtttI+v_+xO>(|%R$J~)kA3|ho^{^(=XGN44zw_yv>;*>A(P@2&IXZlzsWYn@)ow`-Z~)>yRK%xTrWj*osF8y$JAIFt4YB|Q~>B+GQ< zw#SO9^_&}>1(Mb)9$Dww@wZ>VT2t{`iqqA1ejUY40_uGVt5lSZXr7QfS$Qh+^vN|| dIj Date: Fri, 10 Oct 2025 19:37:23 -0700 Subject: [PATCH 02/23] Add EmptyState component and update StateScreen UI --- app/components/EmptyState.tsx | 44 ++++++++++++++++++++++++++++++++ app/screens/StateScreen.tsx | 9 ++++--- assets/icons/arrowDownUp.png | Bin 275 -> 577 bytes assets/icons/arrowDownUp@2x.png | Bin 362 -> 953 bytes assets/icons/arrowDownUp@3x.png | Bin 360 -> 972 bytes 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 app/components/EmptyState.tsx diff --git a/app/components/EmptyState.tsx b/app/components/EmptyState.tsx new file mode 100644 index 0000000..2335acc --- /dev/null +++ b/app/components/EmptyState.tsx @@ -0,0 +1,44 @@ +import { Text, View, ViewStyle, TextStyle } from "react-native" +import { themed } from "../theme/theme" +import { Icon, IconTypes } from "./Icon" + +interface EmptyStateProps { + icon: IconTypes + title: string + description: string +} + +export function EmptyState({ icon, title, description }: EmptyStateProps) { + return ( + + + {title} + {description} + + ) +} + +const $container = themed(({ spacing }) => ({ + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingVertical: spacing.xxxl, + paddingHorizontal: spacing.xl, +})) + +const $title = themed(({ colors, typography, spacing }) => ({ + fontSize: typography.heading, + fontWeight: "600", + color: colors.neutral, + marginTop: spacing.lg, + marginBottom: spacing.sm, +})) + +const $description = themed(({ colors, typography, spacing }) => ({ + fontSize: typography.body, + fontWeight: "400", + color: colors.neutral, + textAlign: "center", + maxWidth: 400, + lineHeight: typography.body * 1.5, +})) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 00fcf6e..4e84d4c 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -9,6 +9,7 @@ import { useKeyboardEvents } from "../utils/system" import type { StateSubscription } from "app/types" import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" +import { EmptyState } from "../components/EmptyState" type StateTab = "Subscriptions" | "Snapshots" @@ -131,9 +132,11 @@ export function StateScreen() { )} ) : ( - - Snapshots will be displayed here - + )} diff --git a/assets/icons/arrowDownUp.png b/assets/icons/arrowDownUp.png index 460a1b4bdd7d183b5e657ec58d86635d5a7cb5c9..5f14ca47c6a345bb2255bc6d62f79d9725149af2 100644 GIT binary patch literal 577 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!Vgr0aT!A$0z)p6N4?xr9N`m}? z8GiA<4`ma&>34Oi&juHf&oACRSR5pMXmv|nOt9p*ST! z>}+9tkj7q?z`l=J;xY3Ui5=lr-|`wvSsoa6qJf>iSc&2E`$iwOm}B`Tg&E529R--y zecKz@@tjwv+}e@?yid8g`BPquTQgy8Aw=d#Wzp$Py&@yv(- literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;w-2k5uS0GJ1IJiK=4yc;HB*-tA z;VU1@KfyB>KVJE8=zt>k!@#9AK&duQ7sn8Z%gG10dm3*Xm?3${^6-HJXRM45FdRuM zMQ z#t5Bh@)K`a&y;z;H|XywomEHIP2?;r(z;%GuT1LiX`Mf(pP4NEzm{wI8_{Ry4@FK$ zU0AyPyZq|Ui%zeazMC=iMY#fZ`|RKKqJ@D!YTv()7yOm-F!D~zzZur7cKr>KjBdX+ zvY%wU#R_{k3P2mZ=%d||w^KjKLKYRR{aW(~iu`*JqipX4BZ;P25ugR(eo&Je~s z{|%;RH;4I>gA8-6)I{i=5`dp}3@frcIbD{P*8 zVJNMRWMgDbs;?NBw-e38Rh#!-f9ek9RSMUAf+% zeD-}wc>`;7{L%HNpEW+e&h|l$VM9h`T`2?ix;5pI;0d z$Da9fCNQ3#FLZ!up1o=Vt4)0r19!#0FAN+YLhQqzmy7~X;xU8Gs`Yy^I1T<=Jg{7+ zl@%xu@IQjDq+q5L&u5`c|3s$AU$hsy{^&(n`M3Z2&!m4HFFy3}>%WrRdFxMKo>KSo o(~o(7>O~8f4}(&s*lGV0@fm?hXGIvxPk}O^r>mdKI;Vst0JCs~?f?J) literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sW&u7Su0Wa$u)Qp!2&hf4B*-tA z;VTzQ+|S3+-$J?H6=wSS@Uh)HFa;=i$o-i|Re;u2$xWKHgJsUtF=IQF^ Jvd$@?2>=q@f_(r0 diff --git a/assets/icons/arrowDownUp@3x.png b/assets/icons/arrowDownUp@3x.png index 13adf3d344e1b3f530b1645626b5329054a2e140..994ae2730d972140630dabff63388e0c71d3dc3f 100644 GIT binary patch literal 972 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1#sNMdu0Wbpu$a~DBv6k)NswPK z1NZ*Jyi<>U(`CPU?fm;^K^w*GUNSH+TX?!ShE&{obL(tgvw;9hLQkRLll=StGgpTN z%0{FvpEx;{-?-fEGb5*rghX2+0zKQm@~O%B9U*dB^^gUi^Hq zHJeV}+Iv6R;Ivxt`?~VRHnnJjm~PeSbZ`Xx)^&7fxRuZKi1A8YumJyrO?xKHS;TndXZv*pF^BiZn>-kj z_bYZV|F~JdgMs0~zsIZ$4ByxVBpCMCH#so=`j&n(m4zvu;hz0TJx-1q#)|##m_gk2 zzZ(vSH++9>r|^OG!QWl{Fsb?jpHk|pE~W1+=|9cbko4|)?Xm?wm{;wM|9-RS6`#hp zy1VJzUDgR(fjXl%>|#y}JURIy`@en97#eIA?o;Dni0J*z{%vW^AKj3izdQ7w9c1xcAMhH|p zGwU&Li96qLy5agCwFk!-Rrvy>1+$s2w0*h7u+{LSnL}>CR^9+<(LA8gR>K5)z5o5k zp0YCdAN+TyiGd+b((=FPqUmLQ|2y*cnH^=!62A`Awz^rK?M;5tx=Cki{TX(6Ztyzr zlaYZVjMKuNf#IMQ<8yWfg$UILe;628R`1N%Uwi+B4zyIKBl4;u)$KIZM|WDl5aHh1?HIYsW2W}eBH z$^`RHz34k2ZnN4XPi4YK_x^U?<-5J3-PgSS8+dH{-m(QZw<(p%|NhpZ7Ts+!D^e*X zt@Xvs$CtttI+v_+xO>(|%R$J~)kA3|ho^{^(=XGN44zw_yv>;*>A(P@2&IXZlzsWYn@)ow`-Z~)>yRK%xTrWj*osF8y$JAIFt4YB|Q~>B+GQ< zw#SO9^_&}>1(Mb)9$Dww@wZ>VT2t{`iqqA1ejUY40_uGVt5lSZXr7QfS$Qh+^vN|| dIj Date: Wed, 15 Oct 2025 20:38:14 -0700 Subject: [PATCH 03/23] Moving forward with implementation --- app/screens/StateScreen.tsx | 222 +++++++++++++++++++++++++++++++---- app/state/connectToServer.ts | 45 +++++++ app/types.ts | 9 ++ 3 files changed, 254 insertions(+), 22 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 4e84d4c..2539594 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -6,10 +6,12 @@ import { TreeViewWithProvider } from "../components/TreeView" import { useState } from "react" import { Divider } from "../components/Divider" import { useKeyboardEvents } from "../utils/system" -import type { StateSubscription } from "app/types" +import type { StateSubscription, Snapshot, Command, CommandType } from "app/types" import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" import { EmptyState } from "../components/EmptyState" +import IRClipboard from "../native/IRClipboard/NativeIRClipboard" +import IRRunShellCommand from "../native/IRRunShellCommand/NativeIRRunShellCommand" type StateTab = "Subscriptions" | "Snapshots" @@ -21,6 +23,8 @@ export function StateScreen() { [clientId: string]: StateSubscription[] }>("stateSubscriptionsByClientId", {}) const [activeTab, setActiveTab] = useGlobal("activeClientId", "") + const [snapshots, setSnapshots] = useGlobal("snapshots", []) + const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] @@ -44,6 +48,86 @@ export function StateScreen() { })) } + const createSnapshot = () => { + if (!activeTab) { + console.log("No active client to create snapshot from") + return + } + sendToCore("state.backup.request", { clientId: activeTab }) + } + + const copySnapshotToClipboard = (snapshot: Snapshot) => { + try { + IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) + console.log("Snapshot copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshot to clipboard:", error) + } + } + + const copyAllSnapshotsToClipboard = () => { + try { + console.log("Copying all snapshots to clipboard", snapshots) + IRClipboard.setString(JSON.stringify(snapshots, null, 2)) + console.log("All snapshots copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshots to clipboard:", error) + } + } + + const downloadSnapshot = async (snapshot: Snapshot) => { + try { + const homeDir = IRRunShellCommand.runSync("echo $HOME").trim() + const downloadDir = `${homeDir}/Downloads` + const filename = `snapshot-${snapshot.name.replace(/\s+/g, "-")}-${Date.now()}.json` + const data = JSON.stringify(snapshot.state, null, 2) + + // Create a temporary file with the data using echo and output redirection + // We need to escape special characters for shell + const escapedData = data.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$") + const command = `echo "${escapedData}" > "${downloadDir}/${filename}"` + + IRRunShellCommand.runSync(command) + console.log(`Snapshot downloaded to ${downloadDir}/${filename}`) + } catch (error) { + console.error("Failed to download snapshot:", error) + } + } + + const downloadAllSnapshots = async () => { + try { + const homeDir = IRRunShellCommand.runSync("echo $HOME").trim() + const downloadDir = `${homeDir}/Downloads` + const filename = `snapshots-all-${Date.now()}.json` + const data = JSON.stringify(snapshots, null, 2) + + // Create a temporary file with the data + const escapedData = data.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$") + const command = `echo "${escapedData}" > "${downloadDir}/${filename}"` + + IRRunShellCommand.runSync(command) + console.log(`All snapshots downloaded to ${downloadDir}/${filename}`) + } catch (error) { + console.error("Failed to download snapshots:", error) + } + } + + const deleteSnapshot = (snapshotId: string) => { + setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) + } + + const toggleSnapshotExpanded = (snapshotId: string) => { + setExpandedSnapshotIds((prev) => { + const newSet = new Set(prev) + if (newSet.has(snapshotId)) { + newSet.delete(snapshotId) + } else { + newSet.add(snapshotId) + } + return newSet + }) + } + if (showAddSubscription) { return ( ) : ( - { - // TODO: Implement copy all snapshots to clipboard - console.log("Copy all snapshots to clipboard") - }} - > - Copy All Snapshots + + Copy All - { - // TODO: Implement add snapshot - console.log("Add snapshot") - }} - > - Add Snapshot + + Download All + + + Create Snapshot )} @@ -132,11 +207,65 @@ export function StateScreen() { )} ) : ( - + <> + {snapshots.length > 0 ? ( + <> + {snapshots.map((snapshot, index) => ( + + toggleSnapshotExpanded(snapshot.id)} + > + + {snapshot.name} + + + { + e.stopPropagation() + copySnapshotToClipboard(snapshot) + }} + > + + + { + e.stopPropagation() + downloadSnapshot(snapshot) + }} + > + + + { + e.stopPropagation() + deleteSnapshot(snapshot.id) + }} + > + + + + + {expandedSnapshotIds.has(snapshot.id) && ( + + + + )} + {index < snapshots.length - 1 && } + + ))} + + ) : ( + + )} + )} @@ -354,3 +483,52 @@ const $subscriptionButton = themed(({ colors, spacing }) => ({ const $stateDivider = themed(({ spacing }) => ({ marginTop: spacing.lg, })) + +const $snapshotItemContainer = themed(({ spacing }) => ({ + marginTop: spacing.md, +})) + +const $snapshotHeader = themed(({ spacing, colors }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.sm, + backgroundColor: colors.cardBackground, + borderRadius: 8, + cursor: "pointer", +})) + +const $snapshotInfo = themed(() => ({ + flex: 1, +})) + +const $snapshotName = themed(({ colors, typography }) => ({ + fontSize: typography.body, + fontWeight: "600", + color: colors.mainText, + fontFamily: typography.code.normal, +})) + +const $snapshotActions = themed(({ spacing }) => ({ + flexDirection: "row", + gap: spacing.xs, + alignItems: "center", +})) + +const $iconButton = themed(({ spacing, colors }) => ({ + padding: spacing.xs, + borderRadius: 4, + cursor: "pointer", + backgroundColor: colors.neutralVery, +})) + +const $snapshotContent = themed(({ spacing, colors }) => ({ + marginTop: spacing.sm, + padding: spacing.md, + backgroundColor: colors.background, + borderRadius: 8, +})) + +const $snapshotDivider = themed(({ spacing }) => ({ + marginTop: spacing.md, +})) diff --git a/app/state/connectToServer.ts b/app/state/connectToServer.ts index e67bf04..df2ee0e 100644 --- a/app/state/connectToServer.ts +++ b/app/state/connectToServer.ts @@ -40,6 +40,7 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub const [_customCommands, setCustomCommands] = withGlobal("customCommands", [], { persist: true, }) + const [_snapshots, setSnapshots] = withGlobal("snapshots", []) ws.socket = new WebSocket(`ws://localhost:${props.port}`) if (!ws.socket) throw new Error("Failed to connect to Reactotron server") @@ -188,6 +189,50 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub setCustomCommands((prev) => prev.filter((cmd) => cmd.id !== commandId)) return } + + // Handle state backup response + if (data.cmd.type === "state.backup.response") { + console.log("Received state.backup.response:", data.cmd) + setSnapshots((prev) => { + // Use the server-provided date to check for duplicates + const serverDate = data.cmd.date + const clientId = data.cmd.clientId + + // Check if we already have a snapshot with the same server date and clientId + const existingSnapshot = prev.find( + (s) => + s.clientId === clientId && + new Date(s.date).getTime() === new Date(serverDate).getTime(), + ) + + if (existingSnapshot) { + console.log("Duplicate snapshot detected, skipping:", { serverDate, clientId }) + return prev + } + + // Format the date as "Wednesday @ 5:00:15 PM" + const snapshotDate = new Date(serverDate) + const dayName = snapshotDate.toLocaleDateString("en-US", { weekday: "long" }) + const timeString = snapshotDate.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true, + }) + const snapshotName = `${dayName} @ ${timeString}` + + const newSnapshot = { + id: `${Date.now()}-${clientId}`, + name: snapshotName, + date: snapshotDate, + state: data.cmd.payload?.state || data.cmd.payload, + clientId: clientId, + } + console.log("Adding snapshot:", newSnapshot) + return [...prev, newSnapshot] + }) + return + } } console.log(data) diff --git a/app/types.ts b/app/types.ts index c1c867d..7d2cdc8 100644 --- a/app/types.ts +++ b/app/types.ts @@ -174,3 +174,12 @@ export type CustomCommand = { }> clientId?: string } + +// Snapshot represents a captured state snapshot that can be saved, restored, or exported +export type Snapshot = { + id: string + name: string + date: Date + state: Record // The actual state data (must be JSON-serializable) + clientId?: string +} From 35a0c5285d302d804dc4645289e3d054c486bf2e Mon Sep 17 00:00:00 2001 From: fpena Date: Wed, 15 Oct 2025 20:53:42 -0700 Subject: [PATCH 04/23] Add snapshot renaming with tooltip and input --- app/screens/StateScreen.tsx | 121 +++++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 2539594..7f9d73f 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -12,6 +12,7 @@ import { Tab } from "../components/Tab" import { EmptyState } from "../components/EmptyState" import IRClipboard from "../native/IRClipboard/NativeIRClipboard" import IRRunShellCommand from "../native/IRRunShellCommand/NativeIRRunShellCommand" +import { Tooltip } from "../components/Tooltip" type StateTab = "Subscriptions" | "Snapshots" @@ -25,6 +26,8 @@ export function StateScreen() { const [activeTab, setActiveTab] = useGlobal("activeClientId", "") const [snapshots, setSnapshots] = useGlobal("snapshots", []) const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) + const [renamingSnapshotId, setRenamingSnapshotId] = useState(null) + const [renameValue, setRenameValue] = useState("") const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] @@ -116,6 +119,17 @@ export function StateScreen() { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } + const renameSnapshot = (snapshotId: string, newName: string) => { + setSnapshots((prev) => prev.map((s) => (s.id === snapshotId ? { ...s, name: newName } : s))) + setRenamingSnapshotId(null) + setRenameValue("") + } + + const startRenaming = (snapshot: Snapshot) => { + setRenamingSnapshotId(snapshot.id) + setRenameValue(snapshot.name) + } + const toggleSnapshotExpanded = (snapshotId: string) => { setExpandedSnapshotIds((prev) => { const newSet = new Set(prev) @@ -217,36 +231,75 @@ export function StateScreen() { onPress={() => toggleSnapshotExpanded(snapshot.id)} > - {snapshot.name} + {renamingSnapshotId === snapshot.id ? ( + { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } else { + setRenamingSnapshotId(null) + setRenameValue("") + } + }} + onSubmitEditing={() => { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } + }} + /> + ) : ( + {snapshot.name} + )} - { - e.stopPropagation() - copySnapshotToClipboard(snapshot) - }} - > - - - { - e.stopPropagation() - downloadSnapshot(snapshot) - }} - > - - - { - e.stopPropagation() - deleteSnapshot(snapshot.id) - }} - > - - + + { + e.stopPropagation() + startRenaming(snapshot) + }} + > + + + + + { + e.stopPropagation() + downloadSnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + copySnapshotToClipboard(snapshot) + }} + > + + + + + { + e.stopPropagation() + deleteSnapshot(snapshot.id) + }} + > + + + {expandedSnapshotIds.has(snapshot.id) && ( @@ -532,3 +585,15 @@ const $snapshotContent = themed(({ spacing, colors }) => ({ const $snapshotDivider = themed(({ spacing }) => ({ marginTop: spacing.md, })) + +const $renameInput = themed(({ colors, typography, spacing }) => ({ + fontSize: typography.body, + fontWeight: "600", + color: colors.mainText, + fontFamily: typography.code.normal, + padding: spacing.xs, + backgroundColor: colors.background, + borderRadius: 4, + borderWidth: 1, + borderColor: colors.primary, +})) From 01d64acf01dfaaf636b5f29afa56a5f401e59e92 Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 16 Oct 2025 20:33:33 -0700 Subject: [PATCH 05/23] Moving forward --- app/components/EmptyState.tsx | 44 ---------------------------------- app/components/Icon.tsx | 1 + app/screens/StateScreen.tsx | 12 ++++------ assets/icons/pen.png | Bin 0 -> 312 bytes assets/icons/pen@2x.png | Bin 0 -> 498 bytes assets/icons/pen@3x.png | Bin 0 -> 588 bytes 6 files changed, 6 insertions(+), 51 deletions(-) delete mode 100644 app/components/EmptyState.tsx create mode 100644 assets/icons/pen.png create mode 100644 assets/icons/pen@2x.png create mode 100644 assets/icons/pen@3x.png diff --git a/app/components/EmptyState.tsx b/app/components/EmptyState.tsx deleted file mode 100644 index 2335acc..0000000 --- a/app/components/EmptyState.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Text, View, ViewStyle, TextStyle } from "react-native" -import { themed } from "../theme/theme" -import { Icon, IconTypes } from "./Icon" - -interface EmptyStateProps { - icon: IconTypes - title: string - description: string -} - -export function EmptyState({ icon, title, description }: EmptyStateProps) { - return ( - - - {title} - {description} - - ) -} - -const $container = themed(({ spacing }) => ({ - flex: 1, - justifyContent: "center", - alignItems: "center", - paddingVertical: spacing.xxxl, - paddingHorizontal: spacing.xl, -})) - -const $title = themed(({ colors, typography, spacing }) => ({ - fontSize: typography.heading, - fontWeight: "600", - color: colors.neutral, - marginTop: spacing.lg, - marginBottom: spacing.sm, -})) - -const $description = themed(({ colors, typography, spacing }) => ({ - fontSize: typography.body, - fontWeight: "400", - color: colors.neutral, - textAlign: "center", - maxWidth: 400, - lineHeight: typography.body * 1.5, -})) diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx index a96ddb8..f2b1617 100644 --- a/app/components/Icon.tsx +++ b/app/components/Icon.tsx @@ -74,6 +74,7 @@ export const iconRegistry = { messageSquare: require("../../assets/icons/messageSquare.png"), panelLeftClose: require("../../assets/icons/panelLeftClose.png"), panelLeftOpen: require("../../assets/icons/panelLeftOpen.png"), + pen: require("../../assets/icons/pen.png"), plug: require("../../assets/icons/plug.png"), questionMark: require("../../assets/icons/questionMark.png"), scrollText: require("../../assets/icons/scrollText.png"), diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 7f9d73f..46771c1 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -9,7 +9,6 @@ import { useKeyboardEvents } from "../utils/system" import type { StateSubscription, Snapshot, Command, CommandType } from "app/types" import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" -import { EmptyState } from "../components/EmptyState" import IRClipboard from "../native/IRClipboard/NativeIRClipboard" import IRRunShellCommand from "../native/IRRunShellCommand/NativeIRRunShellCommand" import { Tooltip } from "../components/Tooltip" @@ -264,7 +263,7 @@ export function StateScreen() { startRenaming(snapshot) }} > - + @@ -312,11 +311,10 @@ export function StateScreen() { ))} ) : ( - + + To take a snapshot of your current redux or mobx-state-tree store, press the Create + Snapshot button in the top right corner of this window. + )} )} diff --git a/assets/icons/pen.png b/assets/icons/pen.png new file mode 100644 index 0000000000000000000000000000000000000000..510b366a5762e923cfc1fb07fcd7ba2f63506eeb GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAc?bA}xB_WvfoBO;B|x(zN`m}? z8F<+*3Gtshb>{JI-HG)--@YlZF`L%cD{@z|Ss$pT#nZ(xMC1J4bDn&M3%p&Rsq-$-buet#)5|5s8x>2bWdM#=YA|DMj*Dc`zYm5q^Yxvcd133o4QyQl19ExM$~ nJ^!ZCsm4vmbQey)a(xwh?tKosb;{4ZK>qi1^>bP0l+XkKWF=lj literal 0 HcmV?d00001 diff --git a/assets/icons/pen@2x.png b/assets/icons/pen@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4551d2610917ca418dbbcca35f2e2f2cb418173e GIT binary patch literal 498 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sr2#%6u0Wdppfa*H0%)sRNswPK z!|&wSw2<%VkvG_)-fgqKvS~%Pp*o+_(LIYR!~8`!*&jb_td*6n2tRV1fq{|D)5S5w z@9?6My{@D2N-ICdLZHwkEIrOZ! kVbk2~T}du(_a4{%WwJ;zl6lo#tPYAfPgg&ebxsLQ07pi$fdBvi literal 0 HcmV?d00001 diff --git a/assets/icons/pen@3x.png b/assets/icons/pen@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..91bb9b0833c44d261a97e687e73f2f745b1462e2 GIT binary patch literal 588 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAjKV}MVHE07*okSnU23Uq;1NswPK z1J~Xy?1xx*)-PZ4?ZUDN@)Exvr#Rj@y?;i%_46~w%i=r@-*x493$<107<`Y`Vqjnl z@N{tui8%cB`t6`*1CiE;b1%8+aEQFL8VkQW{^DrA>+U6^Wqj)8Je`Dj+z(meFPvMwap@Xm{T-Ya`rZ_@EuPC0 zpX_XTS62P~gOd;b%eL;&-|i*o(%08zw{F?lD(=HaCvw|mhpO&~ve+ZPg-K*)T(GWl z_u1=d)s8YHK0yx`Rt9AiuG&|*= Date: Thu, 16 Oct 2025 20:42:12 -0700 Subject: [PATCH 06/23] Refactor snapshot list UI with card layout --- app/screens/StateScreen.tsx | 204 ++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 103 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 46771c1..0f1124b 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -224,89 +224,91 @@ export function StateScreen() { {snapshots.length > 0 ? ( <> {snapshots.map((snapshot, index) => ( - - toggleSnapshotExpanded(snapshot.id)} - > - - {renamingSnapshotId === snapshot.id ? ( - { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } else { - setRenamingSnapshotId(null) - setRenameValue("") - } - }} - onSubmitEditing={() => { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } - }} - /> - ) : ( - {snapshot.name} - )} - - - - { - e.stopPropagation() - startRenaming(snapshot) - }} - > - - - - - { - e.stopPropagation() - downloadSnapshot(snapshot) - }} - > - - - - - { - e.stopPropagation() - copySnapshotToClipboard(snapshot) - }} - > - - - - - { - e.stopPropagation() - deleteSnapshot(snapshot.id) - }} - > - - - - - - {expandedSnapshotIds.has(snapshot.id) && ( - - - - )} - {index < snapshots.length - 1 && } + + + toggleSnapshotExpanded(snapshot.id)} + > + + {renamingSnapshotId === snapshot.id ? ( + { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } else { + setRenamingSnapshotId(null) + setRenameValue("") + } + }} + onSubmitEditing={() => { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } + }} + /> + ) : ( + {snapshot.name} + )} + + + + { + e.stopPropagation() + startRenaming(snapshot) + }} + > + + + + + { + e.stopPropagation() + downloadSnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + copySnapshotToClipboard(snapshot) + }} + > + + + + + { + e.stopPropagation() + deleteSnapshot(snapshot.id) + }} + > + + + + + + {expandedSnapshotIds.has(snapshot.id) && ( + + + + )} + + ))} @@ -535,19 +537,21 @@ const $stateDivider = themed(({ spacing }) => ({ marginTop: spacing.lg, })) -const $snapshotItemContainer = themed(({ spacing }) => ({ - marginTop: spacing.md, -})) - -const $snapshotHeader = themed(({ spacing, colors }) => ({ - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - padding: spacing.sm, - backgroundColor: colors.cardBackground, - borderRadius: 8, - cursor: "pointer", -})) +const $snapshotCard = (isExpanded: boolean) => + themed(({ colors }) => ({ + backgroundColor: colors.cardBackground, + overflow: "hidden", + })) + +const $snapshotHeader = (isExpanded: boolean) => + themed(({ spacing, colors }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.sm, + backgroundColor: colors.cardBackground, + cursor: "pointer", + })) const $snapshotInfo = themed(() => ({ flex: 1, @@ -574,14 +578,8 @@ const $iconButton = themed(({ spacing, colors }) => ({ })) const $snapshotContent = themed(({ spacing, colors }) => ({ - marginTop: spacing.sm, padding: spacing.md, - backgroundColor: colors.background, - borderRadius: 8, -})) - -const $snapshotDivider = themed(({ spacing }) => ({ - marginTop: spacing.md, + backgroundColor: colors.cardBackground, })) const $renameInput = themed(({ colors, typography, spacing }) => ({ From 1600827df9bd815d3a1478f812de6b9b1552b275 Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 17 Oct 2025 07:24:23 -0700 Subject: [PATCH 07/23] Remove snapshot download functionality from StateScreen --- app/screens/StateScreen.tsx | 74 +++++++------------------------------ 1 file changed, 14 insertions(+), 60 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 0f1124b..3e6281f 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -1,5 +1,5 @@ import { Text, ViewStyle, ScrollView, TextStyle, Pressable, View, TextInput } from "react-native" -import { themed } from "../theme/theme" +import { themed, useTheme } from "../theme/theme" import { sendToCore } from "../state/connectToServer" import { useGlobal } from "../state/useGlobal" import { TreeViewWithProvider } from "../components/TreeView" @@ -10,12 +10,12 @@ import type { StateSubscription, Snapshot, Command, CommandType } from "app/type import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" import IRClipboard from "../native/IRClipboard/NativeIRClipboard" -import IRRunShellCommand from "../native/IRRunShellCommand/NativeIRRunShellCommand" import { Tooltip } from "../components/Tooltip" type StateTab = "Subscriptions" | "Snapshots" export function StateScreen() { + const theme = useTheme() const [showAddSubscription, setShowAddSubscription] = useState(false) const [activeStateTab, setActiveStateTab] = useGlobal("activeStateTab", "Subscriptions") @@ -29,6 +29,7 @@ export function StateScreen() { const [renameValue, setRenameValue] = useState("") const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] + const iconColor = theme.colors.mainText const saveSubscription = (path: string) => { if (clientStateSubscriptions.some((s) => s.path === path)) return @@ -77,43 +78,6 @@ export function StateScreen() { } } - const downloadSnapshot = async (snapshot: Snapshot) => { - try { - const homeDir = IRRunShellCommand.runSync("echo $HOME").trim() - const downloadDir = `${homeDir}/Downloads` - const filename = `snapshot-${snapshot.name.replace(/\s+/g, "-")}-${Date.now()}.json` - const data = JSON.stringify(snapshot.state, null, 2) - - // Create a temporary file with the data using echo and output redirection - // We need to escape special characters for shell - const escapedData = data.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$") - const command = `echo "${escapedData}" > "${downloadDir}/${filename}"` - - IRRunShellCommand.runSync(command) - console.log(`Snapshot downloaded to ${downloadDir}/${filename}`) - } catch (error) { - console.error("Failed to download snapshot:", error) - } - } - - const downloadAllSnapshots = async () => { - try { - const homeDir = IRRunShellCommand.runSync("echo $HOME").trim() - const downloadDir = `${homeDir}/Downloads` - const filename = `snapshots-all-${Date.now()}.json` - const data = JSON.stringify(snapshots, null, 2) - - // Create a temporary file with the data - const escapedData = data.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$") - const command = `echo "${escapedData}" > "${downloadDir}/${filename}"` - - IRRunShellCommand.runSync(command) - console.log(`All snapshots downloaded to ${downloadDir}/${filename}`) - } catch (error) { - console.error("Failed to download snapshots:", error) - } - } - const deleteSnapshot = (snapshotId: string) => { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } @@ -157,7 +121,7 @@ export function StateScreen() { {activeStateTab === "Subscriptions" ? ( setShowAddSubscription(true)}> - Add Subscription + Add Subscription - Clear State + Clear State ) : ( - Copy All - - - Download All + Copy All - Create Snapshot + Create Snapshot )} @@ -264,18 +225,7 @@ export function StateScreen() { startRenaming(snapshot) }} > - - - - - { - e.stopPropagation() - downloadSnapshot(snapshot) - }} - > - + @@ -286,7 +236,7 @@ export function StateScreen() { copySnapshotToClipboard(snapshot) }} > - + @@ -297,7 +247,7 @@ export function StateScreen() { deleteSnapshot(snapshot.id) }} > - + @@ -463,6 +413,10 @@ const $button = themed(({ colors, spacing }) => ({ cursor: "pointer", })) +const $buttonText = themed(({ colors }) => ({ + color: colors.mainText, +})) + const $addSubscriptionOuterContainer = themed(({ spacing }) => ({ flex: 1, padding: spacing.xl, From 7555996650e26611b7e716f4147724abceef3338 Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 17 Oct 2025 07:34:20 -0700 Subject: [PATCH 08/23] Add restore logic --- app/components/Icon.tsx | 1 + app/screens/StateScreen.tsx | 47 ++++++++++++++++++++++++---- assets/icons/arrowUpFromLine.png | Bin 0 -> 263 bytes assets/icons/arrowUpFromLine@2x.png | Bin 0 -> 349 bytes assets/icons/arrowUpFromLine@3x.png | Bin 0 -> 347 bytes 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 assets/icons/arrowUpFromLine.png create mode 100644 assets/icons/arrowUpFromLine@2x.png create mode 100644 assets/icons/arrowUpFromLine@3x.png diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx index f2b1617..e025411 100644 --- a/app/components/Icon.tsx +++ b/app/components/Icon.tsx @@ -67,6 +67,7 @@ export function Icon(props: IconProps) { export const iconRegistry = { arrowDownUp: require("../../assets/icons/arrowDownUp.png"), + arrowUpFromLine: require("../../assets/icons/arrowUpFromLine.png"), chevronsLeftRightEllipsis: require("../../assets/icons/chevronsLeftRightEllipsis.png"), circleGauge: require("../../assets/icons/circleGauge.png"), clipboard: require("../../assets/icons/clipboard.png"), diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 3e6281f..5c00ade 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -105,6 +105,29 @@ export function StateScreen() { }) } + const restoreSnapshot = (snapshot: Snapshot) => { + if (!snapshot || !snapshot.state) { + console.error("Invalid snapshot: missing state data") + return + } + + // Use the snapshot's clientId if available, otherwise fall back to the active client + const targetClientId = snapshot.clientId || activeTab + + if (!targetClientId) { + console.error("Cannot restore snapshot: no client available") + return + } + + // Send the restore command to the client + sendToCore("state.restore.request", { + clientId: targetClientId, + state: snapshot.state, + }) + + console.log(`Restoring snapshot "${snapshot.name}" to client ${targetClientId}`) + } + if (showAddSubscription) { return ( - + { e.stopPropagation() - startRenaming(snapshot) + copySnapshotToClipboard(snapshot) }} > - + - + { e.stopPropagation() - copySnapshotToClipboard(snapshot) + restoreSnapshot(snapshot) }} > - + + + + + { + e.stopPropagation() + startRenaming(snapshot) + }} + > + + JzFjD? P1$oEQ)z4*}Q$iB}kAp%g literal 0 HcmV?d00001 diff --git a/assets/icons/arrowUpFromLine@2x.png b/assets/icons/arrowUpFromLine@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d864b63e6368b83408a80abc51bdd9cef06cac GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMdu0Wbpu$a~DBv6k)NswPK z1J_s9-;ZPBzs326y(`T0dByOM4=8ui)5S5wF`sZ&Qcw^cc zrMqXh9nr5+w2b#wz3~5m%*+{k=AUp-*EV0pVt9YsMTgSAW!1la{n`|CQ~BxUz;JuN z4f{-OgX-j5k5!syeY+p1d?~l$Nfi-Fd^ImR|8R9{V9)g#uX6mHqJruGN+taa~6w4326%63nJINUoNS>~KF6*2Ung9U(c1{2Q literal 0 HcmV?d00001 diff --git a/assets/icons/arrowUpFromLine@3x.png b/assets/icons/arrowUpFromLine@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..16adefbf30de539035b08a3822b291de6e09a64d GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI76a#!hT!A!xkbBYNAW#KYNswPK z1KW0vP5tlQ-aq@_wp!>xwPfX*zbzrRB5vG{ zh`jwvJWQ(Q!C$YlzHTRyjUw{>EUSymHk>)3e{A~qOH-a5xLFj#_9RT}*z_ZOGgoS! zIK(BX*|tW+IZf+W{}S^lOy$d}oPKicNUb{cF^4H8*)~*8bW(8eRZqpxeNtMgx~j!q zQC`QUOqt^SQ$p!uJu1pS2@=#fJE9k%k51*~;i*MJ4Ot%PJ zpu%%W;ipz>jCpgz>$C%I3XWd;(X_KiXRc O3L;NeKbLh*2~7Z0&Ve@o literal 0 HcmV?d00001 From ec41f974b365d6c5a675532b90ec6c2119e9d3ca Mon Sep 17 00:00:00 2001 From: fpena Date: Tue, 21 Oct 2025 07:44:43 -0700 Subject: [PATCH 09/23] Adjust style better --- app/screens/StateScreen.tsx | 395 ++++++++++++++++++++---------------- 1 file changed, 220 insertions(+), 175 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 5c00ade..599cbef 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -6,7 +6,7 @@ import { TreeViewWithProvider } from "../components/TreeView" import { useState } from "react" import { Divider } from "../components/Divider" import { useKeyboardEvents } from "../utils/system" -import type { StateSubscription, Snapshot, Command, CommandType } from "app/types" +import type { StateSubscription, Snapshot } from "app/types" import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" import IRClipboard from "../native/IRClipboard/NativeIRClipboard" @@ -93,6 +93,19 @@ export function StateScreen() { setRenameValue(snapshot.name) } + const cancelRenaming = () => { + setRenamingSnapshotId(null) + setRenameValue("") + } + + const handleRenameKeyPress = (snapshotId: string, key: string) => { + if (key === "Enter" && renameValue.trim()) { + renameSnapshot(snapshotId, renameValue.trim()) + } else if (key === "Escape") { + cancelRenaming() + } + } + const toggleSnapshotExpanded = (snapshotId: string) => { setExpandedSnapshotIds((prev) => { const newSet = new Set(prev) @@ -138,175 +151,195 @@ export function StateScreen() { } return ( - - - State - {activeStateTab === "Subscriptions" ? ( - - setShowAddSubscription(true)}> - Add Subscription - - { - setStateSubscriptionsByClientId((prev) => ({ - ...prev, - [activeTab]: [], - })) - sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) - setActiveTab("") - }} - > - Clear State - - - ) : ( - - - Copy All - - - Create Snapshot - - - )} - - - - - - - {activeStateTab === "Subscriptions" ? ( - <> - {clientStateSubscriptions.length > 0 ? ( - <> - {clientStateSubscriptions.map((subscription, index) => ( - - - {subscription.path ? subscription.path : "Full State"} - - - - - - removeSubscription(subscription.path)}> - - - - {index < clientStateSubscriptions.length - 1 && ( - - )} - - ))} - - ) : ( - State is empty - )} - - ) : ( - <> - {snapshots.length > 0 ? ( - <> - {snapshots.map((snapshot, index) => ( - - - toggleSnapshotExpanded(snapshot.id)} - > - - {renamingSnapshotId === snapshot.id ? ( - { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } else { - setRenamingSnapshotId(null) - setRenameValue("") - } - }} - onSubmitEditing={() => { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } - }} - /> - ) : ( - {snapshot.name} - )} - - - - { - e.stopPropagation() - copySnapshotToClipboard(snapshot) - }} - > - - - - - { - e.stopPropagation() - restoreSnapshot(snapshot) - }} - > - - - - - { - e.stopPropagation() - startRenaming(snapshot) - }} - > - - - - - - { - e.stopPropagation() - deleteSnapshot(snapshot.id) - }} - > - - - - - - {expandedSnapshotIds.has(snapshot.id) && ( - - + <> + + + State + {activeStateTab === "Subscriptions" ? ( + + setShowAddSubscription(true)}> + Add Subscription + + { + setStateSubscriptionsByClientId((prev) => ({ + ...prev, + [activeTab]: [], + })) + sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) + setActiveTab("") + }} + > + Clear State + + + ) : ( + + + Copy All + + + Create Snapshot + + + )} + + + + + + + {activeStateTab === "Subscriptions" ? ( + <> + {clientStateSubscriptions.length > 0 ? ( + <> + {clientStateSubscriptions.map((subscription, index) => ( + + + {subscription.path ? subscription.path : "Full State"} + + + + + removeSubscription(subscription.path)}> + + + + {index < clientStateSubscriptions.length - 1 && ( + )} - - - ))} - - ) : ( - - To take a snapshot of your current redux or mobx-state-tree store, press the Create - Snapshot button in the top right corner of this window. - - )} - - )} - - + ))} + + ) : ( + State is empty + )} + + ) : ( + <> + {snapshots.length > 0 ? ( + <> + {snapshots.map((snapshot, index) => ( + + + { + if (renamingSnapshotId !== snapshot.id) { + toggleSnapshotExpanded(snapshot.id) + } + }} + > + + {renamingSnapshotId === snapshot.id ? ( + + + handleRenameKeyPress(snapshot.id, e.nativeEvent.key) + } + onBlur={() => { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } else { + cancelRenaming() + } + }} + autoFocus + selectTextOnFocus + /> + + { + if (renameValue.trim()) { + renameSnapshot(snapshot.id, renameValue.trim()) + } + }} + > + Save + + + + + Cancel + + + + ) : ( + {snapshot.name} + )} + + { + e.stopPropagation() + copySnapshotToClipboard(snapshot) + }} + > + + + + + { + e.stopPropagation() + restoreSnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + startRenaming(snapshot) + }} + > + + + + + { + e.stopPropagation() + deleteSnapshot(snapshot.id) + }} + > + + + + + + {expandedSnapshotIds.has(snapshot.id) && ( + + + + )} + + + + ))} + + ) : ( + + To take a snapshot of your current redux or mobx-state-tree store, press the + Create Snapshot button in the top right corner of this window. + + )} + + )} + + + ) } @@ -542,23 +575,20 @@ const $snapshotHeader = (isExpanded: boolean) => cursor: "pointer", })) -const $snapshotInfo = themed(() => ({ - flex: 1, +const $snapshotInfo = themed(({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.md, })) const $snapshotName = themed(({ colors, typography }) => ({ + flex: 1, fontSize: typography.body, fontWeight: "600", color: colors.mainText, fontFamily: typography.code.normal, })) -const $snapshotActions = themed(({ spacing }) => ({ - flexDirection: "row", - gap: spacing.xs, - alignItems: "center", -})) - const $iconButton = themed(({ spacing, colors }) => ({ padding: spacing.xs, borderRadius: 4, @@ -571,6 +601,13 @@ const $snapshotContent = themed(({ spacing, colors }) => ({ backgroundColor: colors.cardBackground, })) +const $renameContainer = themed(({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.md, + flex: 1, +})) + const $renameInput = themed(({ colors, typography, spacing }) => ({ fontSize: typography.body, fontWeight: "600", @@ -581,4 +618,12 @@ const $renameInput = themed(({ colors, typography, spacing }) => ({ borderRadius: 4, borderWidth: 1, borderColor: colors.primary, + flex: 1, +})) + +const $renameButton = themed(({ colors, spacing }) => ({ + padding: spacing.xxs, + borderRadius: 4, + backgroundColor: colors.neutralVery, + cursor: "pointer", })) From 39f633a91eb5839e4bba56dd5aa52881f00764e5 Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:03:01 -0700 Subject: [PATCH 10/23] Remove snapshot renaming functionality temporarily from StateScreen --- app/screens/StateScreen.tsx | 111 +----------------------------------- 1 file changed, 2 insertions(+), 109 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 599cbef..9255f0e 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -25,8 +25,6 @@ export function StateScreen() { const [activeTab, setActiveTab] = useGlobal("activeClientId", "") const [snapshots, setSnapshots] = useGlobal("snapshots", []) const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) - const [renamingSnapshotId, setRenamingSnapshotId] = useState(null) - const [renameValue, setRenameValue] = useState("") const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] const iconColor = theme.colors.mainText @@ -82,30 +80,6 @@ export function StateScreen() { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } - const renameSnapshot = (snapshotId: string, newName: string) => { - setSnapshots((prev) => prev.map((s) => (s.id === snapshotId ? { ...s, name: newName } : s))) - setRenamingSnapshotId(null) - setRenameValue("") - } - - const startRenaming = (snapshot: Snapshot) => { - setRenamingSnapshotId(snapshot.id) - setRenameValue(snapshot.name) - } - - const cancelRenaming = () => { - setRenamingSnapshotId(null) - setRenameValue("") - } - - const handleRenameKeyPress = (snapshotId: string, key: string) => { - if (key === "Enter" && renameValue.trim()) { - renameSnapshot(snapshotId, renameValue.trim()) - } else if (key === "Escape") { - cancelRenaming() - } - } - const toggleSnapshotExpanded = (snapshotId: string) => { setExpandedSnapshotIds((prev) => { const newSet = new Set(prev) @@ -226,53 +200,10 @@ export function StateScreen() { { - if (renamingSnapshotId !== snapshot.id) { - toggleSnapshotExpanded(snapshot.id) - } - }} + onPress={() => toggleSnapshotExpanded(snapshot.id)} > - {renamingSnapshotId === snapshot.id ? ( - - - handleRenameKeyPress(snapshot.id, e.nativeEvent.key) - } - onBlur={() => { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } else { - cancelRenaming() - } - }} - autoFocus - selectTextOnFocus - /> - - { - if (renameValue.trim()) { - renameSnapshot(snapshot.id, renameValue.trim()) - } - }} - > - Save - - - - - Cancel - - - - ) : ( - {snapshot.name} - )} + {snapshot.name} - - { - e.stopPropagation() - startRenaming(snapshot) - }} - > - - - (({ spacing, colors }) => ({ padding: spacing.md, backgroundColor: colors.cardBackground, })) - -const $renameContainer = themed(({ spacing }) => ({ - flexDirection: "row", - alignItems: "center", - gap: spacing.md, - flex: 1, -})) - -const $renameInput = themed(({ colors, typography, spacing }) => ({ - fontSize: typography.body, - fontWeight: "600", - color: colors.mainText, - fontFamily: typography.code.normal, - padding: spacing.xs, - backgroundColor: colors.background, - borderRadius: 4, - borderWidth: 1, - borderColor: colors.primary, - flex: 1, -})) - -const $renameButton = themed(({ colors, spacing }) => ({ - padding: spacing.xxs, - borderRadius: 4, - backgroundColor: colors.neutralVery, - cursor: "pointer", -})) From f78791df3891f8d81d0132bd2cc1fe9a2fa8b77c Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:06:43 -0700 Subject: [PATCH 11/23] Remove unused isExpanded parameter from style functions --- app/screens/StateScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 9255f0e..6f6f932 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -479,13 +479,13 @@ const $stateDivider = themed(({ spacing }) => ({ marginTop: spacing.lg, })) -const $snapshotCard = (isExpanded: boolean) => +const $snapshotCard = () => themed(({ colors }) => ({ backgroundColor: colors.cardBackground, overflow: "hidden", })) -const $snapshotHeader = (isExpanded: boolean) => +const $snapshotHeader = () => themed(({ spacing, colors }) => ({ flexDirection: "row", justifyContent: "space-between", From b4cd61a931b109ba089686e57f8cd45ca0631bd9 Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:27:02 -0700 Subject: [PATCH 12/23] Add styling to empty state text in StateScreen --- app/screens/StateScreen.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 6f6f932..f43cbdf 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -188,7 +188,7 @@ export function StateScreen() { ))} ) : ( - State is empty + State is empty )} ) : ( @@ -250,7 +250,7 @@ export function StateScreen() { ))} ) : ( - + To take a snapshot of your current redux or mobx-state-tree store, press the Create Snapshot button in the top right corner of this window. @@ -520,3 +520,9 @@ const $snapshotContent = themed(({ spacing, colors }) => ({ padding: spacing.md, backgroundColor: colors.cardBackground, })) + +const $emptyStateText = themed(({ colors, typography, spacing }) => ({ + fontSize: typography.body, + fontWeight: "400", + color: colors.mainText, +})) From b0a4bba79445ce3075500588d2188634c8eb2a40 Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:29:31 -0700 Subject: [PATCH 13/23] Refactor snapshot card and header styles --- app/screens/StateScreen.tsx | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index f43cbdf..b2ec728 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -197,9 +197,9 @@ export function StateScreen() { <> {snapshots.map((snapshot, index) => ( - + toggleSnapshotExpanded(snapshot.id)} > @@ -479,21 +479,19 @@ const $stateDivider = themed(({ spacing }) => ({ marginTop: spacing.lg, })) -const $snapshotCard = () => - themed(({ colors }) => ({ - backgroundColor: colors.cardBackground, - overflow: "hidden", - })) - -const $snapshotHeader = () => - themed(({ spacing, colors }) => ({ - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - padding: spacing.sm, - backgroundColor: colors.cardBackground, - cursor: "pointer", - })) +const $snapshotCard = themed(({ colors }) => ({ + backgroundColor: colors.cardBackground, + overflow: "hidden", +})) + +const $snapshotHeader = themed(({ spacing, colors }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.sm, + backgroundColor: colors.cardBackground, + cursor: "pointer", +})) const $snapshotInfo = themed(({ spacing }) => ({ flexDirection: "row", From dd691d545fd3c756f7a533f068303b635a3aca6d Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:32:53 -0700 Subject: [PATCH 14/23] Update Icon components to use theme color and key --- app/screens/StateScreen.tsx | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index b2ec728..1ef4974 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -1,5 +1,5 @@ import { Text, ViewStyle, ScrollView, TextStyle, Pressable, View, TextInput } from "react-native" -import { themed, useTheme } from "../theme/theme" +import { themed, useTheme, useThemeName } from "../theme/theme" import { sendToCore } from "../state/connectToServer" import { useGlobal } from "../state/useGlobal" import { TreeViewWithProvider } from "../components/TreeView" @@ -16,6 +16,7 @@ type StateTab = "Subscriptions" | "Snapshots" export function StateScreen() { const theme = useTheme() + const [themeName] = useThemeName() const [showAddSubscription, setShowAddSubscription] = useState(false) const [activeStateTab, setActiveStateTab] = useGlobal("activeStateTab", "Subscriptions") @@ -178,7 +179,12 @@ export function StateScreen() { removeSubscription(subscription.path)}> - + {index < clientStateSubscriptions.length - 1 && ( @@ -212,7 +218,12 @@ export function StateScreen() { copySnapshotToClipboard(snapshot) }} > - + @@ -223,7 +234,12 @@ export function StateScreen() { restoreSnapshot(snapshot) }} > - + @@ -234,7 +250,12 @@ export function StateScreen() { deleteSnapshot(snapshot.id) }} > - + From 7be67567f2fbd60238f0c244e467b0fb5b3a0a6f Mon Sep 17 00:00:00 2001 From: fpena Date: Thu, 23 Oct 2025 20:34:48 -0700 Subject: [PATCH 15/23] Remove arrowDownUp icon and references --- app/components/Icon.tsx | 1 - assets/icons/arrowDownUp.png | Bin 577 -> 0 bytes assets/icons/arrowDownUp@2x.png | Bin 953 -> 0 bytes assets/icons/arrowDownUp@3x.png | Bin 972 -> 0 bytes 4 files changed, 1 deletion(-) delete mode 100644 assets/icons/arrowDownUp.png delete mode 100644 assets/icons/arrowDownUp@2x.png delete mode 100644 assets/icons/arrowDownUp@3x.png diff --git a/app/components/Icon.tsx b/app/components/Icon.tsx index e025411..43b3bfe 100644 --- a/app/components/Icon.tsx +++ b/app/components/Icon.tsx @@ -66,7 +66,6 @@ export function Icon(props: IconProps) { } export const iconRegistry = { - arrowDownUp: require("../../assets/icons/arrowDownUp.png"), arrowUpFromLine: require("../../assets/icons/arrowUpFromLine.png"), chevronsLeftRightEllipsis: require("../../assets/icons/chevronsLeftRightEllipsis.png"), circleGauge: require("../../assets/icons/circleGauge.png"), diff --git a/assets/icons/arrowDownUp.png b/assets/icons/arrowDownUp.png deleted file mode 100644 index 5f14ca47c6a345bb2255bc6d62f79d9725149af2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 577 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!Vgr0aT!A$0z)p6N4?xr9N`m}? z8GiA<4`ma&>34Oi&juHf&oACRSR5pMXmv|nOt9p*ST! z>}+9tkj7q?z`l=J;xY3Ui5=lr-|`wvSsoa6qJf>iSc&2E`$iwOm}B`Tg&E529R--y zecKz@@tjwv+}e@?yid8g`BPquTQgy8Aw=d#Wzp$Py&@yv(- diff --git a/assets/icons/arrowDownUp@2x.png b/assets/icons/arrowDownUp@2x.png deleted file mode 100644 index fcc7a52e2fae7f4918087dca773956e1735c80d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&MQ z#t5Bh@)K`a&y;z;H|XywomEHIP2?;r(z;%GuT1LiX`Mf(pP4NEzm{wI8_{Ry4@FK$ zU0AyPyZq|Ui%zeazMC=iMY#fZ`|RKKqJ@D!YTv()7yOm-F!D~zzZur7cKr>KjBdX+ zvY%wU#R_{k3P2mZ=%d||w^KjKLKYRR{aW(~iu`*JqipX4BZ;P25ugR(eo&Je~s z{|%;RH;4I>gA8-6)I{i=5`dp}3@frcIbD{P*8 zVJNMRWMgDbs;?NBw-e38Rh#!-f9ek9RSMUAf+% zeD-}wc>`;7{L%HNpEW+e&h|l$VM9h`T`2?ix;5pI;0d z$Da9fCNQ3#FLZ!up1o=Vt4)0r19!#0FAN+YLhQqzmy7~X;xU8Gs`Yy^I1T<=Jg{7+ zl@%xu@IQjDq+q5L&u5`c|3s$AU$hsy{^&(n`M3Z2&!m4HFFy3}>%WrRdFxMKo>KSo o(~o(7>O~8f4}(&s*lGV0@fm?hXGIvxPk}O^r>mdKI;Vst0JCs~?f?J) diff --git a/assets/icons/arrowDownUp@3x.png b/assets/icons/arrowDownUp@3x.png deleted file mode 100644 index 994ae2730d972140630dabff63388e0c71d3dc3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 972 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1#sNMdu0Wbpu$a~DBv6k)NswPK z1NZ*Jyi<>U(`CPU?fm;^K^w*GUNSH+TX?!ShE&{obL(tgvw;9hLQkRLll=StGgpTN z%0{FvpEx;{-?-fEGb5*rghX2+0zKQm@~O%B9U*dB^^gUi^Hq zHJeV}+Iv6R;Ivxt`?~VRHnnJjm~PeSbZ`Xx)^&7fxRuZKi1A8YumJyrO?xKHS;TndXZv*pF^BiZn>-kj z_bYZV|F~JdgMs0~zsIZ$4ByxVBpCMCH#so=`j&n(m4zvu;hz0TJx-1q#)|##m_gk2 zzZ(vSH++9>r|^OG!QWl{Fsb?jpHk|pE~W1+=|9cbko4|)?Xm?wm{;wM|9-RS6`#hp zy1VJzUDgR(fjXl%>|#y}JURIy`@en97#eIA?o;Dni0J*z{%vW^AKj3izdQ7w9c1xcAMhH|p zGwU&Li96qLy5agCwFk!-Rrvy>1+$s2w0*h7u+{LSnL}>CR^9+<(LA8gR>K5)z5o5k zp0YCdAN+TyiGd+b((=FPqUmLQ|2y*cnH^=!62A`Awz^rK?M;5tx=Cki{TX(6Ztyzr zlaYZVjMKuNf#IMQ<8yWfg$UILe;628R`1N%Uwi+B4zyIK Date: Fri, 24 Oct 2025 13:21:30 -0700 Subject: [PATCH 16/23] Remove unnecessary console logs and error messages --- app/screens/StateScreen.tsx | 14 +++----------- app/state/connectToServer.ts | 1 - 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 1ef4974..07758ff 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -28,7 +28,6 @@ export function StateScreen() { const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] - const iconColor = theme.colors.mainText const saveSubscription = (path: string) => { if (clientStateSubscriptions.some((s) => s.path === path)) return @@ -51,10 +50,8 @@ export function StateScreen() { } const createSnapshot = () => { - if (!activeTab) { - console.log("No active client to create snapshot from") - return - } + if (!activeTab) return + sendToCore("state.backup.request", { clientId: activeTab }) } @@ -94,10 +91,7 @@ export function StateScreen() { } const restoreSnapshot = (snapshot: Snapshot) => { - if (!snapshot || !snapshot.state) { - console.error("Invalid snapshot: missing state data") - return - } + if (!snapshot || !snapshot.state) return // Use the snapshot's clientId if available, otherwise fall back to the active client const targetClientId = snapshot.clientId || activeTab @@ -112,8 +106,6 @@ export function StateScreen() { clientId: targetClientId, state: snapshot.state, }) - - console.log(`Restoring snapshot "${snapshot.name}" to client ${targetClientId}`) } if (showAddSubscription) { diff --git a/app/state/connectToServer.ts b/app/state/connectToServer.ts index df2ee0e..eecb03f 100644 --- a/app/state/connectToServer.ts +++ b/app/state/connectToServer.ts @@ -192,7 +192,6 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub // Handle state backup response if (data.cmd.type === "state.backup.response") { - console.log("Received state.backup.response:", data.cmd) setSnapshots((prev) => { // Use the server-provided date to check for duplicates const serverDate = data.cmd.date From 2d27a2634d1b617c0c1b4b1aca4b4d320fdf33b2 Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 24 Oct 2025 13:22:49 -0700 Subject: [PATCH 17/23] Remove console error on missing client in StateScreen --- app/screens/StateScreen.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 07758ff..5f60d3f 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -96,10 +96,7 @@ export function StateScreen() { // Use the snapshot's clientId if available, otherwise fall back to the active client const targetClientId = snapshot.clientId || activeTab - if (!targetClientId) { - console.error("Cannot restore snapshot: no client available") - return - } + if (!targetClientId) return // Send the restore command to the client sendToCore("state.restore.request", { From 3cd2761daee366c791e4fe3923367318d625db59 Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 24 Oct 2025 13:23:42 -0700 Subject: [PATCH 18/23] Refactor StateScreen to remove unnecessary fragment --- app/screens/StateScreen.tsx | 302 ++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 152 deletions(-) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index 5f60d3f..da2e58c 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -115,161 +115,159 @@ export function StateScreen() { } return ( - <> - - - State - {activeStateTab === "Subscriptions" ? ( - - setShowAddSubscription(true)}> - Add Subscription - - { - setStateSubscriptionsByClientId((prev) => ({ - ...prev, - [activeTab]: [], - })) - sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) - setActiveTab("") - }} - > - Clear State - - - ) : ( - - - Copy All - - - Create Snapshot - - - )} - - - - - - - {activeStateTab === "Subscriptions" ? ( - <> - {clientStateSubscriptions.length > 0 ? ( - <> - {clientStateSubscriptions.map((subscription, index) => ( - - - {subscription.path ? subscription.path : "Full State"} - - - - - - removeSubscription(subscription.path)}> - - + + + State + {activeStateTab === "Subscriptions" ? ( + + setShowAddSubscription(true)}> + Add Subscription + + { + setStateSubscriptionsByClientId((prev) => ({ + ...prev, + [activeTab]: [], + })) + sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) + setActiveTab("") + }} + > + Clear State + + + ) : ( + + + Copy All + + + Create Snapshot + + + )} + + + + + + + {activeStateTab === "Subscriptions" ? ( + <> + {clientStateSubscriptions.length > 0 ? ( + <> + {clientStateSubscriptions.map((subscription, index) => ( + + + {subscription.path ? subscription.path : "Full State"} + + + + - {index < clientStateSubscriptions.length - 1 && ( - - )} + removeSubscription(subscription.path)}> + + - ))} - - ) : ( - State is empty - )} - - ) : ( - <> - {snapshots.length > 0 ? ( - <> - {snapshots.map((snapshot, index) => ( - - - toggleSnapshotExpanded(snapshot.id)} - > - - {snapshot.name} - - { - e.stopPropagation() - copySnapshotToClipboard(snapshot) - }} - > - - - - - { - e.stopPropagation() - restoreSnapshot(snapshot) - }} - > - - - - - { - e.stopPropagation() - deleteSnapshot(snapshot.id) - }} - > - - - - - - {expandedSnapshotIds.has(snapshot.id) && ( - - - - )} - - + {index < clientStateSubscriptions.length - 1 && ( + + )} + + ))} + + ) : ( + State is empty + )} + + ) : ( + <> + {snapshots.length > 0 ? ( + <> + {snapshots.map((snapshot, index) => ( + + + toggleSnapshotExpanded(snapshot.id)} + > + + {snapshot.name} + + { + e.stopPropagation() + copySnapshotToClipboard(snapshot) + }} + > + + + + + { + e.stopPropagation() + restoreSnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + deleteSnapshot(snapshot.id) + }} + > + + + + + + {expandedSnapshotIds.has(snapshot.id) && ( + + + + )} - ))} - - ) : ( - - To take a snapshot of your current redux or mobx-state-tree store, press the - Create Snapshot button in the top right corner of this window. - - )} - - )} - - - + + + ))} + + ) : ( + + To take a snapshot of your current redux or mobx-state-tree store, press the Create + Snapshot button in the top right corner of this window. + + )} + + )} + + ) } From 9289ee7142ceb913f365907ff087fcaa8b8b1310 Mon Sep 17 00:00:00 2001 From: fpena Date: Fri, 24 Oct 2025 13:24:28 -0700 Subject: [PATCH 19/23] Refactor state backup response type check --- app/state/connectToServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/state/connectToServer.ts b/app/state/connectToServer.ts index eecb03f..3aea45a 100644 --- a/app/state/connectToServer.ts +++ b/app/state/connectToServer.ts @@ -191,7 +191,7 @@ export function connectToServer(props: { port: number } = { port: 9292 }): Unsub } // Handle state backup response - if (data.cmd.type === "state.backup.response") { + if (data.cmd.type === CommandType.StateBackupResponse) { setSnapshots((prev) => { // Use the server-provided date to check for duplicates const serverDate = data.cmd.date From 46c216a2dbda90ca8aede27ad74e6ee9a430c185 Mon Sep 17 00:00:00 2001 From: fpena Date: Sat, 25 Oct 2025 09:26:19 -0700 Subject: [PATCH 20/23] Refactor StateScreen to use StateSubscriptions component --- app/components/State/StateSubscriptions.tsx | 71 +++++++++++ app/screens/StateScreen.tsx | 124 +++++--------------- 2 files changed, 102 insertions(+), 93 deletions(-) create mode 100644 app/components/State/StateSubscriptions.tsx diff --git a/app/components/State/StateSubscriptions.tsx b/app/components/State/StateSubscriptions.tsx new file mode 100644 index 0000000..74beb11 --- /dev/null +++ b/app/components/State/StateSubscriptions.tsx @@ -0,0 +1,71 @@ +import { Text, ViewStyle, TextStyle, Pressable, View } from "react-native" +import { themed, useTheme } from "../../theme/theme" +import { TreeViewWithProvider } from "../TreeView" +import { Divider } from "../Divider" +import { Icon } from "../Icon" +import type { StateSubscription } from "app/types" + +interface StateSubscriptionsProps { + subscriptions: StateSubscription[] + onRemoveSubscription: (path: string) => void +} + +export function StateSubscriptions({ + subscriptions, + onRemoveSubscription, +}: StateSubscriptionsProps) { + const theme = useTheme() + + if (subscriptions.length === 0) { + return State is empty + } + + return ( + <> + {subscriptions.map((subscription, index) => ( + + {subscription.path ? subscription.path : "Full State"} + + + + + onRemoveSubscription(subscription.path)}> + + + + {index < subscriptions.length - 1 && } + + ))} + + ) +} + +const $pathText = themed(({ colors, typography, spacing }) => ({ + fontSize: typography.body, + fontWeight: "400", + color: colors.mainText, + marginBottom: spacing.sm, +})) + +const $stateItemContainer = themed(({ spacing }) => ({ + marginTop: spacing.xl, +})) + +const $treeViewInnerContainer = themed(() => ({ + flex: 1, +})) + +const $treeViewContainer = themed(() => ({ + flexDirection: "row", + justifyContent: "space-between", +})) + +const $stateDivider = themed(({ spacing }) => ({ + marginTop: spacing.lg, +})) + +const $emptyStateText = themed(({ colors, typography }) => ({ + fontSize: typography.body, + fontWeight: "400", + color: colors.mainText, +})) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index da2e58c..dc39b23 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -1,5 +1,5 @@ import { Text, ViewStyle, ScrollView, TextStyle, Pressable, View, TextInput } from "react-native" -import { themed, useTheme, useThemeName } from "../theme/theme" +import { themed, useTheme } from "../theme/theme" import { sendToCore } from "../state/connectToServer" import { useGlobal } from "../state/useGlobal" import { TreeViewWithProvider } from "../components/TreeView" @@ -9,16 +9,16 @@ import { useKeyboardEvents } from "../utils/system" import type { StateSubscription, Snapshot } from "app/types" import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" -import IRClipboard from "../native/IRClipboard/NativeIRClipboard" import { Tooltip } from "../components/Tooltip" +import { StateSubscriptions } from "../components/State/StateSubscriptions" +import IRClipboard from "../native/IRClipboard/NativeIRClipboard" type StateTab = "Subscriptions" | "Snapshots" export function StateScreen() { const theme = useTheme() - const [themeName] = useThemeName() const [showAddSubscription, setShowAddSubscription] = useState(false) - const [activeStateTab, setActiveStateTab] = useGlobal("activeStateTab", "Subscriptions") + const [activeStateTab] = useGlobal("activeStateTab", "Subscriptions") const [stateSubscriptionsByClientId, setStateSubscriptionsByClientId] = useGlobal<{ [clientId: string]: StateSubscription[] @@ -55,25 +55,6 @@ export function StateScreen() { sendToCore("state.backup.request", { clientId: activeTab }) } - const copySnapshotToClipboard = (snapshot: Snapshot) => { - try { - IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) - console.log("Snapshot copied to clipboard") - } catch (error) { - console.error("Failed to copy snapshot to clipboard:", error) - } - } - - const copyAllSnapshotsToClipboard = () => { - try { - console.log("Copying all snapshots to clipboard", snapshots) - IRClipboard.setString(JSON.stringify(snapshots, null, 2)) - console.log("All snapshots copied to clipboard") - } catch (error) { - console.error("Failed to copy snapshots to clipboard:", error) - } - } - const deleteSnapshot = (snapshotId: string) => { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } @@ -139,7 +120,7 @@ export function StateScreen() { ) : ( - + copyAllSnapshotsToClipboard(snapshots)}> Copy All @@ -154,42 +135,15 @@ export function StateScreen() { {activeStateTab === "Subscriptions" ? ( - <> - {clientStateSubscriptions.length > 0 ? ( - <> - {clientStateSubscriptions.map((subscription, index) => ( - - - {subscription.path ? subscription.path : "Full State"} - - - - - - removeSubscription(subscription.path)}> - - - - {index < clientStateSubscriptions.length - 1 && ( - - )} - - ))} - - ) : ( - State is empty - )} - + ) : ( <> {snapshots.length > 0 ? ( <> - {snapshots.map((snapshot, index) => ( + {snapshots.map((snapshot) => ( - + @@ -226,7 +175,6 @@ export function StateScreen() { icon="arrowUpFromLine" size={18} color={theme.colors.mainText} - key={`restore-${themeName}`} /> @@ -238,12 +186,7 @@ export function StateScreen() { deleteSnapshot(snapshot.id) }} > - + @@ -347,12 +290,24 @@ function AddSubscription({ ) } -const $pathText = themed(({ colors, typography, spacing }) => ({ - fontSize: typography.body, - fontWeight: "400", - color: colors.mainText, - marginBottom: spacing.sm, -})) +function copyAllSnapshotsToClipboard(snapshots: Snapshot[]): void { + try { + console.log("Copying all snapshots to clipboard", snapshots) + IRClipboard.setString(JSON.stringify(snapshots, null, 2)) + console.log("All snapshots copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshots to clipboard:", error) + } +} + +function copySnapshotToClipboard(snapshot: Snapshot): void { + try { + IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) + console.log("Snapshot copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshot to clipboard:", error) + } +} const $container = themed(({ spacing }) => ({ padding: spacing.xl, @@ -379,19 +334,6 @@ const $tabsContainer = themed(({ spacing }) => ({ marginBottom: spacing.md, })) -const $stateItemContainer = themed(({ spacing }) => ({ - marginTop: spacing.xl, -})) - -const $treeViewInnerContainer = themed(() => ({ - flex: 1, -})) - -const $treeViewContainer = themed(() => ({ - flexDirection: "row", - justifyContent: "space-between", -})) - const $stateContainer = themed(({ spacing }) => ({ marginTop: spacing.sm, })) @@ -483,10 +425,6 @@ const $subscriptionButton = themed(({ colors, spacing }) => ({ cursor: "pointer", })) -const $stateDivider = themed(({ spacing }) => ({ - marginTop: spacing.lg, -})) - const $snapshotCard = themed(({ colors }) => ({ backgroundColor: colors.cardBackground, overflow: "hidden", @@ -527,7 +465,7 @@ const $snapshotContent = themed(({ spacing, colors }) => ({ backgroundColor: colors.cardBackground, })) -const $emptyStateText = themed(({ colors, typography, spacing }) => ({ +const $emptyStateText = themed(({ colors, typography }) => ({ fontSize: typography.body, fontWeight: "400", color: colors.mainText, From decd0ae71cea209f3d31a54ed17de7cbbd9cace0 Mon Sep 17 00:00:00 2001 From: fpena Date: Sat, 25 Oct 2025 09:31:28 -0700 Subject: [PATCH 21/23] Refactor snapshot UI into StateSnapshots component --- app/components/State/StateSnapshots.tsx | 150 ++++++++++++++++++++++++ app/screens/StateScreen.tsx | 140 ++-------------------- 2 files changed, 158 insertions(+), 132 deletions(-) create mode 100644 app/components/State/StateSnapshots.tsx diff --git a/app/components/State/StateSnapshots.tsx b/app/components/State/StateSnapshots.tsx new file mode 100644 index 0000000..2eca5e4 --- /dev/null +++ b/app/components/State/StateSnapshots.tsx @@ -0,0 +1,150 @@ +import { Text, ViewStyle, TextStyle, Pressable, View } from "react-native" +import { themed, useTheme } from "../../theme/theme" +import { TreeViewWithProvider } from "../TreeView" +import { Divider } from "../Divider" +import { Icon } from "../Icon" +import { Tooltip } from "../Tooltip" +import { useState } from "react" +import type { Snapshot } from "app/types" + +interface StateSnapshotsProps { + snapshots: Snapshot[] + onCopySnapshot: (snapshot: Snapshot) => void + onRestoreSnapshot: (snapshot: Snapshot) => void + onDeleteSnapshot: (snapshotId: string) => void +} + +export function StateSnapshots({ + snapshots, + onCopySnapshot, + onRestoreSnapshot, + onDeleteSnapshot, +}: StateSnapshotsProps) { + const theme = useTheme() + const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) + + const toggleSnapshotExpanded = (snapshotId: string) => { + setExpandedSnapshotIds((prev) => { + const newSet = new Set(prev) + if (newSet.has(snapshotId)) { + newSet.delete(snapshotId) + } else { + newSet.add(snapshotId) + } + return newSet + }) + } + + if (snapshots.length === 0) { + return ( + + To take a snapshot of your current redux or mobx-state-tree store, press the Create Snapshot + button in the top right corner of this window. + + ) + } + + return ( + <> + {snapshots.map((snapshot) => ( + + + toggleSnapshotExpanded(snapshot.id)} + > + + {snapshot.name} + + { + e.stopPropagation() + onCopySnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + onRestoreSnapshot(snapshot) + }} + > + + + + + { + e.stopPropagation() + onDeleteSnapshot(snapshot.id) + }} + > + + + + + + {expandedSnapshotIds.has(snapshot.id) && ( + + + + )} + + + + ))} + + ) +} + +const $snapshotCard = themed(({ colors }) => ({ + backgroundColor: colors.cardBackground, + overflow: "hidden", +})) + +const $snapshotHeader = themed(({ spacing, colors }) => ({ + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.sm, + backgroundColor: colors.cardBackground, + cursor: "pointer", +})) + +const $snapshotInfo = themed(({ spacing }) => ({ + flexDirection: "row", + alignItems: "center", + gap: spacing.md, +})) + +const $snapshotName = themed(({ colors, typography }) => ({ + flex: 1, + fontSize: typography.body, + fontWeight: "600", + color: colors.mainText, + fontFamily: typography.code.normal, +})) + +const $iconButton = themed(({ spacing, colors }) => ({ + padding: spacing.xs, + borderRadius: 4, + cursor: "pointer", + backgroundColor: colors.neutralVery, +})) + +const $snapshotContent = themed(({ spacing, colors }) => ({ + padding: spacing.md, + backgroundColor: colors.cardBackground, +})) + +const $emptyStateText = themed(({ colors, typography }) => ({ + fontSize: typography.body, + fontWeight: "400", + color: colors.mainText, +})) diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index dc39b23..bf6f49c 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -1,22 +1,19 @@ import { Text, ViewStyle, ScrollView, TextStyle, Pressable, View, TextInput } from "react-native" -import { themed, useTheme } from "../theme/theme" +import { themed } from "../theme/theme" import { sendToCore } from "../state/connectToServer" import { useGlobal } from "../state/useGlobal" -import { TreeViewWithProvider } from "../components/TreeView" import { useState } from "react" import { Divider } from "../components/Divider" import { useKeyboardEvents } from "../utils/system" import type { StateSubscription, Snapshot } from "app/types" -import { Icon } from "../components/Icon" import { Tab } from "../components/Tab" -import { Tooltip } from "../components/Tooltip" import { StateSubscriptions } from "../components/State/StateSubscriptions" +import { StateSnapshots } from "../components/State/StateSnapshots" import IRClipboard from "../native/IRClipboard/NativeIRClipboard" type StateTab = "Subscriptions" | "Snapshots" export function StateScreen() { - const theme = useTheme() const [showAddSubscription, setShowAddSubscription] = useState(false) const [activeStateTab] = useGlobal("activeStateTab", "Subscriptions") @@ -25,7 +22,6 @@ export function StateScreen() { }>("stateSubscriptionsByClientId", {}) const [activeTab, setActiveTab] = useGlobal("activeClientId", "") const [snapshots, setSnapshots] = useGlobal("snapshots", []) - const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] @@ -59,18 +55,6 @@ export function StateScreen() { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } - const toggleSnapshotExpanded = (snapshotId: string) => { - setExpandedSnapshotIds((prev) => { - const newSet = new Set(prev) - if (newSet.has(snapshotId)) { - newSet.delete(snapshotId) - } else { - newSet.add(snapshotId) - } - return newSet - }) - } - const restoreSnapshot = (snapshot: Snapshot) => { if (!snapshot || !snapshot.state) return @@ -140,74 +124,12 @@ export function StateScreen() { onRemoveSubscription={removeSubscription} /> ) : ( - <> - {snapshots.length > 0 ? ( - <> - {snapshots.map((snapshot) => ( - - - toggleSnapshotExpanded(snapshot.id)} - > - - {snapshot.name} - - { - e.stopPropagation() - copySnapshotToClipboard(snapshot) - }} - > - - - - - { - e.stopPropagation() - restoreSnapshot(snapshot) - }} - > - - - - - { - e.stopPropagation() - deleteSnapshot(snapshot.id) - }} - > - - - - - - {expandedSnapshotIds.has(snapshot.id) && ( - - - - )} - - - - ))} - - ) : ( - - To take a snapshot of your current redux or mobx-state-tree store, press the Create - Snapshot button in the top right corner of this window. - - )} - + )} @@ -424,49 +346,3 @@ const $subscriptionButton = themed(({ colors, spacing }) => ({ borderRadius: 8, cursor: "pointer", })) - -const $snapshotCard = themed(({ colors }) => ({ - backgroundColor: colors.cardBackground, - overflow: "hidden", -})) - -const $snapshotHeader = themed(({ spacing, colors }) => ({ - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - padding: spacing.sm, - backgroundColor: colors.cardBackground, - cursor: "pointer", -})) - -const $snapshotInfo = themed(({ spacing }) => ({ - flexDirection: "row", - alignItems: "center", - gap: spacing.md, -})) - -const $snapshotName = themed(({ colors, typography }) => ({ - flex: 1, - fontSize: typography.body, - fontWeight: "600", - color: colors.mainText, - fontFamily: typography.code.normal, -})) - -const $iconButton = themed(({ spacing, colors }) => ({ - padding: spacing.xs, - borderRadius: 4, - cursor: "pointer", - backgroundColor: colors.neutralVery, -})) - -const $snapshotContent = themed(({ spacing, colors }) => ({ - padding: spacing.md, - backgroundColor: colors.cardBackground, -})) - -const $emptyStateText = themed(({ colors, typography }) => ({ - fontSize: typography.body, - fontWeight: "400", - color: colors.mainText, -})) From e6b978d4b47dbba955a3ff718a1eab422c4d02bd Mon Sep 17 00:00:00 2001 From: fpena Date: Sun, 26 Oct 2025 18:31:28 -0700 Subject: [PATCH 22/23] Refactor state management in State components --- app/components/State/StateSnapshots.tsx | 53 ++++++++++----- app/components/State/StateSubscriptions.tsx | 37 +++++++---- app/screens/StateScreen.tsx | 72 +++------------------ 3 files changed, 71 insertions(+), 91 deletions(-) diff --git a/app/components/State/StateSnapshots.tsx b/app/components/State/StateSnapshots.tsx index 2eca5e4..8b8f802 100644 --- a/app/components/State/StateSnapshots.tsx +++ b/app/components/State/StateSnapshots.tsx @@ -5,24 +5,45 @@ import { Divider } from "../Divider" import { Icon } from "../Icon" import { Tooltip } from "../Tooltip" import { useState } from "react" +import { useGlobal } from "../../state/useGlobal" +import { sendToCore } from "../../state/connectToServer" +import IRClipboard from "../../native/IRClipboard/NativeIRClipboard" import type { Snapshot } from "app/types" -interface StateSnapshotsProps { - snapshots: Snapshot[] - onCopySnapshot: (snapshot: Snapshot) => void - onRestoreSnapshot: (snapshot: Snapshot) => void - onDeleteSnapshot: (snapshotId: string) => void -} - -export function StateSnapshots({ - snapshots, - onCopySnapshot, - onRestoreSnapshot, - onDeleteSnapshot, -}: StateSnapshotsProps) { +export function StateSnapshots() { const theme = useTheme() + const [snapshots, setSnapshots] = useGlobal("snapshots", []) + const [activeClientId, _] = useGlobal("activeClientId", "") const [expandedSnapshotIds, setExpandedSnapshotIds] = useState>(new Set()) + const deleteSnapshot = (snapshotId: string) => { + setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) + } + + const copySnapshotToClipboard = (snapshot: Snapshot) => { + try { + IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) + console.log("Snapshot copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshot to clipboard:", error) + } + } + + const restoreSnapshot = (snapshot: Snapshot) => { + if (!snapshot || !snapshot.state) return + + // Use the snapshot's clientId if available, otherwise fall back to the active client + const targetClientId = snapshot.clientId || activeClientId + + if (!targetClientId) return + + // Send the restore command to the client + sendToCore("state.restore.request", { + clientId: targetClientId, + state: snapshot.state, + }) + } + const toggleSnapshotExpanded = (snapshotId: string) => { setExpandedSnapshotIds((prev) => { const newSet = new Set(prev) @@ -60,7 +81,7 @@ export function StateSnapshots({ style={$iconButton()} onPress={(e) => { e.stopPropagation() - onCopySnapshot(snapshot) + copySnapshotToClipboard(snapshot) }} > @@ -71,7 +92,7 @@ export function StateSnapshots({ style={$iconButton()} onPress={(e) => { e.stopPropagation() - onRestoreSnapshot(snapshot) + restoreSnapshot(snapshot) }} > @@ -82,7 +103,7 @@ export function StateSnapshots({ style={$iconButton()} onPress={(e) => { e.stopPropagation() - onDeleteSnapshot(snapshot.id) + deleteSnapshot(snapshot.id) }} > diff --git a/app/components/State/StateSubscriptions.tsx b/app/components/State/StateSubscriptions.tsx index 74beb11..1cf767c 100644 --- a/app/components/State/StateSubscriptions.tsx +++ b/app/components/State/StateSubscriptions.tsx @@ -4,36 +4,47 @@ import { TreeViewWithProvider } from "../TreeView" import { Divider } from "../Divider" import { Icon } from "../Icon" import type { StateSubscription } from "app/types" +import { useGlobal } from "app/state/useGlobal" +import { sendToCore } from "app/state/connectToServer" -interface StateSubscriptionsProps { - subscriptions: StateSubscription[] - onRemoveSubscription: (path: string) => void -} - -export function StateSubscriptions({ - subscriptions, - onRemoveSubscription, -}: StateSubscriptionsProps) { +export function StateSubscriptions() { const theme = useTheme() + const [stateSubscriptionsByClientId, setStateSubscriptionsByClientId] = useGlobal<{ + [clientId: string]: StateSubscription[] + }>("stateSubscriptionsByClientId", {}) + const [activeClientId, _] = useGlobal("activeClientId", "") + const clientStateSubscriptions = stateSubscriptionsByClientId[activeClientId] || [] + + const removeSubscription = (path: string) => { + const newStateSubscriptions = clientStateSubscriptions.filter((s) => s.path !== path) + sendToCore("state.values.subscribe", { + paths: newStateSubscriptions.map((s) => s.path), + clientId: activeClientId, + }) + setStateSubscriptionsByClientId((prev) => ({ + ...prev, + [activeClientId]: newStateSubscriptions, + })) + } - if (subscriptions.length === 0) { + if (clientStateSubscriptions.length === 0) { return State is empty } return ( <> - {subscriptions.map((subscription, index) => ( + {clientStateSubscriptions.map((subscription, index) => ( {subscription.path ? subscription.path : "Full State"} - onRemoveSubscription(subscription.path)}> + removeSubscription(subscription.path)}> - {index < subscriptions.length - 1 && } + {index < clientStateSubscriptions.length - 1 && } ))} diff --git a/app/screens/StateScreen.tsx b/app/screens/StateScreen.tsx index bf6f49c..ee634fa 100644 --- a/app/screens/StateScreen.tsx +++ b/app/screens/StateScreen.tsx @@ -20,54 +20,23 @@ export function StateScreen() { const [stateSubscriptionsByClientId, setStateSubscriptionsByClientId] = useGlobal<{ [clientId: string]: StateSubscription[] }>("stateSubscriptionsByClientId", {}) - const [activeTab, setActiveTab] = useGlobal("activeClientId", "") - const [snapshots, setSnapshots] = useGlobal("snapshots", []) + const [activeClientId, setActiveClient] = useGlobal("activeClientId", "") + const [snapshots] = useGlobal("snapshots", []) - const clientStateSubscriptions = stateSubscriptionsByClientId[activeTab] || [] + const clientStateSubscriptions = stateSubscriptionsByClientId[activeClientId] || [] const saveSubscription = (path: string) => { if (clientStateSubscriptions.some((s) => s.path === path)) return sendToCore("state.values.subscribe", { paths: [...clientStateSubscriptions.map((s) => s.path), path], - clientId: activeTab, + clientId: activeClientId, }) } - const removeSubscription = (path: string) => { - const newStateSubscriptions = clientStateSubscriptions.filter((s) => s.path !== path) - sendToCore("state.values.subscribe", { - paths: newStateSubscriptions.map((s) => s.path), - clientId: activeTab, - }) - setStateSubscriptionsByClientId((prev) => ({ - ...prev, - [activeTab]: newStateSubscriptions, - })) - } - const createSnapshot = () => { - if (!activeTab) return - - sendToCore("state.backup.request", { clientId: activeTab }) - } - - const deleteSnapshot = (snapshotId: string) => { - setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) - } - - const restoreSnapshot = (snapshot: Snapshot) => { - if (!snapshot || !snapshot.state) return - - // Use the snapshot's clientId if available, otherwise fall back to the active client - const targetClientId = snapshot.clientId || activeTab - - if (!targetClientId) return + if (!activeClientId) return - // Send the restore command to the client - sendToCore("state.restore.request", { - clientId: targetClientId, - state: snapshot.state, - }) + sendToCore("state.backup.request", { clientId: activeClientId }) } if (showAddSubscription) { @@ -93,10 +62,10 @@ export function StateScreen() { onPress={() => { setStateSubscriptionsByClientId((prev) => ({ ...prev, - [activeTab]: [], + [activeClientId]: [], })) - sendToCore("state.values.subscribe", { paths: [], clientId: activeTab }) - setActiveTab("") + sendToCore("state.values.subscribe", { paths: [], clientId: activeClientId }) + setActiveClient("") }} > Clear State @@ -118,19 +87,7 @@ export function StateScreen() { - {activeStateTab === "Subscriptions" ? ( - - ) : ( - - )} + {activeStateTab === "Subscriptions" ? : } ) @@ -222,15 +179,6 @@ function copyAllSnapshotsToClipboard(snapshots: Snapshot[]): void { } } -function copySnapshotToClipboard(snapshot: Snapshot): void { - try { - IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) - console.log("Snapshot copied to clipboard") - } catch (error) { - console.error("Failed to copy snapshot to clipboard:", error) - } -} - const $container = themed(({ spacing }) => ({ padding: spacing.xl, flex: 1, From 2ba3beaebbca1054be669a27feb3ceb0246da9cb Mon Sep 17 00:00:00 2001 From: fpena Date: Sun, 26 Oct 2025 21:54:30 -0700 Subject: [PATCH 23/23] Move copySnapshotToClipboard to top-level function --- app/components/State/StateSnapshots.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/components/State/StateSnapshots.tsx b/app/components/State/StateSnapshots.tsx index 8b8f802..50765c9 100644 --- a/app/components/State/StateSnapshots.tsx +++ b/app/components/State/StateSnapshots.tsx @@ -20,15 +20,6 @@ export function StateSnapshots() { setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId)) } - const copySnapshotToClipboard = (snapshot: Snapshot) => { - try { - IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) - console.log("Snapshot copied to clipboard") - } catch (error) { - console.error("Failed to copy snapshot to clipboard:", error) - } - } - const restoreSnapshot = (snapshot: Snapshot) => { if (!snapshot || !snapshot.state) return @@ -124,6 +115,15 @@ export function StateSnapshots() { ) } +function copySnapshotToClipboard(snapshot: Snapshot) { + try { + IRClipboard.setString(JSON.stringify(snapshot.state, null, 2)) + console.log("Snapshot copied to clipboard") + } catch (error) { + console.error("Failed to copy snapshot to clipboard:", error) + } +} + const $snapshotCard = themed(({ colors }) => ({ backgroundColor: colors.cardBackground, overflow: "hidden",