Skip to content

feat(plugins): plugin indexing module — GitHub showcase, OAuth device flow, metadata/ standard - #16

Closed
4444cjtr wants to merge 28 commits into
howdeploy:mainfrom
4444cjtr:plugin-indexing
Closed

feat(plugins): plugin indexing module — GitHub showcase, OAuth device flow, metadata/ standard#16
4444cjtr wants to merge 28 commits into
howdeploy:mainfrom
4444cjtr:plugin-indexing

Conversation

@4444cjtr

@4444cjtr 4444cjtr commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Модуль индексации плагинов — витрина GitHub, OAuth device flow, стандарт metadata/

Полный модуль индексации плагинов: плагины находятся индексацией публичных GitHub-репозиториев, проходят строгую валидацию манифеста,
фильтруются по платформе и версии хоста и показываются в витрине.

Пример готового, шаблонного плагина, прошедшего тесты:
https://github.com/4444cjtr/canvastty-plugin-example

Скриншоты этого плагина в интерфейсе:
ui1
ui2

Одно действие перед релизом: заменить плейсхолдер OAuth client_id
в src/main/services/GithubAuthService.ts (DEFAULT_OAUTH_CLIENT_ID = "")
на id собственного GitHub OAuth App (Settings → Developer settings → OAuth Apps).


Что такое индексация плагинов

Вкладка плагины находит репозитарии индексацией GitHub:

  1. Поиск репозиториев — GitHub-поиск canvastty-plugin-* in:name (только публичные репозитории).
  2. Чтение манифеста — из каждого найденного репозитория читается
    metadata/canvastty.plugin.json (legacy-путь canvastty.plugin.json в корне
    поддерживается как fallback).
  3. Валидация — строгая проверка манифеста (см. «Лимиты» ниже).
  4. Фильтрация по платформе — репозитории, объявившие platforms без canvastty,
    исключаются (форк под другую платформу не должен попадать в витрину).
  5. Отображение — установленные плагины исключаются из витрины; у остальных
    показываются версия, автор, описание (обрезанное), иконка и версия хоста,
    под которую написан плагин.

Установка скачивает архив репозитория (ветка main), повторно валидирует манифест
и раскладывает файлы в userData/plugins/<pluginId>/.

Ключевые решения и их причины

Стандарт каталога metadata/

Манифест и иконка лежат в metadata/ (metadata/canvastty.plugin.json,
metadata/icon.png). Индексатор смотрит только туда — код плагина можно менять без
переиндексации, а метаданные не смешиваются с рантайм-ассетами. Legacy-пути в корне
принимаются как fallback, чтобы существующие плагины продолжали работать.

Фильтрация по платформе (platforms)

Плагин может поддерживать несколько платформ: ["canvastty", "canvastty-superkruto"].
Если platforms объявлено и не содержит текущую платформу — плагин не
индексируется
: форк под другую платформу не должен засорять витрину.
Отсутствие поля = legacy-плагин, совместим со всеми.

Декларация версии хоста (minHostVersion)

minHostVersion — версия CanvasTTY:

  • В плашке витрины и карточке установленного показывается CanvasTTY:X.Y.Z под автором.
  • Зелёный — плагин написан под более новую версию хоста, чем установлена (информация).
  • Оранжевый — плагин написан под более старую версию.
  • Установка/обновление никогда не блокируются несовпадением версий.

Создано для уменьшения трения юзерэкспириенса. В случаях, когда SDK плагинов обновлен, но автор плагина не обновил репозитарий - юзер узнает об этом без перехода в репо плагина

Обновления плагинов

Из коробки добавлена возможность обновления плагинов. Автообновление при запуске CanvasTTY отброшено, во избежание возможного добавления летенси на старт. Не было добавлено автообновление и при открытии настроек по тем же причинам.

При детекте обновления, в хедере плагина в модуле Установленные плагины - появится кнопка Обновить

Версия берётся из app.getVersion() и передаётся в renderer через новый IPC-канал
app:version.

OAuth device flow (плейсхолдер client_id)

Для поиска в витрине требуется авторизация GitHub (поисковый API GitHub без токена
жёстко лимитирован). Вместо встраивания секрета приложения используем device flow
client_id публичен по дизайну и должен быть заменён на id собственного OAuth App
владельца репозитория (см. верх этого описания).

Лимит описания: 2000 в репо, 400 в UI

Валидатор принимает до 2000 символов (длинные описания сохраняются в репозитории
целиком), но UI обрезает до 400 символов с многоточием. 400 уже пересекает порог целесообразности. Всё что больше - начинает захломлять интерфейс.
Общий хелпер (manifestDescription) используется витриной, карточками установленных и превью.

Оптимизация запросов (rate limits)

Индексатор бережёт GitHub API:

  • Кеш метаданных (githubMetadataCache) — владелец/ветка кешируются на сессию.
  • Батчинг GraphQL — манифесты и иконки запрашиваются пачками (до 8 репо на запрос)
    одним GraphQL-вызовом, а не по одному.
  • Fallback без токена — без токена файлы читаются с raw.githubusercontent.com.
  • Батч иконокplugins:icon принимает массив URL и возвращает
    Record<url, dataUrl|null>; UI никогда не шлёт по одному запросу на иконку.

Поддержка страниц

Установленные: 6 на страницу; витрина: 10 на страницу. Чистая логика живёт в
pluginPagination.ts (без DOM), покрыта юнит-тестами с плагинами-заглушками.

Мультистраничность витрины и установленных плагинов уже созданы с якорным фиксом дрейфа страницы, в случаях когда кол-во элементов на витрине/установленных плагинах отличаются

browser.open для плагинов

Плагины могут открыть URL во встроенном браузере канваса через SDK-метод
browser.open («Открыть в CanvasTTY» на плашках витрины и карточках установленных).
Реализация опирается на существующий одиночный браузер CanvasTTY: PluginFrame
диспатчит событие canvastty:browser-open, App открывает URL через
browser.open(url) и фокусирует камеру.
Для создания возможности открыть репозитарий в браузере CanvasTTY, пришлось внести изменение в SDK

Создано как альтернатива открытия репозитария в браузере, для нелюбителей делать 20 альт табов между окнами.

Лимиты

Валидация манифеста

Поле Лимит Зачем
id ≤ 80 симв. Ключ реестра, папка на диске, ссылки протокола.
name ≤ 80 симв. Заголовок карточки.
version ≤ 40 симв., semver Сравнение обновлений.
description ≤ 2 000 симв. Хранится полностью; UI обрезает до 400.
author ≤ 120 симв. Строка «Автор: …».
icon path ≤ 180 симв. Путь к файлу иконки.
permissions только известные Безопасность: без неизвестных разрешений.
contributions 1–32 Без «манифестов-простыней».
settingsContribution ≤ 64 симв., должен ссылаться на canvas-app Не может указывать на виджет.

Пакет (архив репозитория)

Что Лимит Зачем
Размер архива ≤ 25 МБ Ограничение скачивания.
Число файлов ≤ 500 Защита от zip-бомб и мусора.
Один ассет ≤ 8 МБ Гифки, медиа.
Манифест ≤ 128 КБ Метаданные должны оставаться лёгкими.
Иконка ≤ 512 КБ, 128×128 Лёгкая пакетная загрузка.
storage (на плагин) ≤ 64 КБ Квота плагинного хранилища.

Индексация и поиск

Что Лимит Зачем
Префикс canvastty-plugin-* Только целевые репозитории, без шума.
Результаты поиска ≤ 10 Быстрый UI.
Витрина ≤ 10 на страницу Читаемость.
Установленные исключаются Не предлагать уже установленное.
Таймаут GitHub 15 с Не вешать UI.

Тесты

  • tests/plugin-manager.test.mjs - валидация манифестов, чтение metadata/,
    фильтрация платформ, установка/обновление (токен сбрасывается в тестах).
  • tests/plugin-pagination.test.mjs - границы пагинации, 25/37 плагинов-заглушек,
    без потерь/дублей.
  • tests/host-version.test.mjs - сравнение semver / проверка совместимости.

Варианты улучшений:

  • Добавление полноценного кеширования плагинов с витрины на клиент. Было отброшено с самого начала, дабы не засорять лишним блоатом клиентскую часть.
  • Аренда небольшого сервера, периодическая загрузка на сервер манифестов плагинов, затем передача с сервера на клиент метаданных, вместе с ссылками на гит. Избежание любых возможных проблем с рейтами со стороны гитхаба для юзера. (Отброшено потому что платно не предлагать)
  • Создание дополнительной фильтрации в сам модуль (По версии, по тегам, по автору и пр.)
  • Вынесение плагинов как отдельное окно, как это сделано у ComfyUI (не реализовано из коробки, для сохранения идентичности проекта)
  • Улучшение системы обновления плагинов. Возможно добавление автообновления
  • Уменьшение летенси при загрузке иконок. На данный момент, всё ещё присутствует неконсистентный период в 500-1000мс, когда иконки плагинов дефолтятся на несвязанный с ними элемент, до загрузки icon.png

…me) — home-widgets/canvas-apps hit this before main
…patches canvastty:browser-open, App runs full openBrowser(url) flow
…rowser window on canvas

- New 'shortcuts' settings section (moved keyboard shortcuts from controls)
- browserNewWindow binding (Ctrl+Click) — mouse gesture, soft-validated
- Ctrl+left-click on the browser launcher icon opens a new browser window (newTab) without touching open tabs
- i18n ru/en, tests updated (239 passed)
… a new card)

- BrowserManager owns N BrowserService instances (per-node tabs/viewport/store)
- browserCanvases: BrowserCanvasNode[] replaces single browserCanvas (migration from old value -> 'default' node)
- IPC: browser:create-node / close-node / nodes; all browser calls take windowId
- Renderer renders one BrowserCard per node; plugin browser.open routes via canvastty:browser-open event (App creates node if none)
- BrowserManager.open lazily creates default node when none exist
- Agent gateway stays bound to default node (agent->node binding is the next step)
- tests updated: 239 passed
…, stub)

- BrowserActor.agent.browserWindowId (null = default shared browser)
- AgentGateway takes BrowserNodeResolver instead of a single core; each
  connection resolves its bound node's core on auth and routes all commands
  (execute/subscribe/heartbeat/cursor/disconnect) to it
- CreateSessionRequest/RegisterAgentInput/prepareLaunch carry browserWindowId
- App: launching an agent binds it to the currently selected browser card,
  otherwise default — visible via AgentBadges on the card
- BROWSER_UNAVAILABLE bridge error when the bound node is gone
- tests updated (gateway resolver signature): 239 passed
… links)

- SessionMetadata.browserWindowId persisted; terminal:set-browser-binding IPC
- AgentLinkGraph: SVG layer inside workspace scene — ports on agent cards
  (out) and browser cards (in), cubic bezier links, drag port->port to bind,
  click a link to unbind
- App: create/remove link via setBrowserBinding; binding survives restart
- CSS: ports, stroke/hit paths, draft line while dragging
- typecheck/build clean, 239 tests pass
…inal agents)

A) writeGlobalMcpConfig writes mcp-helper into ~/.codex/config.toml and
   ~/.claude.json so ANY codex/claude launch (CanvasTTY terminal, external
   terminal, IDE) gets browser tools; mcp-helper reads the gateway address
   from ~/.config/canvastty/agent-browser-address (written on gateway start)
B) every terminal session gets CANVASTTY_TERMINAL_SESSION_ID in env; terminal
   sessions can be bound to a browser node via the link graph; mcp-helper in
   guest mode (no capability token) sends the session id, gateway resolves the
   node through TerminalManager.sessionBrowserBinding
- protocol: optional capabilityToken (empty = guest)
- tests updated: 239 pass
- AgentProvider is now an open string (format-validated only), so ANY
  MCP-capable CLI harness passes gateway auth — no code change for new ones
- Guest mode: empty terminalSessionId allowed (default node); BrowserCore
  identity uses agentId as pseudo-session for session-less guests
- BROWSER_PROVIDER_COLORS/labels: opencode, hermes, pi + safe fallback for
  unknown providers; provider labels show the harness name
- writeGlobalMcpConfig auto-registers installed harnesses: hermes via
  'hermes mcp add' (env before --args — nargs='+' pitfall), opencode via
  ~/.config/opencode/opencode.json, pi via ~/.config/pi/mcp.json;
  30s timeout (slow Electron on Ivy Bridge)
- verified end-to-end: hermes mcp test canvastty_browser → Connected,
  23 tools discovered; 239 tests pass
Link graph fixes:
- SVG layer moved OUT of the 1x1 workspace__scene onto the full workspace;
  canvas→screen coords via camera (was: 1x1 SVG → ports not clickable)
- client→SVG coords via getBoundingClientRect (was: 44px TitleBar offset,
  draft line rendered below the cursor)
- removed setPointerCapture (redirected all pointer events to the source
  port → drop never fired); drag now listens on window, drop resolves the
  target via [data-browser-port]

Universal hook (works for ANY harness, not just Hermes):
- writeAgentBrowserContext(cwd) upserts a marked section into AGENTS.md of
  the terminal session's cwd, telling the agent it can drive the visible
  CanvasTTY browser via mcp__canvastty_browser__* tools. AGENTS.md is read
  by Claude Code, Codex, opencode, Hermes, Cursor, etc. Existing content is
  preserved; section is idempotent (markers).
- called from TerminalManager.create for every session
- 3 new tests: create / preserve / idempotent (242 total pass)
resolveCore(null) preferred the hidden 'default' node (created at startup
but absent from browserCanvases), so a session-less guest agent opened tabs
in an invisible browser while the user watched an empty visible card.
Now windowId=null resolves to browserCanvases[0] (first visible node),
falling back to default. Bound-node routing (link graph) is unchanged and
still takes priority via resolveSessionBrowser.
Harnesses (Hermes and others) filter the environment for MCP subprocesses
(_build_safe_env passes only PATH/HOME/XDG_* + config env), so
CANVASTTY_TERMINAL_SESSION_ID set by the CanvasTTY pty terminal never
reaches mcp-helper. Every agent therefore authenticated as session-less
and routed to the same (first visible) node — two agents bound to
different browsers still shared one.

findTerminalSessionId() walks /proc/<pid>/environ up the process tree
(mcp-helper → harness → shell → CanvasTTY pty) and returns the session id
the first ancestor carries. Works for ANY harness; no env forwarding
needed. Windows: env-only (no /proc).

244 tests pass (2 new: ancestor lookup, filtered-env readIdentity).
Harnesses (Hermes and others) filter the environment for MCP subprocesses
(_build_safe_env passes only PATH/HOME/XDG_* + config env), so
CANVASTTY_TERMINAL_SESSION_ID set by the CanvasTTY pty terminal never
reaches mcp-helper. Every agent therefore authenticated as session-less
and routed to the same (first visible) node — two agents bound to
different browsers still shared one.

findTerminalSessionId() walks /proc/<pid>/environ up the process tree
(mcp-helper → harness → shell → CanvasTTY pty) and returns the session id
the first ancestor carries. Works for ANY harness; no env forwarding
needed. Windows: env-only (no /proc).

245 tests pass (3 new: ancestor lookup, chain test, filtered-env identity).
Ports and links were computed from session.position/bounds.position,
which in React only updates on gesture END (liveBounds lives in the card's
local state while dragging). Moving a linked window froze the link points
in place until drop.

Now the graph reads real DOM positions: cards expose
data-canvas-node-id, and AgentLinkGraph samples getBoundingClientRect in
a requestAnimationFrame loop (signature-checked), converting client
coords to SVG space. Ports and bezier links follow the window live during
drag, resize, pan and zoom. Drop still resolves via [data-browser-port].

245 tests pass.
Ports/links now update SVG attributes straight from requestAnimationFrame
(skip React state entirely — React re-render added a frame of lag when
panning the camera or dragging windows). Geometry writes are diffed
against a position cache and the last path d, so static windows cause
zero DOM writes; layout reads are cheap and rAF pauses when the tab is
hidden. 245 tests pass.
The perf pass split the link into two <path> elements (clickable hit +
visible stroke) under different map keys but the rAF loop only updated
the hit path, leaving the stroke at 'M 0 0' — links became invisible
(click target worked, line didn't render). rAF now diffs and updates
both paths. 245 tests pass.
Each card now exposes four ports (top/right/bottom/left). Ports are
opacity 0 by default and appear only while the cursor hovers the card
(pointermove → elementFromPoint → closest data-canvas-node-id; port
circles and link paths carry the attribute too so hovering them keeps
the ports visible). During drag all ports light up so the drop target is
obvious.

Links pick the smartest pair of sides: compare card centers — horizontal
if |dx| > |dy|, else vertical — so a line always connects the closest
edges and tracks windows live (rAF, no React state).

245 tests pass.
A browser port with an incoming link now starts a 'break' drag
(DaVinci-style: pull the wire off the node). The draft line renders red,
and releasing anywhere removes the link (onRemoveLink for the bound
session). Ports without a link keep their plain drop behavior; cursor
shows 'alias' on bound input ports. 245 tests pass.
Dragging a bound browser port now re-drags the EXISTING link (not a hard
cut): the agent end stays put, the browser end follows the cursor. On
release over another browser's port the link is moved (onCreateLink
rewrites browserWindowId); releasing over empty canvas breaks the link
(onRemoveLink); releasing back on the same port is a no-op. Draft line
starts from the smart agent side. 245 tests pass.
Dragging from an agent port that already has a link now enters relink
mode instead of drawing a new connection: the existing line is hidden and
its browser end follows the cursor. Release over another browser moves
the link, over empty canvas breaks it, back on the same port is a no-op.
Links are never duplicated during node-to-node drags. 245 tests pass.
The rAF loop manually restored line opacity (style.opacity = '') after a
relink drag ended; an rAF tick could run BEFORE React committed the new
link state, flashing the old line for one frame. Now hiding the
dragged line is a React class (agent-link--hidden) applied atomically
with the commit: rAF only updates geometry and never touches visibility,
so a line React decided to remove or redirect can't reappear. 245 tests pass.
The previous fix hid the line via a React class while dragging, but on
release the class was removed immediately while onCreateLink/onRemoveLink
round-trip through IPC — for 1-2 frames the old line (stale path) was
visible again. New pendingHide state keeps the line hidden until the
sessions actually update: an effect clears it once no session points at
the hidden node (break) or points elsewhere (move). Atomic with the
commit that carries the new link state, so the old wire never
reappears. 245 tests pass.
A link no longer auto-picks the 'smart' pair of sides; it remembers
where the drag started (agent port side) and where it was dropped
(browser port side, via data-side) and renders between exactly those
two ports. Relink preserves the agent-side anchor and updates only the
browser side; breaking the link clears the stored sides. Draft line
during a fresh link already followed the start side. 245 tests pass.
@4444cjtr

4444cjtr commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Замечания по безопасности (проход по диффу на соответствие RFC 9700 / BCP OAuth 2.0) — ни одно не блокирует мерж; 1 и 3 стоит поправить до перевода PR в Ready.

1. signOut() удаляет локальную сессию, но не отзывает токен на GitHub.
Как public client (device flow, без client_secret) мы не можем использовать серверный отзыв (DELETE /applications/{client_id}/token требует basic auth), так что локальное удаление — максимум возможного, и это нормально. Но стоит задокументировать: «Выйти» удаляет только локальную копию; полный отзыв доступа пользователь должен сделать вручную на github.com/settings/applications. Небольшой штрих: показывать имя залогиненного пользователя в секции «вышел» и давать ссылку на страницу приложений.

2. Новые IPC-хендлеры не проверяют sender frame.
pluginsSearch, pluginsIcon, pluginsManifests и хендлеры githubAuth* принимают запросы от любого renderer-фрейма. Это соответствует существующим хендлерам в файле, но по чек-листу безопасности Electron стоит добавить проверку event.senderFrame (например, URL должен быть file:// главной страницы приложения) хотя бы для хендлеров с побочными эффектами (githubAuthOpenUrlshell.openExternal). Практический риск низкий — токен никогда не пересекает границу IPC — но скомпрометированный renderer сможет открывать произвольные https-ссылки в системном браузере или жечь rate-лимит GitHub.

3. makeShowcaseStubs() содержит захардкоженный GitHub-хэндл автора форка (4444cjtr).
Это тестовая заглушка за env-флагом CANVASTTY_SHOWCASE_STUBS, так что не утечка, но личное имя аккаунта попадает в код апстрима. ✅ Исправлено в 9d95f81 — заменено на нейтральный неймспейс example/canvastty-plugin-showcase-stub-*.

4. github-oauth.json пишется неатомарно.
В отличие от plugin-versions.json (tmp + rename), persist() пишет файл хранилища напрямую; при краше посреди записи останется битый blob. load() это переживает (откатывается к состоянию «вышел из системы»), так что это мелочь, — но паттерн tmp+rename, уже используемый в этом файле, добавляется одной строкой для консистентности.

@4444cjtr
4444cjtr marked this pull request as ready for review August 16, 2026 02:19
4444cjtr and others added 4 commits August 16, 2026 21:36
feat(browser): agent-browser nodes on canvas (multi-window, link graph, any harness)
Subtitle 'Пространственный рабочий стол агентов' replaced by a
rounded-pill badge '--hui' (border-radius 999px, translucent
background, no-drag region). 245 tests pass.
Badge is now yellow (#f2c744, dark text) and the subtitle
'Пространственный рабочий стол агентов' is back next to the title.
Selector updated to span:not(.titlebar__badge) so the badge keeps its
own colors (last-child selector would have overridden them). 245 tests pass.
Moved the --hui badge next to the app name (title → badge → subtitle).
Subtitle styling still applies via span:not(.titlebar__badge). 245 tests pass.

@howdeploy howdeploy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Спасибо за большую и вдумчивую работу, @4444cjtr — витрина плагинов это то, чего проекту не хватало. Видно заботу о лимитах и rate-limit'ах. Но аудит (Codex CLI + мейнтейнерская сторона) нашёл логические баги, которые сломают обещания фичи — нужен раунд правок.

Критично к исправлению (аудит)

  1. Витрина всегда обрезана до 10PluginManager.ts:1398: mapSearchResults() безусловно стопорится на SEARCH_MAX_RESULTS (10), хотя заявлена пагинация до 1000. UI-пагинация сейчас проверяется только локальными стабами и реальный путь не ловит. Нужно решить: либо честные 10 без пагинации, либо рабочая пагинация.
  2. Обновление плагина неатомарноPluginManager.ts:541: для немодульного плагина текущий каталог удаляется ДО rename() (:559-560) — ошибка посередине удаляет рабочую версию без отката; для модульного файлы пишутся поверх существующих (:557, :1779-1785), оставляя смесь версий. Нужен staging + swap + rollback, как в setModules().
  3. Обход фильтров при прямой установкеPluginManager.ts:213: platforms и minHostVersion проверяются только в витрине (:395-408), а previewInstall() по URL их игнорирует. Обновление тоже может заменить совместимую версию несовместимой. Политика должна быть единой для любого источника установки.
  4. Неверная логика hostMismatchPluginSettingsSection.tsx:449: сейчас compareSemver(...) !== 0 помечает несовместимым плагин, написанный под БОЛЕЕ СТАРЫЙ хост. Несовместимость — только minHostVersion > hostVersion.
  5. Неотменяемый OAuth pollingGithubAuthService.ts:170: signOut() не отменяет активный device-flow poll (:178-186), вышедший пользователь может получить сохранённые токены обратно (:221-232); повторные нажатия плодят параллельные flow. Нужны отмена/generation-id и единственный активный flow.
  6. Batch limit не применяетсяPluginManager.ts:1559: GITHUB_GRAPHQL_BATCH_LIMIT = 8 объявлен, но githubGraphqlBatch() строит один запрос на весь вход (:1601-1609). При сотнях репо это отказ API вместо батчинга.

Minors: OAuth-запросы без timeout/AbortSignal (GithubAuthService.ts:149,198,247,277); лишний scope read:user (:147) — по возможности минимизировать; runtime-валидатор не отклоняет неизвестные поля манифеста (PluginManager.ts:807) — «строгая валидация» должна быть строгой.

Вопросы архитектуры (нужно решить до мержа)

  1. Единый конвейер установки. Витрина и прямой URL должны проходить один и тот же валидированный pipeline (манифест → платформа → версия → лимиты пакета → staging). Сейчас пути расходятся.
  2. Модель доверия к коду. В 1.2.0 мы пинили редиректы загрузок к api.github.com/raw.githubusercontent.com — сохраняется ли это в новом индексаторе? И достаточна ли CSP песочницы для кода, скачанного из произвольного паблик-репо (архив с main-ветки может поменяться в любой момент)?
  3. OAuth App перед релизом. Плейсхолдер DEFAULT_OAUTH_CLIENT_ID — ок как дизайн, но добавь, пожалуйста, в docs пошаговую инструкцию для владельца: создание OAuth App, включение device flow, какие поля куда вписать. Без этого фича нерабочая из коробки.
  4. Стыковка с новым ядром навигации (PR #13, влит в 1.2.2). Плагинные iframe и Browser-сёрфейсы теперь подчиняются общей политике focus/ownership. Проверь, что карточки витрины/установленных и browser.open-флоу не обходят новую модель владения вводом (hover-delay focus, latch 250 мс и т.д.).
  5. Пересечение с #15. Этот PR включает #15 целиком. План: сначала мержим исправленный #15, затем ты ребейзишь #16 — дифф похудеет, ревью станет проще.

По мелочи из твоего списка «варианты улучшений»: кэш витрины на клиент и фильтры по тегам/автору — разумно отложить; серверную аренду согласен не требовать. Иконки 500–1000 мс — если найдёшь дешёвый способ прелоада манифест-иконок батчем, будет приятно, но не блокер.

После правок — прогоним CI (я одобрю runs) и повторный аудит. Спасибо!

… flow, metadata/ standard

Full plugin indexing module on top of upstream v1.2.2 (canvas-navigation
input-bridge included from upstream; no conflicts).

- GitHub showcase: search canvastty-plugin-* repos, read metadata/
  canvastty.plugin.json (legacy root fallback), strict manifest validation,
  platform filtering (platforms without PLATFORM_ID are dropped), minHostVersion
  display (green/neutral/orange, no install blocking).
- OAuth device flow for GitHub (public client_id placeholder
  DEFAULT_OAUTH_CLIENT_ID — replace with your own OAuth App).
- Pagination (6 installed / 10 showcase per page), pure logic in
  pluginPagination.ts, unit-tested.
- Description limits: 2000 in manifest, 400 shown (manifestDescription).
- Rate-limit friendly: metadata cache, GraphQL batch (8 repos/request),
  raw.githubusercontent fallback without token, icon batch IPC.
- SDK browser.open for plugins: PluginFrame dispatches canvastty:browser-open,
  App opens URL in the single canvas browser (no multi-window nodes).
- Tests: plugin-manager (25), plugin-pagination (10), host-version (4) —
  83/83 with upstream canvas-navigation suites.

Copy link
Copy Markdown
Owner

Спасибо за обновления. Перенесли ваш финальный целевой commit db8a32b в чистую integration-ветку поверх актуального main после #15; авторство commit сохранено.

Продолжение и наши доработки находятся в #17. Там убрана старая расходящаяся история ветки, сохранена архитектура единственного Browser из #15 и закрыты найденные блокеры по атомарности обновлений, OAuth, platform policy, batch-запросам и строгой валидации.

Закрываю этот PR как superseded by #17.

@howdeploy howdeploy closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants