From 722fa2094afa8b6e16ff4d45ecb49fefa3a98182 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 25 Feb 2026 16:52:04 -0500 Subject: [PATCH 1/9] support focusedGuideKeys in favor of forcedGuideKey --- packages/client/src/clients/guide/client.ts | 15 +++++++++++++-- packages/client/src/clients/guide/types.ts | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index 25aad07cf..72129b9ef 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -214,6 +214,9 @@ const predicate = ( if (debug.forcedGuideKey) { return debug.forcedGuideKey === guide.key; } + if (debug.focusedGuideKeys) { + return !!debug.focusedGuideKeys[guide.key] + } const ineligible = ineligibleGuides[guide.key]; if (ineligible) { @@ -563,7 +566,11 @@ export class KnockGuideClient { // Clear debug state from store this.store.setState((state) => ({ ...state, - debug: { forcedGuideKey: null, previewSessionId: null }, + debug: { + forcedGuideKey: null, + previewSessionId: null, + focusedGuideKeys: {}, + }, previewGuides: {}, // Clear preview guides when exiting debug mode })); @@ -591,6 +598,7 @@ export class KnockGuideClient { debug: { skipEngagementTracking: true, ignoreDisplayInterval: true, + focusedGuideKeys: {}, ...debugOpts, debugging: true, }, @@ -1082,7 +1090,10 @@ export class KnockGuideClient { // Get the next unarchived step. getStep() { // If debugging this guide, return the first step regardless of archive status - if (self.store.state.debug?.forcedGuideKey === this.key) { + if ( + self.store.state.debug?.forcedGuideKey === this.key || + self.store.state.debug?.focusedGuideKeys?.[this.key] + ) { return this.steps[0]; } diff --git a/packages/client/src/clients/guide/types.ts b/packages/client/src/clients/guide/types.ts index 9e09efb93..d57ae5276 100644 --- a/packages/client/src/clients/guide/types.ts +++ b/packages/client/src/clients/guide/types.ts @@ -232,6 +232,7 @@ export type QueryStatus = { export type DebugState = { debugging?: boolean; forcedGuideKey?: string | null; + focusedGuideKeys?: Record; previewSessionId?: string | null; skipEngagementTracking?: boolean; ignoreDisplayInterval?: boolean; From e1ae670a6a75e11f030588e3c14f2544d9469ca7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 25 Feb 2026 17:01:37 -0500 Subject: [PATCH 2/9] add tests --- .../client/test/clients/guide/guide.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/client/test/clients/guide/guide.test.ts b/packages/client/test/clients/guide/guide.test.ts index 4bae5b050..a0e7d1e9e 100644 --- a/packages/client/test/clients/guide/guide.test.ts +++ b/packages/client/test/clients/guide/guide.test.ts @@ -2260,6 +2260,93 @@ describe("KnockGuideClient", () => { expect(result).toBeUndefined(); }); + test("returns an archived guide when focusedGuideKeys includes it", () => { + const archivedGuide = { + ...mockGuideThree, + steps: [ + { + ...mockStep, + message: { + ...mockStep.message, + archived_at: new Date().toISOString(), + }, + }, + ], + }; + + const stateWithArchivedGuide = { + guideGroups: [mockDefaultGroup], + guideGroupDisplayLogs: {}, + guides: { + ...mockGuides, + [mockGuideThree.key]: archivedGuide, + }, + ineligibleGuides: {}, + previewGuides: {}, + queries: {}, + location: undefined, + counter: 0, + debug: { + focusedGuideKeys: { [mockGuideThree.key]: true as const }, + }, + }; + + const client = new KnockGuideClient(mockKnock, channelId); + const result = client["_selectGuide"](stateWithArchivedGuide, { + key: mockGuideThree.key, + }); + + // Should return the focused guide even though it's archived + expect(result!.key).toBe("system_status"); + expect(result!.steps[0]!.message.archived_at).toBeTruthy(); + }); + + test("returns only focused guides when focusedGuideKeys is set", () => { + const stateWithGuides = { + guideGroups: [mockDefaultGroup], + guideGroupDisplayLogs: {}, + guides: mockGuides, + ineligibleGuides: {}, + previewGuides: {}, + queries: {}, + location: undefined, + counter: 0, + debug: { + focusedGuideKeys: { [mockGuideTwo.key]: true as const }, + }, + }; + + const client = new KnockGuideClient(mockKnock, channelId); + const result = client["_selectGuide"](stateWithGuides); + + // Should return the focused guide + expect(result!.key).toBe("feature_tour"); + }); + + test("doesn't return a guide not in focusedGuideKeys", () => { + const stateWithGuides = { + guideGroups: [mockDefaultGroup], + guideGroupDisplayLogs: {}, + guides: mockGuides, + ineligibleGuides: {}, + previewGuides: {}, + queries: {}, + location: undefined, + counter: 0, + debug: { + focusedGuideKeys: { [mockGuideTwo.key]: true as const }, + }, + }; + + const client = new KnockGuideClient(mockKnock, channelId); + const result = client["_selectGuide"](stateWithGuides, { + key: mockGuideOne.key, + }); + + // Guide one is not in focusedGuideKeys, so should not be returned + expect(result).toBeUndefined(); + }); + test("does not return a guide inside a throttle window ", () => { const stateWithGuides = { guideGroups: [ From 98d7f24d8cbf0b74dfe8f06ba2341a5184a3a5b4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 25 Feb 2026 18:10:16 -0500 Subject: [PATCH 3/9] add pin button to control focusedGuideKeys from the toolbar --- .../guide/components/Toolbar/V2/GuideRow.tsx | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx index 37bc18d56..fb2152e5d 100644 --- a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx +++ b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx @@ -1,3 +1,4 @@ +import { useGuideContext, useStore } from "@knocklabs/react-core"; import { Button } from "@telegraph/button"; import { Box, Stack } from "@telegraph/layout"; import { Tag } from "@telegraph/tag"; @@ -9,6 +10,7 @@ import { Code2, Eye, LocateFixed, + Pin, UserCircle2, } from "lucide-react"; import * as React from "react"; @@ -32,6 +34,15 @@ type Props = { }; export const GuideRow = ({ guide, orderIndex }: Props) => { + const { client } = useGuideContext(); + const { debugSettings } = useStore(client.store, (state) => ({ + debugSettings: state.debug, + })); + + const focusedGuideKeys = debugSettings?.focusedGuideKeys || {}; + const hasFocus = Object.keys(focusedGuideKeys).length > 0; + const isFocused = !!focusedGuideKeys[guide.key]; + return ( @@ -43,7 +54,11 @@ export const GuideRow = ({ guide, orderIndex }: Props) => { > {orderIndex + 1} - + {guide.key} @@ -52,6 +67,34 @@ export const GuideRow = ({ guide, orderIndex }: Props) => { {!isUnknownGuide(guide) && ( <> + {guide.annotation.selectable.status !== undefined && + (!hasFocus || isFocused) && ( + <> + + + +
Date: Wed, 25 Feb 2026 23:58:12 -0500
Subject: [PATCH 7/9] priortitize focused guide keys ahead of forced guide key

---
 packages/client/src/clients/guide/client.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts
index 9e28bc457..dfe155fc8 100644
--- a/packages/client/src/clients/guide/client.ts
+++ b/packages/client/src/clients/guide/client.ts
@@ -211,15 +211,15 @@ const predicate = (
   // If in debug mode with a forced guide key, bypass other filtering and always
   // return true for that guide only. This should always run AFTER checking the
   // filters but BEFORE checking archived status and location rules.
-  if (debug.forcedGuideKey) {
-    return debug.forcedGuideKey === guide.key;
-  }
   if (
     debug.focusedGuideKeys &&
     Object.keys(debug.focusedGuideKeys).length > 0
   ) {
     return !!debug.focusedGuideKeys[guide.key];
   }
+  if (debug.forcedGuideKey) {
+    return debug.forcedGuideKey === guide.key;
+  }
 
   const ineligible = ineligibleGuides[guide.key];
   if (ineligible) {

From 728cb65bdf1336b77c9f998b26ce6b72ca56d6fd Mon Sep 17 00:00:00 2001
From: Thomas 
Date: Thu, 26 Feb 2026 15:39:09 -0500
Subject: [PATCH 8/9] more style touch up

---
 .../guide/components/Toolbar/V2/GuideContextDetails.tsx  | 5 +----
 .../src/modules/guide/components/Toolbar/V2/GuideRow.tsx | 4 ++--
 .../react/src/modules/guide/components/Toolbar/V2/V2.tsx | 9 ++++++++-
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/GuideContextDetails.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/GuideContextDetails.tsx
index 21a0f728f..8c916b8c9 100644
--- a/packages/react/src/modules/guide/components/Toolbar/V2/GuideContextDetails.tsx
+++ b/packages/react/src/modules/guide/components/Toolbar/V2/GuideContextDetails.tsx
@@ -14,10 +14,7 @@ export const GuideContextDetails = () => {
   const { defaultGroup, debugSettings } = useStore(client.store, (state) => {
     return {
       defaultGroup: state.guideGroups[0],
-      debugSettings: {
-        skipEngagementTracking: !!state.debug?.skipEngagementTracking,
-        ignoreDisplayInterval: !!state.debug?.ignoreDisplayInterval,
-      },
+      debugSettings: state.debug || {},
     };
   });
   const displayInterval = defaultGroup?.display_interval ?? null;
diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx
index fb2152e5d..2c8f2f721 100644
--- a/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx
+++ b/packages/react/src/modules/guide/components/Toolbar/V2/GuideRow.tsx
@@ -36,10 +36,10 @@ type Props = {
 export const GuideRow = ({ guide, orderIndex }: Props) => {
   const { client } = useGuideContext();
   const { debugSettings } = useStore(client.store, (state) => ({
-    debugSettings: state.debug,
+    debugSettings: state.debug || {},
   }));
 
-  const focusedGuideKeys = debugSettings?.focusedGuideKeys || {};
+  const focusedGuideKeys = debugSettings.focusedGuideKeys || {};
   const hasFocus = Object.keys(focusedGuideKeys).length > 0;
   const isFocused = !!focusedGuideKeys[guide.key];
 
diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx
index 59a2aa397..b724a8d6c 100644
--- a/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx
+++ b/packages/react/src/modules/guide/components/Toolbar/V2/V2.tsx
@@ -1,6 +1,7 @@
 import { useGuideContext } from "@knocklabs/react-core";
 import { Button } from "@telegraph/button";
 import { Box, Stack } from "@telegraph/layout";
+import { Text } from "@telegraph/typography";
 import { Minimize2, Undo2 } from "lucide-react";
 import React from "react";
 
@@ -119,8 +120,14 @@ export const V2 = () => {
           
 
           
-            {result.error && {result.error}}
             
+            {result.error && (
+              
+                
+                  {result.error}
+                
+              
+            )}
             
Date: Thu, 26 Feb 2026 16:42:57 -0500
Subject: [PATCH 9/9] add todo comments

---
 .../src/modules/guide/components/Toolbar/V2/GuideHoverCard.tsx  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/react/src/modules/guide/components/Toolbar/V2/GuideHoverCard.tsx b/packages/react/src/modules/guide/components/Toolbar/V2/GuideHoverCard.tsx
index aa17d9fed..873e57488 100644
--- a/packages/react/src/modules/guide/components/Toolbar/V2/GuideHoverCard.tsx
+++ b/packages/react/src/modules/guide/components/Toolbar/V2/GuideHoverCard.tsx
@@ -55,6 +55,7 @@ export const GuideHoverCard = ({
                 variant="soft"
                 color="default"
                 leadingIcon={{ icon: RotateCcw, alt: "Reset engagement" }}
+                // TODO(KNO-11468): Placeholder button
                 onClick={() => {}}
               >
                 Reset engagement
@@ -64,6 +65,7 @@ export const GuideHoverCard = ({
                 variant="soft"
                 color="default"
                 leadingIcon={{ icon: ExternalLink, alt: "Go to dashboard" }}
+                // TODO(KNO-11819): Placeholder button
                 onClick={() => {}}
               >
                 Go to dashboard