corporateai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (704) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +3 -0
  3. package/.github/workflows/publish-cli.yml +49 -0
  4. package/.mailmap +1 -0
  5. package/AGENTS.md +148 -0
  6. package/CONTRIBUTING.md +75 -0
  7. package/Dockerfile +59 -0
  8. package/Dockerfile.onboard-smoke +42 -0
  9. package/LICENSE +21 -0
  10. package/README.md +93 -0
  11. package/cli/esbuild.config.mjs +11 -0
  12. package/cli/package.json +24 -0
  13. package/cli/scripts/build-cli.mjs +5 -0
  14. package/cli/src/index.ts +27 -0
  15. package/docker-compose.quickstart.yml +18 -0
  16. package/docker-compose.untrusted-review.yml +33 -0
  17. package/docker-compose.yml +38 -0
  18. package/package.json +56 -0
  19. package/patches/embedded-postgres@18.1.0-beta.16.patch +0 -0
  20. package/pnpm-workspace.yaml +4 -0
  21. package/releases/.gitkeep +0 -0
  22. package/releases/v0.0.1.md +36 -0
  23. package/releases/v0.2.7.md +15 -0
  24. package/releases/v0.3.0.md +54 -0
  25. package/releases/v0.3.1.md +55 -0
  26. package/releases/v2026.318.0.md +66 -0
  27. package/releases/v2026.325.0.md +78 -0
  28. package/report/2026-03-13-08-46-token-optimization-implementation.md +48 -0
  29. package/scripts/backup-db.sh +17 -0
  30. package/scripts/build-npm.sh +80 -0
  31. package/scripts/check-forbidden-tokens.mjs +115 -0
  32. package/scripts/clean-onboard-git.sh +14 -0
  33. package/scripts/clean-onboard-npm.sh +13 -0
  34. package/scripts/clean-onboard-ref.sh +86 -0
  35. package/scripts/create-github-release.sh +99 -0
  36. package/scripts/dev-runner-paths.mjs +38 -0
  37. package/scripts/dev-runner.mjs +606 -0
  38. package/scripts/docker-onboard-smoke.sh +306 -0
  39. package/scripts/ensure-plugin-build-deps.mjs +47 -0
  40. package/scripts/generate-company-assets.ts +365 -0
  41. package/scripts/generate-npm-package-json.mjs +113 -0
  42. package/scripts/generate-org-chart-images.ts +694 -0
  43. package/scripts/generate-org-chart-satori-comparison.ts +225 -0
  44. package/scripts/generate-ui-package-json.mjs +31 -0
  45. package/scripts/kill-dev.sh +71 -0
  46. package/scripts/migrate-inline-env-secrets.ts +126 -0
  47. package/scripts/prepare-server-ui-dist.sh +22 -0
  48. package/scripts/provision-worktree.sh +333 -0
  49. package/scripts/release-lib.sh +306 -0
  50. package/scripts/release-package-map.mjs +169 -0
  51. package/scripts/release.sh +312 -0
  52. package/scripts/rollback-latest.sh +111 -0
  53. package/scripts/smoke/openclaw-docker-ui.sh +329 -0
  54. package/scripts/smoke/openclaw-gateway-e2e.sh +954 -0
  55. package/scripts/smoke/openclaw-join.sh +295 -0
  56. package/scripts/smoke/openclaw-sse-standalone.sh +146 -0
  57. package/scripts/workspace-compat.mjs +60 -0
  58. package/server/CHANGELOG.md +130 -0
  59. package/server/package.json +96 -0
  60. package/server/scripts/copy-onboarding-assets.mjs +10 -0
  61. package/server/scripts/dev-watch.ts +33 -0
  62. package/server/src/__tests__/activity-routes.test.ts +70 -0
  63. package/server/src/__tests__/adapter-models.test.ts +105 -0
  64. package/server/src/__tests__/adapter-session-codecs.test.ts +194 -0
  65. package/server/src/__tests__/agent-auth-jwt.test.ts +79 -0
  66. package/server/src/__tests__/agent-instructions-routes.test.ts +318 -0
  67. package/server/src/__tests__/agent-instructions-service.test.ts +361 -0
  68. package/server/src/__tests__/agent-permissions-routes.test.ts +275 -0
  69. package/server/src/__tests__/agent-shortname-collision.test.ts +69 -0
  70. package/server/src/__tests__/agent-skill-contract.test.ts +50 -0
  71. package/server/src/__tests__/agent-skills-routes.test.ts +462 -0
  72. package/server/src/__tests__/app-hmr-port.test.ts +19 -0
  73. package/server/src/__tests__/approval-routes-idempotency.test.ts +110 -0
  74. package/server/src/__tests__/approvals-service.test.ts +107 -0
  75. package/server/src/__tests__/assets.test.ts +250 -0
  76. package/server/src/__tests__/attachment-types.test.ts +97 -0
  77. package/server/src/__tests__/board-mutation-guard.test.ts +105 -0
  78. package/server/src/__tests__/budgets-service.test.ts +311 -0
  79. package/server/src/__tests__/claude-local-adapter-environment.test.ts +92 -0
  80. package/server/src/__tests__/claude-local-adapter.test.ts +31 -0
  81. package/server/src/__tests__/claude-local-skill-sync.test.ts +111 -0
  82. package/server/src/__tests__/cli-auth-routes.test.ts +230 -0
  83. package/server/src/__tests__/codex-local-adapter-environment.test.ts +143 -0
  84. package/server/src/__tests__/codex-local-adapter.test.ts +253 -0
  85. package/server/src/__tests__/codex-local-execute.test.ts +391 -0
  86. package/server/src/__tests__/codex-local-skill-injection.test.ts +175 -0
  87. package/server/src/__tests__/codex-local-skill-sync.test.ts +123 -0
  88. package/server/src/__tests__/companies-route-path-guard.test.ts +56 -0
  89. package/server/src/__tests__/company-branding-route.test.ts +196 -0
  90. package/server/src/__tests__/company-portability-routes.test.ts +175 -0
  91. package/server/src/__tests__/company-portability.test.ts +2186 -0
  92. package/server/src/__tests__/company-skills-routes.test.ts +113 -0
  93. package/server/src/__tests__/company-skills.test.ts +229 -0
  94. package/server/src/__tests__/costs-service.test.ts +226 -0
  95. package/server/src/__tests__/cursor-local-adapter-environment.test.ts +196 -0
  96. package/server/src/__tests__/cursor-local-adapter.test.ts +406 -0
  97. package/server/src/__tests__/cursor-local-execute.test.ts +263 -0
  98. package/server/src/__tests__/cursor-local-skill-injection.test.ts +104 -0
  99. package/server/src/__tests__/cursor-local-skill-sync.test.ts +145 -0
  100. package/server/src/__tests__/dev-runner-paths.test.ts +25 -0
  101. package/server/src/__tests__/dev-server-status.test.ts +66 -0
  102. package/server/src/__tests__/dev-watch-ignore.test.ts +42 -0
  103. package/server/src/__tests__/documents.test.ts +29 -0
  104. package/server/src/__tests__/error-handler.test.ts +53 -0
  105. package/server/src/__tests__/execution-workspace-policy.test.ts +170 -0
  106. package/server/src/__tests__/forbidden-tokens.test.ts +77 -0
  107. package/server/src/__tests__/gemini-local-adapter-environment.test.ts +135 -0
  108. package/server/src/__tests__/gemini-local-adapter.test.ts +190 -0
  109. package/server/src/__tests__/gemini-local-execute.test.ts +172 -0
  110. package/server/src/__tests__/gemini-local-skill-sync.test.ts +90 -0
  111. package/server/src/__tests__/health.test.ts +16 -0
  112. package/server/src/__tests__/heartbeat-process-recovery.test.ts +256 -0
  113. package/server/src/__tests__/heartbeat-run-summary.test.ts +33 -0
  114. package/server/src/__tests__/heartbeat-workspace-session.test.ts +334 -0
  115. package/server/src/__tests__/helpers/embedded-postgres.ts +7 -0
  116. package/server/src/__tests__/hire-hook.test.ts +181 -0
  117. package/server/src/__tests__/instance-settings-routes.test.ts +156 -0
  118. package/server/src/__tests__/invite-accept-gateway-defaults.test.ts +119 -0
  119. package/server/src/__tests__/invite-accept-replay.test.ts +92 -0
  120. package/server/src/__tests__/invite-expiry.test.ts +10 -0
  121. package/server/src/__tests__/invite-join-grants.test.ts +57 -0
  122. package/server/src/__tests__/invite-join-manager.test.ts +33 -0
  123. package/server/src/__tests__/invite-onboarding-text.test.ts +116 -0
  124. package/server/src/__tests__/issue-comment-reopen-routes.test.ts +146 -0
  125. package/server/src/__tests__/issue-goal-fallback.test.ts +99 -0
  126. package/server/src/__tests__/issues-checkout-wakeup.test.ts +48 -0
  127. package/server/src/__tests__/issues-goal-context-routes.test.ts +187 -0
  128. package/server/src/__tests__/issues-service.test.ts +317 -0
  129. package/server/src/__tests__/issues-user-context.test.ts +113 -0
  130. package/server/src/__tests__/log-redaction.test.ts +74 -0
  131. package/server/src/__tests__/monthly-spend-service.test.ts +90 -0
  132. package/server/src/__tests__/normalize-agent-mention-token.test.ts +41 -0
  133. package/server/src/__tests__/openclaw-gateway-adapter.test.ts +626 -0
  134. package/server/src/__tests__/openclaw-invite-prompt-route.test.ts +192 -0
  135. package/server/src/__tests__/opencode-local-adapter-environment.test.ts +97 -0
  136. package/server/src/__tests__/opencode-local-adapter.test.ts +226 -0
  137. package/server/src/__tests__/opencode-local-skill-sync.test.ts +91 -0
  138. package/server/src/__tests__/paperclip-env.test.ts +58 -0
  139. package/server/src/__tests__/paperclip-skill-utils.test.ts +63 -0
  140. package/server/src/__tests__/pi-local-adapter-environment.test.ts +102 -0
  141. package/server/src/__tests__/pi-local-skill-sync.test.ts +95 -0
  142. package/server/src/__tests__/plugin-dev-watcher.test.ts +68 -0
  143. package/server/src/__tests__/plugin-worker-manager.test.ts +43 -0
  144. package/server/src/__tests__/private-hostname-guard.test.ts +56 -0
  145. package/server/src/__tests__/project-shortname-resolution.test.ts +45 -0
  146. package/server/src/__tests__/quota-windows-service.test.ts +56 -0
  147. package/server/src/__tests__/quota-windows.test.ts +1109 -0
  148. package/server/src/__tests__/redaction.test.ts +66 -0
  149. package/server/src/__tests__/routines-e2e.test.ts +276 -0
  150. package/server/src/__tests__/routines-routes.test.ts +271 -0
  151. package/server/src/__tests__/routines-service.test.ts +424 -0
  152. package/server/src/__tests__/storage-local-provider.test.ts +78 -0
  153. package/server/src/__tests__/ui-branding.test.ts +82 -0
  154. package/server/src/__tests__/work-products.test.ts +95 -0
  155. package/server/src/__tests__/workspace-runtime.test.ts +1131 -0
  156. package/server/src/__tests__/worktree-config.test.ts +426 -0
  157. package/server/src/adapters/codex-models.ts +105 -0
  158. package/server/src/adapters/cursor-models.ts +171 -0
  159. package/server/src/adapters/http/execute.ts +42 -0
  160. package/server/src/adapters/http/index.ts +21 -0
  161. package/server/src/adapters/http/test.ts +116 -0
  162. package/server/src/adapters/index.ts +18 -0
  163. package/server/src/adapters/process/execute.ts +77 -0
  164. package/server/src/adapters/process/index.ts +24 -0
  165. package/server/src/adapters/process/test.ts +89 -0
  166. package/server/src/adapters/registry.ts +225 -0
  167. package/server/src/adapters/server-utils-compat.ts +57 -0
  168. package/server/src/adapters/types.ts +30 -0
  169. package/server/src/adapters/utils.ts +48 -0
  170. package/server/src/agent-auth-jwt.ts +141 -0
  171. package/server/src/app.ts +321 -0
  172. package/server/src/attachment-types.ts +74 -0
  173. package/server/src/auth/better-auth.ts +148 -0
  174. package/server/src/board-claim.ts +150 -0
  175. package/server/src/config-file.ts +17 -0
  176. package/server/src/config.ts +260 -0
  177. package/server/src/dev-server-status.ts +103 -0
  178. package/server/src/dev-watch-ignore.ts +36 -0
  179. package/server/src/errors.ts +34 -0
  180. package/server/src/home-paths.ts +95 -0
  181. package/server/src/index.ts +799 -0
  182. package/server/src/log-redaction.ts +146 -0
  183. package/server/src/middleware/auth.ts +178 -0
  184. package/server/src/middleware/board-mutation-guard.ts +66 -0
  185. package/server/src/middleware/error-handler.ts +71 -0
  186. package/server/src/middleware/index.ts +3 -0
  187. package/server/src/middleware/logger.ts +90 -0
  188. package/server/src/middleware/private-hostname-guard.ts +92 -0
  189. package/server/src/middleware/validate.ts +9 -0
  190. package/server/src/onboarding-assets/ceo/AGENTS.md +54 -0
  191. package/server/src/onboarding-assets/ceo/HEARTBEAT.md +72 -0
  192. package/server/src/onboarding-assets/ceo/SOUL.md +33 -0
  193. package/server/src/onboarding-assets/ceo/TOOLS.md +3 -0
  194. package/server/src/onboarding-assets/default/AGENTS.md +3 -0
  195. package/server/src/paths.ts +34 -0
  196. package/server/src/realtime/live-events-ws.ts +274 -0
  197. package/server/src/redaction.ts +59 -0
  198. package/server/src/routes/access.ts +2888 -0
  199. package/server/src/routes/activity.ts +89 -0
  200. package/server/src/routes/agents.ts +2313 -0
  201. package/server/src/routes/approvals.ts +346 -0
  202. package/server/src/routes/assets.ts +341 -0
  203. package/server/src/routes/authz.ts +52 -0
  204. package/server/src/routes/companies.ts +343 -0
  205. package/server/src/routes/company-skills.ts +300 -0
  206. package/server/src/routes/costs.ts +335 -0
  207. package/server/src/routes/dashboard.ts +19 -0
  208. package/server/src/routes/execution-workspaces.ts +182 -0
  209. package/server/src/routes/goals.ts +107 -0
  210. package/server/src/routes/health.ts +94 -0
  211. package/server/src/routes/index.ts +17 -0
  212. package/server/src/routes/instance-settings.ts +95 -0
  213. package/server/src/routes/issues-checkout-wakeup.ts +14 -0
  214. package/server/src/routes/issues.ts +1680 -0
  215. package/server/src/routes/llms.ts +86 -0
  216. package/server/src/routes/org-chart-svg.ts +777 -0
  217. package/server/src/routes/plugin-ui-static.ts +497 -0
  218. package/server/src/routes/plugins.ts +2220 -0
  219. package/server/src/routes/projects.ts +295 -0
  220. package/server/src/routes/routines.ts +300 -0
  221. package/server/src/routes/secrets.ts +166 -0
  222. package/server/src/routes/sidebar-badges.ts +52 -0
  223. package/server/src/secrets/external-stub-providers.ts +32 -0
  224. package/server/src/secrets/local-encrypted-provider.ts +135 -0
  225. package/server/src/secrets/provider-registry.ts +31 -0
  226. package/server/src/secrets/types.ts +23 -0
  227. package/server/src/services/access.ts +381 -0
  228. package/server/src/services/activity-log.ts +95 -0
  229. package/server/src/services/activity.ts +164 -0
  230. package/server/src/services/agent-instructions.ts +735 -0
  231. package/server/src/services/agent-permissions.ts +27 -0
  232. package/server/src/services/agents.ts +694 -0
  233. package/server/src/services/approvals.ts +273 -0
  234. package/server/src/services/assets.ts +23 -0
  235. package/server/src/services/board-auth.ts +355 -0
  236. package/server/src/services/budgets.ts +959 -0
  237. package/server/src/services/companies.ts +313 -0
  238. package/server/src/services/company-export-readme.ts +173 -0
  239. package/server/src/services/company-portability.ts +4263 -0
  240. package/server/src/services/company-skills.ts +2356 -0
  241. package/server/src/services/costs.ts +365 -0
  242. package/server/src/services/cron.ts +373 -0
  243. package/server/src/services/dashboard.ts +110 -0
  244. package/server/src/services/default-agent-instructions.ts +27 -0
  245. package/server/src/services/documents.ts +434 -0
  246. package/server/src/services/execution-workspace-policy.ts +210 -0
  247. package/server/src/services/execution-workspaces.ts +100 -0
  248. package/server/src/services/finance.ts +135 -0
  249. package/server/src/services/goals.ts +81 -0
  250. package/server/src/services/heartbeat-run-summary.ts +35 -0
  251. package/server/src/services/heartbeat.ts +3863 -0
  252. package/server/src/services/hire-hook.ts +114 -0
  253. package/server/src/services/index.ts +32 -0
  254. package/server/src/services/instance-settings.ts +138 -0
  255. package/server/src/services/issue-approvals.ts +175 -0
  256. package/server/src/services/issue-assignment-wakeup.ts +48 -0
  257. package/server/src/services/issue-goal-fallback.ts +56 -0
  258. package/server/src/services/issues.ts +1828 -0
  259. package/server/src/services/live-events.ts +55 -0
  260. package/server/src/services/plugin-capability-validator.ts +450 -0
  261. package/server/src/services/plugin-config-validator.ts +55 -0
  262. package/server/src/services/plugin-dev-watcher.ts +339 -0
  263. package/server/src/services/plugin-event-bus.ts +413 -0
  264. package/server/src/services/plugin-host-service-cleanup.ts +59 -0
  265. package/server/src/services/plugin-host-services.ts +1132 -0
  266. package/server/src/services/plugin-job-coordinator.ts +261 -0
  267. package/server/src/services/plugin-job-scheduler.ts +753 -0
  268. package/server/src/services/plugin-job-store.ts +466 -0
  269. package/server/src/services/plugin-lifecycle.ts +822 -0
  270. package/server/src/services/plugin-loader.ts +1955 -0
  271. package/server/src/services/plugin-log-retention.ts +87 -0
  272. package/server/src/services/plugin-manifest-validator.ts +164 -0
  273. package/server/src/services/plugin-registry.ts +683 -0
  274. package/server/src/services/plugin-runtime-sandbox.ts +222 -0
  275. package/server/src/services/plugin-secrets-handler.ts +355 -0
  276. package/server/src/services/plugin-state-store.ts +238 -0
  277. package/server/src/services/plugin-stream-bus.ts +81 -0
  278. package/server/src/services/plugin-tool-dispatcher.ts +449 -0
  279. package/server/src/services/plugin-tool-registry.ts +450 -0
  280. package/server/src/services/plugin-worker-manager.ts +1343 -0
  281. package/server/src/services/projects.ts +860 -0
  282. package/server/src/services/quota-windows.ts +65 -0
  283. package/server/src/services/routines.ts +1269 -0
  284. package/server/src/services/run-log-store.ts +156 -0
  285. package/server/src/services/secrets.ts +370 -0
  286. package/server/src/services/sidebar-badges.ts +56 -0
  287. package/server/src/services/work-products.ts +124 -0
  288. package/server/src/services/workspace-operation-log-store.ts +156 -0
  289. package/server/src/services/workspace-operations.ts +262 -0
  290. package/server/src/services/workspace-runtime.ts +1565 -0
  291. package/server/src/startup-banner.ts +176 -0
  292. package/server/src/storage/index.ts +35 -0
  293. package/server/src/storage/local-disk-provider.ts +89 -0
  294. package/server/src/storage/provider-registry.ts +18 -0
  295. package/server/src/storage/s3-provider.ts +153 -0
  296. package/server/src/storage/service.ts +131 -0
  297. package/server/src/storage/types.ts +63 -0
  298. package/server/src/ui-branding.ts +217 -0
  299. package/server/src/version.ts +10 -0
  300. package/server/src/worktree-config.ts +468 -0
  301. package/server/tsconfig.json +9 -0
  302. package/server/vitest.config.ts +7 -0
  303. package/skills/paperclip/SKILL.md +365 -0
  304. package/skills/paperclip/references/api-reference.md +647 -0
  305. package/skills/paperclip/references/company-skills.md +193 -0
  306. package/skills/paperclip-create-agent/SKILL.md +142 -0
  307. package/skills/paperclip-create-agent/references/api-reference.md +105 -0
  308. package/skills/paperclip-create-plugin/SKILL.md +102 -0
  309. package/skills/para-memory-files/SKILL.md +104 -0
  310. package/skills/para-memory-files/references/schemas.md +35 -0
  311. package/tests/e2e/onboarding.spec.ts +142 -0
  312. package/tests/e2e/playwright.config.ts +35 -0
  313. package/tests/release-smoke/docker-auth-onboarding.spec.ts +141 -0
  314. package/tests/release-smoke/playwright.config.ts +28 -0
  315. package/tsconfig.base.json +18 -0
  316. package/tsconfig.json +18 -0
  317. package/ui/README.md +12 -0
  318. package/ui/components.json +21 -0
  319. package/ui/index.html +47 -0
  320. package/ui/package.json +73 -0
  321. package/ui/public/android-chrome-192x192.png +0 -0
  322. package/ui/public/android-chrome-512x512.png +0 -0
  323. package/ui/public/apple-touch-icon.png +0 -0
  324. package/ui/public/brands/opencode-logo-dark-square.svg +18 -0
  325. package/ui/public/brands/opencode-logo-light-square.svg +18 -0
  326. package/ui/public/favicon-16x16.png +0 -0
  327. package/ui/public/favicon-32x32.png +0 -0
  328. package/ui/public/favicon.ico +0 -0
  329. package/ui/public/favicon.svg +9 -0
  330. package/ui/public/site.webmanifest +30 -0
  331. package/ui/public/sprites/1-D-1.png +0 -0
  332. package/ui/public/sprites/1-D-2.png +0 -0
  333. package/ui/public/sprites/1-D-3.png +0 -0
  334. package/ui/public/sprites/1-L-1.png +0 -0
  335. package/ui/public/sprites/1-R-1.png +0 -0
  336. package/ui/public/sprites/10-D-1.png +0 -0
  337. package/ui/public/sprites/10-D-2.png +0 -0
  338. package/ui/public/sprites/10-D-3.png +0 -0
  339. package/ui/public/sprites/10-L-1.png +0 -0
  340. package/ui/public/sprites/10-R-1.png +0 -0
  341. package/ui/public/sprites/11-D-1.png +0 -0
  342. package/ui/public/sprites/11-D-2.png +0 -0
  343. package/ui/public/sprites/11-D-3.png +0 -0
  344. package/ui/public/sprites/11-L-1.png +0 -0
  345. package/ui/public/sprites/11-R-1.png +0 -0
  346. package/ui/public/sprites/12-D-1.png +0 -0
  347. package/ui/public/sprites/12-D-2.png +0 -0
  348. package/ui/public/sprites/12-D-3.png +0 -0
  349. package/ui/public/sprites/12-L-1.png +0 -0
  350. package/ui/public/sprites/12-R-1.png +0 -0
  351. package/ui/public/sprites/13-D-1.png +0 -0
  352. package/ui/public/sprites/13-D-2.png +0 -0
  353. package/ui/public/sprites/13-D-3.png +0 -0
  354. package/ui/public/sprites/13-L-1.png +0 -0
  355. package/ui/public/sprites/13-R-1.png +0 -0
  356. package/ui/public/sprites/14-D-1.png +0 -0
  357. package/ui/public/sprites/14-D-2.png +0 -0
  358. package/ui/public/sprites/14-D-3.png +0 -0
  359. package/ui/public/sprites/14-L-1.png +0 -0
  360. package/ui/public/sprites/14-R-1.png +0 -0
  361. package/ui/public/sprites/2-D-1.png +0 -0
  362. package/ui/public/sprites/2-D-2.png +0 -0
  363. package/ui/public/sprites/2-D-3.png +0 -0
  364. package/ui/public/sprites/2-L-1.png +0 -0
  365. package/ui/public/sprites/2-R-1.png +0 -0
  366. package/ui/public/sprites/3-D-1.png +0 -0
  367. package/ui/public/sprites/3-D-2.png +0 -0
  368. package/ui/public/sprites/3-D-3.png +0 -0
  369. package/ui/public/sprites/3-L-1.png +0 -0
  370. package/ui/public/sprites/3-R-1.png +0 -0
  371. package/ui/public/sprites/4-D-1.png +0 -0
  372. package/ui/public/sprites/4-D-2.png +0 -0
  373. package/ui/public/sprites/4-D-3.png +0 -0
  374. package/ui/public/sprites/4-L-1.png +0 -0
  375. package/ui/public/sprites/4-R-1.png +0 -0
  376. package/ui/public/sprites/5-D-1.png +0 -0
  377. package/ui/public/sprites/5-D-2.png +0 -0
  378. package/ui/public/sprites/5-D-3.png +0 -0
  379. package/ui/public/sprites/5-L-1.png +0 -0
  380. package/ui/public/sprites/5-R-1.png +0 -0
  381. package/ui/public/sprites/6-D-1.png +0 -0
  382. package/ui/public/sprites/6-D-2.png +0 -0
  383. package/ui/public/sprites/6-D-3.png +0 -0
  384. package/ui/public/sprites/6-L-1.png +0 -0
  385. package/ui/public/sprites/6-R-1.png +0 -0
  386. package/ui/public/sprites/7-D-1.png +0 -0
  387. package/ui/public/sprites/7-D-2.png +0 -0
  388. package/ui/public/sprites/7-D-3.png +0 -0
  389. package/ui/public/sprites/7-L-1.png +0 -0
  390. package/ui/public/sprites/7-R-1.png +0 -0
  391. package/ui/public/sprites/8-D-1.png +0 -0
  392. package/ui/public/sprites/8-D-2.png +0 -0
  393. package/ui/public/sprites/8-D-3.png +0 -0
  394. package/ui/public/sprites/8-L-1.png +0 -0
  395. package/ui/public/sprites/8-R-1.png +0 -0
  396. package/ui/public/sprites/9-D-1.png +0 -0
  397. package/ui/public/sprites/9-D-2.png +0 -0
  398. package/ui/public/sprites/9-D-3.png +0 -0
  399. package/ui/public/sprites/9-L-1.png +0 -0
  400. package/ui/public/sprites/9-R-1.png +0 -0
  401. package/ui/public/sprites/ceo-lobster.png +0 -0
  402. package/ui/public/sw.js +42 -0
  403. package/ui/public/worktree-favicon-16x16.png +0 -0
  404. package/ui/public/worktree-favicon-32x32.png +0 -0
  405. package/ui/public/worktree-favicon.ico +0 -0
  406. package/ui/public/worktree-favicon.svg +9 -0
  407. package/ui/src/App.tsx +354 -0
  408. package/ui/src/adapters/claude-local/config-fields.tsx +138 -0
  409. package/ui/src/adapters/claude-local/index.ts +13 -0
  410. package/ui/src/adapters/codex-local/config-fields.tsx +104 -0
  411. package/ui/src/adapters/codex-local/index.ts +13 -0
  412. package/ui/src/adapters/cursor/config-fields.tsx +49 -0
  413. package/ui/src/adapters/cursor/index.ts +13 -0
  414. package/ui/src/adapters/gemini-local/config-fields.tsx +51 -0
  415. package/ui/src/adapters/gemini-local/index.ts +13 -0
  416. package/ui/src/adapters/http/build-config.ts +9 -0
  417. package/ui/src/adapters/http/config-fields.tsx +38 -0
  418. package/ui/src/adapters/http/index.ts +12 -0
  419. package/ui/src/adapters/http/parse-stdout.ts +5 -0
  420. package/ui/src/adapters/index.ts +9 -0
  421. package/ui/src/adapters/local-workspace-runtime-fields.tsx +5 -0
  422. package/ui/src/adapters/openclaw-gateway/config-fields.tsx +237 -0
  423. package/ui/src/adapters/openclaw-gateway/index.ts +13 -0
  424. package/ui/src/adapters/opencode-local/config-fields.tsx +72 -0
  425. package/ui/src/adapters/opencode-local/index.ts +13 -0
  426. package/ui/src/adapters/pi-local/config-fields.tsx +49 -0
  427. package/ui/src/adapters/pi-local/index.ts +13 -0
  428. package/ui/src/adapters/process/build-config.ts +18 -0
  429. package/ui/src/adapters/process/config-fields.tsx +77 -0
  430. package/ui/src/adapters/process/index.ts +12 -0
  431. package/ui/src/adapters/process/parse-stdout.ts +5 -0
  432. package/ui/src/adapters/registry.ts +34 -0
  433. package/ui/src/adapters/runtime-json-fields.tsx +122 -0
  434. package/ui/src/adapters/transcript.test.ts +30 -0
  435. package/ui/src/adapters/transcript.ts +62 -0
  436. package/ui/src/adapters/types.ts +34 -0
  437. package/ui/src/api/access.ts +160 -0
  438. package/ui/src/api/activity.ts +37 -0
  439. package/ui/src/api/agents.ts +194 -0
  440. package/ui/src/api/approvals.ts +25 -0
  441. package/ui/src/api/assets.ts +30 -0
  442. package/ui/src/api/auth.ts +74 -0
  443. package/ui/src/api/budgets.ts +21 -0
  444. package/ui/src/api/client.ts +50 -0
  445. package/ui/src/api/companies.ts +59 -0
  446. package/ui/src/api/companySkills.ts +55 -0
  447. package/ui/src/api/costs.ts +60 -0
  448. package/ui/src/api/dashboard.ts +7 -0
  449. package/ui/src/api/execution-workspaces.ts +27 -0
  450. package/ui/src/api/goals.ts +12 -0
  451. package/ui/src/api/health.ts +41 -0
  452. package/ui/src/api/heartbeats.ts +62 -0
  453. package/ui/src/api/index.ts +18 -0
  454. package/ui/src/api/instanceSettings.ts +19 -0
  455. package/ui/src/api/issues.ts +115 -0
  456. package/ui/src/api/plugins.ts +424 -0
  457. package/ui/src/api/projects.ts +34 -0
  458. package/ui/src/api/routines.ts +59 -0
  459. package/ui/src/api/secrets.ts +26 -0
  460. package/ui/src/api/sidebarBadges.ts +7 -0
  461. package/ui/src/components/AccountingModelCard.tsx +69 -0
  462. package/ui/src/components/ActiveAgentsPanel.tsx +157 -0
  463. package/ui/src/components/ActivityCharts.tsx +264 -0
  464. package/ui/src/components/ActivityRow.tsx +147 -0
  465. package/ui/src/components/AgentActionButtons.tsx +51 -0
  466. package/ui/src/components/AgentConfigForm.tsx +1468 -0
  467. package/ui/src/components/AgentIconPicker.tsx +81 -0
  468. package/ui/src/components/AgentProperties.tsx +107 -0
  469. package/ui/src/components/ApprovalCard.tsx +107 -0
  470. package/ui/src/components/ApprovalPayload.tsx +134 -0
  471. package/ui/src/components/AsciiArtAnimation.tsx +355 -0
  472. package/ui/src/components/BillerSpendCard.tsx +146 -0
  473. package/ui/src/components/BreadcrumbBar.tsx +113 -0
  474. package/ui/src/components/BudgetIncidentCard.tsx +101 -0
  475. package/ui/src/components/BudgetPolicyCard.tsx +220 -0
  476. package/ui/src/components/BudgetSidebarMarker.tsx +13 -0
  477. package/ui/src/components/ClaudeSubscriptionPanel.tsx +141 -0
  478. package/ui/src/components/CodexSubscriptionPanel.tsx +158 -0
  479. package/ui/src/components/CommandPalette.tsx +239 -0
  480. package/ui/src/components/CommentThread.tsx +503 -0
  481. package/ui/src/components/CompanyPatternIcon.tsx +212 -0
  482. package/ui/src/components/CompanyRail.tsx +329 -0
  483. package/ui/src/components/CompanySwitcher.tsx +81 -0
  484. package/ui/src/components/CopyText.tsx +56 -0
  485. package/ui/src/components/DevRestartBanner.tsx +89 -0
  486. package/ui/src/components/EmptyState.tsx +27 -0
  487. package/ui/src/components/EntityRow.tsx +69 -0
  488. package/ui/src/components/FilterBar.tsx +39 -0
  489. package/ui/src/components/FinanceBillerCard.tsx +45 -0
  490. package/ui/src/components/FinanceKindCard.tsx +44 -0
  491. package/ui/src/components/FinanceTimelineCard.tsx +72 -0
  492. package/ui/src/components/GoalProperties.tsx +165 -0
  493. package/ui/src/components/GoalTree.tsx +118 -0
  494. package/ui/src/components/Identity.tsx +39 -0
  495. package/ui/src/components/InlineEditor.tsx +248 -0
  496. package/ui/src/components/InlineEntitySelector.tsx +206 -0
  497. package/ui/src/components/InstanceSidebar.tsx +53 -0
  498. package/ui/src/components/IssueDocumentsSection.tsx +892 -0
  499. package/ui/src/components/IssueProperties.tsx +621 -0
  500. package/ui/src/components/IssueRow.tsx +149 -0
  501. package/ui/src/components/IssueWorkspaceCard.tsx +404 -0
  502. package/ui/src/components/IssuesList.tsx +889 -0
  503. package/ui/src/components/JsonSchemaForm.tsx +1048 -0
  504. package/ui/src/components/KanbanBoard.tsx +275 -0
  505. package/ui/src/components/Layout.tsx +441 -0
  506. package/ui/src/components/LiveRunWidget.tsx +160 -0
  507. package/ui/src/components/MarkdownBody.test.tsx +50 -0
  508. package/ui/src/components/MarkdownBody.tsx +152 -0
  509. package/ui/src/components/MarkdownEditor.tsx +622 -0
  510. package/ui/src/components/MetricCard.tsx +53 -0
  511. package/ui/src/components/MobileBottomNav.tsx +123 -0
  512. package/ui/src/components/NewAgentDialog.tsx +223 -0
  513. package/ui/src/components/NewGoalDialog.tsx +283 -0
  514. package/ui/src/components/NewIssueDialog.tsx +1473 -0
  515. package/ui/src/components/NewProjectDialog.tsx +451 -0
  516. package/ui/src/components/OnboardingWizard.tsx +1392 -0
  517. package/ui/src/components/OpenCodeLogoIcon.tsx +22 -0
  518. package/ui/src/components/PackageFileTree.tsx +318 -0
  519. package/ui/src/components/PageSkeleton.tsx +180 -0
  520. package/ui/src/components/PageTabBar.tsx +45 -0
  521. package/ui/src/components/PathInstructionsModal.tsx +143 -0
  522. package/ui/src/components/PriorityIcon.tsx +77 -0
  523. package/ui/src/components/ProjectProperties.tsx +1127 -0
  524. package/ui/src/components/PropertiesPanel.tsx +29 -0
  525. package/ui/src/components/ProviderQuotaCard.tsx +417 -0
  526. package/ui/src/components/QuotaBar.tsx +65 -0
  527. package/ui/src/components/ReportsToPicker.tsx +127 -0
  528. package/ui/src/components/ScheduleEditor.tsx +344 -0
  529. package/ui/src/components/ScrollToBottom.tsx +79 -0
  530. package/ui/src/components/Sidebar.tsx +130 -0
  531. package/ui/src/components/SidebarAgents.tsx +146 -0
  532. package/ui/src/components/SidebarNavItem.tsx +92 -0
  533. package/ui/src/components/SidebarProjects.tsx +234 -0
  534. package/ui/src/components/SidebarSection.tsx +17 -0
  535. package/ui/src/components/StatusBadge.tsx +15 -0
  536. package/ui/src/components/StatusIcon.tsx +71 -0
  537. package/ui/src/components/SwipeToArchive.tsx +152 -0
  538. package/ui/src/components/ToastViewport.tsx +99 -0
  539. package/ui/src/components/WorktreeBanner.tsx +25 -0
  540. package/ui/src/components/agent-config-defaults.ts +31 -0
  541. package/ui/src/components/agent-config-primitives.tsx +476 -0
  542. package/ui/src/components/transcript/RunTranscriptView.test.tsx +84 -0
  543. package/ui/src/components/transcript/RunTranscriptView.tsx +1015 -0
  544. package/ui/src/components/transcript/useLiveRunTranscripts.ts +297 -0
  545. package/ui/src/components/ui/avatar.tsx +107 -0
  546. package/ui/src/components/ui/badge.tsx +48 -0
  547. package/ui/src/components/ui/breadcrumb.tsx +109 -0
  548. package/ui/src/components/ui/button.tsx +64 -0
  549. package/ui/src/components/ui/card.tsx +92 -0
  550. package/ui/src/components/ui/checkbox.tsx +32 -0
  551. package/ui/src/components/ui/collapsible.tsx +33 -0
  552. package/ui/src/components/ui/command.tsx +194 -0
  553. package/ui/src/components/ui/dialog.tsx +156 -0
  554. package/ui/src/components/ui/dropdown-menu.tsx +257 -0
  555. package/ui/src/components/ui/input.tsx +21 -0
  556. package/ui/src/components/ui/label.tsx +22 -0
  557. package/ui/src/components/ui/popover.tsx +88 -0
  558. package/ui/src/components/ui/scroll-area.tsx +56 -0
  559. package/ui/src/components/ui/select.tsx +188 -0
  560. package/ui/src/components/ui/separator.tsx +28 -0
  561. package/ui/src/components/ui/sheet.tsx +143 -0
  562. package/ui/src/components/ui/skeleton.tsx +13 -0
  563. package/ui/src/components/ui/tabs.tsx +89 -0
  564. package/ui/src/components/ui/textarea.tsx +18 -0
  565. package/ui/src/components/ui/tooltip.tsx +57 -0
  566. package/ui/src/components/visual-office/AgentAvatar.tsx +99 -0
  567. package/ui/src/components/visual-office/OfficeViewExact.tsx +417 -0
  568. package/ui/src/components/visual-office/i18n.ts +52 -0
  569. package/ui/src/components/visual-office/office-view/CliUsagePanel.tsx +240 -0
  570. package/ui/src/components/visual-office/office-view/VirtualPadOverlay.tsx +104 -0
  571. package/ui/src/components/visual-office/office-view/buildScene-break-room.ts +248 -0
  572. package/ui/src/components/visual-office/office-view/buildScene-ceo-hallway.ts +345 -0
  573. package/ui/src/components/visual-office/office-view/buildScene-department-agent.ts +242 -0
  574. package/ui/src/components/visual-office/office-view/buildScene-departments.ts +360 -0
  575. package/ui/src/components/visual-office/office-view/buildScene-final-layers.ts +113 -0
  576. package/ui/src/components/visual-office/office-view/buildScene-types.ts +91 -0
  577. package/ui/src/components/visual-office/office-view/buildScene.ts +232 -0
  578. package/ui/src/components/visual-office/office-view/drawing-core.ts +374 -0
  579. package/ui/src/components/visual-office/office-view/drawing-furniture-a.ts +338 -0
  580. package/ui/src/components/visual-office/office-view/drawing-furniture-b.ts +241 -0
  581. package/ui/src/components/visual-office/office-view/model.ts +301 -0
  582. package/ui/src/components/visual-office/office-view/officeTicker.ts +455 -0
  583. package/ui/src/components/visual-office/office-view/officeTickerRoomAndDelivery.ts +133 -0
  584. package/ui/src/components/visual-office/office-view/themes-locale.ts +460 -0
  585. package/ui/src/components/visual-office/office-view/useCliUsage.ts +37 -0
  586. package/ui/src/components/visual-office/office-view/useOfficeDeliveryEffects.ts +465 -0
  587. package/ui/src/components/visual-office/office-view/useOfficePixiRuntime.ts +282 -0
  588. package/ui/src/components/visual-office/types.ts +123 -0
  589. package/ui/src/context/BreadcrumbContext.tsx +44 -0
  590. package/ui/src/context/CompanyContext.tsx +151 -0
  591. package/ui/src/context/DialogContext.tsx +135 -0
  592. package/ui/src/context/LiveUpdatesProvider.test.ts +119 -0
  593. package/ui/src/context/LiveUpdatesProvider.tsx +760 -0
  594. package/ui/src/context/PanelContext.tsx +73 -0
  595. package/ui/src/context/SidebarContext.tsx +43 -0
  596. package/ui/src/context/ThemeContext.tsx +83 -0
  597. package/ui/src/context/ToastContext.tsx +172 -0
  598. package/ui/src/fixtures/runTranscriptFixtures.ts +226 -0
  599. package/ui/src/hooks/useAgentOrder.ts +105 -0
  600. package/ui/src/hooks/useAutosaveIndicator.ts +72 -0
  601. package/ui/src/hooks/useCompanyPageMemory.test.ts +90 -0
  602. package/ui/src/hooks/useCompanyPageMemory.ts +79 -0
  603. package/ui/src/hooks/useDateRange.ts +120 -0
  604. package/ui/src/hooks/useInboxBadge.ts +132 -0
  605. package/ui/src/hooks/useKeyboardShortcuts.ts +40 -0
  606. package/ui/src/hooks/useProjectOrder.ts +106 -0
  607. package/ui/src/index.css +770 -0
  608. package/ui/src/lib/agent-icons.ts +99 -0
  609. package/ui/src/lib/agent-order.ts +107 -0
  610. package/ui/src/lib/agent-skills-state.test.ts +90 -0
  611. package/ui/src/lib/agent-skills-state.ts +41 -0
  612. package/ui/src/lib/assignees.test.ts +92 -0
  613. package/ui/src/lib/assignees.ts +82 -0
  614. package/ui/src/lib/color-contrast.ts +107 -0
  615. package/ui/src/lib/company-export-selection.test.ts +41 -0
  616. package/ui/src/lib/company-export-selection.ts +57 -0
  617. package/ui/src/lib/company-page-memory.ts +65 -0
  618. package/ui/src/lib/company-portability-sidebar.test.ts +101 -0
  619. package/ui/src/lib/company-portability-sidebar.ts +62 -0
  620. package/ui/src/lib/company-routes.ts +88 -0
  621. package/ui/src/lib/company-selection.test.ts +34 -0
  622. package/ui/src/lib/company-selection.ts +18 -0
  623. package/ui/src/lib/groupBy.ts +11 -0
  624. package/ui/src/lib/inbox.test.ts +404 -0
  625. package/ui/src/lib/inbox.ts +292 -0
  626. package/ui/src/lib/instance-settings.test.ts +26 -0
  627. package/ui/src/lib/instance-settings.ts +25 -0
  628. package/ui/src/lib/issueDetailBreadcrumb.ts +24 -0
  629. package/ui/src/lib/legacy-agent-config.test.ts +40 -0
  630. package/ui/src/lib/legacy-agent-config.ts +17 -0
  631. package/ui/src/lib/mention-aware-link-node.test.ts +50 -0
  632. package/ui/src/lib/mention-aware-link-node.ts +67 -0
  633. package/ui/src/lib/mention-chips.ts +168 -0
  634. package/ui/src/lib/mention-deletion.test.ts +87 -0
  635. package/ui/src/lib/mention-deletion.ts +143 -0
  636. package/ui/src/lib/model-utils.ts +16 -0
  637. package/ui/src/lib/onboarding-goal.test.ts +22 -0
  638. package/ui/src/lib/onboarding-goal.ts +18 -0
  639. package/ui/src/lib/onboarding-launch.test.ts +131 -0
  640. package/ui/src/lib/onboarding-launch.ts +54 -0
  641. package/ui/src/lib/onboarding-route.test.ts +80 -0
  642. package/ui/src/lib/onboarding-route.ts +51 -0
  643. package/ui/src/lib/portable-files.ts +42 -0
  644. package/ui/src/lib/project-order.ts +71 -0
  645. package/ui/src/lib/queryKeys.ts +140 -0
  646. package/ui/src/lib/recent-assignees.ts +36 -0
  647. package/ui/src/lib/router.tsx +76 -0
  648. package/ui/src/lib/routine-trigger-patch.test.ts +72 -0
  649. package/ui/src/lib/routine-trigger-patch.ts +31 -0
  650. package/ui/src/lib/status-colors.ts +108 -0
  651. package/ui/src/lib/timeAgo.ts +31 -0
  652. package/ui/src/lib/utils.ts +168 -0
  653. package/ui/src/lib/worktree-branding.ts +65 -0
  654. package/ui/src/lib/zip.test.ts +289 -0
  655. package/ui/src/lib/zip.ts +284 -0
  656. package/ui/src/main.tsx +67 -0
  657. package/ui/src/pages/Activity.tsx +141 -0
  658. package/ui/src/pages/AgentDetail.tsx +4053 -0
  659. package/ui/src/pages/Agents.tsx +415 -0
  660. package/ui/src/pages/ApprovalDetail.tsx +369 -0
  661. package/ui/src/pages/Approvals.tsx +132 -0
  662. package/ui/src/pages/Auth.tsx +180 -0
  663. package/ui/src/pages/BoardClaim.tsx +125 -0
  664. package/ui/src/pages/CliAuth.tsx +184 -0
  665. package/ui/src/pages/Companies.tsx +297 -0
  666. package/ui/src/pages/CompanyExport.tsx +1019 -0
  667. package/ui/src/pages/CompanyImport.tsx +1355 -0
  668. package/ui/src/pages/CompanySettings.tsx +661 -0
  669. package/ui/src/pages/CompanySkills.tsx +1171 -0
  670. package/ui/src/pages/Costs.tsx +1103 -0
  671. package/ui/src/pages/Dashboard.tsx +388 -0
  672. package/ui/src/pages/DesignGuide.tsx +1330 -0
  673. package/ui/src/pages/ExecutionWorkspaceDetail.tsx +82 -0
  674. package/ui/src/pages/GoalDetail.tsx +197 -0
  675. package/ui/src/pages/Goals.tsx +63 -0
  676. package/ui/src/pages/Inbox.tsx +1291 -0
  677. package/ui/src/pages/InstanceExperimentalSettings.tsx +139 -0
  678. package/ui/src/pages/InstanceGeneralSettings.tsx +104 -0
  679. package/ui/src/pages/InstanceSettings.tsx +284 -0
  680. package/ui/src/pages/InviteLanding.tsx +320 -0
  681. package/ui/src/pages/IssueDetail.tsx +1201 -0
  682. package/ui/src/pages/Issues.tsx +116 -0
  683. package/ui/src/pages/MyIssues.tsx +72 -0
  684. package/ui/src/pages/NewAgent.tsx +353 -0
  685. package/ui/src/pages/NotFound.tsx +66 -0
  686. package/ui/src/pages/Org.tsx +132 -0
  687. package/ui/src/pages/OrgChart.tsx +447 -0
  688. package/ui/src/pages/PluginManager.tsx +510 -0
  689. package/ui/src/pages/PluginPage.tsx +156 -0
  690. package/ui/src/pages/PluginSettings.tsx +836 -0
  691. package/ui/src/pages/ProjectDetail.tsx +633 -0
  692. package/ui/src/pages/Projects.tsx +87 -0
  693. package/ui/src/pages/RoutineDetail.tsx +1022 -0
  694. package/ui/src/pages/Routines.tsx +661 -0
  695. package/ui/src/pages/RunTranscriptUxLab.tsx +334 -0
  696. package/ui/src/pages/VisualOffice.tsx +243 -0
  697. package/ui/src/plugins/bridge-init.ts +69 -0
  698. package/ui/src/plugins/bridge.ts +476 -0
  699. package/ui/src/plugins/launchers.tsx +834 -0
  700. package/ui/src/plugins/slots.tsx +855 -0
  701. package/ui/tsconfig.json +21 -0
  702. package/ui/vite.config.ts +23 -0
  703. package/ui/vitest.config.ts +14 -0
  704. package/vitest.config.ts +11 -0
@@ -0,0 +1,1291 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { Link, useLocation, useNavigate } from "@/lib/router";
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { approvalsApi } from "../api/approvals";
5
+ import { accessApi } from "../api/access";
6
+ import { ApiError } from "../api/client";
7
+ import { dashboardApi } from "../api/dashboard";
8
+ import { issuesApi } from "../api/issues";
9
+ import { agentsApi } from "../api/agents";
10
+ import { heartbeatsApi } from "../api/heartbeats";
11
+ import { useCompany } from "../context/CompanyContext";
12
+ import { useBreadcrumbs } from "../context/BreadcrumbContext";
13
+ import { queryKeys } from "../lib/queryKeys";
14
+ import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
15
+ import { EmptyState } from "../components/EmptyState";
16
+ import { PageSkeleton } from "../components/PageSkeleton";
17
+ import { IssueRow } from "../components/IssueRow";
18
+ import { SwipeToArchive } from "../components/SwipeToArchive";
19
+
20
+ import { StatusIcon } from "../components/StatusIcon";
21
+ import { cn } from "../lib/utils";
22
+ import { StatusBadge } from "../components/StatusBadge";
23
+ import { approvalLabel, defaultTypeIcon, typeIcon } from "../components/ApprovalPayload";
24
+ import { timeAgo } from "../lib/timeAgo";
25
+ import { Button } from "@/components/ui/button";
26
+ import { Separator } from "@/components/ui/separator";
27
+ import { Tabs } from "@/components/ui/tabs";
28
+ import {
29
+ Select,
30
+ SelectContent,
31
+ SelectItem,
32
+ SelectTrigger,
33
+ SelectValue,
34
+ } from "@/components/ui/select";
35
+ import {
36
+ Inbox as InboxIcon,
37
+ AlertTriangle,
38
+ XCircle,
39
+ X,
40
+ RotateCcw,
41
+ UserPlus,
42
+ } from "lucide-react";
43
+ import { PageTabBar } from "../components/PageTabBar";
44
+ import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@corporateai/shared";
45
+ import {
46
+ ACTIONABLE_APPROVAL_STATUSES,
47
+ getApprovalsForTab,
48
+ getInboxWorkItems,
49
+ getLatestFailedRunsByAgent,
50
+ getRecentTouchedIssues,
51
+ InboxApprovalFilter,
52
+ saveLastInboxTab,
53
+ shouldShowInboxSection,
54
+ type InboxTab,
55
+ } from "../lib/inbox";
56
+ import { useDismissedInboxItems, useReadInboxItems } from "../hooks/useInboxBadge";
57
+
58
+ type InboxCategoryFilter =
59
+ | "everything"
60
+ | "issues_i_touched"
61
+ | "join_requests"
62
+ | "approvals"
63
+ | "failed_runs"
64
+ | "alerts";
65
+ type SectionKey =
66
+ | "work_items"
67
+ | "alerts";
68
+
69
+ const INBOX_ISSUE_STATUSES = "backlog,todo,in_progress,in_review,blocked,done";
70
+
71
+ function firstNonEmptyLine(value: string | null | undefined): string | null {
72
+ if (!value) return null;
73
+ const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
74
+ return line ?? null;
75
+ }
76
+
77
+ function runFailureMessage(run: HeartbeatRun): string {
78
+ return firstNonEmptyLine(run.error) ?? firstNonEmptyLine(run.stderrExcerpt) ?? "Run exited with an error.";
79
+ }
80
+
81
+ function approvalStatusLabel(status: Approval["status"]): string {
82
+ return status.replaceAll("_", " ");
83
+ }
84
+
85
+ function readIssueIdFromRun(run: HeartbeatRun): string | null {
86
+ const context = run.contextSnapshot;
87
+ if (!context) return null;
88
+
89
+ const issueId = context["issueId"];
90
+ if (typeof issueId === "string" && issueId.length > 0) return issueId;
91
+
92
+ const taskId = context["taskId"];
93
+ if (typeof taskId === "string" && taskId.length > 0) return taskId;
94
+
95
+ return null;
96
+ }
97
+
98
+
99
+ type NonIssueUnreadState = "visible" | "fading" | "hidden" | null;
100
+
101
+ function FailedRunInboxRow({
102
+ run,
103
+ issueById,
104
+ agentName: linkedAgentName,
105
+ issueLinkState,
106
+ onDismiss,
107
+ onRetry,
108
+ isRetrying,
109
+ unreadState = null,
110
+ onMarkRead,
111
+ onArchive,
112
+ archiveDisabled,
113
+ className,
114
+ }: {
115
+ run: HeartbeatRun;
116
+ issueById: Map<string, Issue>;
117
+ agentName: string | null;
118
+ issueLinkState: unknown;
119
+ onDismiss: () => void;
120
+ onRetry: () => void;
121
+ isRetrying: boolean;
122
+ unreadState?: NonIssueUnreadState;
123
+ onMarkRead?: () => void;
124
+ onArchive?: () => void;
125
+ archiveDisabled?: boolean;
126
+ className?: string;
127
+ }) {
128
+ const issueId = readIssueIdFromRun(run);
129
+ const issue = issueId ? issueById.get(issueId) ?? null : null;
130
+ const displayError = runFailureMessage(run);
131
+ const showUnreadSlot = unreadState !== null;
132
+ const showUnreadDot = unreadState === "visible" || unreadState === "fading";
133
+
134
+ return (
135
+ <div className={cn(
136
+ "group border-b border-border px-2 py-2.5 last:border-b-0 sm:px-1 sm:pr-3 sm:py-2",
137
+ className,
138
+ )}>
139
+ <div className="flex items-start gap-2 sm:items-center">
140
+ {showUnreadSlot ? (
141
+ <span className="hidden sm:inline-flex h-4 w-4 shrink-0 items-center justify-center self-center">
142
+ {showUnreadDot ? (
143
+ <button
144
+ type="button"
145
+ onClick={onMarkRead}
146
+ className="inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors hover:bg-blue-500/20"
147
+ aria-label="Mark as read"
148
+ >
149
+ <span className={cn(
150
+ "block h-2 w-2 rounded-full bg-blue-600 transition-opacity duration-300 dark:bg-blue-400",
151
+ unreadState === "fading" ? "opacity-0" : "opacity-100",
152
+ )} />
153
+ </button>
154
+ ) : onArchive ? (
155
+ <button
156
+ type="button"
157
+ onClick={onArchive}
158
+ disabled={archiveDisabled}
159
+ className="inline-flex h-4 w-4 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 disabled:pointer-events-none disabled:opacity-30"
160
+ aria-label="Dismiss from inbox"
161
+ >
162
+ <X className="h-3.5 w-3.5" />
163
+ </button>
164
+ ) : (
165
+ <span className="inline-flex h-4 w-4" aria-hidden="true" />
166
+ )}
167
+ </span>
168
+ ) : null}
169
+ <Link
170
+ to={`/agents/${run.agentId}/runs/${run.id}`}
171
+ className="flex min-w-0 flex-1 items-start gap-2 no-underline text-inherit transition-colors hover:bg-accent/50"
172
+ >
173
+ {!showUnreadSlot && <span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />}
174
+ <span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
175
+ <span className="mt-0.5 shrink-0 rounded-md bg-red-500/20 p-1.5 sm:mt-0">
176
+ <XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
177
+ </span>
178
+ <span className="min-w-0 flex-1">
179
+ <span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
180
+ {issue ? (
181
+ <>
182
+ <span className="font-mono text-muted-foreground mr-1.5">
183
+ {issue.identifier ?? issue.id.slice(0, 8)}
184
+ </span>
185
+ {issue.title}
186
+ </>
187
+ ) : (
188
+ <>Failed run{linkedAgentName ? ` — ${linkedAgentName}` : ""}</>
189
+ )}
190
+ </span>
191
+ <span className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
192
+ <StatusBadge status={run.status} />
193
+ {linkedAgentName && issue ? <span>{linkedAgentName}</span> : null}
194
+ <span className="truncate max-w-[300px]">{displayError}</span>
195
+ <span>{timeAgo(run.createdAt)}</span>
196
+ </span>
197
+ </span>
198
+ </Link>
199
+ <div className="hidden shrink-0 items-center gap-2 sm:flex">
200
+ <Button
201
+ type="button"
202
+ variant="outline"
203
+ size="sm"
204
+ className="h-8 shrink-0 px-2.5"
205
+ onClick={onRetry}
206
+ disabled={isRetrying}
207
+ >
208
+ <RotateCcw className="mr-1.5 h-3.5 w-3.5" />
209
+ {isRetrying ? "Retrying…" : "Retry"}
210
+ </Button>
211
+ {!showUnreadSlot && (
212
+ <button
213
+ type="button"
214
+ onClick={onDismiss}
215
+ className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100"
216
+ aria-label="Dismiss"
217
+ >
218
+ <X className="h-4 w-4" />
219
+ </button>
220
+ )}
221
+ </div>
222
+ </div>
223
+ <div className="mt-3 flex gap-2 sm:hidden">
224
+ <Button
225
+ type="button"
226
+ variant="outline"
227
+ size="sm"
228
+ className="h-8 shrink-0 px-2.5"
229
+ onClick={onRetry}
230
+ disabled={isRetrying}
231
+ >
232
+ <RotateCcw className="mr-1.5 h-3.5 w-3.5" />
233
+ {isRetrying ? "Retrying…" : "Retry"}
234
+ </Button>
235
+ {!showUnreadSlot && (
236
+ <button
237
+ type="button"
238
+ onClick={onDismiss}
239
+ className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
240
+ aria-label="Dismiss"
241
+ >
242
+ <X className="h-4 w-4" />
243
+ </button>
244
+ )}
245
+ </div>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ function ApprovalInboxRow({
251
+ approval,
252
+ requesterName,
253
+ onApprove,
254
+ onReject,
255
+ isPending,
256
+ unreadState = null,
257
+ onMarkRead,
258
+ onArchive,
259
+ archiveDisabled,
260
+ className,
261
+ }: {
262
+ approval: Approval;
263
+ requesterName: string | null;
264
+ onApprove: () => void;
265
+ onReject: () => void;
266
+ isPending: boolean;
267
+ unreadState?: NonIssueUnreadState;
268
+ onMarkRead?: () => void;
269
+ onArchive?: () => void;
270
+ archiveDisabled?: boolean;
271
+ className?: string;
272
+ }) {
273
+ const Icon = typeIcon[approval.type] ?? defaultTypeIcon;
274
+ const label = approvalLabel(approval.type, approval.payload as Record<string, unknown> | null);
275
+ const showResolutionButtons =
276
+ approval.type !== "budget_override_required" &&
277
+ ACTIONABLE_APPROVAL_STATUSES.has(approval.status);
278
+ const showUnreadSlot = unreadState !== null;
279
+ const showUnreadDot = unreadState === "visible" || unreadState === "fading";
280
+
281
+ return (
282
+ <div className={cn(
283
+ "group border-b border-border px-2 py-2.5 last:border-b-0 sm:px-1 sm:pr-3 sm:py-2",
284
+ className,
285
+ )}>
286
+ <div className="flex items-start gap-2 sm:items-center">
287
+ {showUnreadSlot ? (
288
+ <span className="hidden sm:inline-flex h-4 w-4 shrink-0 items-center justify-center self-center">
289
+ {showUnreadDot ? (
290
+ <button
291
+ type="button"
292
+ onClick={onMarkRead}
293
+ className="inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors hover:bg-blue-500/20"
294
+ aria-label="Mark as read"
295
+ >
296
+ <span className={cn(
297
+ "block h-2 w-2 rounded-full bg-blue-600 transition-opacity duration-300 dark:bg-blue-400",
298
+ unreadState === "fading" ? "opacity-0" : "opacity-100",
299
+ )} />
300
+ </button>
301
+ ) : onArchive ? (
302
+ <button
303
+ type="button"
304
+ onClick={onArchive}
305
+ disabled={archiveDisabled}
306
+ className="inline-flex h-4 w-4 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 disabled:pointer-events-none disabled:opacity-30"
307
+ aria-label="Dismiss from inbox"
308
+ >
309
+ <X className="h-3.5 w-3.5" />
310
+ </button>
311
+ ) : (
312
+ <span className="inline-flex h-4 w-4" aria-hidden="true" />
313
+ )}
314
+ </span>
315
+ ) : null}
316
+ <Link
317
+ to={`/approvals/${approval.id}`}
318
+ className="flex min-w-0 flex-1 items-start gap-2 no-underline text-inherit transition-colors hover:bg-accent/50"
319
+ >
320
+ {!showUnreadSlot && <span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />}
321
+ <span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
322
+ <span className="mt-0.5 shrink-0 rounded-md bg-muted p-1.5 sm:mt-0">
323
+ <Icon className="h-4 w-4 text-muted-foreground" />
324
+ </span>
325
+ <span className="min-w-0 flex-1">
326
+ <span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
327
+ {label}
328
+ </span>
329
+ <span className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
330
+ <span className="capitalize">{approvalStatusLabel(approval.status)}</span>
331
+ {requesterName ? <span>requested by {requesterName}</span> : null}
332
+ <span>updated {timeAgo(approval.updatedAt)}</span>
333
+ </span>
334
+ </span>
335
+ </Link>
336
+ {showResolutionButtons ? (
337
+ <div className="hidden shrink-0 items-center gap-2 sm:flex">
338
+ <Button
339
+ size="sm"
340
+ className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
341
+ onClick={onApprove}
342
+ disabled={isPending}
343
+ >
344
+ Approve
345
+ </Button>
346
+ <Button
347
+ variant="destructive"
348
+ size="sm"
349
+ className="h-8 px-3"
350
+ onClick={onReject}
351
+ disabled={isPending}
352
+ >
353
+ Reject
354
+ </Button>
355
+ </div>
356
+ ) : null}
357
+ </div>
358
+ {showResolutionButtons ? (
359
+ <div className="mt-3 flex gap-2 sm:hidden">
360
+ <Button
361
+ size="sm"
362
+ className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
363
+ onClick={onApprove}
364
+ disabled={isPending}
365
+ >
366
+ Approve
367
+ </Button>
368
+ <Button
369
+ variant="destructive"
370
+ size="sm"
371
+ className="h-8 px-3"
372
+ onClick={onReject}
373
+ disabled={isPending}
374
+ >
375
+ Reject
376
+ </Button>
377
+ </div>
378
+ ) : null}
379
+ </div>
380
+ );
381
+ }
382
+
383
+ function JoinRequestInboxRow({
384
+ joinRequest,
385
+ onApprove,
386
+ onReject,
387
+ isPending,
388
+ unreadState = null,
389
+ onMarkRead,
390
+ onArchive,
391
+ archiveDisabled,
392
+ className,
393
+ }: {
394
+ joinRequest: JoinRequest;
395
+ onApprove: () => void;
396
+ onReject: () => void;
397
+ isPending: boolean;
398
+ unreadState?: NonIssueUnreadState;
399
+ onMarkRead?: () => void;
400
+ onArchive?: () => void;
401
+ archiveDisabled?: boolean;
402
+ className?: string;
403
+ }) {
404
+ const label =
405
+ joinRequest.requestType === "human"
406
+ ? "Human join request"
407
+ : `Agent join request${joinRequest.agentName ? `: ${joinRequest.agentName}` : ""}`;
408
+ const showUnreadSlot = unreadState !== null;
409
+ const showUnreadDot = unreadState === "visible" || unreadState === "fading";
410
+
411
+ return (
412
+ <div className={cn(
413
+ "group border-b border-border px-2 py-2.5 last:border-b-0 sm:px-1 sm:pr-3 sm:py-2",
414
+ className,
415
+ )}>
416
+ <div className="flex items-start gap-2 sm:items-center">
417
+ {showUnreadSlot ? (
418
+ <span className="hidden sm:inline-flex h-4 w-4 shrink-0 items-center justify-center self-center">
419
+ {showUnreadDot ? (
420
+ <button
421
+ type="button"
422
+ onClick={onMarkRead}
423
+ className="inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors hover:bg-blue-500/20"
424
+ aria-label="Mark as read"
425
+ >
426
+ <span className={cn(
427
+ "block h-2 w-2 rounded-full bg-blue-600 transition-opacity duration-300 dark:bg-blue-400",
428
+ unreadState === "fading" ? "opacity-0" : "opacity-100",
429
+ )} />
430
+ </button>
431
+ ) : onArchive ? (
432
+ <button
433
+ type="button"
434
+ onClick={onArchive}
435
+ disabled={archiveDisabled}
436
+ className="inline-flex h-4 w-4 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100 disabled:pointer-events-none disabled:opacity-30"
437
+ aria-label="Dismiss from inbox"
438
+ >
439
+ <X className="h-3.5 w-3.5" />
440
+ </button>
441
+ ) : (
442
+ <span className="inline-flex h-4 w-4" aria-hidden="true" />
443
+ )}
444
+ </span>
445
+ ) : null}
446
+ <div className="flex min-w-0 flex-1 items-start gap-2">
447
+ {!showUnreadSlot && <span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />}
448
+ <span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
449
+ <span className="mt-0.5 shrink-0 rounded-md bg-muted p-1.5 sm:mt-0">
450
+ <UserPlus className="h-4 w-4 text-muted-foreground" />
451
+ </span>
452
+ <span className="min-w-0 flex-1">
453
+ <span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
454
+ {label}
455
+ </span>
456
+ <span className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
457
+ <span>requested {timeAgo(joinRequest.createdAt)} from IP {joinRequest.requestIp}</span>
458
+ {joinRequest.adapterType && <span>adapter: {joinRequest.adapterType}</span>}
459
+ </span>
460
+ </span>
461
+ </div>
462
+ <div className="hidden shrink-0 items-center gap-2 sm:flex">
463
+ <Button
464
+ size="sm"
465
+ className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
466
+ onClick={onApprove}
467
+ disabled={isPending}
468
+ >
469
+ Approve
470
+ </Button>
471
+ <Button
472
+ variant="destructive"
473
+ size="sm"
474
+ className="h-8 px-3"
475
+ onClick={onReject}
476
+ disabled={isPending}
477
+ >
478
+ Reject
479
+ </Button>
480
+ </div>
481
+ </div>
482
+ <div className="mt-3 flex gap-2 sm:hidden">
483
+ <Button
484
+ size="sm"
485
+ className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
486
+ onClick={onApprove}
487
+ disabled={isPending}
488
+ >
489
+ Approve
490
+ </Button>
491
+ <Button
492
+ variant="destructive"
493
+ size="sm"
494
+ className="h-8 px-3"
495
+ onClick={onReject}
496
+ disabled={isPending}
497
+ >
498
+ Reject
499
+ </Button>
500
+ </div>
501
+ </div>
502
+ );
503
+ }
504
+
505
+ export function Inbox() {
506
+ const { selectedCompanyId } = useCompany();
507
+ const { setBreadcrumbs } = useBreadcrumbs();
508
+ const navigate = useNavigate();
509
+ const location = useLocation();
510
+ const queryClient = useQueryClient();
511
+ const [actionError, setActionError] = useState<string | null>(null);
512
+ const [allCategoryFilter, setAllCategoryFilter] = useState<InboxCategoryFilter>("everything");
513
+ const [allApprovalFilter, setAllApprovalFilter] = useState<InboxApprovalFilter>("all");
514
+ const { dismissed, dismiss } = useDismissedInboxItems();
515
+ const { readItems, markRead: markItemRead } = useReadInboxItems();
516
+
517
+ const pathSegment = location.pathname.split("/").pop() ?? "mine";
518
+ const tab: InboxTab =
519
+ pathSegment === "mine" || pathSegment === "recent" || pathSegment === "all" || pathSegment === "unread"
520
+ ? pathSegment
521
+ : "mine";
522
+ const issueLinkState = useMemo(
523
+ () =>
524
+ createIssueDetailLocationState(
525
+ "Inbox",
526
+ `${location.pathname}${location.search}${location.hash}`,
527
+ ),
528
+ [location.pathname, location.search, location.hash],
529
+ );
530
+
531
+ const { data: agents } = useQuery({
532
+ queryKey: queryKeys.agents.list(selectedCompanyId!),
533
+ queryFn: () => agentsApi.list(selectedCompanyId!),
534
+ enabled: !!selectedCompanyId,
535
+ });
536
+
537
+ useEffect(() => {
538
+ setBreadcrumbs([{ label: "Inbox" }]);
539
+ }, [setBreadcrumbs]);
540
+
541
+ useEffect(() => {
542
+ saveLastInboxTab(tab);
543
+ }, [tab]);
544
+
545
+ const {
546
+ data: approvals,
547
+ isLoading: isApprovalsLoading,
548
+ error: approvalsError,
549
+ } = useQuery({
550
+ queryKey: queryKeys.approvals.list(selectedCompanyId!),
551
+ queryFn: () => approvalsApi.list(selectedCompanyId!),
552
+ enabled: !!selectedCompanyId,
553
+ });
554
+
555
+ const {
556
+ data: joinRequests = [],
557
+ isLoading: isJoinRequestsLoading,
558
+ } = useQuery({
559
+ queryKey: queryKeys.access.joinRequests(selectedCompanyId!),
560
+ queryFn: async () => {
561
+ try {
562
+ return await accessApi.listJoinRequests(selectedCompanyId!, "pending_approval");
563
+ } catch (err) {
564
+ if (err instanceof ApiError && (err.status === 403 || err.status === 401)) {
565
+ return [];
566
+ }
567
+ throw err;
568
+ }
569
+ },
570
+ enabled: !!selectedCompanyId,
571
+ retry: false,
572
+ });
573
+
574
+ const { data: dashboard, isLoading: isDashboardLoading } = useQuery({
575
+ queryKey: queryKeys.dashboard(selectedCompanyId!),
576
+ queryFn: () => dashboardApi.summary(selectedCompanyId!),
577
+ enabled: !!selectedCompanyId,
578
+ });
579
+
580
+ const { data: issues, isLoading: isIssuesLoading } = useQuery({
581
+ queryKey: queryKeys.issues.list(selectedCompanyId!),
582
+ queryFn: () => issuesApi.list(selectedCompanyId!),
583
+ enabled: !!selectedCompanyId,
584
+ });
585
+ const {
586
+ data: mineIssuesRaw = [],
587
+ isLoading: isMineIssuesLoading,
588
+ } = useQuery({
589
+ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId!),
590
+ queryFn: () =>
591
+ issuesApi.list(selectedCompanyId!, {
592
+ touchedByUserId: "me",
593
+ inboxArchivedByUserId: "me",
594
+ status: INBOX_ISSUE_STATUSES,
595
+ }),
596
+ enabled: !!selectedCompanyId,
597
+ });
598
+ const {
599
+ data: touchedIssuesRaw = [],
600
+ isLoading: isTouchedIssuesLoading,
601
+ } = useQuery({
602
+ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId!),
603
+ queryFn: () =>
604
+ issuesApi.list(selectedCompanyId!, {
605
+ touchedByUserId: "me",
606
+ status: INBOX_ISSUE_STATUSES,
607
+ }),
608
+ enabled: !!selectedCompanyId,
609
+ });
610
+
611
+ const { data: heartbeatRuns, isLoading: isRunsLoading } = useQuery({
612
+ queryKey: queryKeys.heartbeats(selectedCompanyId!),
613
+ queryFn: () => heartbeatsApi.list(selectedCompanyId!),
614
+ enabled: !!selectedCompanyId,
615
+ });
616
+
617
+ const mineIssues = useMemo(() => getRecentTouchedIssues(mineIssuesRaw), [mineIssuesRaw]);
618
+ const touchedIssues = useMemo(() => getRecentTouchedIssues(touchedIssuesRaw), [touchedIssuesRaw]);
619
+ const unreadTouchedIssues = useMemo(
620
+ () => touchedIssues.filter((issue) => issue.isUnreadForMe),
621
+ [touchedIssues],
622
+ );
623
+ const issuesToRender = useMemo(
624
+ () => {
625
+ if (tab === "mine") return mineIssues;
626
+ if (tab === "unread") return unreadTouchedIssues;
627
+ return touchedIssues;
628
+ },
629
+ [tab, mineIssues, touchedIssues, unreadTouchedIssues],
630
+ );
631
+
632
+ const agentById = useMemo(() => {
633
+ const map = new Map<string, string>();
634
+ for (const agent of agents ?? []) map.set(agent.id, agent.name);
635
+ return map;
636
+ }, [agents]);
637
+
638
+ const issueById = useMemo(() => {
639
+ const map = new Map<string, Issue>();
640
+ for (const issue of issues ?? []) map.set(issue.id, issue);
641
+ return map;
642
+ }, [issues]);
643
+
644
+ const failedRuns = useMemo(
645
+ () => getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter((r) => !dismissed.has(`run:${r.id}`)),
646
+ [heartbeatRuns, dismissed],
647
+ );
648
+ const liveIssueIds = useMemo(() => {
649
+ const ids = new Set<string>();
650
+ for (const run of heartbeatRuns ?? []) {
651
+ if (run.status !== "running" && run.status !== "queued") continue;
652
+ const issueId = readIssueIdFromRun(run);
653
+ if (issueId) ids.add(issueId);
654
+ }
655
+ return ids;
656
+ }, [heartbeatRuns]);
657
+
658
+ const approvalsToRender = useMemo(() => {
659
+ let filtered = getApprovalsForTab(approvals ?? [], tab, allApprovalFilter);
660
+ if (tab === "mine") {
661
+ filtered = filtered.filter((a) => !dismissed.has(`approval:${a.id}`));
662
+ }
663
+ return filtered;
664
+ }, [approvals, tab, allApprovalFilter, dismissed]);
665
+ const showJoinRequestsCategory =
666
+ allCategoryFilter === "everything" || allCategoryFilter === "join_requests";
667
+ const showTouchedCategory =
668
+ allCategoryFilter === "everything" || allCategoryFilter === "issues_i_touched";
669
+ const showApprovalsCategory =
670
+ allCategoryFilter === "everything" || allCategoryFilter === "approvals";
671
+ const showFailedRunsCategory =
672
+ allCategoryFilter === "everything" || allCategoryFilter === "failed_runs";
673
+ const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts";
674
+ const failedRunsForTab = useMemo(() => {
675
+ if (tab === "all" && !showFailedRunsCategory) return [];
676
+ return failedRuns;
677
+ }, [failedRuns, tab, showFailedRunsCategory]);
678
+
679
+ const joinRequestsForTab = useMemo(() => {
680
+ if (tab === "all" && !showJoinRequestsCategory) return [];
681
+ if (tab === "mine") return joinRequests.filter((jr) => !dismissed.has(`join:${jr.id}`));
682
+ return joinRequests;
683
+ }, [joinRequests, tab, showJoinRequestsCategory, dismissed]);
684
+
685
+ const workItemsToRender = useMemo(
686
+ () =>
687
+ getInboxWorkItems({
688
+ issues: tab === "all" && !showTouchedCategory ? [] : issuesToRender,
689
+ approvals: tab === "all" && !showApprovalsCategory ? [] : approvalsToRender,
690
+ failedRuns: failedRunsForTab,
691
+ joinRequests: joinRequestsForTab,
692
+ }),
693
+ [approvalsToRender, issuesToRender, showApprovalsCategory, showTouchedCategory, tab, failedRunsForTab, joinRequestsForTab],
694
+ );
695
+
696
+ const agentName = (id: string | null) => {
697
+ if (!id) return null;
698
+ return agentById.get(id) ?? null;
699
+ };
700
+
701
+ const approveMutation = useMutation({
702
+ mutationFn: (id: string) => approvalsApi.approve(id),
703
+ onSuccess: (_approval, id) => {
704
+ setActionError(null);
705
+ queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
706
+ navigate(`/approvals/${id}?resolved=approved`);
707
+ },
708
+ onError: (err) => {
709
+ setActionError(err instanceof Error ? err.message : "Failed to approve");
710
+ },
711
+ });
712
+
713
+ const rejectMutation = useMutation({
714
+ mutationFn: (id: string) => approvalsApi.reject(id),
715
+ onSuccess: () => {
716
+ setActionError(null);
717
+ queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
718
+ },
719
+ onError: (err) => {
720
+ setActionError(err instanceof Error ? err.message : "Failed to reject");
721
+ },
722
+ });
723
+
724
+ const approveJoinMutation = useMutation({
725
+ mutationFn: (joinRequest: JoinRequest) =>
726
+ accessApi.approveJoinRequest(selectedCompanyId!, joinRequest.id),
727
+ onSuccess: () => {
728
+ setActionError(null);
729
+ queryClient.invalidateQueries({ queryKey: queryKeys.access.joinRequests(selectedCompanyId!) });
730
+ queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) });
731
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId!) });
732
+ queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
733
+ },
734
+ onError: (err) => {
735
+ setActionError(err instanceof Error ? err.message : "Failed to approve join request");
736
+ },
737
+ });
738
+
739
+ const rejectJoinMutation = useMutation({
740
+ mutationFn: (joinRequest: JoinRequest) =>
741
+ accessApi.rejectJoinRequest(selectedCompanyId!, joinRequest.id),
742
+ onSuccess: () => {
743
+ setActionError(null);
744
+ queryClient.invalidateQueries({ queryKey: queryKeys.access.joinRequests(selectedCompanyId!) });
745
+ queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) });
746
+ },
747
+ onError: (err) => {
748
+ setActionError(err instanceof Error ? err.message : "Failed to reject join request");
749
+ },
750
+ });
751
+
752
+ const [retryingRunIds, setRetryingRunIds] = useState<Set<string>>(new Set());
753
+
754
+ const retryRunMutation = useMutation({
755
+ mutationFn: async (run: HeartbeatRun) => {
756
+ const payload: Record<string, unknown> = {};
757
+ const context = run.contextSnapshot as Record<string, unknown> | null;
758
+ if (context) {
759
+ if (typeof context.issueId === "string" && context.issueId) payload.issueId = context.issueId;
760
+ if (typeof context.taskId === "string" && context.taskId) payload.taskId = context.taskId;
761
+ if (typeof context.taskKey === "string" && context.taskKey) payload.taskKey = context.taskKey;
762
+ }
763
+ const result = await agentsApi.wakeup(run.agentId, {
764
+ source: "on_demand",
765
+ triggerDetail: "manual",
766
+ reason: "retry_failed_run",
767
+ payload,
768
+ });
769
+ if (!("id" in result)) {
770
+ throw new Error("Retry was skipped because the agent is not currently invokable.");
771
+ }
772
+ return { newRun: result, originalRun: run };
773
+ },
774
+ onMutate: (run) => {
775
+ setRetryingRunIds((prev) => new Set(prev).add(run.id));
776
+ },
777
+ onSuccess: ({ newRun, originalRun }) => {
778
+ queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId) });
779
+ queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId, originalRun.agentId) });
780
+ navigate(`/agents/${originalRun.agentId}/runs/${newRun.id}`);
781
+ },
782
+ onSettled: (_data, _error, run) => {
783
+ if (!run) return;
784
+ setRetryingRunIds((prev) => {
785
+ const next = new Set(prev);
786
+ next.delete(run.id);
787
+ return next;
788
+ });
789
+ },
790
+ });
791
+
792
+ const [fadingOutIssues, setFadingOutIssues] = useState<Set<string>>(new Set());
793
+ const [archivingIssueIds, setArchivingIssueIds] = useState<Set<string>>(new Set());
794
+ const [fadingNonIssueItems, setFadingNonIssueItems] = useState<Set<string>>(new Set());
795
+ const [archivingNonIssueIds, setArchivingNonIssueIds] = useState<Set<string>>(new Set());
796
+
797
+ const invalidateInboxIssueQueries = () => {
798
+ if (!selectedCompanyId) return;
799
+ queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
800
+ queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
801
+ queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
802
+ queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
803
+ };
804
+
805
+ const archiveIssueMutation = useMutation({
806
+ mutationFn: (id: string) => issuesApi.archiveFromInbox(id),
807
+ onMutate: (id) => {
808
+ setActionError(null);
809
+ setArchivingIssueIds((prev) => new Set(prev).add(id));
810
+ },
811
+ onSuccess: () => {
812
+ invalidateInboxIssueQueries();
813
+ },
814
+ onError: (err, id) => {
815
+ setActionError(err instanceof Error ? err.message : "Failed to archive issue");
816
+ setArchivingIssueIds((prev) => {
817
+ const next = new Set(prev);
818
+ next.delete(id);
819
+ return next;
820
+ });
821
+ },
822
+ onSettled: (_data, error, id) => {
823
+ if (error) return;
824
+ window.setTimeout(() => {
825
+ setArchivingIssueIds((prev) => {
826
+ const next = new Set(prev);
827
+ next.delete(id);
828
+ return next;
829
+ });
830
+ }, 500);
831
+ },
832
+ });
833
+
834
+ const markReadMutation = useMutation({
835
+ mutationFn: (id: string) => issuesApi.markRead(id),
836
+ onMutate: (id) => {
837
+ setFadingOutIssues((prev) => new Set(prev).add(id));
838
+ },
839
+ onSuccess: () => {
840
+ invalidateInboxIssueQueries();
841
+ },
842
+ onSettled: (_data, _error, id) => {
843
+ setTimeout(() => {
844
+ setFadingOutIssues((prev) => {
845
+ const next = new Set(prev);
846
+ next.delete(id);
847
+ return next;
848
+ });
849
+ }, 300);
850
+ },
851
+ });
852
+
853
+ const markAllReadMutation = useMutation({
854
+ mutationFn: async (issueIds: string[]) => {
855
+ await Promise.all(issueIds.map((issueId) => issuesApi.markRead(issueId)));
856
+ },
857
+ onMutate: (issueIds) => {
858
+ setFadingOutIssues((prev) => {
859
+ const next = new Set(prev);
860
+ for (const issueId of issueIds) next.add(issueId);
861
+ return next;
862
+ });
863
+ },
864
+ onSuccess: () => {
865
+ invalidateInboxIssueQueries();
866
+ },
867
+ onSettled: (_data, _error, issueIds) => {
868
+ setTimeout(() => {
869
+ setFadingOutIssues((prev) => {
870
+ const next = new Set(prev);
871
+ for (const issueId of issueIds) next.delete(issueId);
872
+ return next;
873
+ });
874
+ }, 300);
875
+ },
876
+ });
877
+
878
+ const handleMarkNonIssueRead = (key: string) => {
879
+ setFadingNonIssueItems((prev) => new Set(prev).add(key));
880
+ markItemRead(key);
881
+ setTimeout(() => {
882
+ setFadingNonIssueItems((prev) => {
883
+ const next = new Set(prev);
884
+ next.delete(key);
885
+ return next;
886
+ });
887
+ }, 300);
888
+ };
889
+
890
+ const handleArchiveNonIssue = (key: string) => {
891
+ setArchivingNonIssueIds((prev) => new Set(prev).add(key));
892
+ setTimeout(() => {
893
+ dismiss(key);
894
+ setArchivingNonIssueIds((prev) => {
895
+ const next = new Set(prev);
896
+ next.delete(key);
897
+ return next;
898
+ });
899
+ }, 200);
900
+ };
901
+
902
+ const nonIssueUnreadState = (key: string): NonIssueUnreadState => {
903
+ if (tab !== "mine") return null;
904
+ const isRead = readItems.has(key);
905
+ const isFading = fadingNonIssueItems.has(key);
906
+ if (isFading) return "fading";
907
+ if (!isRead) return "visible";
908
+ return "hidden";
909
+ };
910
+
911
+ if (!selectedCompanyId) {
912
+ return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
913
+ }
914
+
915
+ const hasRunFailures = failedRuns.length > 0;
916
+ const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures && !dismissed.has("alert:agent-errors");
917
+ const showBudgetAlert =
918
+ !!dashboard &&
919
+ dashboard.costs.monthBudgetCents > 0 &&
920
+ dashboard.costs.monthUtilizationPercent >= 80 &&
921
+ !dismissed.has("alert:budget");
922
+ const hasAlerts = showAggregateAgentError || showBudgetAlert;
923
+ const showWorkItemsSection = workItemsToRender.length > 0;
924
+ const showAlertsSection = shouldShowInboxSection({
925
+ tab,
926
+ hasItems: hasAlerts,
927
+ showOnMine: hasAlerts,
928
+ showOnRecent: hasAlerts,
929
+ showOnUnread: hasAlerts,
930
+ showOnAll: showAlertsCategory && hasAlerts,
931
+ });
932
+
933
+ const visibleSections = [
934
+ showAlertsSection ? "alerts" : null,
935
+ showWorkItemsSection ? "work_items" : null,
936
+ ].filter((key): key is SectionKey => key !== null);
937
+
938
+ const allLoaded =
939
+ !isJoinRequestsLoading &&
940
+ !isApprovalsLoading &&
941
+ !isDashboardLoading &&
942
+ !isIssuesLoading &&
943
+ !isMineIssuesLoading &&
944
+ !isTouchedIssuesLoading &&
945
+ !isRunsLoading;
946
+
947
+ const showSeparatorBefore = (key: SectionKey) => visibleSections.indexOf(key) > 0;
948
+ const markAllReadIssues = (tab === "mine" ? mineIssues : unreadTouchedIssues)
949
+ .filter((issue) => issue.isUnreadForMe && !fadingOutIssues.has(issue.id) && !archivingIssueIds.has(issue.id));
950
+ const unreadIssueIds = markAllReadIssues
951
+ .map((issue) => issue.id);
952
+ const canMarkAllRead = unreadIssueIds.length > 0;
953
+
954
+ return (
955
+ <div className="space-y-6">
956
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
957
+ <div className="flex flex-wrap items-center gap-2">
958
+ <Tabs value={tab} onValueChange={(value) => navigate(`/inbox/${value}`)}>
959
+ <PageTabBar
960
+ items={[
961
+ {
962
+ value: "mine",
963
+ label: "Mine",
964
+ },
965
+ {
966
+ value: "recent",
967
+ label: "Recent",
968
+ },
969
+ { value: "unread", label: "Unread" },
970
+ { value: "all", label: "All" },
971
+ ]}
972
+ />
973
+ </Tabs>
974
+
975
+ {canMarkAllRead && (
976
+ <Button
977
+ type="button"
978
+ variant="outline"
979
+ size="sm"
980
+ className="h-8 shrink-0"
981
+ onClick={() => markAllReadMutation.mutate(unreadIssueIds)}
982
+ disabled={markAllReadMutation.isPending}
983
+ >
984
+ {markAllReadMutation.isPending ? "Marking…" : "Mark all as read"}
985
+ </Button>
986
+ )}
987
+ </div>
988
+
989
+ {tab === "all" && (
990
+ <div className="flex flex-wrap items-center gap-2 sm:justify-end">
991
+ <Select
992
+ value={allCategoryFilter}
993
+ onValueChange={(value) => setAllCategoryFilter(value as InboxCategoryFilter)}
994
+ >
995
+ <SelectTrigger className="h-8 w-[170px] text-xs">
996
+ <SelectValue placeholder="Category" />
997
+ </SelectTrigger>
998
+ <SelectContent>
999
+ <SelectItem value="everything">All categories</SelectItem>
1000
+ <SelectItem value="issues_i_touched">My recent issues</SelectItem>
1001
+ <SelectItem value="join_requests">Join requests</SelectItem>
1002
+ <SelectItem value="approvals">Approvals</SelectItem>
1003
+ <SelectItem value="failed_runs">Failed runs</SelectItem>
1004
+ <SelectItem value="alerts">Alerts</SelectItem>
1005
+ </SelectContent>
1006
+ </Select>
1007
+
1008
+ {showApprovalsCategory && (
1009
+ <Select
1010
+ value={allApprovalFilter}
1011
+ onValueChange={(value) => setAllApprovalFilter(value as InboxApprovalFilter)}
1012
+ >
1013
+ <SelectTrigger className="h-8 w-[170px] text-xs">
1014
+ <SelectValue placeholder="Approval status" />
1015
+ </SelectTrigger>
1016
+ <SelectContent>
1017
+ <SelectItem value="all">All approval statuses</SelectItem>
1018
+ <SelectItem value="actionable">Needs action</SelectItem>
1019
+ <SelectItem value="resolved">Resolved</SelectItem>
1020
+ </SelectContent>
1021
+ </Select>
1022
+ )}
1023
+ </div>
1024
+ )}
1025
+ </div>
1026
+
1027
+ {approvalsError && <p className="text-sm text-destructive">{approvalsError.message}</p>}
1028
+ {actionError && <p className="text-sm text-destructive">{actionError}</p>}
1029
+
1030
+ {!allLoaded && visibleSections.length === 0 && (
1031
+ <PageSkeleton variant="inbox" />
1032
+ )}
1033
+
1034
+ {allLoaded && visibleSections.length === 0 && (
1035
+ <EmptyState
1036
+ icon={InboxIcon}
1037
+ message={
1038
+ tab === "mine"
1039
+ ? "Inbox zero."
1040
+ : tab === "unread"
1041
+ ? "No new inbox items."
1042
+ : tab === "recent"
1043
+ ? "No recent inbox items."
1044
+ : "No inbox items match these filters."
1045
+ }
1046
+ />
1047
+ )}
1048
+
1049
+ {showWorkItemsSection && (
1050
+ <>
1051
+ {showSeparatorBefore("work_items") && <Separator />}
1052
+ <div>
1053
+ <div className="overflow-hidden rounded-xl border border-border bg-card">
1054
+ {workItemsToRender.map((item) => {
1055
+ const isMineTab = tab === "mine";
1056
+
1057
+ if (item.kind === "approval") {
1058
+ const approvalKey = `approval:${item.approval.id}`;
1059
+ const isArchiving = archivingNonIssueIds.has(approvalKey);
1060
+ const row = (
1061
+ <ApprovalInboxRow
1062
+ key={approvalKey}
1063
+ approval={item.approval}
1064
+ requesterName={agentName(item.approval.requestedByAgentId)}
1065
+ onApprove={() => approveMutation.mutate(item.approval.id)}
1066
+ onReject={() => rejectMutation.mutate(item.approval.id)}
1067
+ isPending={approveMutation.isPending || rejectMutation.isPending}
1068
+ unreadState={nonIssueUnreadState(approvalKey)}
1069
+ onMarkRead={() => handleMarkNonIssueRead(approvalKey)}
1070
+ onArchive={isMineTab ? () => handleArchiveNonIssue(approvalKey) : undefined}
1071
+ archiveDisabled={isArchiving}
1072
+ className={
1073
+ isArchiving
1074
+ ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
1075
+ : "transition-all duration-200 ease-out"
1076
+ }
1077
+ />
1078
+ );
1079
+ return isMineTab ? (
1080
+ <SwipeToArchive
1081
+ key={approvalKey}
1082
+ disabled={isArchiving}
1083
+ onArchive={() => handleArchiveNonIssue(approvalKey)}
1084
+ >
1085
+ {row}
1086
+ </SwipeToArchive>
1087
+ ) : row;
1088
+ }
1089
+
1090
+ if (item.kind === "failed_run") {
1091
+ const runKey = `run:${item.run.id}`;
1092
+ const isArchiving = archivingNonIssueIds.has(runKey);
1093
+ const row = (
1094
+ <FailedRunInboxRow
1095
+ key={runKey}
1096
+ run={item.run}
1097
+ issueById={issueById}
1098
+ agentName={agentName(item.run.agentId)}
1099
+ issueLinkState={issueLinkState}
1100
+ onDismiss={() => dismiss(runKey)}
1101
+ onRetry={() => retryRunMutation.mutate(item.run)}
1102
+ isRetrying={retryingRunIds.has(item.run.id)}
1103
+ unreadState={nonIssueUnreadState(runKey)}
1104
+ onMarkRead={() => handleMarkNonIssueRead(runKey)}
1105
+ onArchive={isMineTab ? () => handleArchiveNonIssue(runKey) : undefined}
1106
+ archiveDisabled={isArchiving}
1107
+ className={
1108
+ isArchiving
1109
+ ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
1110
+ : "transition-all duration-200 ease-out"
1111
+ }
1112
+ />
1113
+ );
1114
+ return isMineTab ? (
1115
+ <SwipeToArchive
1116
+ key={runKey}
1117
+ disabled={isArchiving}
1118
+ onArchive={() => handleArchiveNonIssue(runKey)}
1119
+ >
1120
+ {row}
1121
+ </SwipeToArchive>
1122
+ ) : row;
1123
+ }
1124
+
1125
+ if (item.kind === "join_request") {
1126
+ const joinKey = `join:${item.joinRequest.id}`;
1127
+ const isArchiving = archivingNonIssueIds.has(joinKey);
1128
+ const row = (
1129
+ <JoinRequestInboxRow
1130
+ key={joinKey}
1131
+ joinRequest={item.joinRequest}
1132
+ onApprove={() => approveJoinMutation.mutate(item.joinRequest)}
1133
+ onReject={() => rejectJoinMutation.mutate(item.joinRequest)}
1134
+ isPending={approveJoinMutation.isPending || rejectJoinMutation.isPending}
1135
+ unreadState={nonIssueUnreadState(joinKey)}
1136
+ onMarkRead={() => handleMarkNonIssueRead(joinKey)}
1137
+ onArchive={isMineTab ? () => handleArchiveNonIssue(joinKey) : undefined}
1138
+ archiveDisabled={isArchiving}
1139
+ className={
1140
+ isArchiving
1141
+ ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
1142
+ : "transition-all duration-200 ease-out"
1143
+ }
1144
+ />
1145
+ );
1146
+ return isMineTab ? (
1147
+ <SwipeToArchive
1148
+ key={joinKey}
1149
+ disabled={isArchiving}
1150
+ onArchive={() => handleArchiveNonIssue(joinKey)}
1151
+ >
1152
+ {row}
1153
+ </SwipeToArchive>
1154
+ ) : row;
1155
+ }
1156
+
1157
+ const issue = item.issue;
1158
+ const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id);
1159
+ const isFading = fadingOutIssues.has(issue.id);
1160
+ const isArchiving = archivingIssueIds.has(issue.id);
1161
+ const row = (
1162
+ <IssueRow
1163
+ key={`issue:${issue.id}`}
1164
+ issue={issue}
1165
+ issueLinkState={issueLinkState}
1166
+ className={
1167
+ isArchiving
1168
+ ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out"
1169
+ : "transition-all duration-200 ease-out"
1170
+ }
1171
+ desktopMetaLeading={(
1172
+ <>
1173
+ <span className="hidden shrink-0 sm:inline-flex">
1174
+ <StatusIcon status={issue.status} />
1175
+ </span>
1176
+ <span className="shrink-0 font-mono text-xs text-muted-foreground">
1177
+ {issue.identifier ?? issue.id.slice(0, 8)}
1178
+ </span>
1179
+ {liveIssueIds.has(issue.id) && (
1180
+ <span className="inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-0.5 sm:gap-1.5 sm:px-2">
1181
+ <span className="relative flex h-2 w-2">
1182
+ <span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-75" />
1183
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" />
1184
+ </span>
1185
+ <span className="hidden text-[11px] font-medium text-blue-600 dark:text-blue-400 sm:inline">
1186
+ Live
1187
+ </span>
1188
+ </span>
1189
+ )}
1190
+ </>
1191
+ )}
1192
+ mobileMeta={
1193
+ issue.lastExternalCommentAt
1194
+ ? `commented ${timeAgo(issue.lastExternalCommentAt)}`
1195
+ : `updated ${timeAgo(issue.updatedAt)}`
1196
+ }
1197
+ unreadState={
1198
+ isUnread ? "visible" : isFading ? "fading" : "hidden"
1199
+ }
1200
+ onMarkRead={() => markReadMutation.mutate(issue.id)}
1201
+ onArchive={
1202
+ isMineTab
1203
+ ? () => archiveIssueMutation.mutate(issue.id)
1204
+ : undefined
1205
+ }
1206
+ archiveDisabled={isArchiving || archiveIssueMutation.isPending}
1207
+ trailingMeta={
1208
+ issue.lastExternalCommentAt
1209
+ ? `commented ${timeAgo(issue.lastExternalCommentAt)}`
1210
+ : `updated ${timeAgo(issue.updatedAt)}`
1211
+ }
1212
+ />
1213
+ );
1214
+
1215
+ return isMineTab ? (
1216
+ <SwipeToArchive
1217
+ key={`issue:${issue.id}`}
1218
+ disabled={isArchiving || archiveIssueMutation.isPending}
1219
+ onArchive={() => archiveIssueMutation.mutate(issue.id)}
1220
+ >
1221
+ {row}
1222
+ </SwipeToArchive>
1223
+ ) : row;
1224
+ })}
1225
+ </div>
1226
+ </div>
1227
+ </>
1228
+ )}
1229
+
1230
+ {showAlertsSection && (
1231
+ <>
1232
+ {showSeparatorBefore("alerts") && <Separator />}
1233
+ <div>
1234
+ <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1235
+ Alerts
1236
+ </h3>
1237
+ <div className="divide-y divide-border border border-border">
1238
+ {showAggregateAgentError && (
1239
+ <div className="group/alert relative flex items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50">
1240
+ <Link
1241
+ to="/agents"
1242
+ className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
1243
+ >
1244
+ <AlertTriangle className="h-4 w-4 shrink-0 text-red-600 dark:text-red-400" />
1245
+ <span className="text-sm">
1246
+ <span className="font-medium">{dashboard!.agents.error}</span>{" "}
1247
+ {dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors
1248
+ </span>
1249
+ </Link>
1250
+ <button
1251
+ type="button"
1252
+ onClick={() => dismiss("alert:agent-errors")}
1253
+ className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/alert:opacity-100"
1254
+ aria-label="Dismiss"
1255
+ >
1256
+ <X className="h-3.5 w-3.5" />
1257
+ </button>
1258
+ </div>
1259
+ )}
1260
+ {showBudgetAlert && (
1261
+ <div className="group/alert relative flex items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50">
1262
+ <Link
1263
+ to="/costs"
1264
+ className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
1265
+ >
1266
+ <AlertTriangle className="h-4 w-4 shrink-0 text-yellow-400" />
1267
+ <span className="text-sm">
1268
+ Budget at{" "}
1269
+ <span className="font-medium">{dashboard!.costs.monthUtilizationPercent}%</span>{" "}
1270
+ utilization this month
1271
+ </span>
1272
+ </Link>
1273
+ <button
1274
+ type="button"
1275
+ onClick={() => dismiss("alert:budget")}
1276
+ className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/alert:opacity-100"
1277
+ aria-label="Dismiss"
1278
+ >
1279
+ <X className="h-3.5 w-3.5" />
1280
+ </button>
1281
+ </div>
1282
+ )}
1283
+ </div>
1284
+ </div>
1285
+ </>
1286
+ )}
1287
+
1288
+ </div>
1289
+ );
1290
+ }
1291
+