diff --git a/CHANGELOG.md b/CHANGELOG.md index 933dbdc6..b55e0e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ [English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md) · [简体中文](CHANGELOG.zh-CN.md) +## Unreleased + +Agent teams, task budgets and per-card history. The accounts, assistant, context and environments plugins ship their parts separately. + +- **Flows.** Four built-in orchestration flows (split and synthesize, executor and reviewer, three options and a judge, find and verify bugs) and project flows in `.canvastty/flows/*.yaml`. A project flow is used only after the person approves its current instructions. The launcher offers flows and can save a running task tree as one. +- **Task trees on the canvas.** Parent and subagent cards are linked, the orchestrator card summarizes its subagents, **Gather task** brings them together, and the canvas can be arranged by tree, status, project or grid, with undo. +- **Task board.** `list_tasks`, `claim_task` (atomic, dependencies first), `update_task` and `complete_task` for agents; the person adds, assigns and closes tasks in the card's details. +- **Review, worktrees and retry.** `spawn_agent` takes `review: true` (a separate reviewer in Plan whose verdict is added to the result) and `isolate: "worktree"`. `retry_agent` restarts a failed or quiet subagent with its original request and a masked failure tail, at most twice. +- **Budgets.** Time, token and cost limits per task tree with a warning at 80 %. At the limit the tree gets no new input or subagents and its processes are paused until the person changes the limit. Usage a CLI does not report is shown as no data. +- **Model routing.** A trusted plugin with `model:route` can choose the model and effort of a subagent started without an explicit model, only among those `list_providers` lists; the card shows the choice and its reason. +- **Card details.** A masked timeline of hook events with filters and a Markdown report; usage by model, account and task with periods, CSV export and manual prices; git checkpoints before each turn with a diff preview and confirmed restore; notifications on macOS, the phone and the glasses with a quiet mode. +- **Secrets on request.** `request_secret` asks the person for one configured provider key for 10 minutes, one turn or the session; `run_secret_request` then sends typed HTTPS API requests with it, so the key itself never reaches the agent. Grants can be revoked. +- **Network policy.** Per project: open, allowed domains or offline, applied when an agent starts. macOS uses its sandbox profile; Linux uses bubblewrap with a local proxy and, for strict modes, Landlock (ABI 9 or newer). A host that cannot enforce a strict mode refuses the launch. +- **Workspace.** Command palette with output search, an explicit broadcast mode for typing into several agents, a preview before dropped files, links or text reach an agent (paths outside the project need consent), and versioned workspace snapshots and presets without secrets; importing a Bypass card needs confirmation. +- **Questions.** `ask_user` shows an agent's bounded question; eligible questions also reach the paired phone. +- **Faster first frame.** Terminal code loads right after the first frame. Keys typed meanwhile reach the shell in order, and a failed load affects only terminal cards, which offer **Retry**. + +What automated checks do not cover is listed in [verification](docs/post-1.7-verification.md#not-covered-by-automated-checks). + ## 1.7.1 - Added built-in stable-release updates with explicit download and installation, active-session shutdown confirmation, and signed Sparkle updates on macOS. Install this first updater-enabled release manually; later compatible releases can be installed from the app. diff --git a/CHANGELOG.ru.md b/CHANGELOG.ru.md index 73764993..88ef1a8a 100644 --- a/CHANGELOG.ru.md +++ b/CHANGELOG.ru.md @@ -2,6 +2,25 @@ [English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md) · [简体中文](CHANGELOG.zh-CN.md) +## Не выпущено + +Команды агентов, бюджеты задач и история каждой карточки. Плагины accounts, assistant, context и environments выпускают свои части отдельно. + +- **Сценарии.** Четыре встроенных сценария оркестрации (разбить и собрать, исполнитель и ревьюер, три варианта и судья, найти и подтвердить ошибки) и проектные сценарии в `.canvastty/flows/*.yaml`. Проектный сценарий используется только после того, как человек одобрит его текущие инструкции. Лаунчер предлагает сценарии и может сохранить работающее дерево задачи как сценарий. +- **Дерево задачи на холсте.** Карточки оркестратора и подагентов связаны, карточка оркестратора показывает сводку, **Собрать задачу** сводит их вместе, холст раскладывается по дереву, статусу, проекту или сеткой, с отменой. +- **Доска задач.** `list_tasks`, `claim_task` (атомарно, сначала зависимости), `update_task` и `complete_task` для агентов; человек добавляет, назначает и закрывает задачи в подробностях карточки. +- **Ревью, worktree и повтор.** `spawn_agent` принимает `review: true` (отдельный ревьюер в режиме Plan, его вердикт добавляется к результату) и `isolate: "worktree"`. `retry_agent` перезапускает упавшего или замолчавшего подагента с исходным запросом и маскированным хвостом ошибки, не больше двух раз. +- **Бюджеты.** Лимиты времени, токенов и денег на дерево задачи с предупреждением на 80 %. При достижении лимита дерево не получает новый ввод и подагентов, его процессы приостанавливаются, пока человек не изменит лимит. Расход, о котором CLI не сообщает, показывается как отсутствие данных. +- **Выбор модели.** Доверенный плагин с `model:route` может выбрать модель и усилие подагента, запущенного без явной модели, только из списка `list_providers`; карточка показывает выбор и причину. +- **Подробности карточки.** Маскированный журнал событий хуков с фильтрами и отчётом в Markdown; расход по модели, аккаунту и задаче с периодами, выгрузкой CSV и ручными ценами; git-точки перед каждым ходом с просмотром диффа и подтверждённым откатом; уведомления в macOS, на телефоне и в очках с режимом тишины. +- **Секреты по запросу.** `request_secret` просит у человека один настроенный ключ провайдера на 10 минут, один ход или сессию; `run_secret_request` отправляет с ним типизированные HTTPS-запросы к API, сам ключ агенту не передаётся. Разрешения можно отозвать. +- **Сетевая политика.** Для проекта: открыта, только разрешённые домены или без сети; применяется при запуске агента. На macOS — профиль песочницы, на Linux — bubblewrap с локальным прокси и, для строгих режимов, Landlock (ABI 9 и новее). Если система не может обеспечить строгий режим, запуск отклоняется. +- **Рабочее пространство.** Палитра команд с поиском по выводу, явный режим группового ввода в нескольких агентов, предпросмотр перед передачей файлов, ссылок или текста агенту (пути вне проекта требуют согласия), версионированные снимки и пресеты рабочего пространства без секретов; импорт карточки в режиме Bypass требует подтверждения. +- **Вопросы.** `ask_user` показывает ограниченный вопрос агента; подходящие вопросы приходят и на сопряжённый телефон. +- **Быстрее первый кадр.** Код терминала загружается сразу после первого кадра. Набранное в это время доходит до оболочки по порядку, а ошибка загрузки затрагивает только карточки терминалов, в которых есть **Повторить**. + +Что не покрыто автоматическими проверками, перечислено в [проверке](docs/post-1.7-verification.md#not-covered-by-automated-checks) (на английском). + ## 1.7.1 - Добавлены обновления стабильных версий из приложения: загрузка и установка по запросу, подтверждение остановки активных сессий и подписанные обновления Sparkle на macOS. Первую версию с этой функцией нужно установить вручную; последующие совместимые версии можно устанавливать из приложения. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 303881fe..121858ad 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -2,6 +2,25 @@ [English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md) · [简体中文](CHANGELOG.zh-CN.md) +## 未发布 + +智能体团队、任务预算和每张卡片的历史。accounts、assistant、context 和 environments 插件分别发布各自的部分。 + +- **流程。** 四个内置编排流程(拆分并汇总、执行者与审查者、三个方案与评判、查找并验证缺陷),以及 `.canvastty/flows/*.yaml` 中的项目流程。项目流程只有在用户批准其当前指令后才会使用。启动器提供这些流程,也可以把正在运行的任务树保存为流程。 +- **画布上的任务树。** 编排者与子智能体卡片相互关联,编排者卡片显示汇总,**Gather task** 把它们集中到一起;画布可按树、状态、项目或网格排列,并可撤销。 +- **任务看板。** 智能体使用 `list_tasks`、`claim_task`(原子操作,先满足依赖)、`update_task` 和 `complete_task`;用户在卡片详情中添加、分配和关闭任务。 +- **审查、worktree 与重试。** `spawn_agent` 支持 `review: true`(在 Plan 模式下的独立审查者,其结论附加到结果中)和 `isolate: "worktree"`。`retry_agent` 用原始请求和经过遮蔽的失败输出末尾重新启动失败或无响应的子智能体,最多两次。 +- **预算。** 每个任务树的时间、token 和费用上限,达到 80% 时提醒。达到上限后,该任务树不再接收新输入或新子智能体,其进程会暂停,直到用户修改上限。CLI 未报告的用量显示为无数据。 +- **模型路由。** 拥有 `model:route` 的受信任插件可以为未指定模型的子智能体选择模型和推理强度,但只能从 `list_providers` 列出的选项中选择;卡片会显示选择及原因。 +- **卡片详情。** 带筛选的遮蔽钩子事件时间线和 Markdown 报告;按模型、账户和任务统计的用量,支持时间段、CSV 导出和手动价格;每轮之前的 git 检查点,可预览差异并经确认后恢复;macOS、手机和眼镜通知,支持免打扰。 +- **按需使用密钥。** `request_secret` 请用户为一个已配置的提供方密钥授权 10 分钟、一轮或整个会话;随后 `run_secret_request` 用它发送类型化的 HTTPS API 请求,密钥本身不会交给智能体。授权可以撤销。 +- **网络策略。** 按项目设置:开放、仅允许的域名或离线,在智能体启动时生效。macOS 使用沙箱配置文件;Linux 使用带本地代理的 bubblewrap,严格模式另用 Landlock(ABI 9 或更新)。无法执行严格模式的系统会拒绝启动。 +- **工作区。** 带输出搜索的命令面板、向多个智能体同时输入的显式广播模式、文件/链接/文本交给智能体前的预览(项目外路径需要同意),以及不含密钥的版本化工作区快照和预设;导入 Bypass 卡片需要确认。 +- **提问。** `ask_user` 显示智能体的有界问题;符合条件的问题也会发送到已配对的手机。 +- **更快的首帧。** 终端代码在首帧之后立即加载。期间输入的按键按顺序到达 shell;加载失败只影响终端卡片,卡片上提供 **重试**。 + +自动检查未覆盖的内容列在[验证说明](docs/post-1.7-verification.md#not-covered-by-automated-checks)中(英文)。 + ## 1.7.1 - 新增应用内稳定版本更新:由用户下载和安装,停止活动会话前确认,macOS 使用 Sparkle 签名更新。首个支持更新的版本需要手动安装;后续兼容版本可在应用内安装。 diff --git a/README.md b/README.md index 5efdb879..f03c7612 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ Launch a shell or agent in a project directory, move and resize its live termina Agents start in **Auto** by default: the CLI's own auto mode (Claude Code, Codex, Grok), CanvasTTY's per-run rules for OpenCode, or, for a CLI without one, its approval bypass only inside agent isolation. Manual, Accept edits, Plan and Bypass are offered only where the CLI has them; Bypass is acknowledged once per CLI and never handed to a subagent. Around every mode sit layers the person controls in Settings → Agents: base protection (hooks that refuse elevation, `curl | sh`, disk commands and writes outside the project before the tool call runs), delegation rules (a subagent never gets more than its orchestrator and stays in its project; 2 levels and 8 live subagents by default), and an OS isolation layer (macOS `sandbox-exec`, Linux bubblewrap; none on Windows yet, where subagents run in Manual). When an isolated session ends, CanvasTTY checks the repositories it touched for git settings that would run programs outside isolation and offers to neutralize them. -An **Orchestrator** session gets the `canvastty_agents` tools: `list_providers` (installed agents, sign-in state, models, efforts, profiles), `spawn_agent` with optional `model`, `effort` and `profile`, `wait_for_agent` (up to 600 s) and `get_agent_result`, which returns a Codex or OpenCode subagent's final reply as `answer` (up to 4,096 characters, masked). See [agent orchestration and isolation](docs/agent-orchestration.md) and [protection layers](docs/installing-and-security.md#agent-protection-layers). +An **Orchestrator** session gets the `canvastty_agents` tools: `list_providers` (installed agents, sign-in state, models, efforts, profiles), `spawn_agent` with optional `model`, `effort` and `profile`, `wait_for_agent` (up to 100 s per call, capped at 50 s for OpenCode) and `get_agent_result`, which returns a Codex or OpenCode subagent's final reply as `answer` (up to 4,096 characters, masked). See [agent orchestration and isolation](docs/agent-orchestration.md) and [protection layers](docs/installing-and-security.md#agent-protection-layers). + +Beyond single subagents, an orchestrator can follow a built-in or person-approved project flow, share a task board with its subagents (`list_tasks`, `claim_task`, `update_task`, `complete_task`), ask for a reviewer or a separate git worktree, retry a failed subagent (`retry_agent`), ask the person a question (`ask_user`) and, with the person's approval, use a provider key for typed API requests without seeing it (`request_secret`). The person sets time, token and cost budgets per task tree and a network policy per project; each card's details hold its timeline, usage, report and git checkpoints. ## Windows shells and provider CLIs diff --git a/README.ru.md b/README.ru.md index 4b765441..b8579a1c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -33,6 +33,8 @@ https://github.com/user-attachments/assets/444612f7-cda1-4fd6-8514-2f4fac9cc520 Сессия с ролью **Оркестратор** получает инструменты `canvastty_agents`: `list_providers` (установленные агенты, состояние входа, модели, effort, профили), `spawn_agent` с необязательными `model`, `effort` и `profile`, `wait_for_agent` (до 600 с) и `get_agent_result`, который возвращает финальный ответ субагента Codex или OpenCode в `answer` (до 4 096 символов, с маскировкой). Подробнее: [оркестрация и изоляция агентов](docs/agent-orchestration.md) (англ.) и [слои защиты](docs/installing-and-security.ru.md#слои-защиты-агентов). +Кроме отдельных подагентов, оркестратор может следовать встроенному или одобренному человеком проектному сценарию, вести общую доску задач с подагентами (`list_tasks`, `claim_task`, `update_task`, `complete_task`), запросить ревьюера или отдельный git worktree, повторить упавшего подагента (`retry_agent`), задать человеку вопрос (`ask_user`) и с одобрения человека использовать ключ провайдера для типизированных запросов к API, не видя его (`request_secret`). Человек задаёт бюджеты времени, токенов и денег на дерево задачи и сетевую политику проекта; в подробностях каждой карточки есть журнал, расход, отчёт и git-точки. + ## Терминалы и CLI-провайдеры в Windows В Windows кнопка Terminal запускает встроенный Windows PowerShell в чистой сессии `-NoLogo -NoProfile`, а при его недоступности использует `pwsh` или `cmd.exe`. Перед передачей в `node-pty`/ConPTY CanvasTTY находит для Codex, Claude, Kimi, OpenCode, Hermes и Grok Build конкретный файл `.exe`, `.com`, `.cmd` или `.bat`: сначала в пользовательском `PATH`, затем в стандартных каталогах CLI. diff --git a/README.zh-CN.md b/README.zh-CN.md index 2b7341b3..f7520dcb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,6 +33,8 @@ https://github.com/user-attachments/assets/444612f7-cda1-4fd6-8514-2f4fac9cc520 以 **Orchestrator** 角色启动的会话会获得 `canvastty_agents` 工具:`list_providers`(已安装的智能体、登录状态、模型、effort、profile)、带可选 `model`、`effort` 与 `profile` 的 `spawn_agent`、`wait_for_agent`(最长 600 秒),以及 `get_agent_result`——它以 `answer` 返回 Codex 或 OpenCode 子智能体的最终回复(最多 4,096 个字符,已脱敏)。详见[智能体编排与隔离](docs/agent-orchestration.md)(英文)和[防护层](docs/installing-and-security.zh-CN.md#智能体防护层)。 +除了单个子智能体,编排者还可以遵循内置流程或经用户批准的项目流程,与子智能体共享任务看板(`list_tasks`、`claim_task`、`update_task`、`complete_task`),请求审查者或独立的 git worktree,重试失败的子智能体(`retry_agent`),向用户提问(`ask_user`),并在用户批准后使用提供方密钥发送类型化 API 请求而看不到密钥本身(`request_secret`)。用户可为每个任务树设置时间、token 和费用预算,为每个项目设置网络策略;每张卡片的详情中有时间线、用量、报告和 git 检查点。 + ## Windows 终端与服务商 CLI 在 Windows 上,Terminal 启动器会以干净的 `-NoLogo -NoProfile` 会话打开系统自带的 Windows PowerShell;如果不可用,则回退到 `pwsh` 或 `cmd.exe`。在交给 `node-pty`/ConPTY 之前,CanvasTTY 会先从用户 `PATH`、再从标准的用户级 CLI 目录中,为 Codex、Claude、Kimi、OpenCode、Hermes 与 Grok Build 解析出具体的 `.exe`、`.com`、`.cmd` 或 `.bat` 启动文件。 diff --git a/agent/orchestrator/SKILL.md b/agent/orchestrator/SKILL.md index 1b68bdb2..f47e06e0 100644 --- a/agent/orchestrator/SKILL.md +++ b/agent/orchestrator/SKILL.md @@ -11,7 +11,7 @@ A session started from CanvasTTY's launcher with the **Orchestrator** role has t 1. `list_providers`: which agents CanvasTTY can launch here. Each entry's `id` is the exact `spawn_agent.provider` value; prefer entries that are `available` with `signIn: "ok"` (`unknown` only means CanvasTTY has not checked yet). When plugins offer launch options (for example an accounts plugin's `list_routes` tool), the answer names that tool. 2. `spawn_agent` once per part of the task: a known `provider` id, an absolute `cwd` and a self-contained prompt. An unknown provider is refused with a pointer back to `list_providers`. **If the person names a model, pass it as `model`** in the format `list_providers` gives for that provider (`model.format`; OpenCode takes `provider/model`, for example `zai-coding-plan/glm-5.3-flash`, and `model.known` lists what `opencode models` reported); without `model` the CLI uses its own default. `effort` sets the reasoning effort where the CLI has one (`efforts`). Without `profile` the subagent gets your own launch profile, or the next lower one its CLI has (never more than yours, never YOLO); `list_providers` shows each provider's `profiles`. `cwd` must be this project's folder or a folder inside it. A refusal says why (a folder outside the project, a profile above yours, a limit the person set): change the request, do not retry it. -3. `wait_for_agent` for each subagent (`timeoutSeconds` up to 600; call it again after `timeout`) instead of polling. `needs_approval` means the person must answer a prompt in that card: never answer it yourself. +3. `wait_for_agent` for each subagent (`timeoutSeconds` up to 100; OpenCode calls return within 50 seconds; call it again after `timeout`) instead of polling. `needs_approval` means the person must answer a prompt in that card: never answer it yourself. 4. `get_agent_result` to read what the subagent produced; check the actual files before reporting success. Do not explore the filesystem, `PATH` or config folders (for example `~/.local/bin` or `~/.config/`) looking for agents or their configuration: `list_providers` is what CanvasTTY can launch, and delegation goes through `spawn_agent`. @@ -30,6 +30,8 @@ Use the bundled `canvastty-control.mjs` CLI with a running CanvasTTY instance wh Inside an Orchestrator session this creates a subagent of that session (its profile at most, never YOLO, in a folder inside its project). From the person's own terminal, `--yolo` is allowed only for a CLI the person acknowledged YOLO for in CanvasTTY's launcher. + On a scoped Orchestrator connection, run `execution-targets` before creating a worker. When its response has `enabled: true`, pass a returned ID as `--execution-target `; choose a target matching the provider and requested model. An empty target list means the person must approve a destination in settings. This command is read-only and cannot change approvals. + Run `providers` first to see which providers are installed and signed in. If the person names a model, add `--model ` (OpenCode: `provider/model`); `--effort ` works for Codex, Claude and Grok. `--provider` accepts codex, claude, qwen, kimi, opencode, hermes, grok, omp and pi; each worker uses that provider's own normal/YOLO launch flags. The create response includes `capabilities`: only Codex workers have `result: true` (captured final answer) and `menus: true` (startup/approval menus that `choose`/`dismiss` can act on). For other providers, `result` ends as `no_result` and menus must be resolved by the user; treat their `screen` text as the only evidence. From the person's own terminal CLI creation defaults to YOLO, using the provider's full-access, no-approval launch flag; inside an Orchestrator session it defaults to that session's profile. It does not change global provider settings. `--profile normal` is available when the user requests their ordinary configured profile; do not silently replace their selected permissions. diff --git a/docs/agent-orchestration.md b/docs/agent-orchestration.md index 0e8b6e3f..765cc26d 100644 --- a/docs/agent-orchestration.md +++ b/docs/agent-orchestration.md @@ -10,9 +10,15 @@ A session launched from the desktop with the **Orchestrator** role gets the `can | --- | --- | | `list_providers` | The agent providers CanvasTTY can launch as subagents: `id` (the exact `spawn_agent.provider` value), `name`, `installed`/`available` (from the provider CLI registry resolved at startup or on a recheck), `signIn` (`ok`, `signed_out`, `expired` or `unknown`, from the last usage-limits read; it never starts a read and never reads credentials), `subagent` and `orchestrator` support, `model` (whether the CLI takes one, its format, and for OpenCode the models a cached `opencode models` listing reported: run in the background with a 5 s timeout, refreshed after 10 minutes, never awaited) and `efforts`, and plugin launch options (`launchOptions`, plus `launchOptionTools` such as an accounts plugin's `list_routes`). Fast: cached state only, no network calls. | | `spawn_agent` | Launches a subagent (profile: see below). `provider` must be a `list_providers` id (the schema lists them); an unknown one is refused with `INVALID_REQUEST` naming `list_providers`. Optional `model` goes to the CLI's own `--model` for that run (OpenCode `provider/model`; Codex, Claude, Qwen, Kimi alias, Grok, OMP, Pi, Cursor; Hermes, MiniMax, Devin and Antigravity take none) and optional `effort` to its reasoning effort (Codex `-c model_reasoning_effort`, Claude `--effort`, Grok `--reasoning-effort`). Both are checked per provider, kept on restart and restore, and never written to the CLI's configuration. If the person names a model, the orchestrator passes it as `model`. An OpenCode model its own `opencode models` list does not contain is refused before launch with up to five closest listed ids (OpenCode would otherwise stop with only "Unexpected server error"); when that list cannot be read, the model is passed as given. A subagent that exits anyway reports the last lines of its screen as `exitLines` in `wait_for_agent`, `observe_agent` and `get_agent_result`. | -| `wait_for_agent` | `{ sessionId, timeoutSeconds ≤ 600 }` (default 55). Returns when the subagent is `idle` (after a prompt: only once a turn that started after the latest delivered prompt has ended, so the CLI's startup idle does not count), `needs_approval`, `done`/`failed` (exited), `quiet` (reports no status and its screen stopped changing), `closed`, or on `timeout`, with `status`, `exitCode`, `waitedMs` and the masked terminal tail (masked before it is cut). Only this orchestrator's own subagents; it stops at once when the call is canceled or the orchestrator disconnects. | -| `get_agent_result`, `observe_agent` | `get_agent_result`: `answer`, the final reply of the subagent's last turn as the agent itself reported it (Codex through its Stop hook, OpenCode through CanvasTTY's plugin reading the session's last assistant message at `session.idle`; at most 4,096 characters with the end kept, masked, in memory only and cleared when the next turn starts), plus `status`, the exit state and the masked terminal tail. Other providers have no `answer`: their tail is raw screen output. `observe_agent`: the current status and tail. | +| `wait_for_agent` | `{ sessionId, timeoutSeconds ≤ 100 }` (default 55; OpenCode caps each call at 50 seconds). A timeout returns progress while the agent continues; call again to keep waiting. Pending reviews do not extend this deadline. Returns when the subagent is `idle` (after a prompt: only once a turn that started after the latest delivered prompt has ended, so the CLI's startup idle does not count), `needs_approval`, `done`/`failed` (exited), `quiet` (reports no status and its screen stopped changing), `closed`, or on `timeout`, with `status`, `exitCode`, `waitedMs` and the masked terminal tail (masked before it is cut). Only this orchestrator's own subagents; it stops at once when the call is canceled or the orchestrator disconnects. | +| `get_agent_result`, `observe_agent` | `get_agent_result`: `answer`, the final reply of the subagent's last turn as the agent itself reported it (Codex through its Stop hook, OpenCode through CanvasTTY's plugin reading the session's last assistant message at `session.idle`; at most 4,096 characters with the end kept, masked, in memory only and cleared when the next turn starts), plus `status`, the exit state and the masked terminal tail. Other providers have no `answer`: their tail is raw screen output. An unfinished review returns `review.status: "pending"` immediately; call `get_agent_result` later for its outcome. `observe_agent`: the current status and tail. | | `send_to_agent`, `cancel_agent`, `list_agents` | Follow-up prompts, disposing a subagent, listing this session's subagents. | +| `retry_agent` | Restarts one failed or quiet subagent with its original prompt, profile, model and folder plus a short masked failure tail; at most two retries per original agent. | +| `ask_user` | A question for the person (up to 8 options or free text) that waits for the person's answer; eligible questions also reach the paired phone. | +| `list_tasks`, `claim_task`, `update_task`, `complete_task` | The task board of this orchestration tree. Claiming is atomic and waits for dependencies; only the owner or the root orchestrator completes a task, and only the root reassigns tasks or changes dependencies. | +| `get_task_budget` | The tree's limits, usage and remaining budget. Agents can read it; only the person changes it. | +| `list_orchestration_templates`, `apply_orchestration_template` | Built-in flows and the project's `.canvastty/flows/*.yaml`. A project flow's instructions are returned only after the person has approved its current content. | +| `request_secret`, `run_secret_request` | Ask the person for one configured provider key, then send typed HTTPS API requests with it. The key is never returned to the agent. | ### Subagent profiles @@ -36,6 +42,14 @@ The workflow is `list_providers` → `spawn_agent` (one per part) → `wait_for_ The control CLI below is a separate surface for automation; `providers` is its equivalent of `list_providers`. +## Teams, budgets and review + +`spawn_agent` also takes `review: true`, which starts a separate reviewer in Plan once the worker's turn ends and adds its verdict to the result (if no reviewer can start, the result says so), and `isolate: "worktree"`, which runs the subagent in its own git worktree from an environments plugin. A subagent started without `model` can have its model and effort chosen by a trusted plugin with `model:route`, only among the candidates `list_providers` reports; the card shows the choice and its reason. + +The person sets time, token and cost limits for a task tree in the orchestrator card's details. At 80 % the card warns; at the limit the tree gets no new input or subagents and its POSIX process groups, and every descendant found by parent process at that moment (including ones that started their own session), are paused until the person raises or clears the limit. A process already re-parented away before the pause cannot be attributed, and Windows only blocks new input and launches; it cannot pause running work. Usage a CLI does not report counts as no data. + +Each card's details show a masked timeline of hook events, usage by model, account and task, a Markdown report, git checkpoints taken before each turn (restore asks for confirmation and keeps untracked files), notification settings, secret grants and the project's network policy. + ## Delegation rules The core applies these to every way an agent can start another one: `spawn_agent`, an orchestrator's control connection (`create` below), a saved subagent that is restored, and a restart. No agent-facing tool can change them. @@ -89,6 +103,8 @@ Use `npm run control -- ` or `node scripts/canvastty-control.mjs `; omission and IDs outside that task's allowed targets are refused. Discovery is read-only, accepts no session selector, and reflects current approvals. Agents cannot edit approvals or select an implicit default. + ## Workflow ```sh diff --git a/docs/canvastty-plugin.schema.json b/docs/canvastty-plugin.schema.json index 72f2e886..ba17fdc9 100644 --- a/docs/canvastty-plugin.schema.json +++ b/docs/canvastty-plugin.schema.json @@ -37,7 +37,7 @@ "type": "array", "maxItems": 12, "uniqueItems": true, - "items": { "enum": ["storage", "secrets", "sessions:read", "limits:read", "launcher:open", "external:open", "browser:open", "media:library", "playlists:read", "playlists:write", "hermes:hud", "network", "launch:contribute", "environment:provide", "decision:provide", "tools:agents", "sessions:events", "sessions:read-screen", "sessions:launch", "sessions:control", "cards:decorate", "browser:engine"] } + "items": { "enum": ["storage", "secrets", "sessions:read", "limits:read", "launcher:open", "external:open", "browser:open", "media:library", "playlists:read", "playlists:write", "hermes:hud", "network", "launch:contribute", "environment:provide", "decision:provide", "tools:agents", "sessions:events", "sessions:read-screen", "sessions:launch", "sessions:control", "cards:decorate", "model:route", "browser:engine"] } }, "contributions": { "type": "array", @@ -83,7 +83,7 @@ "type": "array", "maxItems": 12, "uniqueItems": true, - "items": { "enum": ["storage", "secrets", "sessions:read", "limits:read", "launcher:open", "external:open", "browser:open", "media:library", "playlists:read", "playlists:write", "hermes:hud", "network", "launch:contribute", "environment:provide", "decision:provide", "tools:agents", "sessions:events", "sessions:read-screen", "sessions:launch", "sessions:control", "cards:decorate", "browser:engine"] } + "items": { "enum": ["storage", "secrets", "sessions:read", "limits:read", "launcher:open", "external:open", "browser:open", "media:library", "playlists:read", "playlists:write", "hermes:hud", "network", "launch:contribute", "environment:provide", "decision:provide", "tools:agents", "sessions:events", "sessions:read-screen", "sessions:launch", "sessions:control", "cards:decorate", "model:route", "browser:engine"] } }, "files": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "$ref": "#/$defs/moduleAsset" } } } @@ -122,6 +122,7 @@ "description": { "type": "string", "minLength": 1, "maxLength": 240 }, "entry": { "type": "string", "minLength": 1, "maxLength": 180, "pattern": "^(?!/)(?!.*\\\\)(?!\\.\\.?/)(?!.*(?:/\\.\\.?/|/\\.\\.?$|//)).+\\.(?:js|mjs|cjs)$" }, "module": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$" }, + "modelRouter": { "type": "boolean", "description": "Offers canvastty.model.route; requires model:route. Core validates only an offered model and effort within two seconds." }, "launch": { "$ref": "#/$defs/serviceLaunch" }, "environments": { "description": "Session environments (needs environment:provide; at most 8 kinds per plugin, unique across its services): kinds offered under Where in the launcher; the host calls canvastty.environment.prepare/wrap/resume/release/describe.", @@ -149,7 +150,7 @@ }, "cardActions": { "description": "Card actions (needs cards:decorate): items in the options menu of matching cards; the host calls canvastty.cards.invoke and shows the answer's message as a toast.", - "type": "array", "minItems": 1, "maxItems": 8, + "type": "array", "minItems": 1, "maxItems": 16, "items": { "$ref": "#/$defs/cardAction" } }, "browserEngine": { "$ref": "#/$defs/browserEngine" } diff --git a/docs/installing-and-security.md b/docs/installing-and-security.md index c8148025..b7962548 100644 --- a/docs/installing-and-security.md +++ b/docs/installing-and-security.md @@ -89,10 +89,18 @@ Agents work in their own mode; CanvasTTY's layers sit outside it as a quiet safe | The agent's own permission system | What its CLI promises in the chosen mode (Claude Code, Codex, OpenCode, Grok…). | Anything the CLI's own classifier or reviewer lets through. | | Base protection (hooks) | Denies elevation, pipes into a shell, disk commands, fork bombs, writes and deletes outside the project and access to CanvasTTY's private data, before the tool call runs, for agents whose CLI has hooks (Claude Code, Codex, Qwen Code, OpenCode). A decision plugin's "ask" is shown by Claude Code; for a CLI that cannot ask it is a deny with the reason. | It reads commands; code that hides what it does, or a CLI without hooks, is not covered. It is a guard, not a sandbox. | | Delegation rules | A subagent never gets more than its orchestrator (never YOLO), works only in its orchestrator's project folder, within the depth and count limits the person set; orchestrators get a control connection of their own; no agent-facing tool changes settings, protection, profiles, trust or isolation; plugin launch options from an agent only where the plugin declared them delegable. | What a plugin the person trusted does with its own permissions. | -| Agent isolation (OS layer) | On macOS (sandbox-exec) and Linux (bubblewrap), for subagents, plugin-started agents and every agent not in Manual: the whole process tree writes only in the project, its own temporary folder and its CLI's own folders; keys, other CLIs' credentials and CanvasTTY's tokens are unreadable; no other process, app, Apple event, preference write, launchd job or foreign Unix socket (Docker, tmux, SSH agent) is reachable. Fails closed. | Network is not restricted. Linux cannot filter Unix sockets, only hide the user runtime folder. The CLI's own sandbox cannot run inside the layer on macOS, so it is switched off there (approvals unchanged) and commands can use the network. A Claude launch may rewrite the login keychain file to save a refreshed sign-in, so it could also damage that file (other items stay behind their own access lists). Windows has no layer yet: subagents run in Manual there unless the person turns isolation off. | +| Agent isolation (OS layer) | On macOS (sandbox-exec) and Linux (bubblewrap), for subagents, plugin-started agents and every agent not in Manual: the whole process tree writes only in the project, its own temporary folder and its CLI's own folders; keys, other CLIs' credentials and CanvasTTY's tokens are unreadable. The layer blocks foreign processes, apps, Apple events, preference writes and launchd jobs. macOS blocks foreign Unix sockets; Linux hides the user runtime folder. Strict network modes also use Landlock to deny external Unix sockets except the exact runtime, orchestration and authorized proxy endpoints; local CLI sockets created inside the sandbox remain usable. A validated plugin worktree hides its private `plugin-data` parent and sibling worktrees while keeping its own files editable. The original task checkout remains readable and unwritable. Shared Git metadata stays read-only under isolation so an agent cannot change shared objects, references or index data; `git add` and `git commit` are unavailable inside the sandbox. The host environment collects file edits into a review for the user to accept. Plan has no Git metadata writes. Fails closed. | Network is unrestricted in open mode. Strict Linux network modes require the packaged native helper and Landlock ABI 9 or newer, enabled by the kernel. Unsupported kernels refuse strict launches with a reason; open mode retains the existing bubblewrap behavior. The CLI's own sandbox cannot run inside the layer on macOS, so it is switched off there (approvals unchanged). A Claude launch may rewrite the login keychain file to save a refreshed sign-in, so it could also damage that file (other items stay behind their own access lists). Windows has no layer yet: subagents run in Manual there unless the person turns isolation off. | +| Network policy | Per project: open, allowed domains (optionally provider APIs and package registries) or offline, applied when an agent starts and shown on its card. The policy lives in CanvasTTY's private data, so a project file or plugin cannot widen it; an extra HTTPS API domain is allowed only by the person in Settings. | It applies from the next agent start. Strict modes need the macOS sandbox, or on Linux the packaged helper and Landlock ABI 9; elsewhere a strict launch is refused. Plain SSH sessions are not covered. | +| Provider keys on request | An agent asks for one configured provider key with `request_secret`; the person allows it for 10 minutes, one turn or the session, and `run_secret_request` sends typed HTTPS requests with it. The key itself is never given to the agent, and a grant can be revoked in the card's details. | What the provider's API does with the requests the person approved. | + +One-turn provider-key approval is available only when the current launch installed reliable turn-completion hooks and the host has observed its active turn. It is unavailable with lifecycle hooks disabled and for providers without a per-turn completion event (including the current Hermes integration). A new submitted task, hook disable, launch replacement or turn completion revokes the turn grant; enabling hooks later requires a new launch. Ten-minute and session approvals keep their separate lifetimes. + +Allowed network domains include the root and dot-separated subdomains (`example.com` permits `api.example.com`, not `notexample.com` or `example.com.attacker.test`). An explicit `*.example.com` permits descendants only. DNS resolution must still produce a public address before the proxy connects. Isolation keeps approval configuration read-only, including OpenCode's project and custom config files and its `agent`/`agents` directories. CLI state and caches remain writable. A moved CLI home is refused if its real path overlaps host credentials or private CanvasTTY data; a selected account folder below `account-homes` remains available. Credential exclusions use the host environment captured before launch contributions, even if the launch changes `HOME`. +For the existing Accounts plugin, the host derives one transient exception from its own plugin data directory and the exact selected account ID. Only that provider's `plugin-data/canvastty-accounts/homes/-` subtree is reopened. The final sandbox wrapper rechecks every directory below userData for symlinks and canonical containment; the plugin cannot return an exception list. Parent data, sibling accounts and worktrees remain hidden, and the CLI's approval configuration remains write-protected. + OpenCode Auto inspects only regular local config files of at most 1 MiB. An existing source that cannot be inspected adds no Auto allowances. A launch contributor's uninspectable config is refused; only absent optional directory candidates are skipped. JSONC comments and trailing commas are parsed without changing quoted strings. **Git audit.** When an isolated agent session ends, is closed or is restored after a quit, CanvasTTY checks the repositories under its folder whose git folder changed. If git would now run something outside isolation (a `core.hooksPath`, a filter or diff driver, `fsmonitor`, a hook file, `info/attributes`), a notice lists exactly what changed; **Neutralize** removes those keys and disables those files, **Keep as is** leaves them. It does not undo other file changes inside the project. diff --git a/docs/installing-and-security.ru.md b/docs/installing-and-security.ru.md index 0532080d..009d30be 100644 --- a/docs/installing-and-security.ru.md +++ b/docs/installing-and-security.ru.md @@ -85,7 +85,9 @@ gh variable set SPARKLE_PUBLIC_ED_KEY --repo howdeploy/CanvasTTY \ - **Режимы запуска**: Авто (по умолчанию), Вручную, Правки, План, Обход — только там, где их поддерживает CLI. Обход выбирает человек для каждого CLI, он проверяется в main-процессе, никогда не даётся субагенту и всё равно остаётся внутри слоёв ниже. - **Базовая защита** (хуки) запрещает повышение прав, `curl | sh`, дисковые команды и запись вне проекта у агентов с хуками; это проверка команд, а не песочница. - **Правила делегирования**: субагент получает не больше оркестратора (никогда YOLO), работает только в папке проекта, в пределах глубины и числа, заданных человеком; агенты не могут менять настройки, защиту, профили и изоляцию. -- **Изоляция агентов** (macOS — sandbox-exec, Linux — bubblewrap): запись только в проект, свою временную папку и папки своего CLI; ключи и токены недоступны; другие процессы, приложения и демоны недоступны; при сбое запуск отклоняется. Сеть не ограничивается; на Windows слоя пока нет, и субагенты там работают вручную. Подробности — в [agent-orchestration.md](agent-orchestration.md). +- **Изоляция агентов** (macOS — sandbox-exec, Linux — bubblewrap): запись только в проект, свою временную папку и папки своего CLI; ключи и токены недоступны; другие процессы, приложения и демоны недоступны; при сбое запуск отклоняется. Без сетевой политики сеть не ограничивается; на Windows слоя пока нет, и субагенты там работают вручную. Подробности — в [agent-orchestration.md](agent-orchestration.md). +- **Сетевая политика** проекта: открыта, разрешённые домены (по выбору — API провайдеров и реестры пакетов) или без сети; применяется при следующем запуске агента и видна на карточке. Политика хранится в приватных данных CanvasTTY, проектный файл или плагин не могут её расширить. Строгим режимам нужна песочница macOS или, на Linux, встроенный помощник и Landlock ABI 9; иначе строгий запуск отклоняется. Обычные SSH-сессии не покрываются. +- **Ключи по запросу**: агент просит один настроенный ключ провайдера через `request_secret`, человек разрешает его на 10 минут, ход или сессию, а `run_secret_request` отправляет с ним типизированные HTTPS-запросы. Сам ключ агенту не передаётся; разрешение можно отозвать в подробностях карточки. **Аудит git.** Когда изолированная сессия агента заканчивается, закрывается или восстанавливается после выхода, CanvasTTY проверяет репозитории в её папке, у которых изменилась папка git. Если git теперь запустит что-то вне изоляции (`core.hooksPath`, фильтр или diff-драйвер, `fsmonitor`, файл хука, `info/attributes`), появляется уведомление со списком изменений; **Обезвредить** удаляет эти ключи и отключает файлы, **Оставить как есть** ничего не трогает. Другие изменения файлов в проекте аудит не откатывает. diff --git a/docs/installing-and-security.zh-CN.md b/docs/installing-and-security.zh-CN.md index 605deda0..3cf3cd37 100644 --- a/docs/installing-and-security.zh-CN.md +++ b/docs/installing-and-security.zh-CN.md @@ -85,7 +85,9 @@ gh variable set SPARKLE_PUBLIC_ED_KEY --repo howdeploy/CanvasTTY \ - **启动模式**:自动(默认)、手动、接受编辑、计划、绕过,仅在 CLI 支持时提供。绕过由用户按 CLI 确认,在主进程中检查,绝不交给子智能体,并且仍处于下列各层之内。 - **基础保护**(hooks)为有 hooks 的智能体拒绝提权、`curl | sh`、磁盘命令以及项目外的写入;它检查命令,不是沙箱。 - **委派规则**:子智能体的权限不超过其编排者(绝不为 YOLO),只在项目文件夹内工作,并受用户设定的深度和数量限制;智能体无法更改设置、防护、配置档或隔离。 -- **智能体隔离**(macOS 使用 sandbox-exec,Linux 使用 bubblewrap):只能写入项目、自身临时目录和其 CLI 的目录;密钥和令牌不可读;其他进程、应用和守护进程不可达;无法建立时拒绝启动。网络不受限制;Windows 暂无此层,子智能体在那里以手动模式运行。详见 [agent-orchestration.md](agent-orchestration.md)。 +- **智能体隔离**(macOS 使用 sandbox-exec,Linux 使用 bubblewrap):只能写入项目、自身临时目录和其 CLI 的目录;密钥和令牌不可读;其他进程、应用和守护进程不可达;无法建立时拒绝启动。未设置网络策略时网络不受限制;Windows 暂无此层,子智能体在那里以手动模式运行。详见 [agent-orchestration.md](agent-orchestration.md)。 +- **网络策略**按项目设置:开放、仅允许的域名(可选包括提供方 API 和软件包仓库)或离线;在下次启动智能体时生效,并显示在卡片上。策略保存在 CanvasTTY 的私有数据中,项目文件或插件无法放宽它。严格模式需要 macOS 沙箱,或在 Linux 上需要随附的 helper 和 Landlock ABI 9;否则严格启动会被拒绝。普通 SSH 会话不在此范围内。 +- **按需使用密钥**:智能体通过 `request_secret` 请求一个已配置的提供方密钥,用户授权 10 分钟、一轮或整个会话,随后 `run_secret_request` 用它发送类型化 HTTPS 请求。密钥本身不会交给智能体;授权可在卡片详情中撤销。 **Git 审计。** 隔离的智能体会话结束、被关闭或在退出后恢复时,CanvasTTY 会检查其文件夹下 git 目录发生变化的仓库。如果 git 现在会在隔离外运行某些东西(`core.hooksPath`、filter 或 diff 驱动、`fsmonitor`、hook 文件、`info/attributes`),会出现一条列出具体变化的通知;**Neutralize** 删除这些键并停用这些文件,**Keep as is** 保持不变。它不会撤销项目内的其他文件改动。 diff --git a/docs/plugin-api.d.ts b/docs/plugin-api.d.ts index 8ab1b19a..2541f201 100644 --- a/docs/plugin-api.d.ts +++ b/docs/plugin-api.d.ts @@ -278,6 +278,17 @@ export interface CanvasTTYServiceDecide { /** Params of the host request `canvastty.decide`. Tool input is agent-influenced data, never instructions. */ export interface CanvasTTYDecisionRequest { + /** Evidence from this live process's host wrapper, never launch settings. Missing/remote/unwrapped is unverified. + * Isolation does not hide the selected CLI's own credentials and an open network is unrestricted. */ + executionProtection?: { state: "unverified" } | { + state: "applied"; location: "local"; layer: "seatbelt" | "bubblewrap"; + filesystem: "read-only-project" | "project-and-runtime"; + network: "open" | "offline" | "allowed-domains"; + }; + /** Host-owned root task/privacy for this plugin only; absent on older hosts. Never agent tool input. */ + /** Host root privacy. dataClass remains conservative for older plugins. With privacy present, resolve + * selected="default" from this plugin's settings, then take max(resolved, floor). Invalid context is D3. */ + launchOptions?: { task?: string; dataClass?: string; privacy?: { version: 1; selected: string; floor: "D0" | "D1" | "D2" | "D3" } }; event: "pre-tool"; sessionId: string; provider: "codex" | "claude" | "qwen" | "opencode"; @@ -423,6 +434,9 @@ export interface CanvasTTYLaunchContext { environment: { pluginId: string; kind: string } | null; /** A subagent on this computer in (or below) the folder the person chose for its top-level agent: that real path. */ trustedFolder?: string; + /** Host-owned capability sent only to selected Accounts when execution policy requires public route proof. + * Older cores reject unknown contribution fields: omit accountRoute unless this is exactly true. */ + accountRouteEvidence?: true; } /** @@ -431,6 +445,12 @@ export interface CanvasTTYLaunchContext { * conversation arguments refuse it as well. */ export interface CanvasTTYLaunchContribution { + /** Only the selected canvastty-accounts service may attribute the configured account. */ + accountId?: string; + /** Only when accountRouteEvidence is true; copied from the same immutable account snapshot used for args/env. + * No credentials. model <= 200 chars; endpoint is canonical host:port <= 300 chars (no URL/path). + * Binds configured model, host:port and kind, not URL protocol/path or native CLI inference attestation. */ + accountRoute?: { model: string; endpoint: string; kind: "ollama" | "ollama-cloud" | "api-key" }; /** At most 32; values up to 8 KB. `{launchFiles}` becomes this run's folder of `files`. */ env?: Record; /** Env name -> the plugin's own secret key (needs `secrets`); the host sets the value and masks it. */ diff --git a/docs/plugins.md b/docs/plugins.md index 0047ad17..91f8a552 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -244,7 +244,7 @@ An environment is where a card runs: a git worktree, a container, a remote host. "services": [{ "id": "worktree", "title": "Git worktree", "entry": "services/worktree.mjs", "environments": [{ - "kind": "worktree", "label": "Git worktree", + "kind": "worktree", "label": "Git worktree", "executionLocation": "local", "description": "A branch in its own folder", "appliesTo": ["terminal", "claude"], "fields": [{ "key": "branch", "label": "Branch", "kind": "text", "default": "", "maxLength": 80 }], @@ -253,8 +253,14 @@ An environment is where a card runs: a git worktree, a container, a remote host. }] ``` +`executionLocation` declares `local` or `remote` in the trusted service manifest. Remote and undeclared locations require the experimental opt-in; launcher choices cannot override this declaration. `keeps.isolated` does not determine location (a container may be local). For older manifests, only `canvastty-environments` installed from `https://github.com/BIackFIame/canvastty-plugin-environments.git`, with service/kind `worktree`/`worktree` or `container`/`container`, is recognized as local. An explicit declaration takes precedence. Cleanup remains available after opt-out. + `keeps` declares what of CanvasTTY's protection reaches the agent there: `launch` (the launch's arguments and environment reach the agent unchanged, so CanvasTTY's hooks and the profile's per-run settings work), `isolated` (the agent does not run on this computer's files: a container or a remote host) and `confines` (the environment itself confines the agent to the project). Undeclared means no: any profile but normal is refused without `launch`, and the card says that base protection does not reach the agent there. An `isolated` environment is not wrapped in CanvasTTY's agent isolation again (the card names the environment's own boundary); any other runs inside it, with the environment's folder as the project. +Checkpoint restoration also refuses repositories when the current index or saved checkpoint contains submodules: parent-repository snapshots do not preserve dirty or untracked data inside nested repositories. Safeguard and restore that state with Git separately; CanvasTTY does not recursively delete or reset submodules. + +Checkpoint restoration cannot suspend an isolated agent by pausing its local SSH/container wrapper. CanvasTTY refuses restoration for related isolated launches, including after the wrapper exits; that exit does not establish remote workload termination. The host remembers the isolation declaration and project scope used by successful launches in the saved host-only card record, independently of later plugin changes or app restarts. Legacy placed cards without this evidence, and malformed evidence, are treated as unknown isolation scope and refuse restoration. These guards cover tracked host sessions, not untracked external workloads. + CanvasTTY keeps the card, the PTY, the saved record and the restore order; the service answers five host-only requests (surfaces cannot send them): | Request | Params | Answer | Budget | @@ -338,6 +344,12 @@ A service may offer up to 16 `tools` to agents. They appear in the `canvastty_ag ### Session events and plugin-owned cards (`sessions:*`) +`canvastty.activity` is a separate host-only integration for the enabled, native-code-trusted Assistant service installed from a canonical Assistant repository. `sessions:events` does not grant activity access. Tool equality fingerprints sent to that service are host-secret HMAC values scoped to the card and fingerprint kind; they are not plain hashes of tool input or output. Install provenance is checked on each delivery. With the experimental opt-in, only `limit.exhausted` and `route.outcome` may also reach the enabled, native-code-trusted `canvastty-accounts` / `accounts` service installed from `https://github.com/BIackFIame/canvastty-plugin-accounts.git`; it receives no tool activity or fingerprints and needs no Assistant installation. +OpenCode tool outcomes use its direct `tool.execute.after` hook and terminal `message.part.updated` tool states, correlated with a root-turn `tool.execute.before` call. Duplicate, child-session and obsolete completions are ignored. Shell nonzero exit codes are errors; missing exit status remains unknown. Separately trusted native `after-tool` hooks keep their provider-payload permission: the direct callback supplies `{ input, output }`, and a tool-part completion supplies its provider event. This does not grant `sessions:events` subscribers access to those payloads. +Kimi core hooks report `PostToolUse` and `PostToolUseFailure`; Hermes reports `post_tool_call`, independently of native plugin hooks. Kimi's `tool_output` is an opaque `str(ret)` that can also represent a tool error: its class stays `unknown`, while an explicit failure event reports `error`. It supplies no verified changed-path evidence. Hermes shell-hook `extra` carries the result, explicit outcome status and stable turn ID; result text is never interpreted as a status. + + + A service with `sessions:events` calls `sessions.subscribe` `{ ownedOnly? }` (again after every start). The answer lists the open cards; after that the host sends `canvastty.sessions.event` notifications: ```ts @@ -369,7 +381,7 @@ A foreign or unknown id gets the same error, so a plugin cannot probe other card ### Card badges and actions (`cards:decorate`) -A service with `cards:decorate` can put a badge on any card and declare up to 8 `cardActions`: +A service with `cards:decorate` can put a badge on any card and declare up to 16 `cardActions`: ```json "permissions": ["cards:decorate"], @@ -385,6 +397,10 @@ A service with `cards:decorate` can put a badge on any card and declare up to 8 The full example is [`examples/plugins/collect-demo`](../examples/plugins/collect-demo): the card action **Show changes** on cards in the `worktree` environment (from `env-worktree`) shows `git diff --stat` of the worktree and sets a "N changed" badge, and the tool `collect-demo__diffstat` gives orchestrators the same for their own folder or a subagent's, which it learns about from session events. +### Model routing (`model:route`) + +A service with `model:route` and `"modelRouter": true` can choose the model and effort for a subagent that an orchestrator starts without `model`. The host calls `canvastty.model.route` with the masked task (up to 8,000 characters), provider, profile, folder, any requested effort, the task budget and a list of `candidates` (`id`, `model`, `reasoningEffort`, `default`) built from what `list_providers` reports. The service answers `{ candidateId, reason }` within two seconds. The host accepts only a listed candidate that keeps an explicitly requested effort; an invalid answer, an error or no answer in time leaves the provider default, and the card shows why. An explicit `model` is never routed. + ### Browser engines (`browser:engine`) A service may run another browser engine for agents' background tabs, for example a headless engine that reads pages with far less CPU and memory than Chromium. It declares one `browserEngine`: @@ -427,6 +443,7 @@ host.onStorageChange(listener) notifies every live contribution of the same plug | `sessions:launch` | `sessions.create` | Starts visible agent cards through the normal launch | | `sessions:control` | `sessions.send`, `sessions.stop` | Types into and closes only the cards the plugin started | | `cards:decorate` | `cards.setBadge`, a service's `cardActions`, `canvastty.cards.invoke` | Plain-text badges on cards and actions in their menu | +| `model:route` | A service's `modelRouter` and `canvastty.model.route` | Receives the masked task text and budget of subagents started without a model; can only pick one of the offered candidates | | `browser:engine` | A service's `browserEngine` and `canvastty.browserEngine.*` | Receives the URLs of agents' background tabs and serves them from its own engine; no cookies, profile or credentials | | `limits:read` | `limits.get` | The same sanitized `LimitsSnapshot` used by HOME | | `launcher:open` | `launcher.open` | Opens the built-in provider Focus Card or terminal action; it does not bypass user launch choices | @@ -542,6 +559,8 @@ The current installer intentionally rejects private repositories, GitHub `/tree/ Browsing and searching the showcase work without an account through GitHub's public search API. Signing in is optional and only raises GitHub's search limits; when the anonymous limit is reached, CanvasTTY shows when to try again. The optional showcase sign-in uses GitHub's OAuth device flow. Build maintainers can [register an OAuth App and enable Device Flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app), then store its public client ID in the `CANVASTTY_GITHUB_CLIENT_ID` GitHub Actions repository variable. Official builds bake in that value when configured; local builds can use `GITHUB_OAUTH_CLIENT_ID` or `CANVASTTY_GITHUB_CLIENT_ID`, and either variable can also override the bundled value at runtime. No client secret is shipped or required. Sign-in opens GitHub in CanvasTTY's built-in Browser by default and offers the system browser as an explicit fallback. Without a client ID the UI reports that OAuth is unavailable, while direct repository inspection and installation continue to work. Signing out removes the encrypted local session; revoke the OAuth grant separately under [GitHub application settings](https://github.com/settings/applications) when needed. +An active sign-in has a Cancel action. Denial, expiry, provider failure, and cancellation clear the old code, show the outcome, and offer sign-in again. Cancelling a pending flow preserves an existing local account; if authorization finishes first, the completed sign-in is retained. + ## Author checklist - Use only structured host data and explicit loading/unavailable/error states. diff --git a/docs/plugins.ru.md b/docs/plugins.ru.md index 5d2cfe9d..be05371e 100644 --- a/docs/plugins.ru.md +++ b/docs/plugins.ru.md @@ -347,7 +347,7 @@ interface PluginSessionEvent { ### Метки и действия на окнах (`cards:decorate`) -Сервис с `cards:decorate` может поставить метку на любое окно и объявить до 8 `cardActions`: +Сервис с `cards:decorate` может поставить метку на любое окно и объявить до 16 `cardActions`: ```json "permissions": ["cards:decorate"], @@ -363,6 +363,10 @@ interface PluginSessionEvent { Полный пример — [`examples/plugins/collect-demo`](../examples/plugins/collect-demo): действие **Show changes** на окнах в среде `worktree` (из `env-worktree`) показывает `git diff --stat` worktree и ставит метку «N changed», а инструмент `collect-demo__diffstat` даёт оркестраторам то же для своей папки или папки субагента, о котором плагин узнаёт из событий сессий. +### Выбор модели (`model:route`) + +Сервис с `model:route` и `"modelRouter": true` может выбрать модель и усилие для подагента, которого оркестратор запускает без `model`. Хост вызывает `canvastty.model.route` с маскированным текстом задачи (до 8 000 символов), провайдером, профилем, папкой, запрошенным усилием, бюджетом задачи и списком `candidates` (`id`, `model`, `reasoningEffort`, `default`), собранным из данных `list_providers`. Сервис отвечает `{ candidateId, reason }` в течение двух секунд. Хост принимает только кандидата из списка, сохраняющего явно запрошенное усилие; при неверном ответе, ошибке или таймауте используется модель провайдера по умолчанию, и карточка показывает причину. Явно указанная `model` не маршрутизируется. + ### Браузерные движки (`browser:engine`) Сервис может запускать другой браузерный движок для фоновых вкладок агентов, например headless-движок, который читает страницы с гораздо меньшими затратами CPU и памяти, чем Chromium. Он объявляет один `browserEngine`: @@ -405,6 +409,7 @@ host.onStorageChange(listener) сообщает всем открытым пов | `sessions:launch` | `sessions.create` | Запускает видимые окна агентов через обычный запуск | | `sessions:control` | `sessions.send`, `sessions.stop` | Вводит текст и закрывает только окна, запущенные плагином | | `cards:decorate` | `cards.setBadge`, `cardActions` сервиса, `canvastty.cards.invoke` | Текстовые метки на окнах и действия в их меню | +| `model:route` | `modelRouter` сервиса и `canvastty.model.route` | Получает маскированный текст задачи и бюджет подагентов, запущенных без модели; может выбрать только одного из предложенных кандидатов | | `browser:engine` | `browserEngine` сервиса и `canvastty.browserEngine.*` | Получает URL фоновых вкладок агентов и открывает их в своём движке; без cookies, профиля и учётных данных | | `limits:read` | `limits.get` | Тот же очищенный `LimitsSnapshot`, который использует HOME | | `launcher:open` | `launcher.open` | Открывает штатную Focus Card или запуск терминала; не обходит пользовательский выбор | @@ -506,6 +511,8 @@ Canvas-приложение продолжает жить, пока его ка Просмотр и поиск в витрине работают без входа, через публичный поиск GitHub. Вход необязателен и только повышает лимиты поиска GitHub; когда анонимный лимит исчерпан, CanvasTTY показывает, когда можно повторить. Опциональный вход в витрину использует GitHub OAuth Device Flow. Сопровождающий сборку может [зарегистрировать OAuth App и включить Device Flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app), затем сохранить его публичный client ID в repository variable GitHub Actions `CANVASTTY_GITHUB_CLIENT_ID`. Официальная сборка встраивает это значение, когда оно настроено; для локальной сборки подходят `GITHUB_OAUTH_CLIENT_ID` и `CANVASTTY_GITHUB_CLIENT_ID`, а при запуске любая из них может переопределить встроенный ID. Client secret в приложение не встраивается и не требуется. По умолчанию вход открывает GitHub во встроенном Browser CanvasTTY, а системный браузер остаётся явным fallback. Без client ID интерфейс прямо сообщает, что OAuth недоступен, но проверка и установка по прямой ссылке продолжают работать. Отключение удаляет локальную зашифрованную сессию; при необходимости отдельно отзовите доступ в [настройках приложений GitHub](https://github.com/settings/applications). +У активного входа есть кнопка отмены. Отказ в доступе, истечение кода, ошибка провайдера и отмена убирают старый код, показывают результат и позволяют начать вход заново. Отмена ожидающего входа сохраняет уже подключённый локальный аккаунт; если авторизация успела завершиться, подключение сохраняется. + ## Чек-лист автора - Используйте только structured host data и явные loading/unavailable/error states. diff --git a/docs/plugins.zh-CN.md b/docs/plugins.zh-CN.md index c91ff2c6..55565158 100644 --- a/docs/plugins.zh-CN.md +++ b/docs/plugins.zh-CN.md @@ -345,7 +345,7 @@ interface PluginSessionEvent { ### 卡片标记与动作(`cards:decorate`) -具有 `cards:decorate` 的服务可以在任意卡片上设置标记,并声明最多 8 个 `cardActions`: +具有 `cards:decorate` 的服务可以在任意卡片上设置标记,并声明最多 16 个 `cardActions`: ```json "permissions": ["cards:decorate"], @@ -361,6 +361,10 @@ interface PluginSessionEvent { 完整示例见 [`examples/plugins/collect-demo`](../examples/plugins/collect-demo):在 `worktree` 环境(来自 `env-worktree`)的卡片上,动作 **Show changes** 显示 worktree 的 `git diff --stat` 并设置“N changed”标记;工具 `collect-demo__diffstat` 为编排器提供同样的信息,针对它自己的文件夹或某个子 agent 的文件夹(插件通过会话事件得知子 agent)。 +### 模型路由(`model:route`) + +具有 `model:route` 并设置 `"modelRouter": true` 的服务,可以为编排者未指定 `model` 而启动的子智能体选择模型和推理强度。宿主调用 `canvastty.model.route`,传入经过遮蔽的任务文本(最多 8,000 个字符)、提供方、配置档、文件夹、请求的推理强度、任务预算,以及根据 `list_providers` 构建的 `candidates` 列表(`id`、`model`、`reasoningEffort`、`default`)。服务须在两秒内返回 `{ candidateId, reason }`。宿主只接受列表中且保留显式请求的推理强度的候选;若回答无效、出错或超时,则使用提供方默认模型,卡片会显示原因。显式指定的 `model` 不会被路由。 + ### 浏览器引擎(`browser:engine`) 服务可以为 agent 的后台标签页运行另一种浏览器引擎,例如以远低于 Chromium 的 CPU 和内存读取页面的无头引擎。它声明一个 `browserEngine`: @@ -403,6 +407,7 @@ host.onStorageChange(listener) 会把 host.storage.set 的写入通知给同一 | `sessions:launch` | `sessions.create` | 通过常规启动流程启动可见的 agent 卡片 | | `sessions:control` | `sessions.send`、`sessions.stop` | 只能向本插件启动的卡片输入文本并关闭它们 | | `cards:decorate` | `cards.setBadge`、服务的 `cardActions`、`canvastty.cards.invoke` | 卡片上的纯文本标记及其菜单中的动作 | +| `model:route` | 服务的 `modelRouter` 与 `canvastty.model.route` | 接收未指定模型的子智能体的遮蔽任务文本和预算;只能从提供的候选中选择一个 | | `browser:engine` | 服务的 `browserEngine` 和 `canvastty.browserEngine.*` | 接收 agent 后台标签页的 URL 并用自己的引擎打开;没有 cookie、配置文件或凭据 | | `limits:read` | `limits.get` | 与 HOME 使用的同一个脱敏 `LimitsSnapshot` | | `launcher:open` | `launcher.open` | 打开内置服务商的 Focus Card 或终端动作;不会绕过用户的启动选择 | @@ -504,6 +509,8 @@ if (library) { 无需账号即可通过 GitHub 公共搜索 API 浏览和搜索展示页。登录是可选的,只会提高 GitHub 搜索限额;达到匿名限额时,CanvasTTY 会显示何时可以重试。可选的展示页登录使用 GitHub OAuth Device Flow。构建维护者可以[注册 OAuth App 并启用 Device Flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app),再将公开的 client ID 保存到 GitHub Actions 仓库变量 `CANVASTTY_GITHUB_CLIENT_ID`。正式构建会在该变量已配置时嵌入其值;本地构建可使用 `GITHUB_OAUTH_CLIENT_ID` 或 `CANVASTTY_GITHUB_CLIENT_ID`,运行时也可以用任一变量覆盖内置值。应用不包含也不需要 client secret。默认情况下,登录会在 CanvasTTY 内置浏览器中打开 GitHub,同时明确提供系统浏览器作为备用选项。未配置 client ID 时,界面会明确显示 OAuth 不可用,但仍可通过仓库链接检查和安装插件。退出登录只删除本机的加密会话;需要时请另行在 [GitHub 应用设置](https://github.com/settings/applications)中撤销授权。 +登录进行中时可以取消。拒绝授权、验证码过期、提供方错误或取消操作都会清除旧验证码,显示结果,并允许重新登录。取消等待中的登录流程会保留已有的本地账号;如果授权先完成,则保留已完成的登录。 + ## 作者检查清单 - 只使用结构化的 host 数据和明确的 loading/unavailable/error 状态。 diff --git a/docs/post-1.7-verification.md b/docs/post-1.7-verification.md new file mode 100644 index 00000000..b7b8e5df --- /dev/null +++ b/docs/post-1.7-verification.md @@ -0,0 +1,75 @@ +# Verifying the post-1.7 agent features + +How to repeat the checks for the orchestration, workspace and protection features added after 1.7. Every command runs from the repository root. Nothing here needs a real account, a paid agent or your own CanvasTTY data. + +## Use a throwaway home + +Tests and smokes read the same folders as the CLIs they imitate. Point them at an empty home so no real token, account or setting is read: + +```sh +export HOME="$(mktemp -d)" +export XDG_CONFIG_HOME="$HOME/.config" XDG_DATA_HOME="$HOME/.local/share" +export CODEX_HOME="$HOME/.codex" GROK_HOME="$HOME/.grok" CLAUDE_CONFIG_DIR="$HOME/.claude" +git config --global user.name "CanvasTTY test" && git config --global user.email "test@example.invalid" +``` + +The git identity is needed by the worktree acceptance tests. + +## Core checks + +```sh +npm ci +npm run typecheck +npm test +npm run build +npm run audit:secrets +``` + +CI uses Node 22, which loads TypeScript test imports and `node:sqlite` without flags. On Node 23 add `NODE_OPTIONS='--experimental-strip-types --experimental-sqlite'`. Tests that start local gateways need permission to open local sockets. + +Behavior that is easy to break has its own test file, for example: + +| Area | Test | +| --- | --- | +| Typing into a terminal before its code has loaded; a failed load or a failing card | `tests/deferred-terminal-card.test.mjs` | +| Dragging one card does not render the others | `tests/canvas-card-render-cost.test.mjs` | +| A damaged saved preset does not hide the others | `tests/workspace-presets-validation.test.mjs` | +| Account attribution and the default sign-in | `tests/launch-contributors.test.mjs` | + +Run one file with `node --test tests/.test.mjs`. + +## Real application smokes + +Each smoke starts the built app (`npm run build` first) with a fresh home and stub agents: + +```sh +npm run smoke:backlog # task tree, flows and their approval, task board, checkpoints, usage, broadcast +npm run smoke:plugin-review # plugin change review dialog +npm run smoke:accounts-limit # limit notice and handoff preview (CANVASTTY_ACCOUNTS_PLUGIN_DIR = an accounts plugin checkout) +npm run smoke:github-auth # showcase sign-in, cancel and retry +``` + +The terminal loading smoke needs a dedicated build: `npx electron-vite build --mode terminal-smoke`, then `node scripts/smoke-terminal-loading-electron.mjs`. Rebuild normally afterwards. + +## Mutation check + +`npm run test:mutation:backlog` changes selected guards one at a time and expects a test to fail for each. It requires a clean checkout (or `--source-ref `) and writes its report to `artifacts/backlog-mutation-report.md`, which is not tracked. It covers the listed guards only, not the whole application. + +## Measurements + +`scripts/bench-renderer-graph.mjs` (static JavaScript the window loads), `scripts/bench-startup-optimization.mjs` (hidden-window startup), `scripts/bench-usage-optimization.mjs` (usage counters) and `scripts/bench-redaction-optimization.mjs` (masking throughput) measure one machine with synthetic input. Compare two builds by running each several times, interleaved; a single run is not a result. + +## Linux socket guard + +The strict network modes on Linux need the packaged native helper and Landlock ABI 9 or newer. On such a Linux machine with Go installed: `cd native/canvastty-helper && go test ./...`. An older kernel must refuse strict launches with a reason. + +## Plugins + +The accounts, assistant, context and environments plugins live in their own repositories; run `npm test` and `npm run build` in each. + +## Not covered by automated checks + +- The full bubblewrap and provider launch on Linux with strict network modes, and amd64 runtime. +- Pausing a budget stops the agent's process group and the descendants found by parent process at pause time, including ones that started their own session; a process already re-parented to init is not attributable, and Windows cannot pause running work. +- Live providers: resumed conversations, routing quality of the assistant plugin, every provider's question and approval forwarding. +- Physical phone and Even G2 delivery. diff --git a/electron.vite.config.ts b/electron.vite.config.ts index eefc3536..4ecc1383 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -14,7 +14,11 @@ export default defineConfig({ plugins: [externalizeDepsPlugin()], build: { rollupOptions: { - input: resolve("src/main/index.ts") + input: { + index: resolve("src/main/index.ts"), + TerminalOutputHistoryWorker: resolve("src/main/services/TerminalOutputHistoryWorker.ts"), + SessionTimelineIndexWorker: resolve("src/main/services/SessionTimelineIndexWorker.ts") + } } } }, diff --git a/examples/plugins/env-worktree/canvastty.plugin.json b/examples/plugins/env-worktree/canvastty.plugin.json index a00e7d5a..94c41838 100644 --- a/examples/plugins/env-worktree/canvastty.plugin.json +++ b/examples/plugins/env-worktree/canvastty.plugin.json @@ -15,6 +15,7 @@ "environments": [ { "kind": "worktree", + "executionLocation": "local", "label": "Git worktree", "description": "A branch in its own folder; the project folder stays untouched.", "fields": [ diff --git a/integrations/even-g2/src/main.mjs b/integrations/even-g2/src/main.mjs index eefa1be5..4a345c13 100644 --- a/integrations/even-g2/src/main.mjs +++ b/integrations/even-g2/src/main.mjs @@ -1017,6 +1017,17 @@ async function event(e) { paint(); } } +let attentionNotice = ""; +let attentionId = ""; +function showAttention(data) { + if(data.attention && data.attention.id!==attentionId) { + attentionId=data.attention.id; + attentionNotice=`${data.attention.title}: ${data.attention.kind}`; + notice=attentionNotice; + } else if(!data.attention && notice===attentionNotice) { + notice="";attentionNotice="";attentionId=""; + } +} async function poll() { if (!alive) return; try { @@ -1033,6 +1044,7 @@ async function poll() { "/g2/api/terminal?id=" + encodeURIComponent(selected), ); model.terminal(data); + showAttention(data); } if (native) { const state = { diff --git a/integrations/mobile/src/App.tsx b/integrations/mobile/src/App.tsx index a1214b36..fb1db26d 100644 --- a/integrations/mobile/src/App.tsx +++ b/integrations/mobile/src/App.tsx @@ -1,19 +1,16 @@ import { useEffect, useRef, useState, type FormEvent } from "react"; -import { Terminal } from "@xterm/xterm"; -import "@xterm/xterm/css/xterm.css"; import { CANVAS_LAUNCHER_ITEMS, PROVIDER_LABELS, type ProviderId } from "../../../src/shared/providerCatalog.ts"; import { CompanionClient, forgetPairing, loadSaved, markApproved, startPairing, - type Action, type Output, type Overview, type Session, + type Action, type Overview, type Session, } from "./client.ts"; import "./style.css"; +import type { CompanionQuestion } from "../../../src/shared/companion.ts"; const statusNames: Record = { idle: "Ready", working: "Working", needs_approval: "Needs attention", unavailable: "Activity unknown", done: "Done", failed: "Failed", }; -const keys = ["ctrl-c", "enter", "up", "down", "left", "right", "tab", "backspace", "escape"] as const; -const keyLabels = ["Ctrl C", "Enter", "↑", "↓", "←", "→", "Tab", "⌫", "Esc"]; const PIN_KEY = "canvastty.mobile.pins.v1"; function savedPins(): string[] { try { @@ -28,29 +25,6 @@ function shortDate(value: number): string { return Number.isFinite(value) && value > 0 ? new Date(value).toLocaleString() : "Unknown start"; } -function RawTerminal({ frame, sessionId }: { frame: Output | null; sessionId: string }) { - const element = useRef(null); - const terminal = useRef(null); - useEffect(() => { - if (!element.current) return; - const view = new Terminal({ convertEol: false, disableStdin: true, scrollback: 1200, - fontFamily: "\"JetBrains Mono\", \"Cascadia Code\", monospace", fontSize: 12, - theme: { background: "#292a35", foreground: "#f8f7f1" } }); - view.open(element.current); - terminal.current = view; - return () => { terminal.current = null; view.dispose(); }; - }, [sessionId]); - useEffect(() => { - const view = terminal.current; - if (!view || !frame) return; - if (frame.cols > 0 && frame.rows > 0 && (view.cols !== frame.cols || view.rows !== frame.rows)) - view.resize(frame.cols, frame.rows); - if (frame.gap) view.reset(); - if (frame.data) view.write(frame.data); - }, [frame]); - return
; -} - export default function App() { const [client, setClient] = useState(() => { const saved = loadSaved(); @@ -65,31 +39,22 @@ export default function App() { const [menuOpen, setMenuOpen] = useState(true); const [filter, setFilter] = useState("all"); const [pins, setPins] = useState(savedPins); - const [mode, setMode] = useState<"readable" | "raw">("readable"); - const [readable, setReadable] = useState<{ sessionId: string; body: string } | null>(null); - const [frame, setFrame] = useState<{ sessionId: string; output: Output } | null>(null); - const [gapFor, setGapFor] = useState(""); - const [draft, setDraft] = useState(""); + const [readable, setReadable] = useState<{ sessionId: string; body: string; question?: CompanionQuestion | null } | null>(null); + const [replyDraft, setReplyDraft] = useState({ requestId: "", text: "" }); const [rename, setRename] = useState(null); - const [launch, setLaunch] = useState("terminal"); const [mutation, setMutation] = useState(false); const [refresh, setRefresh] = useState(0); const pairController = useRef(null); const mutationInFlight = useRef(false); const viewRevision = useRef(0); - const outputCursor = useRef<{ sessionId: string; cursor: number | null }>({ sessionId: "", cursor: null }); // Reset synchronously before the polling effect starts for a new selection. function selectSession(id: string, closeMenu = true) { if (closeMenu) setMenuOpen(false); if (id === selected) return; viewRevision.current += 1; - outputCursor.current = { sessionId: id, cursor: null }; - setFrame(null); - setGapFor(""); setReadable(null); setRename(null); - setDraft(""); setSelected(id); } @@ -176,30 +141,8 @@ export default function App() { } const sessionId = selected; if (sessionId && next.sessions.some(session => session.id === sessionId)) { - if (mode === "raw") { - let cursor = outputCursor.current.sessionId === sessionId ? outputCursor.current.cursor : null; - const chunks: string[] = []; - let gapSeen = false; - let last: Output | null = null; - // Consume bounded pages in order, then render once (React batches updates). - for (let page = 0; page < 4; page++) { - const result = await client.action({ type: "session.output", sessionId, cursor }, signal); - if (signal.aborted) return; - if (result.gap) { chunks.length = 0; gapSeen = true; } - chunks.push(result.data); - cursor = result.offset; // Absolute END offset, not start of this chunk. - last = result; - if (!result.hasMore) break; - } - if (last) { - outputCursor.current = { sessionId, cursor }; - if (gapSeen) setGapFor(sessionId); - if (chunks.some(Boolean) || gapSeen) setFrame({ sessionId, output: { ...last, data: chunks.join(""), gap: gapSeen } }); - } - } else { - const view = await client.action<{ body: string; revision: string }>({ type: "session.read", sessionId }, signal); - if (!signal.aborted) setReadable({ sessionId, body: view.body }); - } + const view = await client.action<{ body: string; revision: string; question?: CompanionQuestion | null }>({ type: "session.read", sessionId }, signal); + if (!signal.aborted) setReadable({ sessionId, body: view.body, question: view.question }); } delay = 2500; } catch (failure) { @@ -227,7 +170,7 @@ export default function App() { clearTimeout(timer); document.removeEventListener("visibilitychange", visibility); }; - }, [client, selected, mode, refresh]); + }, [client, selected, refresh]); async function pair(event: FormEvent) { event.preventDefault(); @@ -261,6 +204,7 @@ export default function App() { } async function act(action: Action, onSuccess?: (value: T) => void) { if (!client || mutationInFlight.current || !connected) return; + if (action.type === "session.interrupt" && !window.confirm("Stop this agent's current turn?")) return; const revision = viewRevision.current; mutationInFlight.current = true; setMutation(true); @@ -280,6 +224,14 @@ export default function App() { } const sessions = overview?.sessions || []; const current = sessions.find(session => session.id === selected); + const question = readable?.sessionId === selected ? readable.question : null; + function reply(answer: string | number) { + if (!current || !question || !window.confirm(`Send this answer to ${current.title}?`)) return; + void act({ type: "session.reply", sessionId: current.id, requestId: question.id, answer }, () => { + setReadable(view => view ? { ...view, question: null } : view); + setReplyDraft({ requestId: "", text: "" }); + }); + } const sorted = sessions.filter(session => filter === "all" || session.provider === filter) .sort((a, b) => Number(pins.includes(b.id)) - Number(pins.includes(a.id)) || b.startedAt - a.startedAt); const groups = [ @@ -287,9 +239,6 @@ export default function App() { { label: "Working", items: sorted.filter(session => session.status === "working") }, { label: "Other sessions · recent first", items: sorted.filter(session => !["needs_approval", "failed", "working"].includes(session.status)) }, ]; - const available = CANVAS_LAUNCHER_ITEMS.filter(provider => overview?.providers?.[provider]); - const launchProvider = available.includes(launch) ? launch : available[0]; - const canInput = connected && overview?.permissions.allowInput && !mutation && current?.exitCode === null; return
@@ -298,12 +247,12 @@ export default function App() { {client?.state === "approved" && }
- {!globalThis.isSecureContext &&

Open this companion through Tailscale Serve HTTPS or USB reverse loopback; Web Crypto requires a secure context.

} + {!globalThis.isSecureContext &&

Open this companion through trusted private-network HTTPS, Tailscale Serve HTTPS, or USB reverse loopback; Web Crypto requires a secure context.

} {error &&

{error}

} {!client ?

Pair this browser

On the desktop, enable the companion, choose shared sessions and open a six-digit pairing code. Approval is required on the desktop.

-

Host: {location.origin}. To pair a different desktop, open its own Tailscale HTTPS or USB loopback /mobile/ address first.

+

Host: {location.origin}. To pair a different desktop, open its own trusted private-network HTTPS, Tailscale HTTPS, or USB loopback /mobile/ address first.

@@ -316,23 +265,35 @@ export default function App() {
DESKTOP / SHARED

Shared sessions

{groups.map(group => group.items.length > 0 &&

{group.label}

{group.items.map(session =>
- +
)}
)} {!sorted.length &&

{sessions.length ? "No sessions for this provider." : "No sessions shared yet. Select sessions on the desktop."}

} - {overview?.permissions.allowCreate &&
{ event.preventDefault(); if (launchProvider) void act<{ id: string }>({ type: "session.create", provider: launchProvider }, session => selectSession(session.id)); }}>
}
- {!current ?

Choose a shared session from the menu to inspect its output.

: <> + {!current ?

Choose a shared session to view its status and questions.

: <>
{PROVIDER_LABELS[current.provider]} · {statusNames[current.status]}

{current.title}

Started {shortDate(current.startedAt)}{current.exitCode !== null ? ` · Exit ${current.exitCode}` : ""}
- {overview?.permissions.allowInput && } + {overview?.permissions.allowRename && } {overview?.permissions.allowClose && } + {overview?.permissions.allowInterrupt && current.exitCode === null && }
{rename !== null &&
{ event.preventDefault(); const title = rename.trim(); if (title) void act({ type: "session.rename", sessionId: current.id, title }, () => setRename(null)); }}> setRename(event.target.value)} />
} -
- {mode === "raw" ? <>{gapFor === current.id &&

Earlier terminal output is no longer buffered; showing the available tail.

} :
{(readable?.sessionId === current.id && readable.body) || "Waiting for session output…"}
} - {overview?.permissions.allowInput && <>
{ event.preventDefault(); if (draft.trim()) void act({ type: "session.input", sessionId: current.id, text: draft }, () => setDraft("")); }}>