jackwener/OpenCLI · error · Error

Cannot create ${role} tab group without tabs

Error message

Cannot create ${role} tab group without tabs

What it means

createOwnedGroup groups a set of tabIds into a chrome-managed tab group owned by OpenCLI (for a given role such as automation/UI containers). Chrome's tabs.group API cannot be called with zero tabs, so the function guards ids.length === 0 and throws a plain Error naming the role. It prevents an opaque Chrome API failure ('No tab IDs given') from escaping.

Source

Thrown at extension/src/background.ts:847

  ids: number[],
): Promise<OwnedContainerGroup> {
  if (ids.length === 0) return group;
  await ensureTabsInWindow(ids, group.windowId);
  const tabs = await Promise.all(ids.map((id) => chrome.tabs.get(id).catch(() => null)));
  const missing = tabs
    .filter((tab): tab is chrome.tabs.Tab => tab !== null && tab.id !== undefined && tab.groupId !== group.id)
    .map((tab) => tab.id!);
  if (missing.length > 0) await chrome.tabs.group({ groupId: group.id, tabIds: missing });
  updateOwnedSessionWindowForTabs(role, ids, group.windowId);
  return group;
}

async function createOwnedGroup(
  role: OwnedWindowRole,
  windowId: number,
  ids: number[],
): Promise<OwnedContainerGroup> {
  if (ids.length === 0) throw new Error(`Cannot create ${role} tab group without tabs`);
  await ensureTabsInWindow(ids, windowId);
  const groupId = await chrome.tabs.group({ tabIds: ids, createProperties: { windowId } });
  ownedContainers[role].groupId = groupId;
  ownedContainers[role].windowId = windowId;
  // Record in the ledger and persist BEFORE the title/color update lands so a
  // worker crash between the two API calls can self-heal on resume:
  // `ensureCanonicalGroupTitle` repairs the title on the next ensure cycle
  // once the ledger surfaces the untitled orphan. We must not `tabs.ungroup`
  // on failure or the recorded id dangles.
  if (role === 'interactive') interactiveGroupLedger.add(groupId);
  await persistRuntimeState();
  const group = await chrome.tabGroups.update(groupId, {
    color: OWNED_TAB_GROUP_COLOR,
    title: CONTAINER_TAB_GROUP_TITLE[role],
    collapsed: false,
  });
  updateOwnedSessionWindowForTabs(role, ids, group.windowId);
  return { id: group.id, windowId: group.windowId, title: group.title };

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Ensure at least one open tab id exists before calling createOwnedGroup (filter dead ids and bail early if none)
  2. Recreate a lease via the normal open/automation entry point instead of regrouping an empty set
  3. If this happens on extension restart, clear/rebuild the persisted ownedContainers ledger

Example fix

// before
await createOwnedGroup(role, windowId, ids);
// after
const alive = await filterOpenTabIds(ids);
if (alive.length === 0) return null; // skip grouping, nothing to own
await createOwnedGroup(role, windowId, alive);
Defensive patterns

Strategy: validation

Validate before calling

const openIds = await Promise.all(ids.map(id => chrome.tabs.get(id).catch(() => null)));
const alive = ids.filter((_, i) => openIds[i]?.id != null);
if (alive.length === 0) return; // nothing to group

Type guard

const hasTabs = (ids) => Array.isArray(ids) && ids.length > 0;

Try / catch

try {
  await createOwnedGroup(role, windowId, ids);
} catch (e) {
  if (/without tabs/.test(e.message)) console.warn(`No tabs to group for ${role}; skipping`);
  else throw e;
}

Prevention

When it happens

Trigger: An internal caller passes an empty ids array — e.g. attempting to restore/persist a container whose ledger of tab ids is empty, or passing only already-closed tab ids after filtering.

Common situations: Extension restarted (worker crash) and resumed with an empty container ledger; all leased tabs were closed by the user before regrouping; a bug in tab-id collection filtering everything out.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/1e19746f2dd35ddf. Report an issue: GitHub.