Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Starting from 0.2.0, CLI / Extension / DSH Plugin share the same version number.
explains that it belongs after a supporting subcommand, replacing the
misleading `--version` suggestion and usage line
([#387](https://github.com/Tencent/BrowserSkill/issues/387)).
- DSH Plugin: the right-sidebar observation tab auto-opens again on DSH
0.2.0+, which removed `current` from the `sessions.list` snapshot. The
auto-open target now falls back to the `uiSession` main binding (exposed by
DSH 0.1.5 and 0.2.x alike), and conversation switches on 0.2.x hosts keep
re-triggering it
([#484](https://github.com/Tencent/BrowserSkill/issues/484)).

## [0.3.3] - 2026-10-14

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,28 @@ export interface NativeSidebarSlots {
): () => void;
}

/**
* Session catalog list, narrowed to the fields the auto-open reads.
* DSH 0.2.0 removed `current` from this snapshot (view selection moved to
* the uiSession service); it stays optional so one shape covers all hosts.
*/
export interface SessionListSource {
getSnapshot(): { current: string | undefined };
getSnapshot(): { current?: string | undefined };
subscribe(listener: () => void): () => void;
}

/** Reading of the uiSession service's renderer-facing main-binding adapter. */
export interface UiSessionService {
adapter?: {
/** Main-view session binding; `key` is its session id, undefined when absent. */
current?: MainSessionBindingSource;
};
current?: MainSessionBindingSource;
}

/** `key`-addressed binding source exposed by uiSession on DSH 0.1.5 and 0.2.x alike. */
export interface MainSessionBindingSource {
getSnapshot(): { key?: string | undefined };
subscribe(listener: () => void): () => void;
}

Expand Down Expand Up @@ -167,6 +187,22 @@ export function registerObservationSidebar(
| { register(definition: NativeTabDefinition): () => void }
| undefined;
const sessions = (host.get("sessions") as { list?: SessionListSource } | undefined)?.list;
// DSH 0.2.0+ removed `current` from the session list; the uiSession service
// owns the main-view binding instead. Optional on every host: 0.1.5 exposes
// it through adapter.current as well, so either shape can drive the fallback.
const mainSession = (host.get("uiSession") as UiSessionService | undefined) ?? undefined;
const mainBinding =
mainSession?.adapter?.current ??
(mainSession && "current" in mainSession ? mainSession.current : undefined);
const mainBindingUsable =
typeof mainBinding?.getSnapshot === "function" && typeof mainBinding?.subscribe === "function";
/** Prefer the list's own selection (0.1.5); fall back to the main binding (0.2.x). */
const currentSession = (): string | undefined => {
const listed = sessions?.getSnapshot().current;
if (listed !== undefined) return listed;
if (!mainBindingUsable) return undefined;
return mainBinding.getSnapshot().key;
};
if (
typeof sidebar?.openTabIn !== "function" ||
typeof tabs?.register !== "function" ||
Expand Down Expand Up @@ -227,10 +263,7 @@ export function registerObservationSidebar(
const attempt = () => {
if (disposed || pending !== request) return;
try {
if (
sessions.getSnapshot().current !== sessionId ||
store.presentation.getSnapshot().floating
) {
if (currentSession() !== sessionId || store.presentation.getSnapshot().floating) {
cancelOpen();
return;
}
Expand All @@ -255,7 +288,7 @@ export function registerObservationSidebar(
};
function reveal() {
try {
const current = sessions?.getSnapshot().current;
const current = currentSession();
if (current !== undefined) open(current);
} catch (error) {
fail(error);
Expand All @@ -275,7 +308,7 @@ export function registerObservationSidebar(
};
const evaluate = () => {
if (disposed) return;
const current = sessions.getSnapshot().current;
const current = currentSession();
const observations = store.getSnapshot().sessions;
for (const id of autoOpened) {
if (!observations.some((session) => visibleToScope(session, id))) autoOpened.delete(id);
Expand Down Expand Up @@ -352,6 +385,9 @@ export function registerObservationSidebar(
store.acquire();
disposers.push(store.subscribe(schedule));
disposers.push(sessions.subscribe(schedule));
// DSH 0.2.0+: conversation switches update the uiSession main binding
// without necessarily touching the session list, so follow it too.
if (mainBindingUsable) disposers.push(mainBinding.subscribe(schedule));
disposers.push(store.presentation.subscribe(schedule));
connect();
schedule();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,23 @@ function observation(initial: SessionObservation[] = []) {

/** Native contracts: keyed slots use implementation id, navigation uses kind and explicit session. */
function nativeHost(
options: { ready?: boolean; declared?: boolean; betterSidebar?: boolean } = {},
options: {
ready?: boolean;
declared?: boolean;
betterSidebar?: boolean;
listCurrent?: boolean;
} = {},
) {
let current: string | undefined = "conv-1";
let ready = options.ready ?? true;
// DSH 0.2.0+ removed `current` from the session list; mirror that shape when
// listCurrent is false and expose the uiSession main binding instead.
const listCarriesCurrent = options.listCurrent ?? true;
const declared = new Set(options.declared === false ? [] : [BODY, TITLE]);
const watchers = new Set<{ name: string; start: () => () => void; stop?: () => void }>();
const components = new Map<string, (props: NativeTabProps) => ReactNode>();
const listeners = new Set<() => void>();
const bindingListeners = new Set<() => void>();
const roots = new Map<string, ReturnType<typeof render>>();
let definition: NativeTabDefinition | undefined;
const register = vi.fn((value: NativeTabDefinition) => {
Expand Down Expand Up @@ -97,13 +106,28 @@ function nativeHost(
sidebarRightTabs: { register },
sessions: {
list: {
getSnapshot: () => ({ current }),
getSnapshot: () => (listCarriesCurrent ? { current } : { ids: [], byId: {} }),
subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
},
},
...(listCarriesCurrent
? {}
: {
uiSession: {
adapter: {
current: {
getSnapshot: () => ({ key: current }),
subscribe(listener: () => void) {
bindingListeners.add(listener);
return () => bindingListeners.delete(listener);
},
},
},
},
}),
...(options.betterSidebar ? { betterSidebar } : {}),
};
const host: NativeSidebarHost = {
Expand Down Expand Up @@ -140,7 +164,11 @@ function nativeHost(
},
switchSession(id: string | undefined) {
current = id;
for (const listener of [...listeners]) listener();
if (listCarriesCurrent) {
for (const listener of [...listeners]) listener();
} else {
for (const listener of [...bindingListeners]) listener();
}
},
close(sessionId: string) {
roots.get(sessionId)?.unmount();
Expand Down Expand Up @@ -274,6 +302,43 @@ describe("automatic presentation", () => {
expect(n.openTabIn).toHaveBeenCalledTimes(2);
});

it("auto-opens through the uiSession main binding when the session list has no current (DSH 0.2.0+)", async () => {
const h = observation([BUSY, FOREIGN]);
const n = nativeHost({ listCurrent: false });
install(n.host, h.store);
await waitFor(() =>
expect(n.openTabIn).toHaveBeenCalledExactlyOnceWith("conv-1", OBSERVATION_TAB_KIND),
);
await act(async () => n.switchSession("conv-2"));
expect(n.openTabIn).toHaveBeenLastCalledWith("conv-2", OBSERVATION_TAB_KIND);
await act(async () => n.switchSession("conv-1"));
expect(n.openTabIn).toHaveBeenCalledTimes(2);
});

it("waits for a selected main binding conversation before opening on DSH 0.2.0+", async () => {
const h = observation([FOREIGN]);
const n = nativeHost({ listCurrent: false });
n.switchSession(undefined);
install(n.host, h.store);
await act(async () => {});
expect(n.openTabIn).not.toHaveBeenCalled();
await act(async () => n.switchSession("conv-1"));
expect(n.openTabIn).not.toHaveBeenCalled();
await act(async () => h.push([BUSY]));
expect(n.openTabIn).toHaveBeenCalledTimes(1);
});

it("registers without any current-session source but never auto-opens", async () => {
const h = observation([BUSY]);
const n = nativeHost({ listCurrent: false });
delete n.values.uiSession;
install(n.host, h.store);
await act(async () => {});
expect(n.register).toHaveBeenCalled();
expect(n.openTabIn).not.toHaveBeenCalled();
expect(h.store.presentation.getSnapshot().sidebarAvailable).toBe(true);
});

it("waits for a selected conversation with its own browser and opens again for a new task", async () => {
const h = observation([FOREIGN]);
const n = nativeHost();
Expand Down
Loading