The active account, always one glance away.
The AISW extension is a thin client of the aisw CLI. This spec defines every state it shows in VS Code and Cursor, the CLI JSON that drives each state, the exact copy, and the assets needed to ship.
- Status label
- The context name when it matches exactly. Otherwise the profiles that are actually active.
- Workspace mismatch
- Warning colors on the status bar item, plus one notification per window with a one-click switch.
- Terminal flows
- Shell integration reads the exit code, then the extension refreshes and verifies on its own. Requires
engines.vscode ^1.93.
Principles
These four rules settle most design questions. When a proposal breaks one, the proposal changes.
- The CLI is the authority.The extension never reads credential files or the keyring, and never infers state the CLI didn't report. Every label comes from a JSON field named in this spec.
- Color means risk.The status bar item gets a warning background only when the wrong account could authenticate in this repo. Other problems use an icon and leave the background alone.
- One click per switch, never zero.Opening a workspace, focusing the window or changing settings can refresh the display, but only a user action can change accounts.
- Success is quiet, problems are specific.Confirmations go to the status bar for five seconds. Notifications are for problems, and each one names the cause and offers the fix as a button.
Status bar item
One item on the left (primary) side, because account state applies to the whole window. Its priority puts it right after the source control branch. The item's name is AISW Account, so users can hide it from the status bar context menu.
| State | Item | Driven by | Click runs |
|---|---|---|---|
| Loading | AISW | Activation, before the handshake completes. Never shown longer than the 30 s CLI timeout. | Nothing |
| Context exact | client-acme | status --context → context.status == "exact", label from context.active | aisw.switchContext |
| Context drift | acme · drift | context.status == "drift" with exactly one entry in context.drift_candidates. With several, use the profile label below. | aisw.switchContext, candidate listed first as “Reapply acme” |
| Ambiguous | work · 2 contexts | context.status == "ambiguous", first name from context.matches. The tooltip lists all matches. | aisw.switchContext |
| No context | claude:work +2 | context.status == "none". First tool in tools[] order with a non-null active_profile; +n counts the other tools that have one. | aisw.switchProfile |
| Nothing active | No active profile | Profiles exist in list, but every active_profile is null. | aisw.switchProfile |
| First run | AISW: Set Up | list has no profiles for any tool. | aisw.getStarted (walkthrough) |
| Workspace mismatch color | personal ≠ client-acme | workspace status → status is mismatch, unmanaged (left side reads none) or ambiguous_active. Right side is expected_context. | aisw.switchToWorkspaceContext |
| Binding broken | client-acme | workspace status → invalid_context. The label stays the normal one; the icon flags it. | aisw.switchContext; the tooltip offers “Rebind in Terminal” |
| Switching | Switching to client-acme… | A mutation is in flight. Other AISW commands queue behind it. | Nothing |
| Not installed | Install aisw | No binary at any location in the detection order. | aisw.installCli |
| Installing | Installing aisw… | An install or update command is running in the AISW terminal. | aisw.showTerminal |
| Setting points nowhere | AISW: Check binaryPath | aisw.binaryPath is set, but the file is missing or not executable. | Opens the setting |
| CLI too old or new | AISW: Update aisw | version/capabilities → unsupported cli_api_version, schema version, or a missing feature flag. | aisw.installCli in update mode (too old) or aisw.diagnose (too new) |
| Remote window | AISW | vscode.env.remoteName is set. No CLI process starts, including from commands. | Tooltip only |
| CLI error | AISW: Error | Any other CliError from a read. The tooltip shows error.message. | aisw.showOutput |
Tooltip
| Claude Code | acme |
| Codex CLI | acme-ci |
| Gemini CLI | acme |
| Antigravity | — |
github.com/acme/*)A MarkdownString with supportThemeIcons. Its isTrusted is { enabledCommands: [...] } listing only AISW command IDs.
- The table always lists all four tools in
tools[]order. A tool with no active profile shows an em dash. - The workspace line comes from
workspace status:matched_ruleon a match,expected_contexton a mismatch, and nothing forno_expected_context. - On drift, the table shows the candidate's profile beside the active one for the tools that differ.
accessibilityInformation.labelreads the whole state as a sentence, for example “AISW account: context client-acme, matches this workspace.”
Installing aisw
Most people meet the extension right after installing it from the Marketplace, and at that moment aisw is usually missing. The extension never bundles or downloads the binary itself (ADR-0001). It finds an existing install, or runs the official installer the user picks in a visible terminal, then picks up the result without a restart.
- Find before asking.Check the setting, then PATH, then the places the official installers write to. Many “missing” binaries are installed, just not on the PATH VS Code started with.
- Install in plain sight.Install commands run in the AISW terminal, so the user can read exactly what runs. The extension only starts the command the user chose.
- Offer what works here.Methods are filtered by operating system and by the tools found on the machine. A Mac without Homebrew never sees a Homebrew command.
- No restart.When the install command ends, the extension finds the new binary itself, even if the new PATH only applies to new shells.
How the extension finds aisw
| Order | Where | If found |
|---|---|---|
| 1 | aisw.binaryPath, when set | Used as is. If the file is missing or not executable, show the invalid-setting state. No fallback, so a wrong setting is never hidden. |
| 2 | aisw on the extension host's PATH | Used as is. |
| 3 | ~/.local/bin/aisw (install script)/opt/homebrew/bin/aisw, /usr/local/bin/aisw, /home/linuxbrew/.linuxbrew/bin/aisw (Homebrew)~/.cargo/bin/aisw (Cargo) | Used for this session. The tooltip and diagnostics show the path; nothing is written to settings. |
| 4 | Nothing found | Show the Install aisw state. |
Step 3 checks only these fixed absolute paths, never a path inside the workspace. Every candidate must pass the version --json handshake before it is used. Detection runs again when an install command ends, when the AISW terminal closes, when aisw.binaryPath changes, and on window focus while aisw is missing.
First run
- Install the aisw CLI
AISW uses the aisw command-line tool to store and switch your accounts. Your credentials never pass through the editor.
Already installed somewhere else? Locate aisw
- Import the accounts you're signed into
- Group profiles into a context
- Bind this repository
~ $ brew install burakdede/tap/aisw ==> Fetching burakdede/tap/aisw ==> Installing aisw from burakdede/tap 🍺 /opt/homebrew/Cellar/aisw/0.3.10 ~ $
onContext:aisw.cliInstalled. Its media panel shows what happens after the click: the command runs in the AISW terminal and the status bar moves on to Set Up.- Homebrewbrew install burakdede/tap/aiswrecommendedUpdates with brew upgrade.
- Install scriptcurl -fsSL …/install.sh | shPuts aisw in ~/.local/bin and checks its SHA-256.
- Cargocargo install aisw --lockedBuilds from source with your Rust toolchain.
- Locate an Existing aisw…
- Open Installation Guide
brew is found, and Cargo only when cargo is found; both use the same lookup as aisw. The first available method is focused. Selecting one runs it in the AISW terminal straight away; the full command is in the description.~ $ sh -c "curl -fsSL https://raw.githubusercontent.com/burakdede/aisw/main/install.sh | sh" Installing aisw latest (aarch64-apple-darwin) Install dir: /Users/you/.local/bin Downloading binary... Downloading checksum... Verifying checksum... aisw latest installed to /Users/you/.local/bin/aisw Note: /Users/you/.local/bin is not in your PATH. ~ $
~/.local/bin: the new binary works in VS Code at once, without editing shell files or restarting.showOpenDialog with openLabel: "Use This aisw". The file must pass the handshake before it is saved to aisw.binaryPath in user settings. If it fails: “That file isn't a compatible aisw: reason.”After the install command
aisw init --no-shell-hook in the AISW terminal. aisw detects the current Claude Code, Codex, Gemini and Antigravity logins and asks before importing each one; shell files stay untouched. The status bar then shows the imported profiles, which is the first real payoff.| Claude Code | work |
| Codex CLI | work |
| Gemini CLI | — |
| Antigravity | — |
~/.local/bin, which VS Code may not see until it restarts. The extension uses the binary anyway and says where it came from. Diagnostics suggest adding the directory to PATH.Wrong path and outdated versions
- Homebrewbrew upgrade aiswinstalled withFound at /opt/homebrew/bin/aisw.
- Install scriptcurl -fsSL …/install.sh | shInstalls a second copy in ~/.local/bin.
- Open Release Notes
Cellar, ~/.cargo/bin, ~/.local/bin). Cargo updates use cargo install aisw --locked --force.Switching
Both pickers use createQuickPick, so they can show busy while the list refreshes. A selected row runs exactly one CLI mutation. The status bar then switches to its spinner until the refresh and the scoped verify finish.
- client-acmeclaude:acme · codex:acme-ci · gemini:acmethis workspace
- personalclaude:me · codex:mecontexts
- ossclaude:me · gemini:oss
- Switch a Single Profile…
expected_context comes first under a “this workspace” separator. A check marks context.active. Descriptions come from context list mappings, in tool:profile form.- workoauthClaude Code
- work-apiapi_key
- workchatgptCodex CLI
- Add Profile…
auth field from list. Filtering is on the profile name; the group label stays visible. The agy key is shown as “Antigravity” and invoked as antigravity.Empty states
- Create a Context in the Terminal…aisw context create
- Switch a Single Profile…
aisw context create into a new AISW terminal without pressing Enter (sendText(text, false)), so the user finishes the flags. Refresh happens when the command ends.- Add Profile…sign in or import in the terminal
- Get Started with AISW
After the switch
The adapter runs use or context use with --json, then re-reads status, then verifies only the tools listed in result.affected_tools. Tools the switch didn't touch never block a successful switch.
window.setStatusBarMessage for 5 s. No toast.issues[0]; the full report goes to the output channel. If result.warnings is not empty, the same notification lists them.Notifications
Every notification names the problem in its first clause and puts the fix on the primary button. Remediation buttons come from the CLI's error.remediation when kind == "run_command" and safe == true. Clicking one opens the AISW terminal with that command.
workspaceState; the warning color on the status bar stays. Disabled by aisw.workspaceMismatch.notify.error.message. “Nothing was changed” relies on the CLI's transactional rollback and is shown only for ok: false envelopes.Verify Active Accounts
verify --json exits 1 when summary.status == "fail", and its output is a report, not an envelope. The adapter must parse stdout on a non-zero exit. Tools with stored_profiles == 0 are left out, so a tool you never set up doesn't raise a warning.[info] aisw 0.3.10 · CLI API 1 · schema 1 · progress 1
[info] status --context exit 0 212 ms
[info] context use client-acme exit 0 388 ms
[warn] verify exit 1 301 ms
codex fail tool binary not found on PATH
→ Install codex or run 'aisw doctor --json'
createOutputChannel('AISW', { log: true }). Each command logs its subcommand, exit code and duration. Arguments are limited to tool, profile and context names; secrets never appear in arguments.Profile lifecycle
Adding a profile can involve OAuth or an API key, so it runs in the integrated terminal and the CLI keeps its native prompts. Removing a profile handles no secrets, so it runs in the background behind a native confirmation, with Undo.
Add Profile
- Claude Codeoauth, api_key, from_env, from_live
- Codex CLIoauth, api_key, from_env, from_live
- Gemini CLIoauth, api_key, from_env, from_live
- Antigravityoauth, from_live
capabilities.tools, with auth_methods as the description. Step 2 asks for the profile name, validated as 1–32 letters, numbers, hyphens or underscores, and rejected if the name already exists in list.~/src/client-acme-api $ aisw add claude work How do you want to authenticate Claude Code? › Sign in with browser (OAuth) Use an API key Waiting for browser sign-in… Saved Claude Code profile 'work'. ~/src/client-acme-api $
name: "AISW", iconPath: ThemeIcon('account'), color: ThemeColor('terminal.ansiBlue') and isTransient: true. The command runs through shellIntegration.executeCommand(binary, args), which quotes arguments correctly on every shell.onDidEndTerminalShellExecution matches the execution. The extension then refreshes and verifies the tool before showing this.exitCode is undefined (Ctrl+C, or a shell that doesn't report it), the extension refreshes anyway and says “Couldn't read the result of aisw add. AISW refreshed its state.”If shell integration hasn't activated within 3 s, the extension falls back to sendText with POSIX shell quoting, and refreshes when the terminal closes. That fallback can't read an exit code, so it shows no success notification.
Remove Profile
showWarningMessage(…, { modal: true, detail }). A modal is justified here because the action is destructive. The active profile is shown in the picker but disabled, with the description “switch away first”.remove <tool> <name> --yes --json. Undo runs backup restore <result.backup_ids[0]> --yes --json. I verified that this round trip restores the profile on aisw 0.3.10.Commands and settings
All commands declare category: "AISW", so the palette shows “AISW: Switch Context” while menus show the short title. Context keys hide commands that can't run.
| Command ID | Title | Icon | Enablement |
|---|---|---|---|
aisw.switchContext | Switch Context | arrow-swap | aisw.state == ready |
aisw.switchProfile | Switch Profile | account | aisw.state == ready |
aisw.switchToWorkspaceContext new | Switch to Workspace Context | repo | aisw.workspaceMismatch |
aisw.verify | Verify Active Accounts | pass-filled | aisw.state == ready |
aisw.refresh | Refresh | refresh | always (no-op in remote) |
aisw.addProfile | Add Profile… | add | aisw.state == ready |
aisw.removeProfile | Remove Profile… | trash | aisw.hasProfiles |
aisw.installCli new | Install or Update aisw… | cloud-download | !isRemote |
aisw.locateCli new | Locate aisw… | folder-opened | !isRemote |
aisw.diagnose | Show CLI Diagnostics | info | always (not in remote) |
aisw.showOutput new | Show Output | output | always |
aisw.getStarted new | Get Started | book | always |
Context keys set via setContext: aisw.state (loading | ready | cliMissing | installing | binaryPathInvalid | incompatible | remote | error), aisw.cliInstalled, aisw.workspaceMismatch, aisw.hasProfiles, aisw.hasContexts. In remote windows every command, including Refresh, is a no-op. No default keybindings ship, to avoid conflicts; the README suggests one.
Settings
| Setting | Default | Scope | Why |
|---|---|---|---|
aisw.binaryPath | "" (PATH) | machine | Security. With the default scope, a cloned repo's .vscode/settings.json can point this setting at a binary inside the repo, and it runs on window focus. Machine scope blocks that. Also list the setting in capabilities.untrustedWorkspaces.restrictedConfigurations. |
aisw.workspaceMismatch.notify new | true | window | Turns off the mismatch notification. The warning color on the status bar stays. |
aisw.refreshDebounceMs | 500 | window | Debounce for refreshes on focus. |
Get Started walkthrough
Contributed through contributes.walkthroughs. Its steps complete on real state, not on clicks:
- Install the aisw CLI. Buttons run
aisw.installCliandaisw.locateCli. Completes ononContext:aisw.cliInstalled. - Import the accounts you're signed into. Runs
aisw init --no-shell-hookin the AISW terminal, with Add Profile as the alternative. Completes ononContext:aisw.hasProfiles. - Group profiles into a context. Completes on
onContext:aisw.hasContexts. - Bind this repository. Types
aisw workspace bind . --contextinto the AISW terminal.
Icons and assets
integrations/vscode/media/icon.png: a 256 × 256 RGBA PNG cut from website/public/aisw-logo.png. The baked-in drop shadow is removed, the corners are transparent, and a 1 px navy hairline at 16% keeps the white tile visible on light pages. The manifest requires at least 128 px and recommends 256 px for Retina screens.
The status bar, Quick Picks and notifications use codicons only, because the status bar guidelines ask for familiar icons. The brand appears in the Marketplace listing and the walkthrough.
#077FFE#02CDFD#001436// integrations/vscode/package.json "icon": "media/icon.png", "galleryBanner": { "color": "#001436", "theme": "dark" }, "keywords": ["claude code", "codex", "gemini", "account switcher", "profiles"], "engines": { "vscode": "^1.93.0" }, "capabilities": { "virtualWorkspaces": false, "untrustedWorkspaces": { "supported": "limited", "restrictedConfigurations": ["aisw.binaryPath"] } }
Pin @types/vscode to the engines minimum (~1.93.0). The lockfile currently resolves 1.140, which lets code compile against APIs that older VS Code and Cursor builds don't have.
Codicon map
| Meaning | Codicon | Used in |
|---|---|---|
| Account state | account | Status bar, terminal icon, Switch Profile |
| Work in progress | sync~spin | Status bar while loading or switching |
| Wrong account could run | warning | Status bar mismatch, broken binding, verify issues |
| Extension can't work | error | CLI missing, incompatible, read failure |
| Something to install | cloud-download | Status bar when aisw is missing, install picker |
| Not available here | circle-slash | Remote window |
| Bound to this repo | repo | Workspace row in Switch Context |
| Active row | check | Quick Pick rows, success status message |
| Verified | pass-filled | Tooltip workspace line, Verify command |
Implementation
The current extension.ts mixes state, rendering and commands in one class. Split it so each state in this spec is a pure function of CLI JSON and can be tested from fixtures without VS Code.
src/ cliAdapter.ts spawn, envelope parsing, verify-report parsing, timeouts contract.ts payload types, version and feature checks state.ts deriveViewState(status, list, contexts, workspace) → ViewState (pure) statusBar.ts render(ViewState) → text, icon, background, tooltip, a11y label pickers.ts Switch Context, Switch Profile, Add, Remove Quick Picks terminalFlows.ts AISW terminal, shell-integration execution, exit handling notifications.ts copy deck plus remediation-to-button mapping refresh.ts scheduler: handshake cache, sequence guard, mutation queue extension.ts activation and wiring only
Refresh lifecycle
stateDiagram-v2 [*] --> Loading: activate Loading --> Remote: env.remoteName set Loading --> CliMissing: ENOENT Loading --> Incompatible: version or feature check fails Loading --> Ready: handshake ok and reads ok Ready --> Ready: focus (debounced): status --context + workspace status Ready --> Switching: user picks a context or profile Switching --> Ready: refresh + verify affected_tools Switching --> Ready: ok false (rollback), error toast CliMissing --> Loading: binaryPath changed or Refresh Incompatible --> Loading: binaryPath changed or Refresh
- Handshake cache. Run
versionandcapabilitiesonce perbinaryPath. Run them again only after a setting change, an explicit Refresh, or anunsupported_commanderror. A focus refresh then costs two processes instead of six. - Sequence guard. Each refresh takes a sequence number. A result is rendered only if no newer refresh has started.
- Mutation queue. One mutation at a time. While one runs, focus refreshes are skipped.
- Scoped verify. Parse the report on exit 1, then check only
tools[]entries whosetoolis inaffected_tools.
Fixture matrix
One fixture directory per state in the status bar table. Each holds the JSON for status --context, list, context list and workspace status. deriveViewState is tested against every fixture, and the real-CLI compatibility test seeds an isolated AISW_HOME to produce at least the exact, none, mismatch and verify-fail states.
Checklist
Ordered by dependency. The first four items unblock everything else.
- #257Parse the verify report on a non-zero exit, and scope verification after a switch to
affected_tools. - #257Make
aisw.binaryPathmachine-scoped and declare limited support for untrusted workspaces. - #257Apply the remote guard in
createAdapter()so no command starts a CLI process remotely. - #257Binary detection order, install and update picker, Locate, and post-install import, as in Installing aisw.
- #256Add the fixture matrix and
deriveViewStatetests. - #258Status bar states, tooltip and accessibility label, as specified above.
- #258Grouped Quick Picks, empty states, the workspace row, and
aisw.switchToWorkspaceContext. - #258Mismatch notification with once-per-window logic and the
notifysetting. - #258Handshake cache, sequence guard, mutation queue.
- #259Shell-integration terminal flow with exit handling and the sendText fallback; bump
enginesto ^1.93. - #259Background remove with a modal confirmation and Undo through
backup restore. - #260Icon, gallery banner, keywords, walkthrough, and pinned
@types/vscode. - #260Screenshots of every state in VS Code and Cursor, light and dark, attached to the PR.