all repos — sz-chrome-remote-cdp @ a2588bb315a56a8dfcd3aa8a261cdc19b160d079

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

src/profile/picker.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
/**
 * The profile picker, shared by /browser-profile and first-run setup so both
 * show the user exactly the same list.
 *
 * Rendering rules:
 *   - one row per profile, labelled "Name (email)" — or "Name (Profile 3)" when
 *     the profile has no signed-in account, so every row keeps its shape
 *   - the pinned profile is marked with a check
 *   - a final row clears the selection, since a pin must be undoable
 *
 * `ctx.ui.select` resolves to undefined when the user presses Escape AND in
 * non-interactive modes (pi's print/RPC UI context returns undefined for every
 * dialog), so callers must treat "no answer" as "change nothing".
 */

import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import type { BrowserProfile } from "./list";
import type { ProfilePin } from "./store";

export const CLEAR_SELECTION_LABEL = "— Clear selection (use whichever window is focused) —";

export type ProfileChoice =
  | { readonly kind: "profile"; readonly profile: BrowserProfile }
  | { readonly kind: "clear" }
  | { readonly kind: "cancelled" };

/**
 * Two profiles can legitimately share a name and account (the same person
 * signed into two profiles). Labels must stay distinct or the selection cannot
 * be mapped back, so a repeated label gains its directory.
 */
const disambiguate = (profiles: ReadonlyArray<BrowserProfile>): ReadonlyArray<string> => {
  const counts = new Map<string, number>();
  for (const p of profiles) counts.set(p.label, (counts.get(p.label) ?? 0) + 1);
  return profiles.map((p) => ((counts.get(p.label) ?? 0) > 1 ? `${p.label} [${p.dir}]` : p.label));
};

/**
 * Show the picker and report what the user chose. Returns "cancelled" when
 * there is no UI or the user dismissed the dialog.
 */
export const promptForProfile = async (
  ctx: ExtensionContext,
  profiles: ReadonlyArray<BrowserProfile>,
  currentPin: ProfilePin | null,
  title = "Select browser profile",
): Promise<ProfileChoice> => {
  if (!ctx.hasUI || profiles.length === 0) return { kind: "cancelled" };

  const labels = disambiguate(profiles);
  const rows = labels.map((label, i) => {
    const isPinned = currentPin?.profileDir === profiles[i]?.dir;
    return isPinned ? `✓ ${label}` : `  ${label}`;
  });
  const options = [...rows, CLEAR_SELECTION_LABEL];

  const answer = await ctx.ui.select(title, options);
  if (answer === undefined) return { kind: "cancelled" };
  if (answer === CLEAR_SELECTION_LABEL) return { kind: "clear" };

  const index = options.indexOf(answer);
  const profile = index >= 0 ? profiles[index] : undefined;
  if (!profile) return { kind: "cancelled" };
  return { kind: "profile", profile };
};

/** Build the durable pin for a chosen profile. */
export const pinFor = (userDataDir: string, profile: BrowserProfile): ProfilePin => ({
  userDataDir,
  profileDir: profile.dir,
  label: profile.label,
  savedAt: new Date().toISOString(),
});