Files
spaceshell/DOCS/MAIN.md
T
2026-06-09 19:50:15 +07:00

437 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# spacesh — техническая спецификация
> Терминал-воркспейс под AI-агентов. macOS-first. Tauri 2 + Rust + React/TypeScript.
> Версия документа: v0.1 (handoff-ready). Назначение: спецификация для реализации.
---
## 0. Допущения и зафиксированный скоуп
Документ фиксирует решения, принятые на этапе проектирования. Несколько вещей сознательно **вынесены за рамки v1**:
- **Учёт токенов и остатков лимитов — НЕ входит в v1.** Решено убрать из уравнения; провайдерские API для остатков грязные/против ToS, а ценность размыта существующими menu-bar трекерами. Может вернуться позже как отдельный подписчик на шину.
- **Юридический слой (авторизация пользователей, хранение чужих токенов, ToS-обвязка) — НЕ входит в v1.** Инструмент в первую очередь делается под себя. Вопрос продажи откладывается.
- **Встроенный браузер — выкинут.** Большая поверхность поддержки, нулевая ценность для агентского флоу.
- **Полноценный редактор (Monaco-класс) — отложен.** В v1 — дерево файлов + diff-просмотр изменений агента (CodeMirror 6), не IDE.
**Фича «модификации» уточнена как внешние нотификации.** В v1 это рассылка уведомлений о событиях агентов во внешние каналы — **в первую очередь Telegram и MAX**. Реализуется как подписчик внутри демона на события шины (см. §9.2). Guardrails (перехват команд) — не входит в v1.
---
## 1. Идея проекта
### 1.1. Проблема
Один AI-агент в терминале — управляем глазами. Пять-восемь агентов параллельно в разных проектах — нет: непонятно, кто закончил, кто упал, кто ждёт ввода, к кому возвращаться. Обычный терминал/tmux не сообщает статус сам, раскладки не переживают перезапуск удобно, а единого способа дёрнуть нужную панель из скрипта нет.
### 1.2. Что такое spacesh
Терминал-воркспейс, где **бэкенд-демон владеет живыми сессиями**, а GUI и CLI — тонкие клиенты к нему. Воркспейс на проект, реальные PTY со сплитами, видимый статус каждого агента (детерминированный, через хуки агентов), единый CLI поверх той же командной шины. Всё с клавиатуры, macOS-native.
### 1.3. Ключевые отличия от аналогов (pilotry и пр.)
1. **Демон переживает GUI.** Краш/обновление приложения не убивает агента в середине задачи. Прямая дорога к remote/SSH-воркспейсам (демон на ноде — GUI на макбуке).
2. **macOS-native.** Аналоги Windows-first; spacesh строится сразу под WKWebView, launchd, нативные хоткеи и уведомления.
3. **Детерминированный статус** через хуки агентов вместо угадывания по выводу.
4. **Единая командная шина** — GUI и CLI идут через один socket, CLI получается почти бесплатно.
### 1.4. Чем spacesh НЕ является
Не IDE, не браузер, не замена tmux в смысле мультиплексора для серверов (хотя архитектурно к remote готов), не агент сам по себе — spacesh запускает чужие официальные CLI (Claude Code, Codex, Gemini, opencode, shell) в PTY и не подменяет их логику.
### 1.5. Целевой пользователь (v1)
Senior-инфраструктурщик/разработчик, гоняющий несколько агентов параллельно на macOS, скриптующий своё окружение, ценящий клавиатурный флоу и переживаемость сессий.
---
## 2. Принципы и нефункциональные требования
| Принцип | Следствие в реализации |
|---|---|
| Демон — единственный источник истины | GUI и CLI не хранят состояние; только шлют команды и слушают события |
| Keyboard-first | Весь рабочий цикл закрывается с клавиатуры; мышь — только ресайз |
| macOS-native | launchd user-agent для демона, нативные уведомления с actions, WKWebView-aware фронт |
| Лёгкость | Системный webview (Tauri), без встроенного Chromium |
| Переживаемость | Сессии живут в демоне между перезапусками GUI |
| Расширяемость через шину | Новые фичи = новые команды/события/подписчики, спина не двигается |
**Бюджеты производительности:**
- Латентность ввода (keypress → echo в панели): < 16 мс при нормальной нагрузке.
- Батчинг вывода PTY: коалесцировать ~4–8 мс **или** до ~16 КБ, что раньше; не эмитить событие `output` по байту.
- Backpressure: при отставании рендера демон не должен расти в памяти неограниченно — кольцевой лимит на неподтверждённый поток к клиенту.
- Repaint при reattach: снапшот экрана панели < 100 мс на типовой грид.
---
## 3. Архитектура — топология
```
┌─────────────────────────────────────────────────────────┐
│ spacesh-daemon (spaceshd) │
│ владеет: │
│ • PTY-процессы (по surface) │
│ • терминальный грид каждой панели (alacritty_terminal)│
│ • модель воркспейсов / дерево сплитов / раскладки │
│ • движок статусов │
│ • подписчики (нотификации Telegram/MAX) │
│ │
│ хостит: Unix-domain socket ~/.spacesh/sock │
└───────────────┬───────────────────────┬──────────────────┘
│ │
attach/команды/события команды/события (--json)
│ │
┌──────────┴─────────┐ ┌────────┴───────────┐
│ GUI-клиент (Tauri) │ │ CLI-клиент (spacesh)│
│ рендер xterm.js │ │ тонкий бинарь │
│ ввод, attach/det. │ │ shell-completions │
└────────────────────┘ └─────────────────────┘
```
Один socket, один протокол. И клик в GUI, и `spacesh focus s_8f3` из скрипта — это одна и та же команда `focus`. Никакой дубликации операционной логики.
---
## 4. Процессная модель (вариант B — демон)
### 4.1. Жизненный цикл демона
- Демон стартует лениво: первый клиент (GUI или CLI), не найдя socket, поднимает `spaceshd` и ждёт готовности.
- Под macOS оборачивается в **launchd user-agent** (`~/Library/LaunchAgents/xyz.spacesh.daemon.plist`) с `KeepAlive` — автоперезапуск при краше, автостарт при логине (опционально, по настройке).
- Демон single-instance: лок-файл `~/.spacesh/daemon.lock` + проверка живости socket.
- Graceful shutdown по команде `shutdown` или при отсутствии сессий и клиентов дольше TTL (настраиваемо; по умолчанию демон живёт, пока живы сессии).
### 4.2. Переживаемость сессий
PTY-процессы принадлежат демону, а не GUI. Закрытие/краш/обновление GUI **не убивает** агентов. При следующем запуске GUI выполняет `attach` к живым панелям и репейнтит их из снапшота грида (см. §6.4).
Раскладка (определение воркспейсов, дерево сплитов, привязка агентов к панелям) персистится на диск отдельно от живых процессов (§8.4) — так что даже полная перезагрузка демона восстанавливает структуру (с перезапуском агентов), а краш GUI восстанавливает и структуру, и живые процессы.
### 4.3. Эволюция к remote (вне v1, но заложено)
Socket абстрагирован за трейтом транспорта. UDS локально сегодня → проброс через SSH (`ssh -L` к UDS на ноде) завтра → нативный TCP+mTLS позже. Демон не знает, локальный клиент или удалённый.
---
## 5. Протокол командной шины
### 5.1. Транспорт и кадрирование
- Транспорт: Unix-domain socket, асинхронный (tokio).
- Кадрирование: **length-prefixed** (`u32` BE длина + payload).
- Сериализация: **JSON** в v1 (читаемость, отладка, `--json` для CLI). Возможна замена на MessagePack при упоре в объём вывода — изолировано в `spacesh-proto`.
- Мультиплексирование: один socket на клиента; внутри — корреляция запрос/ответ по `id`, плюс независимый поток push-событий.
### 5.2. Конверт сообщения
```jsonc
// Запрос (клиент → демон)
{ "id": 42, "kind": "req", "cmd": "focus", "args": { "surface_id": "s_8f3" } }
// Ответ (демон → клиент), коррелируется по id
{ "id": 42, "kind": "res", "ok": true, "data": { /* ... */ } }
{ "id": 42, "kind": "res", "ok": false, "error": { "code": "NOT_FOUND", "msg": "..." } }
// Событие (демон → подписчики), без id запроса
{ "kind": "evt", "evt": "state", "data": { "surface_id": "s_8f3", "state": "done" } }
```
### 5.3. Команды (request/response)
| Команда | Аргументы | Ответ | Назначение |
|---|---|---|---|
| `open` | `{ path }` | `{ workspace_id }` | Открыть директорию воркспейсом; повторный вызов не плодит дубль |
| `close_workspace` | `{ workspace_id }` | `{}` | Закрыть воркспейс (с подтверждением при живых панелях) |
| `new_surface` | `{ workspace_id, agent, split? }` | `{ surface_id }` | Поднять панель с агентом; `split: right\|down` — сразу сплитом |
| `close` | `{ surface_id }` | `{}` | Закрыть панель |
| `focus` | `{ surface_id }` | `{}` | Сфокусировать панель и поднять окно |
| `input` | `{ surface_id, bytes }` | `{}` | Ввод с клавиатуры (base64 в JSON) |
| `resize` | `{ surface_id, cols, rows }` | `{}` | Ресайз PTY (→ SIGWINCH) |
| `attach` | `{ surface_id }` | `{ snapshot, cols, rows, cursor }` | Подписаться на панель + получить снапшот для репейнта |
| `detach` | `{ surface_id }` | `{}` | Отписаться от потока панели |
| `status` | `{ json? }` | `{ workspaces[] }` | Список воркспейсов и панелей с состояниями |
| `set_state` | `{ surface_id, state }` | `{}` | Внешний сигнал статуса (от хуков агента, см. §7) |
| `move_surface` | `{ surface_id, target }` | `{}` | Перетаскивание/переупорядочивание |
| `apply_preset` | `{ workspace_id, preset_id }` | `{ surface_ids[] }` | Развернуть пресет раскладки |
| `shutdown` | `{}` | `{}` | Остановить демон |
### 5.4. События (push)
| Событие | Данные | Назначение |
|---|---|---|
| `output` | `{ surface_id, bytes }` | Живой поток вывода PTY для рендера (батчированный) |
| `state` | `{ surface_id, state }` | Смена статуса: `done\|wait\|work\|error\|idle` |
| `exit` | `{ surface_id, code }` | Процесс панели завершился |
| `surface_created` | `{ surface_id, workspace_id, agent }` | Появилась панель |
| `surface_closed` | `{ surface_id }` | Панель закрыта |
### 5.5. Жизненный цикл attach/detach (сердце варианта B)
```
GUI стартует
→ status // получить структуру
→ для каждой видимой панели: attach { surface_id }
← res { snapshot, cursor } // репейнт xterm.js из снапшота
← evt output ... // дальше живой поток
GUI сворачивается/закрывается
→ detach (или просто рвёт соединение — демон чистит подписку по closed-socket)
GUI возвращается
→ attach снова → новый снапшот → живой поток
```
Снапшот собирается демоном из Rust-грида (`alacritty_terminal`) и сериализуется в ANSI-дамп, который xterm.js скармливает себе для мгновенного репейнта. Это закрывает главную боль демон-архитектуры — восстановление экрана панели после reattach — без отдельного ring-buffer.
---
## 6. Терминальный слой
### 6.1. PTY
- Крейт: **`portable-pty`** (спавн, master/slave, resize). Кроссплатформенно — пригодится, даже если v1 только macOS.
- На surface: один PTY + дочерний процесс (агентский CLI или shell), рабочая директория = папка воркспейса, прокинутый env.
### 6.2. Reader-loop, батчинг, backpressure
- Отдельная async-задача на PTY читает в буфер.
- **Коалесцирование:** накапливать вывод и эмитить `output` раз в ~4–8 мс или по достижении ~16 КБ. Билд-лог на 50k строк не должен порождать 50k событий.
- **Backpressure:** на каждого подписчика — ограниченный канал. При переполнении (рендер не успевает) — дропать/сжимать промежуточные кадры, но грид в демоне (§6.3) обновлять всегда — он авторитетный.
### 6.3. Гибрид-парсинг (xterm.js рендер / Rust-грид семантика)
Один байтовый поток PTY раздаётся в **три** места:
1. **GUI** (событие `output`) → **xterm.js** парсит ANSI и рисует. Аддоны: `webgl` (рендер), `search`, `serialize`, `web-links`.
2. **Rust-грид** (`alacritty_terminal`) в демоне → авторитетная модель экрана/скроллбэка. Используется для:
- детекции статуса (fallback-паттерны, §7.3),
- поиска по скроллбэку на стороне демона,
- извлечения вывода последней команды (по маркерам OSC 133),
- снапшота при reattach (§6.4).
3. Тот же грид служит источником снапшота — отдельный буфер не нужен.
Развод **отображения** (xterm.js) и **анализа** (alacritty-грид) снимает ложный выбор «или рендер, или умная модель». Два парсера на один stream дёшево.
> Выбор `alacritty_terminal` (а не голого `vte`): он держит полноценный грид с курсором и скроллбэком из коробки, что нужно и для семантики, и для снапшота. `vte` дал бы только парсинг esc-последовательностей без модели экрана.
### 6.4. Снапшот и репейнт
При `attach` демон сериализует текущий грид панели → ANSI-дамп (видимая область + позиция курсора + при необходимости часть скроллбэка) → GUI пишет его в свежий инстанс xterm.js. Дальше едет живой `output`. Бюджет — см. §2.
---
## 7. Детекция статуса (инверсия)
Статус не угадывается из вывода, а **приходит в демон как команда** `set_state`. Источники, по убыванию надёжности:
### 7.1. Хуки агентов (основной путь)
- **Claude Code** поддерживает хуки (Stop / Notification и родственные). Вешаем на них вызов CLI:
```
spacesh notify --surface $SPACESH_SURFACE_ID --state done # на завершение
spacesh notify --surface $SPACESH_SURFACE_ID --state wait # на запрос ввода
```
CLI шлёт `set_state` в socket → демон ставит статус → событие `state` → кольцо панели меняет цвет. Детерминированно.
- `$SPACESH_SURFACE_ID` инжектируется в env панели при спавне — так хук знает, про какую панель сообщает.
> **Внимание при реализации:** точные имена хуков и формат payload у Claude Code меняются между версиями. Слой интеграции с хуками изолировать в адаптер и версионировать; не зашивать имена в ядро.
### 7.2. OSC 133 для shell
Для обычных shell-панелей инжектировать shell-integration с **OSC 133** (semantic prompt marking): чёткие маркеры «команда началась / закончилась + exit code». Даёт статус (`work` между A/C-маркерами, `done`/`error` по exit code), а заодно навигацию по промптам и извлечение вывода последней команды.
### 7.3. Fallback — паттерны вывода
Для агентов без хуков — эвристики поверх alacritty-грида (спиннеры, промпт-паттерны, запросы подтверждения). Изолировано в один модуль, считается best-effort. Меньшинство кейсов.
### 7.4. Состояния
`work` (работает) · `wait` (ждёт ввода) · `done` (готов) · `error` (ошибка) · `idle` (простаивает). Маппятся на цвет кольца панели и бейдж в сайдбаре.
---
## 8. Модель воркспейсов и раскладки
### 8.1. Сущности
- **Workspace** — папка проекта + дерево сплитов + метаданные (имя, цвет, группа, флаг «не забыть»).
- **Surface (панель)** — лист дерева сплитов; PTY + агент + состояние.
- **Split node** — внутренний узел: ориентация (`h`/`v`) + дети + пропорции.
- **Group** — именованная цветная коллекция воркспейсов с порядком.
### 8.2. Дерево сплитов
Рекурсивное бинарное (или n-арное) дерево. Ресайз — изменение пропорций узла. 10 пресет-раскладок: `1`, `2↔`, `2↕`, `2+1`, `1+2`, `3`, `2×2`, `4`, `2×3`, `2×4`.
### 8.3. Пресеты
Пресет = (раскладка панелей) + (привязка агента к каждой панели). Сохранённый пресет → запуск в один клик (`apply_preset`). Хранится в конфиге пользователя.
### 8.4. Персистентность
- Раскладка и метаданные — на диск (`~/.spacesh/state.json` или SQLite, если вырастет), отдельно от живых процессов.
- Сохранение по изменению (debounce), атомарная запись (temp + rename).
- При старте демона без живых сессий — структура восстанавливается, агенты перезапускаются по запросу/настройке.
### 8.5. UX-метаданные (дёшево, эффект большой)
- **Цветные группы** воркспейсов.
- **Метка «не забыть»** — unread-модель как в почте: кружок на воркспейсе, пока к нему не вернулись.
- **Свой порядок** — перетаскивание, раскладка сохраняется.
- **Центр событий** — лента уведомлений (готов/ждёт/ошибка) с «отметить прочитанным».
---
## 9. Фичи поверх ядра (v1)
### 9.1. Зум-панели
Временный фуллскрин одной панели поверх раскладки. Чисто **UI-состояние GUI** — дерево сплитов не трогается, демона не касается. Хоткей-тоггл, по выходу возврат к прежней раскладке.
### 9.2. Внешние нотификации (Telegram, MAX)
Когда окно свёрнуто или ты не за макбуком, событие агента должно догнать снаружи. Реализуется как **подписчик внутри `spaceshd`** на события шины — спине это ничего не двигает, чистое расширение.
**Механика:**
- Подписчик слушает события `state` (по умолчанию реагирует на `done`, `wait`, `error`; набор настраивается) и `exit`.
- На совпадении формирует сообщение (`воркспейс · агент · состояние`, время, опционально хвост вывода) и диспетчит во включённые каналы.
- Каждый канал — отдельный **адаптер** за общим трейтом `NotificationChannel { async fn send(&self, msg) }`. Добавить новый канал = новый адаптер, ядро не трогается.
**Адаптеры v1:**
| Канал | Транспорт | Конфиг | Заметки |
|---|---|---|---|
| **Telegram** | Bot API, HTTPS `sendMessage` | `bot_token`, `chat_id` | Бот создаётся через @BotFather. Только исходящие — webhook/long-poll не нужны |
| **MAX** | MAX Bot API (`dev.max.ru`), HTTPS, JSON | `bot_token`, `chat_id` | Токен от Master Bot. Авторизация заголовком `Authorization: <token>` (query-параметр устарел). Только исходящие |
> MAX Bot API построен по тем же принципам, что Telegram (токен → HTTPS-запросы → JSON), но детали отличаются: тип апдейтов в явном поле `update_type`, свой набор методов и формат сообщений. Для нотификаций нам нужен только метод отправки сообщения — приём апдейтов/webhook не требуется. Есть официальный TS-клиент `@maxhub/max-bot-api` и python-клиенты, если не захочется бить по HTTP напрямую.
**Поведение:**
- **Анти-спам / дедуп:** не слать повторное уведомление об одном и том же состоянии панели; коалесцировать всплески (несколько `done` подряд → одно сводное). Окно дедупа настраивается.
- **Rate limits:** соблюдать лимиты каналов (у MAX базово ~30 msg/s на бота; Telegram ~30 msg/s глобально, ~1 msg/s на чат). Для персонального инструмента не критично, но очередь с троттлингом заложить.
- **Фильтр по воркспейсу/группе:** опционально слать только по помеченным воркспейсам (увязывается с меткой «не забыть», §8.5).
- **Доставка best-effort:** сбой канала логируется, ретраится ограниченно, не блокирует работу демона.
**Конфиг (пример, `~/.spacesh/config.toml`):**
```toml
[notifications]
on_states = ["done", "wait", "error"]
dedup_window_secs = 30
[notifications.telegram]
enabled = true
bot_token = "env:SPACESH_TG_TOKEN"
chat_id = "123456789"
[notifications.max]
enabled = true
bot_token = "env:SPACESH_MAX_TOKEN"
chat_id = "..."
```
> Токены — через ссылку на env-переменную, не хранить в открытом виде в конфиге.
---
## 10. Структура кода
### 10.1. Rust workspace (cargo)
```
spacesh/
├── crates/
│ ├── spacesh-proto/ # типы команд/событий, serde, кадрирование. Шарится всеми.
│ ├── spacesh-core/ # модель воркспейсов/сплитов, грид (alacritty_terminal),
│ │ # движок статусов. Без I/O — юнит-тестируемо.
│ ├── spacesh-pty/ # спавн/io PTY (portable-pty), reader-loop, батчинг, resize.
│ ├── spaceshd/ # демон: владеет сессиями, хостит socket, fan-out событий,
│ │ # персист раскладки, launchd-обвязка, подписчики.
│ └── spacesh-cli/ # тонкий бинарь-клиент к socket.
└── app/ # Tauri-приложение
├── src-tauri/ # Rust-сторона: клиент к socket + мост в webview
└── src/ # React/TS фронт
```
| Крейт | Ответственность | Ключевые зависимости |
|---|---|---|
| `spacesh-proto` | Протокол: типы, serde, length-prefix framing | `serde`, `serde_json` |
| `spacesh-core` | Доменная модель, грид, статусы | `alacritty_terminal` |
| `spacesh-pty` | PTY io, батчинг, backpressure | `portable-pty`, `tokio` |
| `spaceshd` | Демон, socket-сервер, fan-out, персист, подписчики-нотификации (Telegram/MAX) | `tokio`, `reqwest`, `spacesh-*` |
| `spacesh-cli` | CLI-клиент | `clap`, `tokio` |
| `app/src-tauri` | Мост socket ↔ webview | `tauri` 2, `tokio` |
### 10.2. React/TS фронт
| Компонент | Ответственность |
|---|---|
| `TerminalView` | xterm.js + аддоны (webgl/search/serialize/links) на панель; ввод → `input`, репейнт из снапшота |
| `LayoutEngine` | рекурсивное дерево сплитов, ресайз мышью, **зум-панели** |
| `Sidebar` | воркспейсы/группы, цвета, метка «не забыть», бейджи статуса |
| `EventCenter` | лента уведомлений, «отметить прочитанным» |
| `Wizard` | новый воркспейс (папка+раскладка→агент в панель), пресеты |
| `Settings` | конфиг нотификаций (Telegram/MAX), хоткеи, правила; редактирование `config.toml` |
| `socketBridge` | подписка на события демона через Tauri IPC, отправка команд |
---
## 11. Хоткеи (черновой набор)
| Действие | Хоткей |
|---|---|
| Новый воркспейс | `Cmd + N` |
| Переключить воркспейс | `Cmd + 1…9` |
| Сплит панели | `Cmd + Shift + T` |
| К свежему агенту | `Cmd + Shift + U` |
| Соседняя панель | `Cmd + Opt + ← ↑ → ↓` |
| Зум панели (тоггл) | `Cmd + Shift + Z` |
| Поиск по скроллбэку | `Cmd + F` |
| Настройки | `Cmd + ,` |
> macOS-маппинг (Cmd вместо Ctrl). Сверить конфликты с системными/агентскими хоткеями; вынести в настраиваемую схему.
---
## 12. Порядок реализации (milestones)
**M0 — живой терминал через socket (вертикальный срез).**
`spacesh-proto` + `spaceshd` с командами `open` / `new_surface` / `input` / `output` + реальный PTY (`spacesh-pty`). Цель: байты летают GUI ↔ демон ↔ PTY. Минимальный фронт с одним `TerminalView`.
**M1 — переживаемость и reattach.**
Грид в демоне (`spacesh-core` + alacritty), `attach`/`detach` со снапшотом, репейнт xterm.js. launchd-обвязка. Цель: убил GUI — агент жив, открыл — экран восстановился.
**M2 — раскладки и воркспейсы.**
`LayoutEngine`, дерево сплитов, ресайз, персист (`state.json`), сайдбар с воркспейсами. Пресеты + `apply_preset`. Wizard.
**M3 — статусы.**
`set_state`, интеграция хуков Claude Code, OSC 133 для shell, fallback-паттерны. Кольца + бейджи + центр событий + нативные уведомления.
**M4 — CLI.**
`spacesh-cli` поверх готовой шины: `open`/`status`/`focus`/`close`/`new-surface`/`notify`, `--json`, shell-completions.
**M5 — фичи поверх.**
Зум-панели, внешние нотификации (Telegram + MAX, §9.2), поиск по скроллбэку, diff-просмотр изменений агента (CodeMirror 6).
**M6 (post-v1) — remote.**
Транспорт-трейт → проброс socket через SSH → демон на ноде, GUI на макбуке.
---
## 13. Открытые вопросы и риски
| # | Вопрос/риск | Заметка |
|---|---|---|
| 1 | Имена/формат хуков Claude Code дрейфуют по версиям | Изолировать в версионируемый адаптер; не зашивать в ядро |
| 2 | Нотификации: блокировка бота за массовые рассылки/спам (особенно MAX) | Только исходящие, низкий объём, дедуп + троттлинг; не использовать для рассылок |
| 3 | WKWebView ≠ WebView2: IME (рус/англ), фокус, DnD, перехват клавиш | Отлаживать на macOS с нуля, не переносить допущения с Windows-аналогов |
| 4 | Производительность xterm.js при шквальном выводе | webgl-аддон обязателен; батчинг §6.2; профилировать на реальных билд-логах |
| 5 | Подпись/нотаризация для раздачи (Developer ID, stapling) | Заложить в CI заранее, иначе Gatekeeper ломает «скачал и запустил» |
| 6 | Автообновление вне App Store | Tauri updater / Sparkle; решить на M1 |
| 7 | Single-instance демона и гонки при старте | Лок-файл + проверка живости socket; обработать «socket есть, демон мёртв» |
| 8 | Объём `output` по JSON-socket | Если упрёмся — MessagePack, изменение локализовано в `spacesh-proto` |
---
*Конец спецификации v0.1.*