Skip to content

feat: console harness onboarding - #210

Merged
sergiofilhowz merged 1 commit into
mainfrom
feat/console-harness-onboarding
Jun 2, 2026
Merged

feat: console harness onboarding#210
sergiofilhowz merged 1 commit into
mainfrom
feat/console-harness-onboarding

Conversation

@sergiofilhowz

@sergiofilhowz sergiofilhowz commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
  • added official iii logo
  • minor improvements on icons
  • added favicon
  • added harness onboarding to console

Check Storybook

image

Summary by CodeRabbit

  • New Features

    • Added chat blocking when harness worker is unavailable, with contextual placeholder messaging in composer.
    • Added installation interface for harness setup with visual stage tracking and error handling.
    • Added copy command button for terminal interactions.
  • Style

    • Updated header icons and wordmark styling.
    • Refreshed dialog overlay appearance.
    • Added favicon to page.
  • Tests

    • Added Storybook stories for new empty state variants and wordmark styling.

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jun 2, 2026 11:32am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements harness worker monitoring and installation UI for the iii console. It introduces a useHarnessStatus hook that tracks backend harness presence via real-time events, gates chat submission when the harness is unavailable, renders context-aware empty states showing installation progress or configuration prompts, and applies UI polish including header icon updates and Wordmark SVG refactoring.

Changes

Harness Status and Chat Integration

Layer / File(s) Summary
Harness status hook and SDK trigger registration
console/web/src/hooks/use-harness-status.ts, console/web/src/lib/iii-client.ts
useHarnessStatus monitors harness worker presence, loading state, and installation progress via presence checks and real-time event subscriptions; IiiClient exposes registerTrigger for engine-side trigger binding and unregisters all triggers on disposal.
Harness status in conversations context
console/web/src/lib/conversations-context.tsx
ConversationsContextValue now includes harnessStatus computed from useHarnessStatus(backend.id === 'real'), distributing harness state to all chat consumers.
Chat submission and editor blocking
console/web/src/components/chat/ChatView.tsx, console/web/src/components/chat/Composer.tsx
ChatView derives harnessBlocked and prevents submission when harness blocks; Composer accepts blocked and blockedPlaceholder props, disabling editor/buttons and showing contextual placeholder text during harness installation.
EmptyState component and variant rendering
console/web/src/components/chat/EmptyState.tsx
New five-variant component (ready, no-provider, no-harness, installing, install-failed) with dedicated body subcomponents, an InstallConsole showing per-stage progress with symbols and percentages, and error formatting via normalizeErrorMessage.
EmptyState Storybook stories and fixtures
console/web/src/components/chat/EmptyState.stories.tsx
Seven stories (Ready, NoProvider, NoHarness, Installing, InstallingNoEvents, InstallFailed, Dock) with typed InstallStage fixtures simulating download progress and failure scenarios, plus decorators constraining layout.
MessageList empty state resolution
console/web/src/components/chat/MessageList.tsx
MessageList uses optional conversations context and a new resolveEmptyState helper that maps harness/catalog loading status to variant selection and provides context-aware CTAs including provider configuration navigation.

UI Polish and Visual Updates

Layer / File(s) Summary
Header icon updates
console/web/src/App.tsx
Replaces text glyphs (? and ) with CircleQuestionMark and SettingsIcon from lucide-react, adjusting sizing and wrapper markup.
Wordmark SVG implementation and tone support
console/web/src/components/ui/Wordmark.tsx, console/web/src/components/ui/Wordmark.stories.tsx
Rewrites Wordmark from CSS-styled text to SVG assets (iii-ink.svg, iii-white.svg), introduces tone prop (auto/ink/inverse) for theme-aware color selection, and adds InverseOnInk story.
Favicon setup and copy button utility
console/web/index.html, console/web/src/main.tsx, console/web/src/components/chat/sandbox/terminal/CopyCommandButton.tsx
HTML title changed to "iii console" and favicon link added; main.tsx dynamically sets favicon; new CopyCommandButton component provides clipboard API with visual feedback.
Dialog overlay darkening
console/web/src/components/ui/Dialog.tsx
Dark-mode overlay background updated from dark:bg-ink/70 to dark:bg-black/90 for improved visibility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • iii-hq/workers#164: Refactors MessageList/EmptyState rendering pipeline around density/variant states, which this PR builds upon by implementing harness-driven variant selection.
  • iii-hq/workers#141: Modifies the same chat UI components (ChatView.tsx, Composer.tsx, MessageList.tsx) that are extended here for harness-block gating.

Suggested reviewers

  • andersonleal

🐰 A harness comes to chat,
With stages and with signs,
Icons gleam in lucide light,
While the console shines.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: console harness onboarding' accurately summarizes the main feature addition—harness worker onboarding for the console—and is consistent with the PR's primary objective and most substantial changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/console-harness-onboarding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 13 skipped (no docs/).

Layer Result
structure
vale
ai
render

Note

17 stale rendered artifact(s) detected on main, unrelated to this PR. This PR is fine; the drift was already there. A maintainer should open a chore PR to re-render these.

  • shell/README.md
  • shell/skill.md
  • shell/skills/chmod.md
  • shell/skills/exec.md
  • shell/skills/exec_bg.md
  • shell/skills/grep.md
  • shell/skills/kill.md
  • shell/skills/list.md
  • shell/skills/ls.md
  • shell/skills/mkdir.md
  • shell/skills/mv.md
  • shell/skills/read.md
  • …and 5 more (see the workflow logs)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@console/web/src/components/chat/EmptyState.tsx`:
- Around line 207-213: The branch hides errorMessage whenever any stages exist;
change the condition to show the fallback error unless a failed stage is already
present. In EmptyState.tsx update the check that currently uses stages.length
=== 0 to instead detect whether a failed stage exists (e.g., !stages.some(stage
=> stage.type === 'failed')) so the errorMessage is shown when failed is true
and no explicit failed stage was emitted.

In `@console/web/src/components/chat/sandbox/terminal/CopyCommandButton.tsx`:
- Around line 15-23: The handleCopy logic currently stacks timeouts and can
reset `copied` from earlier clicks; fix this by adding a ref (e.g.,
`copyTimeoutRef`) to store the timer id, clear it before creating a new timeout,
set `copied` true as before, then assign the new timeout id to the ref; also add
a cleanup (useEffect return) to clear the timeout on unmount so
`setCopied(false)` from an old timer never fires after the component is gone.
Use the existing `copied`, `setCopied`, and `handleCopy` identifiers to locate
where to add the ref and cleanup.

In `@console/web/src/components/ui/Wordmark.tsx`:
- Around line 19-42: Wordmark applies the consumer-provided className only to
the img root for 'ink'/'inverse' but to child imgs for 'auto', breaking layout
contracts; change Wordmark so the root element always receives sizeClass
(cn(IMG_CLASS, className)) and return a wrapper (span) for all tone variants,
with inner <img> elements using only image-specific classes (e.g., IMG_CLASS or
icon-only classes) and not the consumer className; update the branches that
currently return an <img> directly (the 'ink' and 'inverse' cases) to return a
wrapper with sizeClass and a single <img> child using inkUrl/whiteUrl
respectively, keeping aria attributes unchanged.

In `@console/web/src/hooks/use-harness-status.ts`:
- Around line 182-205: The presence-check flow can miss a race where a harness
is added between checkHarnessPresent() returning false and registerTrigger()
being bound; move the trigger subscription setup (client.on(HARNESS_WATCH_FN,
...) and client.registerTrigger({...})) to occur before you finalize the first
probe result, or alternatively perform an immediate re-check (call
checkHarnessPresent(client) again and update setPresent()) right after
registerTrigger() completes; ensure you still respect the cancelled guard and
only setLoading(false) after the trigger is bound and the final presence
decision is applied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b57bd36d-dc17-4680-8a1c-ed1121e447b6

📥 Commits

Reviewing files that changed from the base of the PR and between 8e90d57 and f89dd05.

⛔ Files ignored due to path filters (3)
  • console/web/src/icons/favicon.svg is excluded by !**/*.svg
  • console/web/src/icons/iii-ink.svg is excluded by !**/*.svg
  • console/web/src/icons/iii-white.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • console/web/index.html
  • console/web/src/App.tsx
  • console/web/src/components/chat/ChatView.tsx
  • console/web/src/components/chat/Composer.tsx
  • console/web/src/components/chat/EmptyState.stories.tsx
  • console/web/src/components/chat/EmptyState.tsx
  • console/web/src/components/chat/MessageList.tsx
  • console/web/src/components/chat/sandbox/terminal/CopyCommandButton.tsx
  • console/web/src/components/ui/Dialog.tsx
  • console/web/src/components/ui/Wordmark.stories.tsx
  • console/web/src/components/ui/Wordmark.tsx
  • console/web/src/hooks/use-harness-status.ts
  • console/web/src/lib/conversations-context.tsx
  • console/web/src/lib/iii-client.ts
  • console/web/src/main.tsx

Comment on lines +207 to +213
{failed ? (
<div className="flex flex-col gap-3">
{errorMessage && stages.length === 0 ? (
<span className="font-mono text-[12.5px] text-alert lowercase break-all">
{errorMessage}
</span>
) : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Show the fallback error even when partial progress exists.

If worker::add rejects after emitting started/downloading but never publishes a failed stage, this branch hides errorMessage just because stages.length > 0. The failed state then loses the only actionable reason. Gate this on whether a failed stage is already present, not on whether any stage exists.

Suggested fix
-          {errorMessage && stages.length === 0 ? (
+          {errorMessage && !stages.some((stage) => stage.stage === 'failed') ? (
             <span className="font-mono text-[12.5px] text-alert lowercase break-all">
               {errorMessage}
             </span>
           ) : null}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/EmptyState.tsx` around lines 207 - 213, The
branch hides errorMessage whenever any stages exist; change the condition to
show the fallback error unless a failed stage is already present. In
EmptyState.tsx update the check that currently uses stages.length === 0 to
instead detect whether a failed stage exists (e.g., !stages.some(stage =>
stage.type === 'failed')) so the errorMessage is shown when failed is true and
no explicit failed stage was emitted.

Comment on lines +15 to +23
const [copied, setCopied] = useState(false)

const handleCopy = useCallback(() => {
if (typeof navigator === 'undefined' || !navigator.clipboard) return
void navigator.clipboard.writeText(text).then(() => {
setCopied(true)
window.setTimeout(() => setCopied(false), 1200)
})
}, [text])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Re-arm the reset timer instead of stacking multiple timeouts.

Each successful click schedules a new timeout, so repeated copies can flip copied back to false based on an older click instead of the latest one. Store the timer id in a ref, clear it before starting a new one, and clean it up on unmount.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/chat/sandbox/terminal/CopyCommandButton.tsx`
around lines 15 - 23, The handleCopy logic currently stacks timeouts and can
reset `copied` from earlier clicks; fix this by adding a ref (e.g.,
`copyTimeoutRef`) to store the timer id, clear it before creating a new timeout,
set `copied` true as before, then assign the new timeout id to the ref; also add
a cleanup (useEffect return) to clear the timeout on unmount so
`setCopied(false)` from an old timer never fires after the component is gone.
Use the existing `copied`, `setCopied`, and `handleCopy` identifiers to locate
where to add the ref and cleanup.

Comment on lines +19 to +42
export function Wordmark({ className, tone = 'auto' }: WordmarkProps) {
const sizeClass = cn(IMG_CLASS, className)

if (tone === 'ink') {
return <img src={inkUrl} alt="iii" className={sizeClass} />
}

if (tone === 'inverse') {
return <img src={whiteUrl} alt="iii" className={sizeClass} />
}

function Glyph() {
return (
<span aria-hidden className="inline-flex flex-col items-center gap-[3px]">
{/* tittle */}
<span className="block size-[4px] bg-ink" />
{/* stem */}
<span className="block w-[4px] h-[12px] bg-ink" />
<span role="img" aria-label="iii" className="inline-flex">
<img
src={inkUrl}
alt=""
aria-hidden
className={cn(sizeClass, '[html[data-theme=dark]_&]:hidden')}
/>
<img
src={whiteUrl}
alt=""
aria-hidden
className={cn(sizeClass, 'hidden [html[data-theme=dark]_&]:inline')}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep className on a stable root element across all tone variants.

ink/inverse apply className to the returned root node, but auto pushes it down to both <img> children and leaves the wrapper unstyled. That makes layout utilities like mx-*, self-*, inline-*, and selector hooks behave differently depending on tone, which is a shared-component contract break.

Proposed direction
 export function Wordmark({ className, tone = 'auto' }: WordmarkProps) {
-  const sizeClass = cn(IMG_CLASS, className)
+  const rootClass = cn('inline-flex', className)

   if (tone === 'ink') {
-    return <img src={inkUrl} alt="iii" className={sizeClass} />
+    return (
+      <span role="img" aria-label="iii" className={rootClass}>
+        <img src={inkUrl} alt="" aria-hidden className={IMG_CLASS} />
+      </span>
+    )
   }

   if (tone === 'inverse') {
-    return <img src={whiteUrl} alt="iii" className={sizeClass} />
+    return (
+      <span role="img" aria-label="iii" className={rootClass}>
+        <img src={whiteUrl} alt="" aria-hidden className={IMG_CLASS} />
+      </span>
+    )
   }

   return (
-    <span role="img" aria-label="iii" className="inline-flex">
+    <span role="img" aria-label="iii" className={rootClass}>
       <img
         src={inkUrl}
         alt=""
         aria-hidden
-        className={cn(sizeClass, '[html[data-theme=dark]_&]:hidden')}
+        className={cn(IMG_CLASS, '[html[data-theme=dark]_&]:hidden')}
       />
       <img
         src={whiteUrl}
         alt=""
         aria-hidden
-        className={cn(sizeClass, 'hidden [html[data-theme=dark]_&]:inline')}
+        className={cn(IMG_CLASS, 'hidden [html[data-theme=dark]_&]:inline')}
       />
     </span>
   )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function Wordmark({ className, tone = 'auto' }: WordmarkProps) {
const sizeClass = cn(IMG_CLASS, className)
if (tone === 'ink') {
return <img src={inkUrl} alt="iii" className={sizeClass} />
}
if (tone === 'inverse') {
return <img src={whiteUrl} alt="iii" className={sizeClass} />
}
function Glyph() {
return (
<span aria-hidden className="inline-flex flex-col items-center gap-[3px]">
{/* tittle */}
<span className="block size-[4px] bg-ink" />
{/* stem */}
<span className="block w-[4px] h-[12px] bg-ink" />
<span role="img" aria-label="iii" className="inline-flex">
<img
src={inkUrl}
alt=""
aria-hidden
className={cn(sizeClass, '[html[data-theme=dark]_&]:hidden')}
/>
<img
src={whiteUrl}
alt=""
aria-hidden
className={cn(sizeClass, 'hidden [html[data-theme=dark]_&]:inline')}
export function Wordmark({ className, tone = 'auto' }: WordmarkProps) {
const rootClass = cn('inline-flex', className)
if (tone === 'ink') {
return (
<span role="img" aria-label="iii" className={rootClass}>
<img src={inkUrl} alt="" aria-hidden className={IMG_CLASS} />
</span>
)
}
if (tone === 'inverse') {
return (
<span role="img" aria-label="iii" className={rootClass}>
<img src={whiteUrl} alt="" aria-hidden className={IMG_CLASS} />
</span>
)
}
return (
<span role="img" aria-label="iii" className={rootClass}>
<img
src={inkUrl}
alt=""
aria-hidden
className={cn(IMG_CLASS, '[html[data-theme=dark]_&]:hidden')}
/>
<img
src={whiteUrl}
alt=""
aria-hidden
className={cn(IMG_CLASS, 'hidden [html[data-theme=dark]_&]:inline')}
/>
</span>
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/ui/Wordmark.tsx` around lines 19 - 42, Wordmark
applies the consumer-provided className only to the img root for 'ink'/'inverse'
but to child imgs for 'auto', breaking layout contracts; change Wordmark so the
root element always receives sizeClass (cn(IMG_CLASS, className)) and return a
wrapper (span) for all tone variants, with inner <img> elements using only
image-specific classes (e.g., IMG_CLASS or icon-only classes) and not the
consumer className; update the branches that currently return an <img> directly
(the 'ink' and 'inverse' cases) to return a wrapper with sizeClass and a single
<img> child using inkUrl/whiteUrl respectively, keeping aria attributes
unchanged.

Comment on lines +182 to +205
void (async () => {
const client = await getIiiClient()
try {
const found = await checkHarnessPresent(client)
if (!cancelled) setPresent(found)
} catch {
if (!cancelled) setPresent(false)
} finally {
if (!cancelled) setLoading(false)
}
if (cancelled) return

// Subscribe to harness add events for live progress + CLI detection.
// If the engine doesn't publish the `worker` trigger type, fall back to
// the progress-less path (install() re-checks presence on resolve).
try {
offHandler = client.on(HARNESS_WATCH_FN, (data: unknown) => {
handlerRef.current(data)
})
offTrigger = client.registerTrigger({
type: 'worker',
function_id: `${HARNESS_WATCH_FN}::${client.browserId}`,
config: { operations: ['add'] },
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register the trigger before finishing the first presence check.

There’s a race here: if harness is added from the CLI after checkHarnessPresent() returns false but before registerTrigger() is bound, that add event is lost and the UI stays stuck in no-harness until a reload. Since this hook is the source of truth for external install detection, the trigger needs to be active before the initial probe result is finalized, or you need a second probe immediately after binding.

Suggested fix
     void (async () => {
       const client = await getIiiClient()
+      try {
+        offHandler = client.on(HARNESS_WATCH_FN, (data: unknown) => {
+          handlerRef.current(data)
+        })
+        offTrigger = client.registerTrigger({
+          type: 'worker',
+          function_id: `${HARNESS_WATCH_FN}::${client.browserId}`,
+          config: { operations: ['add'] },
+        })
+      } catch {
+        offTrigger?.()
+        offHandler?.()
+        offTrigger = undefined
+        offHandler = undefined
+      }
+
       try {
         const found = await checkHarnessPresent(client)
         if (!cancelled) setPresent(found)
       } catch {
         if (!cancelled) setPresent(false)
       } finally {
         if (!cancelled) setLoading(false)
       }
       if (cancelled) return
-
-      // Subscribe to harness add events for live progress + CLI detection.
-      // If the engine doesn't publish the `worker` trigger type, fall back to
-      // the progress-less path (install() re-checks presence on resolve).
-      try {
-        offHandler = client.on(HARNESS_WATCH_FN, (data: unknown) => {
-          handlerRef.current(data)
-        })
-        offTrigger = client.registerTrigger({
-          type: 'worker',
-          function_id: `${HARNESS_WATCH_FN}::${client.browserId}`,
-          config: { operations: ['add'] },
-        })
-      } catch {
-        offTrigger?.()
-        offHandler?.()
-        offTrigger = undefined
-        offHandler = undefined
-      }
     })()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void (async () => {
const client = await getIiiClient()
try {
const found = await checkHarnessPresent(client)
if (!cancelled) setPresent(found)
} catch {
if (!cancelled) setPresent(false)
} finally {
if (!cancelled) setLoading(false)
}
if (cancelled) return
// Subscribe to harness add events for live progress + CLI detection.
// If the engine doesn't publish the `worker` trigger type, fall back to
// the progress-less path (install() re-checks presence on resolve).
try {
offHandler = client.on(HARNESS_WATCH_FN, (data: unknown) => {
handlerRef.current(data)
})
offTrigger = client.registerTrigger({
type: 'worker',
function_id: `${HARNESS_WATCH_FN}::${client.browserId}`,
config: { operations: ['add'] },
})
void (async () => {
const client = await getIiiClient()
try {
offHandler = client.on(HARNESS_WATCH_FN, (data: unknown) => {
handlerRef.current(data)
})
offTrigger = client.registerTrigger({
type: 'worker',
function_id: `${HARNESS_WATCH_FN}::${client.browserId}`,
config: { operations: ['add'] },
})
} catch {
offTrigger?.()
offHandler?.()
offTrigger = undefined
offHandler = undefined
}
try {
const found = await checkHarnessPresent(client)
if (!cancelled) setPresent(found)
} catch {
if (!cancelled) setPresent(false)
} finally {
if (!cancelled) setLoading(false)
}
if (cancelled) return
})()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/hooks/use-harness-status.ts` around lines 182 - 205, The
presence-check flow can miss a race where a harness is added between
checkHarnessPresent() returning false and registerTrigger() being bound; move
the trigger subscription setup (client.on(HARNESS_WATCH_FN, ...) and
client.registerTrigger({...})) to occur before you finalize the first probe
result, or alternatively perform an immediate re-check (call
checkHarnessPresent(client) again and update setPresent()) right after
registerTrigger() completes; ensure you still respect the cancelled guard and
only setLoading(false) after the trigger is bound and the final presence
decision is applied.

@sergiofilhowz
sergiofilhowz merged commit 64ee045 into main Jun 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant