Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/UI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This contract preserves the approved MVP concept and prevents feature ownership
## Home zone

- HOME uses fixed-size `82 × 72` logical cells with `18px` gaps so enlarging the page never makes existing widgets smaller. Fresh profiles start at `16 × 12`, while the original arrangement remains in the upper-left `12 × 8` area and leaves explicit spare space for plugins. Its persisted boundary and cell grid are shown only in Edit HOME, where the bottom-right corner resizes the area up to the generous `48 × 36` safety ceiling.
- The wide left tile contains the user-selected subset of the Codex, Claude, Qwen Code, Kimi, OpenCode Go, and Grok Build limit rows; only providers with a detected CLI are selected by default. Visible rows divide the tile height equally, while four or more rows switch to a compact density that keeps every icon, countdown, and usage rail inside the default tile. An empty selection shows an explicit empty state. Display selection does not disable background limit reads. Every offered row has a source-backed adapter: Qwen reports explicit `cli-not-found` or `unsupported-protocol` because its multi-provider CLI exposes no universal quota API. Rows with readable quotas prefer the provider's longest real default quota window (the weekly window when exposed), and fall back to another real window only when needed. Each available row shows the countdown to that window's `resetsAt` and the matching usage rail. Values below one day use `HH:MM`; longer values use `Nд HHч`/`Nd HHh`. Window length stays in accessible metadata; unavailable data is labeled and has no fake reset or percentage. A short yellow rail means the last valid snapshot is shown because its latest refresh failed; it is not selection or usage progress.
- The wide left tile contains the user-selected subset of the Codex, Claude, Qwen Code, Kimi, OpenCode Go, and Grok Build limit rows; only providers with a detected CLI are selected by default. Visible rows divide the tile height equally, while four or more rows switch to a compact density that keeps every icon, countdown, and usage rail inside the default tile. An empty selection shows an explicit empty state. Display selection does not disable background limit reads. Every offered row has a source-backed adapter: Qwen reports explicit `cli-not-found` or `unsupported-protocol` because its multi-provider CLI exposes no universal quota API. Rows with readable quotas prefer the provider's longest real default quota window (the weekly window when exposed), and fall back to another real window only when needed. Each available row shows the countdown to that window's `resetsAt` and the matching usage rail. Its share reads as used and left (for example "9% used · 91% left"); in a tile up to four grid columns wide the countdown stays beside the percentage and that text wraps on its own line below them, and only a row too short for two lines shortens it, with the full text on hover. Values below one day use `HH:MM`; longer values use `Nд HHч`/`Nd HHh`. Window length stays in accessible metadata; unavailable data is labeled and has no fake reset or percentage. A short yellow rail means the last valid snapshot is shown because its latest refresh failed; it is not selection or usage progress.
- The right tile is the only session list. Its viewport shows three rows and scrolls when more real sessions exist; it never discards rows. Ordinary wheel input over this list stays local without requiring a prior focus click, while explicit On/Key/full-navigation capture still belongs to the canvas. Each row shows provider mark, localized semantic state, and identity. Appearance can keep every row monochrome or color the whole row by status: idle/unavailable/finished states are gray, working is sage green, and waiting for input is yellow. Hovering a failed row's error mark exposes its bounded, terminal-control-free final PTY output, including a supplied traceback, in an accessible panel; its Copy action copies that displayed output only. When the PTY emitted no visible output, the panel instead states that fact with the actual exit code. It never invents a reason or derives the state from terminal text. Session duration and limit-style progress rails never appear here.
- The clock is the dominant middle tile and renders only `HH:MM`. The adjacent media tile is an autonomous widget: click to pick or replace an image/GIF; remove from the widget itself.
- The bottom dock always contains Terminal and Browser, plus the agent providers enabled in Settings. Its equal-width columns are derived from the buttons that are actually visible, so disabling an agent never leaves an empty slot. Fresh and migrated profiles select only agents with a detected CLI. Missing agents are removed from saved launcher and limit selections; hiding a launcher never stops or removes an existing session. Settings is a separate tile. Browser opens or focuses the one built-in browser card and never launches an external browser. Agent badges never overlay the Browser launcher icon.
Expand Down
2 changes: 1 addition & 1 deletion docs/UI_CONTRACT.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
## Зона Home

- HOME использует фиксированные логические ячейки `82 × 72` с промежутками `18px`, поэтому увеличение страницы не уменьшает существующие виджеты. Новый профиль начинает с `16 × 12`; исходная композиция остаётся в верхней левой области `12 × 8`, оставляя явный запас для плагинов. Сохраняемые граница и сетка видны только в Edit HOME, где правый нижний угол меняет область до безопасного потолка `48 × 36`.
- Широкая левая плитка содержит выбранное пользователем подмножество строк лимитов Codex, Claude, Qwen Code, Kimi, OpenCode Go и Grok Build; по умолчанию выбраны только провайдеры с найденным CLI. Видимые строки делят высоту плитки поровну, а при четырёх и более строках переходят в компактную плотность, чтобы каждая иконка, countdown и usage rail оставались внутри дефолтной плитки. Пустой выбор показывает явное пустое состояние. Настройка отображения не отключает фоновые запросы лимитов. У каждой строки есть source-backed adapter: Qwen честно показывает `cli-not-found` или `unsupported-protocol`, потому что его мультипровайдерный CLI не имеет универсального quota API. Строки с читаемыми квотами предпочитают самое длинное настоящее default quota window провайдера (weekly, если доступно) и только при необходимости переключаются на другое реальное окно. Каждая доступная строка показывает countdown до `resetsAt` этого окна и соответствующий usage rail. Значения короче суток используют `HH:MM`, длиннее — `Nд HHч`/`Nd HHh`. Длина окна остаётся в accessible metadata; недоступные данные подписаны и не получают выдуманный reset или percentage. Короткая жёлтая полоска означает, что после неудачного свежего обновления показан последний валидный snapshot; это не выбор строки и не progress использования.
- Широкая левая плитка содержит выбранное пользователем подмножество строк лимитов Codex, Claude, Qwen Code, Kimi, OpenCode Go и Grok Build; по умолчанию выбраны только провайдеры с найденным CLI. Видимые строки делят высоту плитки поровну, а при четырёх и более строках переходят в компактную плотность, чтобы каждая иконка, countdown и usage rail оставались внутри дефолтной плитки. Пустой выбор показывает явное пустое состояние. Настройка отображения не отключает фоновые запросы лимитов. У каждой строки есть source-backed adapter: Qwen честно показывает `cli-not-found` или `unsupported-protocol`, потому что его мультипровайдерный CLI не имеет универсального quota API. Строки с читаемыми квотами предпочитают самое длинное настоящее default quota window провайдера (weekly, если доступно) и только при необходимости переключаются на другое реальное окно. Каждая доступная строка показывает countdown до `resetsAt` этого окна и соответствующий usage rail. Её доля читается как использованное и оставшееся (например «9% использовано · осталось 91%»); в плитке шириной до четырёх колонок сетки countdown остаётся рядом с процентом, а этот текст переносится отдельной строкой под ними, и только строка, в которой нет места для двух строк текста, сокращает его, с полным текстом при наведении. Значения короче суток используют `HH:MM`, длиннее — `Nд HHч`/`Nd HHh`. Длина окна остаётся в accessible metadata; недоступные данные подписаны и не получают выдуманный reset или percentage. Короткая жёлтая полоска означает, что после неудачного свежего обновления показан последний валидный snapshot; это не выбор строки и не progress использования.
- Правая плитка — единственный список сессий. Viewport показывает три строки и прокручивается при большем числе реальных сессий, не отбрасывая их. Каждая строка показывает mark провайдера, локализованный semantic state и identity. Hover на error mark неудачной сессии открывает доступную панель с ограниченным финальным выводом PTY без terminal-control последовательностей, включая переданный traceback; действие Copy копирует только этот показанный вывод. Если PTY не передал видимого вывода, панель сообщает этот факт с фактическим кодом завершения. UI не выдумывает причину и не определяет state по тексту терминала. Session duration и progress rails в стиле лимитов здесь не появляются.
- Часы — доминирующая средняя плитка и показывают только `HH:MM`. Соседняя media tile автономна: нажмите, чтобы выбрать/заменить image/GIF; удаление находится в самом виджете.
- Нижний dock всегда содержит Terminal и Browser, а также включённых в Settings агентских провайдеров. Его равные по ширине колонки вычисляются из реально видимых кнопок, поэтому отключённый агент не оставляет пустую ячейку. В новых и мигрированных профилях выбраны только агенты с найденным CLI. Отсутствующие агенты удаляются из сохранённых списков запуска и лимитов; скрытие launcher-кнопки не останавливает и не удаляет существующую сессию. Settings — отдельная плитка. Browser открывает или фокусирует единственную встроенную карточку браузера и никогда не запускает внешний браузер. Agent badges не накладываются на launcher-иконку Browser.
Expand Down
2 changes: 1 addition & 1 deletion docs/UI_CONTRACT.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
## Home 区域

- HOME 使用固定的 `82 × 72` 逻辑 cell 与 `18px` 间距,因此扩大页面不会让已有 widget 变小。新 profile 从 `16 × 12` 开始;原布局保留在左上角 `12 × 8` 区域,并为插件留下明确空间。持久化边界与 cell grid 只在 Edit HOME 中显示;右下角可将区域调整到 `48 × 36` 的安全上限。
- 左侧宽 tile 只包含用户选择的 Codex、Claude、Qwen Code、Kimi、OpenCode Go 与 Grok Build 限额行;默认只选择已检测到 CLI 的服务商。可见行等分 tile 高度;四行或更多时切换为紧凑密度,确保每个图标、倒计时和 usage rail 都留在默认 tile 内。空选择显示明确的空状态。显示设置不会停止后台读取限额。每行都有 source-backed adapter;Qwen 因多服务商 CLI 没有通用 quota API 而明确显示 `cli-not-found` 或 `unsupported-protocol`。可读取配额的行优先显示服务商最长的真实默认配额窗口(若提供则为 weekly),仅在必要时 fallback 到另一个真实窗口。它显示该窗口 `resetsAt` 的倒计时及对应 usage rail。短于一天使用 `HH:MM`,更长时间使用 `Nд HHч`/`Nd HHh`。窗口长度保留在 accessible metadata 中;不可用数据必须明确标记,不得伪造 reset 或 percentage。短黄色竖线表示最新刷新失败、当前显示的是最后一个有效 snapshot;它不是选择状态或 usage progress。
- 左侧宽 tile 只包含用户选择的 Codex、Claude、Qwen Code、Kimi、OpenCode Go 与 Grok Build 限额行;默认只选择已检测到 CLI 的服务商。可见行等分 tile 高度;四行或更多时切换为紧凑密度,确保每个图标、倒计时和 usage rail 都留在默认 tile 内。空选择显示明确的空状态。显示设置不会停止后台读取限额。每行都有 source-backed adapter;Qwen 因多服务商 CLI 没有通用 quota API 而明确显示 `cli-not-found` 或 `unsupported-protocol`。可读取配额的行优先显示服务商最长的真实默认配额窗口(若提供则为 weekly),仅在必要时 fallback 到另一个真实窗口。它显示该窗口 `resetsAt` 的倒计时及对应 usage rail。其份额以已用和剩余表示(例如 “9% used · 91% left”);在不超过四个网格列宽的 tile 中,倒计时保持在百分比旁边,该文字在其下方单独换行显示;只有高度不足以容纳两行文字的行才会缩短它,悬停时显示完整文字。短于一天使用 `HH:MM`,更长时间使用 `Nд HHч`/`Nd HHh`。窗口长度保留在 accessible metadata 中;不可用数据必须明确标记,不得伪造 reset 或 percentage。短黄色竖线表示最新刷新失败、当前显示的是最后一个有效 snapshot;它不是选择状态或 usage progress。
- 右侧 tile 是唯一的 session list。Viewport 显示三行,真实会话更多时滚动,并且不丢弃任何行。每行显示 provider mark、本地化 semantic state 与 identity。Hover 失败行的 error mark 会打开 accessible panel,显示经过长度限制、去除 terminal-control 序列的最终 PTY 输出(包括提供的 traceback);Copy action 只复制该显示输出。若 PTY 没有可见输出,面板会如实显示这一点及实际退出码。UI 不会编造原因,也不会从 terminal text 推断 state。这里不显示 session duration 或 limit-style progress rail。
- Clock 是主导的中间 tile,只渲染 `HH:MM`。旁边的 media tile 是自治 widget:点击选择或替换 image/GIF;删除操作留在 widget 内。
- 底部 dock 始终包含 Terminal 与 Browser,以及 Settings 中启用的 agent provider。等宽列数由实际可见按钮计算,因此禁用 agent 不会留下空白单元格。新建和迁移后的 profile 只选择已检测到 CLI 的 agent。缺失的 agent 会从已保存的启动器与限额列表中移除;隐藏 launcher 按钮不会停止或删除现有 session。Settings 是独立 tile。Browser 打开或聚焦唯一的内置浏览器卡片,绝不启动外部浏览器。Agent badge 不会覆盖 Browser launcher 图标。
Expand Down
13 changes: 12 additions & 1 deletion src/main/services/PluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from "../../shared/contracts.ts";
import { isValidSemver } from "../../shared/hostVersion.ts";
import { PLUGIN_VISIBILITY_BRIDGE_SOURCE } from "../../shared/pluginVisibility.ts";
import { PLUGIN_NATIVE_CODE_OFF_ERROR, PLUGIN_SERVICE_MODULE_OFF_ERROR } from "../../shared/pluginServiceNotice.ts";
import type { PluginServiceSpec } from "./PluginServiceSupervisor.ts";
import type { LaunchContributor } from "./LaunchPipeline.ts";
import type { EnvironmentProvider } from "./EnvironmentRegistry.ts";
Expand Down Expand Up @@ -1223,11 +1224,21 @@ export class PluginManager {
return structuredClone(contribution);
}

/**
* The plugin's own UI may ask `serviceId` now: it is in the active manifest and its native code is trusted. A
* service whose module is off or whose native code is not trusted gets a fixed text the renderer turns into a
* localized notice with a way to the right settings section (pluginServiceNotice).
*/
assertService(pluginId: string, serviceId: string): void {
const plugin = activePlugin(this.requireEnabledPlugin(pluginId));
const installed = this.requireEnabledPlugin(pluginId);
const plugin = activePlugin(installed);
if (!plugin.manifest.services?.some((service) => service.id === serviceId)) {
if (installed.manifest.services?.some((service) => service.id === serviceId)) throw new Error(PLUGIN_SERVICE_MODULE_OFF_ERROR);
throw new Error("Plugin service does not exist.");
}
if (!installed.nativeCodeTrusted || !this.serviceTrust.get(pluginId)?.[serviceId]) {
throw new Error(PLUGIN_NATIVE_CODE_OFF_ERROR);
}
}

hasPermission(pluginId: string, permission: PluginPermission): boolean {
Expand Down
3 changes: 2 additions & 1 deletion src/main/services/PluginServiceSupervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ export class PluginServiceSupervisor {
private send(pluginId: string, serviceId: string, method: string, params: unknown, timeoutMs: number): Promise<unknown> {
const record = this.services.get(serviceKey(pluginId, serviceId));
if (!record || !record.child || (record.state !== "running" && record.state !== "starting")) {
return Promise.reject(new Error("Plugin service is not running."));
// A trusted service that crashed, failed to start or is waiting to restart says why.
return Promise.reject(new Error(`Plugin service is not running.${record?.lastError ? ` Last error: ${record.lastError}` : ""}`));
}
if (record.pending.size >= MAX_PENDING_REQUESTS) {
return Promise.reject(new Error("Plugin service is busy."));
Expand Down
Loading
Loading