跳到正文
原文
Google AI:DEV 作者专属(RSS)· Krish Verma·· 8 小时前AI 评分31

为桌面 AI 助手 Ankita 打造常驻伴侣岛:审批、吉祥物与 14px 唤醒条

I'm building a companion island for my desktop AI assistant: approvals, a mascot, and a 14px wake strip

AI 导读

开发者正在为开源桌面 AI 助手 Ankita 构建一个常驻置顶的"伴侣岛",用于在主窗口隐藏时展示状态并处理审批请求。该组件包含 240×64 的 Petit tab、368×64 的 Home 卡片和 368×232 的展开视图,收起时仅留 14px 唤醒条;ApprovalRegistry 将待审批请求存在条目本身,使后订阅的岛仍能列出全部待处理项。

正文

I'm building a desktop AI assistant, and it has a permission problem that no amount of prompt engineering fixes.

Ankita asks for approval before it does anything irreversible — sends a message, runs a script, opens a browser session. That's the right behaviour. The problem is where that question appears: in the main chat window. Minimise the app, hide it to the tray, alt-tab to your IDE — and the approval request sits there in a hidden window, waiting on you, while the agent sits there waiting on the request. Nobody is wrong here. But nothing happens.

So I'm building a companion island: a small always-on-top pill pinned to the top edge of the screen that only exists when the main window is gone. It's the one UI element the assistant is allowed to keep while it's otherwise out of your way.

Three views, one rule

The island has three views, and the sizes are deliberate constants, not vibes:

  • Petit tab (240×64): the collapsed state. A sliver of the assistant that's always there.
  • Home (368×64): the full card — mascot plus a status line.
  • Home expanded (368×232): grows when approvals are pending, and lists them.

There's also a tucked view, which parks the tab just above the screen edge so only a 14-pixel strip shows. Fourteen pixels is the whole product thesis: just enough to notice peripherally, just enough to click to wake it. Hover it and it peeks out; click it and the main window comes back.

All of this geometry lives in a module with zero Electron imports, on purpose. islandSizeFor(view, approvalCount) clamps everything to the known presets — the comment in the code says it plainly: "a bad caller can never stretch the window." Positions, mode transitions, edge margins (8px so narrow displays never clip it) — all of it is unit-testable with plain node:test. I test desktop geometry the same way I test server code, because desktop geometry is the thing that breaks on everyone else's machine.

The approvals finally leave the chat window

The interesting bit isn't the pill — it's the plumbing behind it. The ApprovalRegistry stores every pending request on the entry itself, not just as an emitted event. The code comment calls the island out by name: "a late subscriber such as the island can list what is still pending." The island subscribes late — it might be created after a request went out — and still gets the full list. The expanded view shows the tool name and what it wants to do, and you approve or deny from there without ever restoring the main window.

That's the design decision I'm proudest of: the island is not a second chat window. It doesn't let you talk to the agent. It does exactly two things — show status, and clear approvals. One job, no scope creep.

Yes, there's a mascot

The island hosts a small companion mascot with states (idle, working, thinking), emotes, eye shapes, and cursor-follow — it glances toward your pointer. The animation engine is ported from Coucou's open-source Mochi engine (MIT licensed), re-skinned as Ankita's own character with original sound cues left out rather than ripped off. States are driven by tweens with easing functions, and the engine reports a busy flag so the renderer knows when everything has settled.

Is a mascot frivolous in a tool that runs shell commands for you? Maybe. But the island only earns its screen space if you leave it visible, and a tiny face that looks at you when you approve things is weirdly effective at that.

What's shipping around it

The island is riding along with two unreleased changes I'm finishing at the same time:

  • Keyless Composio sign-in. Connecting an app now runs a browser OAuth 2.1 + PKCE flow — dynamic client registration as a public client, a single-use http://127.0.0.1:<port>/callback loopback redirect, S256 code exchange. No public URL, no domain, no broker to host. The token goes straight to the OS vault; the grant file keeps only the grant ID and timestamps. And COMPOSIO_API_KEY keeps working until the deprecation window closes, because breaking existing setups on a Tuesday is a choice you make once.
  • MCP approval tiers. The old trusted-server bypass is gone. Every tool call resolves through a four-level gate — auto-allow, ask once, always ask, deny — in a fixed order: blocklist, explicit tool rule, per-app rule, locked app defaults, heuristic. And heuristics can never deny, and a call that loosens protection can never run under auto-approve. A scheduled job unattended at 3 AM can't quietly grant itself permission to stop asking.

Both are in the Unreleased section of the changelog now. The island is the visible face of the same philosophy: the assistant should ask well, ask rarely, and never strand you.

Why this and not a notification

I considered just firing OS notifications for approvals. They're free, they're native, and I already had the code path half-built for the proactive loop. I decided against it for a reason that's hard to measure: notifications are a queue you clear. An island is a presence you glance at. Pending approvals aren't interruptions competing with Slack — they're the agent's state, and state deserves a persistent surface. The 14px strip is my bet that ambient awareness beats interruptive pings.

It's not finished — I'm still debugging animation edge cases in the slide transitions as I write this. If you've built always-on-top companions, floating widgets, or anything that has to be visible-but-not-annoying, I'd genuinely like to hear what you learned. What's the right amount of pixels for a background presence?

Ankita is open source: https://github.com/akyourowngames/A.N.K.I.T.A

来源:Google AI:DEV 作者专属(RSS) · dev.to