Notchi:给 GNOME 顶栏养一只像素宠物,实时反应 Claude Code 在干嘛
🛠️ 工具 开源项目 GNOME Claude Code ubuntu 6

Notchi 的五只角色

起因:我老是忘了 Claude Code 还在等我

用 Claude Code 写代码,最割裂的体验是:它在后台思考、调工具、跑命令,而我切到浏览器查文档去了。等回过神,终端里它早就停在「需要你授权才能继续」那一行,干等了我五分钟。

我一直很喜欢 macOS 上的「刘海宠物」(notch pet)——一只小东西待在屏幕顶端,随系统状态做反应。Ubuntu 的顶栏没有刘海,但同样是块始终可见、不会被窗口盖住的区域。于是有了 Notchi:一个 GNOME 42 扩展,把一只像素宠物塞进顶栏,让它实时表演 Claude Code 的状态——思考、干活、求授权、完成、发呆。余光一扫就知道该不该我接手。

项目地址:https://github.com/fnidore/notchi-gnome

它长什么样

五只原创像素角色,各有性格:史莱姆 Slime、线条狗 Linedog、棕毛 Coco、双马尾 Peach、银发 Shiro。每只都有六个基础状态,「思考」还细分情绪:

六种状态

思考时的情绪变体

整条链路是怎么打通的

核心难点其实是一句话:Claude Code 的事件,怎么传到 GNOME Shell 进程里去? 这俩是完全独立的进程。我的方案是 unix socket:

Claude Code 事件
  → hooks (settings.json)
    → notchi-send.py        # 发送端,毫秒级退出
      → unix socket (~/.cache/notchi/notchi.sock)
        → GNOME 扩展 (Gio.SocketService 监听)
          → 顶栏宠物切换状态

Claude Code 的 hook 机制会在特定事件(提交 prompt、调工具前后、需要通知等)触发外部命令。我把 notchi-send.py 挂上去,它把事件经 socket 丢给扩展,扩展驱动顶栏的宠物。

第一条铁律:发送端绝不能拖累 Claude Code

hook 是同步挂在 Claude Code 主流程上的——发送端慢一拍,你打字就卡一拍。所以 notchi-send.py 的设计原则是「能连就连,连不上立刻滚」:

def sock_path():
    p = os.environ.get("NOTCHI_SOCK")
    if p:
        return p
    home = os.path.expanduser("~")
    return os.path.join(home, ".cache", "notchi", "notchi.sock")

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(0.3)               # 0.3 秒还连不上就放弃
s.connect(sock_path())
s.sendall((line + "\n").encode("utf-8"))

关键就两点:0.3 秒超时,以及外层一个兜底的 except Exception: pass + sys.exit(0)。哪怕扩展没开、socket 不存在、GNOME 崩了,发送端也会在一瞬间无声退出,绝不让宠物拖累正经干活。socket 路径默认 ~/.cache/notchi/notchi.sock,也可用环境变量 NOTCHI_SOCK 覆盖。

扩展这头:用 Gio.SocketService 监听

GNOME 扩展跑在 GJS(GNOME 的 JS 运行时)里,用 Gio.SocketService 起一个 unix socket 服务:

start() {
    const dir = GLib.path_get_dirname(this._path);
    GLib.mkdir_with_parents(dir, 0o700);   // 私有目录
    this._service = new Gio.SocketService();
    const addr = new Gio.UnixSocketAddress({ path: this._path });
    this._service.add_address(addr, Gio.SocketType.STREAM,
        Gio.SocketProtocol.DEFAULT, null);
    this._service.connect('incoming', (service, connection) => {
        this._handle(connection);
        return false;
    });
    this._service.start();
}

每个连接按行读一条 JSON,解析后丢进事件处理。

状态机:事件怎么变成表情

宠物的「灵魂」是一张事件→状态的映射表,直接对应 Claude Code 的 hook 事件名:

const EVENT_MAP = {
    UserPromptSubmit: 'thinking',  PreToolUse: 'working',
    PostToolUse: 'working',         Notification: 'attention',
    Stop: 'done',                   SubagentStop: 'working',
    PreCompact: 'thinking',         SessionStart: 'idle',
    SessionEnd: 'idle',
};

done / error 这种「终态」会挂一个定时器自动回到 idle,免得宠物一直举着「完成」的牌子:

if (state === 'done' || state === 'error') {
    s.doneTimer = GLib.timeout_add_seconds(
        GLib.PRIORITY_DEFAULT, DONE_REVERT_SECONDS,
        () => { s.state = 'idle'; /* ... */ });
}

「思考」时它为什么会有不同表情

我不想让「思考」永远是一张呆脸。但要判断你这条 prompt 是开心还是焦虑,又不能把你的内容传到云上——那太离谱了。所以情绪分析纯本地关键词匹配,跑在发送端:

_ANXIOUS = ("报错", "错误", "失败", "崩", ..., "error", "fail", "bug", ...)
_HAPPY   = ("谢谢", "感谢", ...,           "thanks", "great", ...)
_CONFUSED= ("怎么", "为什么", ...,         "how", "why", ...)

优先级 anxious > happy > confused > neutral,多个问号也算困惑。命中后把结果塞进事件再发出去:

if obj.get("hook_event_name") == "UserPromptSubmit":
    obj["notchi_mood"] = classify_mood(obj.get("prompt", ""))
line = json.dumps(obj, ensure_ascii=False)   # 保留中文,不转义

于是你骂「怎么又报错了」,宠物就一脸焦虑陪你 debug——但这判断从没离开过你的机器。扩展那头按 state + mood 选脸,没有 SVG 时还有 emoji 兜底:

if (state === 'thinking') {
    return { happy:'😊', anxious:'😰', confused:'😕', neutral:'🤔' }[mood] || '🤔';
}

开多个会话怎么办:character +N

我常常同时开两三个 Claude Code 窗口。Notchi 用一个 Map<session_id, {...}> 跟踪每个会话,新会话用游标轮流分配不同角色:

this._sessions = new Map();
if (!s) {
    s = { pet: PETS[this._petCursor % PETS.length],
          family: this._familyForNew(), state: 'idle', /* ... */ };
    this._petCursor++;
    this._sessions.set(sid, s);
}

顶栏只显示「主角」,其余的折成一个角标 +N

const extra = this._sessions.size > 1 ? this._sessions.size - 1 : 0;
if (extra > 0) { this._badge.text = ` +${extra}`; this._badge.visible = true; }

点开宠物会展开面板:每个会话的角色、状态、已跑时长、最近事件,外加实时用量(5 小时 / 7 天额度),以及刷新、重置、设置。用量是 notchi-usage.py 自动发现 ~/.claude 下的账户、在本地读 OAuth token 拉取的——同样不出本机。

安装

make install   # 装扩展+发送端+图标,并安全合并 Claude hooks(带备份、幂等)
make enable    # 启用扩展
make prefs     # 打开设置,挑角色和音效

make install 会调 install.sh 把 hook 幂等地合进 ~/.claude/settings.json(重复跑不会写脏)。音效有三档:关闭、常开、终端聚焦时静音。装完开一个新会话,宠物就在顶栏动起来了。

环境要求

  • GNOME 42+,Ubuntu / Linux
  • 能访问 settings.json 的 Claude Code
  • 用量显示需要 Anthropic API token(本地读取)

踩过的坑 & 一点感想

  • 为什么是 unix socket 而不是写文件轮询:轮询有延迟、还费电;socket 是事件驱动,宠物反应几乎是即时的。
  • 为什么发送端要「失败即静默」:调试阶段我一度让它抛异常,结果扩展一没开,每次提交 prompt 终端都吐一堆红字——本末倒置。工具型的东西,宁可自己悄悄失效,也不能打扰主流程。
  • 它本质是个把「状态可视化」的小玩具,但意外地解压:干活时余光瞄一眼顶栏,就知道 AI 在忙啥、要不要我搭把手。

如果你也整天和 Claude Code 待在一起,欢迎试试,也欢迎来 GitHub 画新角色或提 issue。我另外两个 GNOME / Linux 小工具——把托盘图标折叠进顶栏的 Tray CollapseLinux 全局语音输入 Voice Input——如果你也在折腾桌面环境,或许用得上。

Notchi:给 GNOME 顶栏养一只像素宠物,实时反应 Claude Code 在干嘛
http://localhost:8090/archives/notchi-gnome
作者
fnidore
发布于
更新于
许可