From bfebdff3cf619fd3d1f1e01cead8a9e80ec0e5a2 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 16 May 2026 20:03:30 +0300 Subject: [PATCH] chore: save local dev updates --- README.md | 4 +- ...gastown-paperclip-comparison-2026-05-16.md | 107 ++++++++++++++++++ docs/research/real-competitors-comparison.md | 2 +- package.json | 6 +- patches/@radix-ui__react-presence@1.1.5.patch | 86 ++++++++++++++ pnpm-lock.yaml | 26 +++-- .../components/dashboard/CliStatusBanner.tsx | 5 +- .../providerDashboardRateLimits.test.ts | 34 ++++++ .../dashboard/providerDashboardRateLimits.ts | 6 + .../team/dialogs/projectPathOptions.ts | 2 +- .../components/ui/presence.react19.test.tsx | 72 ++++++++++++ 11 files changed, 335 insertions(+), 15 deletions(-) create mode 100644 docs/research/gastown-paperclip-comparison-2026-05-16.md create mode 100644 patches/@radix-ui__react-presence@1.1.5.patch create mode 100644 src/renderer/components/ui/presence.react19.test.tsx diff --git a/README.md b/README.md index 678494d5..40eb7624 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ For feature architecture and implementation guidance: | **Git worktree isolation** | ✅ Optional | ✅ Core primitive | ✅ Worktrees / branches | ⚠️ Background branches/VMs | ⚠️ Manual worktrees | | **Multi-agent backend** | ✅ Claude, Codex + OpenCode teammates | ✅ Claude, Codex, Gemini, Copilot + more | ✅ BYO agents: Claude, Codex, Cursor/OpenCode, HTTP | ⚠️ Multi-model agents, no team backend | ⚠️ Claude-only experimental teams | | **Org chart / governance** | ⚠️ Roles + approvals, no org chart | ⚠️ Roles + escalation | ✅ Org chart + board governance | ⚠️ Team admin only | ❌ | -| **Budget controls** | ⚠️ Cost/token visibility, no hard caps | ⚠️ Cost tiers + digest, no hard caps | ✅ Per-agent budgets + hard stops | ⚠️ Usage + BG spend limits | ⚠️ `/cost` + workspace limits | +| **Budget controls** | ⚠️ Cost/token visibility, no hard caps | ⚠️ Cost tiers + digest, no hard caps | ✅ Per-agent budgets + hard stops | ⚠️ Usage + BG spend limits | ⚠️ `/usage` + workspace limits | | **Price** | **Free OSS UI**, provider access needed | Free OSS, runtime plans needed | Free OSS, self-hosted + infra | Free + paid usage | Claude plan or API usage | -Fact sources checked on May 5, 2026: [detailed research notes](docs/research/gastown-paperclip-comparison-2026-05-05.md), [Gastown README](https://github.com/gastownhall/gastown), [Gastown provider guide](https://github.com/gastownhall/gastown/blob/main/docs/agent-provider-integration.md), [Gastown scheduler](https://github.com/gastownhall/gastown/blob/main/docs/design/scheduler.md), [Paperclip README](https://github.com/paperclipai/paperclip), [Paperclip adapters](https://github.com/paperclipai/paperclip/blob/master/docs/adapters/overview.md), [Paperclip budgets](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/costs-and-budgets.md), [Paperclip runtime services](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/execution-workspaces-and-runtime-services.md), [Paperclip Kanban source](https://github.com/paperclipai/paperclip/blob/master/ui/src/components/KanbanBoard.tsx), [Cursor Background Agents](https://docs.cursor.com/en/background-agents), [Cursor Diffs & Review](https://docs.cursor.com/en/agent/review), [Cursor Bugbot](https://docs.cursor.com/en/bugbot), [Cursor pricing](https://docs.cursor.com/en/account/usage), [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams), [Claude Code subagents](https://code.claude.com/docs/en/sub-agents), [Claude Code workflows](https://code.claude.com/docs/en/common-workflows), [Claude Code costs](https://code.claude.com/docs/en/costs), [Claude pricing](https://claude.com/pricing). +Fact sources checked on May 16, 2026: [detailed research notes](docs/research/gastown-paperclip-comparison-2026-05-16.md), [Gastown README](https://github.com/gastownhall/gastown), [Gastown provider guide](https://github.com/gastownhall/gastown/blob/main/docs/agent-provider-integration.md), [Gastown scheduler](https://github.com/gastownhall/gastown/blob/main/docs/design/scheduler.md), [Gastown release](https://github.com/gastownhall/gastown/releases/tag/v1.1.0), [Paperclip README](https://github.com/paperclipai/paperclip), [Paperclip adapters](https://github.com/paperclipai/paperclip/blob/master/docs/adapters/overview.md), [Paperclip budgets](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/costs-and-budgets.md), [Paperclip runtime services](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/execution-workspaces-and-runtime-services.md), [Paperclip Kanban source](https://github.com/paperclipai/paperclip/blob/master/ui/src/components/KanbanBoard.tsx), [Paperclip work products](https://github.com/paperclipai/paperclip/blob/master/packages/shared/src/validators/work-product.ts), [Paperclip release](https://github.com/paperclipai/paperclip/releases/tag/v2026.513.0), [Cursor Background Agents](https://docs.cursor.com/en/background-agents), [Cursor Diffs & Review](https://docs.cursor.com/en/agent/review), [Cursor Bugbot](https://docs.cursor.com/en/bugbot), [Cursor pricing](https://docs.cursor.com/en/account/usage), [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams), [Claude Code subagents](https://code.claude.com/docs/en/sub-agents), [Claude Code workflows](https://code.claude.com/docs/en/common-workflows), [Claude Code costs](https://code.claude.com/docs/en/costs), [Claude pricing](https://claude.com/pricing). --- diff --git a/docs/research/gastown-paperclip-comparison-2026-05-16.md b/docs/research/gastown-paperclip-comparison-2026-05-16.md new file mode 100644 index 00000000..e8762e10 --- /dev/null +++ b/docs/research/gastown-paperclip-comparison-2026-05-16.md @@ -0,0 +1,107 @@ +# Gastown и Paperclip comparison для лендинга и README + +> Дата проверки: 2026-05-16 +> Цель: публичная таблица `Agent Teams | Gastown | Paperclip | Cursor | Claude Code CLI` без угадываний по конкурентам. +> Метод: `gh repo view`, `gh api` по первичным GitHub-файлам, официальные docs Cursor и Claude Code, страница Claude pricing. + +## Snapshot + +| Проект | Позиционирование | Статус на 2026-05-16 | Лицензия | +|---|---|---:|---| +| **Gastown** | multi-agent workspace manager для coding agents | `15,228★`, latest `v1.1.0` от `2026-05-07`, push `2026-05-15` | MIT | +| **Paperclip** | control plane для autonomous AI companies | `65,796★`, latest `v2026.513.0` от `2026-05-13`, push `2026-05-16` | MIT | + +## Что изменилось после проверки 2026-05-05 + +- **Gastown**: свежий GitHub snapshot изменился с `v1.0.1` на `v1.1.0`; README/provider/scheduler факты для публичной таблицы остались валидными. +- **Paperclip**: свежий GitHub snapshot изменился с `v2026.428.0` на `v2026.513.0`; README/adapters/budget/runtime/Kanban facts остались валидными. +- **Claude Code costs**: официальный cost guide теперь называет `/usage` как команду для session token/cost tracking. Поэтому публичная строка `Budget controls` для Claude Code CLI обновлена с `/cost + workspace limits` на `/usage + workspace limits`. +- **Claude pricing**: Team pricing page явно включает Claude Code в Team seats; публичная строка `Claude plan or API usage` остаётся корректной. +- **Cursor**: official docs по Background Agents, Diffs & Review, Bugbot и usage/pricing по-прежнему поддерживают текущие формулировки таблицы. Background Agents остаются remote/async agents on separate branches/VMs with auto-run terminal commands; Bugbot остаётся PR-review product with its own pricing. + +## Проверенные публичные формулировки + +### Gastown + +- README по-прежнему позиционирует Gas Town как workspace manager для Claude Code, GitHub Copilot, Codex, Gemini и других coding agents. +- Provider guide по-прежнему описывает tmux/provider contract для Claude, Gemini, Codex, Cursor, AMP, OpenCode, Copilot и других. +- Scheduler docs по-прежнему подтверждают `scheduler.max_polecats`, deferred dispatch, capacity governor, pause/resume и daemon dispatch cycle. +- Dashboard остаётся monitoring view for agents, convoys, hooks, queues, issues and escalations, а не Kanban product. +- Refinery merge queue есть, но это не hunk-level diff review UI. + +Публичная оценка не меняется: + +- `Task dependencies` - `✅ Beads DAG waves` +- `Kanban board` - `❌ Dashboard, not Kanban` +- `Per-task code review` - `⚠️ Merge queue, no diff UI` +- `Budget controls` - `⚠️ Cost tiers + digest, no hard caps` + +### Paperclip + +- README по-прежнему описывает org charts, budgets, governance, goal alignment and agent coordination. +- Adapter overview подтверждает Claude Local, Codex Local, Gemini experimental, OpenCode Local, Cursor, OpenClaw Gateway, Process and HTTP adapters. +- Budget docs подтверждают per-agent monthly budgets, warning threshold at 80%, hard stop at 100%, auto-pause and no more heartbeats. +- Runtime services docs подтверждают manual UI-managed services/jobs and execution workspaces with isolated checkout/branch/runtime state. +- Kanban source по-прежнему содержит `backlog`, `todo`, `in_progress`, `in_review`, `blocked`, `done`, `cancelled` and `@dnd-kit`. +- Work product validators подтверждают `preview_url`, `runtime_service`, `pull_request`, `branch`, `commit`, `artifact`, `document` and review statuses. + +Публичная оценка не меняется: + +- `Kanban board` - `✅ 7 columns, drag-and-drop` +- `Per-task code review` - `⚠️ PR/work products, no inline diff` +- `Hunk-level review` - `❌ Bring your own review` +- `Budget controls` - `✅ Per-agent budgets + hard stops` + +### Cursor + +- Background Agents are asynchronous remote agents that edit/run code in isolated machines, use GitHub branches, support follow-ups and can auto-run terminal commands. +- Diffs & Review still supports diff review with accept/reject flows and selective acceptance. +- Bugbot still focuses on PR review and comments with explanations/fix suggestions; pricing remains separate from normal Cursor subscriptions. +- Usage/pricing docs still describe Free + paid usage, included agent usage by plan, dashboard token/usage breakdowns, background-agent access and spend limits. + +Публичная оценка не меняется: + +- `Full autonomy` - `⚠️ Background agents, not teams` +- `Hunk-level review` - `✅` +- `Review workflow` - `⚠️ PR/BugBot only` +- `Flexible autonomy` - `⚠️ BG agents auto-run commands` +- `Price` - `Free + paid usage` + +### Claude Code CLI + +- Agent teams are still experimental and disabled by default through `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`. +- Official docs still confirm shared task list, mailbox, direct teammate messaging, task dependencies, plan approval requests, quality-gate hooks and local team/task storage. +- Agent teams require Claude Code `v2.1.32` or later according to the docs. +- Worktrees remain an official workflow for isolated sessions, but this is not the same as a product-level worktree strategy UI. +- Cost docs now use `/usage` for detailed token usage statistics, plus workspace spend limits and usage reporting in Console for API users. + +Публичная оценка после свежей проверки: + +- `Agent-to-agent messaging` - `✅ Team mailbox, no UI` +- `Linked tasks` - `✅ Shared task list` +- `Task dependencies` - `✅ Team task deps, no UI` +- `Budget controls` - `⚠️ /usage + workspace limits` +- `Multi-agent backend` - `⚠️ Claude-only experimental teams` + +## Источники + +- Gastown repo: +- Gastown v1.1.0: +- Gastown provider guide: +- Gastown scheduler docs: +- Paperclip repo: +- Paperclip v2026.513.0: +- Paperclip adapters: +- Paperclip costs and budgets docs: +- Paperclip runtime services docs: +- Paperclip Kanban source: +- Paperclip work products source: +- Cursor Background Agents: +- Cursor Diffs & Review: +- Cursor Bugbot: +- Cursor usage/pricing: +- Claude Code agent teams: +- Claude Code subagents: +- Claude Code common workflows: +- Claude Code costs: +- Claude pricing: diff --git a/docs/research/real-competitors-comparison.md b/docs/research/real-competitors-comparison.md index 26121cae..e189bb39 100644 --- a/docs/research/real-competitors-comparison.md +++ b/docs/research/real-competitors-comparison.md @@ -1,6 +1,6 @@ # Реальные конкуренты для Comparison в README -> ⚠️ Update 2026-05-05: публичная таблица README/landing теперь сравнивает нас с `Gastown` и `Paperclip`, а не с `Claude Code Agent Teams` и `GoClaw`. Актуальная research-опора: [gastown-paperclip-comparison-2026-05-05.md](gastown-paperclip-comparison-2026-05-05.md). Ниже оставлен старый broader draft как исторический контекст. +> ⚠️ Update 2026-05-16: публичная таблица README/landing теперь сравнивает нас с `Gastown` и `Paperclip`, а не с `Claude Code Agent Teams` и `GoClaw`. Актуальная research-опора: [gastown-paperclip-comparison-2026-05-16.md](gastown-paperclip-comparison-2026-05-16.md). Ниже оставлен старый broader draft как исторический контекст. > Дата проверки: 2026-04-13 > Статус: внутренний comparison draft diff --git a/package.json b/package.json index 4dcb49dc..b886f02b 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", @@ -350,7 +351,10 @@ "electron", "node-pty", "cpu-features" - ] + ], + "patchedDependencies": { + "@radix-ui/react-presence@1.1.5": "patches/@radix-ui__react-presence@1.1.5.patch" + } }, "knip": { "entry": [ diff --git a/patches/@radix-ui__react-presence@1.1.5.patch b/patches/@radix-ui__react-presence@1.1.5.patch new file mode 100644 index 00000000..8dc0bcac --- /dev/null +++ b/patches/@radix-ui__react-presence@1.1.5.patch @@ -0,0 +1,86 @@ +diff --git a/dist/index.js b/dist/index.js +index 944abc2652716a5047360c8abc2d48e700a3f3f8..e0d98a35aead8e057a304734627e0022643cb71c 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -62,6 +62,8 @@ var Presence = (props) => { + Presence.displayName = "Presence"; + function usePresence(present) { + const [node, setNode] = React2.useState(); ++ const nodeRef = React2.useRef(); ++ const nodeCleanupGenerationRef = React2.useRef(0); + const stylesRef = React2.useRef(null); + const prevPresentRef = React2.useRef(present); + const prevAnimationNameRef = React2.useRef("none"); +@@ -146,8 +148,27 @@ function usePresence(present) { + return { + isPresent: ["mounted", "unmountSuspended"].includes(state), + ref: React2.useCallback((node2) => { +- stylesRef.current = node2 ? getComputedStyle(node2) : null; +- setNode(node2); ++ const syncNode = (nextNode) => { ++ if (nodeRef.current === nextNode) { ++ stylesRef.current = nextNode ? getComputedStyle(nextNode) : null; ++ return; ++ } ++ nodeRef.current = nextNode; ++ stylesRef.current = nextNode ? getComputedStyle(nextNode) : null; ++ setNode(nextNode); ++ }; ++ nodeCleanupGenerationRef.current += 1; ++ const cleanupGeneration = nodeCleanupGenerationRef.current; ++ if (node2) { ++ syncNode(node2); ++ return; ++ } ++ queueMicrotask(() => { ++ if (nodeCleanupGenerationRef.current !== cleanupGeneration) { ++ return; ++ } ++ syncNode(null); ++ }); + }, []) + }; + } +diff --git a/dist/index.mjs b/dist/index.mjs +index 0efe02a3c45790c11f94f71563bf1db7257b799f..67db6319fb2af1008e2da62ec8cdb21e4903ea1e 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -26,6 +26,8 @@ var Presence = (props) => { + Presence.displayName = "Presence"; + function usePresence(present) { + const [node, setNode] = React2.useState(); ++ const nodeRef = React2.useRef(); ++ const nodeCleanupGenerationRef = React2.useRef(0); + const stylesRef = React2.useRef(null); + const prevPresentRef = React2.useRef(present); + const prevAnimationNameRef = React2.useRef("none"); +@@ -110,8 +112,27 @@ function usePresence(present) { + return { + isPresent: ["mounted", "unmountSuspended"].includes(state), + ref: React2.useCallback((node2) => { +- stylesRef.current = node2 ? getComputedStyle(node2) : null; +- setNode(node2); ++ const syncNode = (nextNode) => { ++ if (nodeRef.current === nextNode) { ++ stylesRef.current = nextNode ? getComputedStyle(nextNode) : null; ++ return; ++ } ++ nodeRef.current = nextNode; ++ stylesRef.current = nextNode ? getComputedStyle(nextNode) : null; ++ setNode(nextNode); ++ }; ++ nodeCleanupGenerationRef.current += 1; ++ const cleanupGeneration = nodeCleanupGenerationRef.current; ++ if (node2) { ++ syncNode(node2); ++ return; ++ } ++ queueMicrotask(() => { ++ if (nodeCleanupGenerationRef.current !== cleanupGeneration) { ++ return; ++ } ++ syncNode(null); ++ }); + }, []) + }; + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8b7e807..be2c7ae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,11 @@ overrides: lodash-es: ^4.18.1 uuid: ^11.1.1 +patchedDependencies: + '@radix-ui/react-presence@1.1.5': + hash: afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e + path: patches/@radix-ui__react-presence@1.1.5.patch + importers: .: @@ -138,6 +143,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': + specifier: ^1.1.5 + version: 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-select': specifier: ^2.2.6 version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -13909,7 +13917,7 @@ snapshots: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) @@ -13926,7 +13934,7 @@ snapshots: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) @@ -13984,7 +13992,7 @@ snapshots: '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) @@ -14055,7 +14063,7 @@ snapshots: '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) react: 19.2.4 @@ -14093,7 +14101,7 @@ snapshots: '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) @@ -14117,7 +14125,7 @@ snapshots: '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) @@ -14157,7 +14165,7 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-presence@1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) @@ -14251,7 +14259,7 @@ snapshots: '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) @@ -14270,7 +14278,7 @@ snapshots: '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index d1f60117..e27ca731 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -123,7 +123,10 @@ const DashboardRateLimitChips = ({ > {item.label} - + {item.remaining} { label: '5h left', remaining: '75%', resetsAt: 'reset unknown', + isDepleted: false, }, { label: 'Weekly left', remaining: '50%', resetsAt: 'reset unknown', + isDepleted: false, }, ]); }); @@ -188,10 +190,42 @@ describe('providerDashboardRateLimits', () => { label: '5h left', remaining: '80%', resetsAt: 'reset unknown', + isDepleted: false, }, ]); }); + test('marks fully depleted limits when no quota remains', () => { + const connection = createCodexConnection(); + + const items = getCodexDashboardRateLimits( + createProvider({ + providerId: 'codex', + displayName: 'Codex', + authMethod: 'oauth_token', + connection: { + ...connection, + codex: { + ...connection.codex!, + rateLimits: { + ...connection.codex!.rateLimits!, + primary: { + usedPercent: 100, + windowDurationMins: 300, + resetsAt: null, + }, + }, + }, + }, + }) + ); + + expect(items?.[0]).toMatchObject({ + remaining: '0%', + isDepleted: true, + }); + }); + test('shows Anthropic rate limit skeletons when subscription mode is selected in config', () => { expect( shouldShowDashboardRateLimitSkeleton({ diff --git a/src/renderer/components/dashboard/providerDashboardRateLimits.ts b/src/renderer/components/dashboard/providerDashboardRateLimits.ts index 3a194381..969a72ed 100644 --- a/src/renderer/components/dashboard/providerDashboardRateLimits.ts +++ b/src/renderer/components/dashboard/providerDashboardRateLimits.ts @@ -14,6 +14,7 @@ export interface DashboardRateLimitItem { label: string; remaining: string; resetsAt: string; + isDepleted: boolean; } export interface DashboardRateLimitSkeletonModeInput { @@ -180,6 +181,10 @@ function formatDashboardResetTime(timestampSeconds: number | null | undefined): }); } +function isRateLimitDepleted(usedPercent: number | null | undefined): boolean { + return typeof usedPercent === 'number' && Number.isFinite(usedPercent) && usedPercent >= 100; +} + function buildRateLimitItem( label: string, usedPercent: number, @@ -189,6 +194,7 @@ function buildRateLimitItem( label, remaining: formatCodexRemainingPercent(usedPercent) ?? 'Unknown', resetsAt: formatDashboardResetTime(resetsAt), + isDepleted: isRateLimitDepleted(usedPercent), }; } diff --git a/src/renderer/components/team/dialogs/projectPathOptions.ts b/src/renderer/components/team/dialogs/projectPathOptions.ts index 6cdae4b0..496ca3e9 100644 --- a/src/renderer/components/team/dialogs/projectPathOptions.ts +++ b/src/renderer/components/team/dialogs/projectPathOptions.ts @@ -13,7 +13,7 @@ export interface ProjectPathProject extends Project { filesystemState?: DashboardRecentProjectFilesystemState; } -export interface ProjectPathOptionMeta { +export interface ProjectPathOptionMeta extends Record { discoverySource?: DashboardRecentProjectSource; filesystemState?: DashboardRecentProjectFilesystemState; } diff --git a/src/renderer/components/ui/presence.react19.test.tsx b/src/renderer/components/ui/presence.react19.test.tsx new file mode 100644 index 00000000..a44a68a2 --- /dev/null +++ b/src/renderer/components/ui/presence.react19.test.tsx @@ -0,0 +1,72 @@ +import React, { act, useState } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { Presence } from '@radix-ui/react-presence'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const flushMicrotasks = async (): Promise => { + await Promise.resolve(); + await Promise.resolve(); +}; + +describe('Radix Presence React 19 compatibility', () => { + beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + }); + + afterEach(() => { + document.body.innerHTML = ''; + vi.unstubAllGlobals(); + }); + + it('does not recurse when a composed child ref changes identity and returns cleanup', async () => { + const refEvents: string[] = []; + + const Harness = (): React.JSX.Element => { + const [tick, setTick] = useState(0); + + return ( +
+ + +
{ + refEvents.push(node ? 'node' : 'null'); + return () => { + refEvents.push('cleanup'); + }; + }} + > + tick {tick} +
+
+
+ ); + }; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(); + await flushMicrotasks(); + }); + + await act(async () => { + host.querySelector('button')?.click(); + await flushMicrotasks(); + }); + + expect(host.textContent).toContain('tick 1'); + expect(refEvents.length).toBeLessThan(10); + expect(refEvents).toContain('cleanup'); + + await act(async () => { + root.unmount(); + await flushMicrotasks(); + }); + }); +});