-
Notifications
You must be signed in to change notification settings - Fork 1
Open the thread's simulator tab when its agent starts driving (opt-in) #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dhiyaancnirmal
wants to merge
1
commit into
vburojevic:main
Choose a base branch
from
dhiyaancnirmal:feat/auto-open-simulator-panel
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * Open this thread's simulator tab when its agent starts driving. | ||
| * | ||
| * The server says so on `DRIVE_CHANNEL` — once per driving session, and only | ||
| * when the `openSimulatorOnDrive` setting is on, so a client never has to ask | ||
| * whether the feature is enabled. Every connected client hears every signal | ||
| * (V1 realtime has no per-channel subscriptions); this thread's composer is | ||
| * the one that acts, and only on a signal naming it. | ||
| * | ||
| * Opening the same action twice focuses the existing tab, so an announcement | ||
| * that lands while the tab is already open is a no-op rather than a duplicate. | ||
| * A tab the person closed stays closed until the next session — the server's | ||
| * quiet window, not this hook, decides when that is. | ||
| */ | ||
| import { useBbNavigate, useRealtime } from "@get-bb/plugin-sdk/app"; | ||
| import { DRIVE_CHANNEL, type DriveSignal } from "../../src/sim/channel.js"; | ||
|
|
||
| /** The `threadPanelAction` id registered in `app.tsx`. */ | ||
| export const SIMULATOR_PANEL_ACTION = "simulator"; | ||
|
|
||
| function isDriveSignal(payload: unknown): payload is DriveSignal { | ||
| return ( | ||
| typeof payload === "object" && | ||
| payload !== null && | ||
| typeof (payload as { threadId?: unknown }).threadId === "string" | ||
| ); | ||
| } | ||
|
|
||
| export function useOpenOnDrive(threadId: string | null): void { | ||
| const navigate = useBbNavigate(); | ||
| useRealtime(DRIVE_CHANNEL, (payload: unknown) => { | ||
| if (threadId === null || !isDriveSignal(payload) || payload.threadId !== threadId) return; | ||
| // A declined open (no side panel on this surface) is logged by the host; | ||
| // there is nothing for a banner to do about it. | ||
| navigate.openThreadPanel({ actionId: SIMULATOR_PANEL_ACTION }); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * When does "an agent is driving the simulator" begin? | ||
| * | ||
| * Every tool call and every CLI gesture takes the lease and gives it back, so | ||
| * the lease alone cannot tell a *session* from a *step*: a thread tapping five | ||
| * times in ten seconds acquires five times. Announcing each one would reopen | ||
| * the side panel five times — and, worse, reopen a tab the person had just | ||
| * closed because they did not want it. | ||
| * | ||
| * So a session is a run of acquisitions by one thread with no gap longer than | ||
| * `quietMs`, and it is announced exactly once, at its first acquisition. The | ||
| * window slides: as long as the thread keeps driving, nothing new is said. A | ||
| * closed tab therefore stays closed until the agent stops for a while and | ||
| * comes back — the same rhythm the lease TTL already gives the human. | ||
| * | ||
| * Pure, and keyed on the thread rather than the device: it is the thread's | ||
| * panel that opens, whichever simulator it happens to be driving. | ||
| */ | ||
|
|
||
| export const DRIVE_QUIET_MS = 90_000; | ||
|
|
||
| export class DriveAnnouncer { | ||
| private lastSeen = new Map<string, number>(); | ||
|
|
||
| constructor( | ||
| private readonly quietMs: number = DRIVE_QUIET_MS, | ||
| private readonly now: () => number = Date.now, | ||
| ) {} | ||
|
|
||
| /** | ||
| * Record that `threadId` just took the lease. Returns true when this is the | ||
| * first acquisition of a new session — the one worth telling the UI about. | ||
| */ | ||
| touch(threadId: string): boolean { | ||
| const at = this.now(); | ||
| const previous = this.lastSeen.get(threadId); | ||
| this.lastSeen.set(threadId, at); | ||
| return previous === undefined || at - previous > this.quietMs; | ||
| } | ||
|
|
||
| /** Forget a thread, so its next acquisition announces again. */ | ||
| forget(threadId: string): void { | ||
| this.lastSeen.delete(threadId); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * A driving session is announced once, at its start, and not again until the | ||
| * thread has been quiet for longer than the window. | ||
| */ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { DriveAnnouncer } from "../../src/sim/announce.js"; | ||
|
|
||
| function announcer(quietMs = 1_000) { | ||
| let now = 0; | ||
| const instance = new DriveAnnouncer(quietMs, () => now); | ||
| return { instance, advance: (ms: number) => (now += ms) }; | ||
| } | ||
|
|
||
| describe("DriveAnnouncer", () => { | ||
| it("announces the first acquisition of a thread", () => { | ||
| const { instance } = announcer(); | ||
| expect(instance.touch("thr_a")).toBe(true); | ||
| }); | ||
|
|
||
| it("stays silent while the same thread keeps driving inside the window", () => { | ||
| const { instance, advance } = announcer(1_000); | ||
| expect(instance.touch("thr_a")).toBe(true); | ||
| advance(400); | ||
| expect(instance.touch("thr_a")).toBe(false); | ||
| advance(900); | ||
| // 900ms since the last touch, not since the first: the window slides. | ||
| expect(instance.touch("thr_a")).toBe(false); | ||
| }); | ||
|
|
||
| it("announces again once the thread has been quiet for longer than the window", () => { | ||
| const { instance, advance } = announcer(1_000); | ||
| instance.touch("thr_a"); | ||
| advance(1_001); | ||
| expect(instance.touch("thr_a")).toBe(true); | ||
| }); | ||
|
|
||
| it("treats a gap of exactly the window as still driving", () => { | ||
| const { instance, advance } = announcer(1_000); | ||
| instance.touch("thr_a"); | ||
| advance(1_000); | ||
| expect(instance.touch("thr_a")).toBe(false); | ||
| }); | ||
|
|
||
| it("tracks threads independently", () => { | ||
| const { instance, advance } = announcer(1_000); | ||
| expect(instance.touch("thr_a")).toBe(true); | ||
| advance(100); | ||
| expect(instance.touch("thr_b")).toBe(true); | ||
| advance(100); | ||
| expect(instance.touch("thr_a")).toBe(false); | ||
| expect(instance.touch("thr_b")).toBe(false); | ||
| }); | ||
|
|
||
| it("announces again after forget, whatever the clock says", () => { | ||
| const { instance } = announcer(1_000); | ||
| instance.touch("thr_a"); | ||
| instance.forget("thr_a"); | ||
| expect(instance.touch("thr_a")).toBe(true); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
openSimulatorOnDriveis disabled, every successful acquisition still callsdrives.touch(threadId)and advances the session window. If a user enables the option within 90 seconds of such an acquisition, the agent's next drive returnsfalsefromtouch, so no signal is published and the newly enabled setting appears ineffective until the thread has been quiet for the full window. Only record touches while the option is enabled, or forget existing sessions when it transitions from off to on.Useful? React with 👍 / 👎.