stagent 0.9.6 → 0.11.0
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.
- package/README.md +20 -44
- package/dist/cli.js +66 -18
- package/docs/.coverage-gaps.json +144 -56
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +20 -2
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +11 -2
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +14 -4
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +39 -2
- package/docs/journeys/personal-use.md +32 -8
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -33
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/documents/page.tsx +4 -1
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +8 -9
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +670 -49
- package/src/components/chat/chat-input.tsx +104 -10
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +790 -0
- package/src/components/chat/chat-shell.tsx +151 -401
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +266 -7
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +31 -2
- package/src/components/tasks/task-card.tsx +29 -3
- package/src/components/tasks/task-chip-bar.tsx +54 -1
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +217 -21
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +18 -0
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +64 -60
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +46 -24
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +146 -4
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +159 -18
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +7 -19
- package/src/lib/chat/stream-telemetry.ts +137 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +48 -1
- package/src/lib/chat/tool-catalog.ts +35 -4
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +41 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +89 -21
- package/src/lib/chat/tools/workflow-tools.ts +275 -32
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +10 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +17 -16
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +62 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +94 -23
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +54 -0
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +76 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +205 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +236 -17
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +16 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +212 -7
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/test/setup.ts +10 -0
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
|
|
8
|
+
interface InstanceConfig {
|
|
9
|
+
instanceId: string;
|
|
10
|
+
branchName: string;
|
|
11
|
+
isPrivateInstance: boolean;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Guardrails {
|
|
16
|
+
prePushHookInstalled: boolean;
|
|
17
|
+
prePushHookVersion: string;
|
|
18
|
+
pushRemoteBlocked: string[];
|
|
19
|
+
consentStatus: "not_yet" | "enabled" | "declined_permanently";
|
|
20
|
+
firstBootCompletedAt: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UpgradeState {
|
|
24
|
+
lastPolledAt: number | null;
|
|
25
|
+
upgradeAvailable: boolean;
|
|
26
|
+
commitsBehind: number;
|
|
27
|
+
lastSuccessfulUpgradeAt: number | null;
|
|
28
|
+
pollFailureCount: number;
|
|
29
|
+
lastPollError: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ConfigResponse {
|
|
33
|
+
devMode: boolean;
|
|
34
|
+
config: InstanceConfig | null;
|
|
35
|
+
guardrails: Guardrails | null;
|
|
36
|
+
upgrade: UpgradeState | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Settings → Instance section. Compact horizontal strip with title + actions
|
|
41
|
+
* in a top bar and metadata in a 4-column grid below. On the canonical dev
|
|
42
|
+
* repo (devMode=true) collapses to a single-row notice to avoid pretending
|
|
43
|
+
* the main branch is an instance.
|
|
44
|
+
*/
|
|
45
|
+
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
export function InstanceSection() {
|
|
48
|
+
const router = useRouter();
|
|
49
|
+
const [state, setState] = useState<ConfigResponse | null>(null);
|
|
50
|
+
const [loading, setLoading] = useState(true);
|
|
51
|
+
const [busy, setBusy] = useState<"check" | "init" | "upgrade" | null>(null);
|
|
52
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
async function loadConfig() {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch("/api/instance/config", { cache: "no-store" });
|
|
58
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
59
|
+
const data = (await res.json()) as ConfigResponse;
|
|
60
|
+
setState(data);
|
|
61
|
+
return data;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
64
|
+
return null;
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Silent background refresh — used after auto-check on mount so we don't
|
|
71
|
+
// flicker the whole card back to its loading state.
|
|
72
|
+
async function refreshConfigSilent() {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch("/api/instance/config", { cache: "no-store" });
|
|
75
|
+
if (!res.ok) return;
|
|
76
|
+
const data = (await res.json()) as ConfigResponse;
|
|
77
|
+
setState(data);
|
|
78
|
+
} catch {
|
|
79
|
+
// Swallow — this is a best-effort refresh after auto-check.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
(async () => {
|
|
86
|
+
const data = await loadConfig();
|
|
87
|
+
if (cancelled || !data || data.devMode || !data.config) return;
|
|
88
|
+
// If the cached upgrade state is older than 5 minutes, silently force
|
|
89
|
+
// a fresh check. This self-heals after manual `git pull` + merge in
|
|
90
|
+
// the terminal, so users don't see a stale "N updates pending" count.
|
|
91
|
+
const lastPolled = data.upgrade?.lastPolledAt ?? 0;
|
|
92
|
+
const ageMs = Date.now() - lastPolled * 1000;
|
|
93
|
+
if (ageMs > STALE_THRESHOLD_MS) {
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch("/api/instance/upgrade/check", {
|
|
96
|
+
method: "POST",
|
|
97
|
+
});
|
|
98
|
+
if (res.ok && !cancelled) {
|
|
99
|
+
await refreshConfigSilent();
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Silent — manual "Check for upgrades" button remains as fallback.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
async function checkNow() {
|
|
112
|
+
setBusy("check");
|
|
113
|
+
setMessage(null);
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch("/api/instance/upgrade/check", { method: "POST" });
|
|
116
|
+
if (res.status === 202) {
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
setMessage(`Check skipped: ${body.skipped ?? body.error ?? "unknown"}`);
|
|
119
|
+
} else if (res.ok) {
|
|
120
|
+
setMessage("Check complete");
|
|
121
|
+
await loadConfig();
|
|
122
|
+
} else {
|
|
123
|
+
throw new Error(`HTTP ${res.status}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
127
|
+
} finally {
|
|
128
|
+
setBusy(null);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function startUpgrade() {
|
|
133
|
+
setBusy("upgrade");
|
|
134
|
+
setMessage(null);
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch("/api/instance/upgrade", { method: "POST" });
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const body = await res.json().catch(() => ({}));
|
|
139
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
140
|
+
}
|
|
141
|
+
const data = (await res.json()) as { taskId: string };
|
|
142
|
+
router.push(`/tasks/${data.taskId}`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
145
|
+
setBusy(null);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function reinit() {
|
|
150
|
+
setBusy("init");
|
|
151
|
+
setMessage(null);
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch("/api/instance/init", { method: "POST" });
|
|
154
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
155
|
+
await loadConfig();
|
|
156
|
+
setMessage("Instance setup re-run complete");
|
|
157
|
+
} catch (err) {
|
|
158
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
159
|
+
} finally {
|
|
160
|
+
setBusy(null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (loading) {
|
|
165
|
+
return (
|
|
166
|
+
<section className="rounded-xl border bg-card px-5 py-4">
|
|
167
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
168
|
+
<p className="mt-1 text-sm text-muted-foreground">Loading…</p>
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dev mode: main dev repo. Instance bootstrap is gated off. Show a slim
|
|
174
|
+
// single-row notice so the Settings page layout stays stable without
|
|
175
|
+
// misrepresenting the dev repo as an instance.
|
|
176
|
+
if (state?.devMode) {
|
|
177
|
+
return (
|
|
178
|
+
<section className="rounded-xl border bg-card px-5 py-3 flex items-center justify-between gap-4 flex-wrap">
|
|
179
|
+
<div className="flex items-center gap-3">
|
|
180
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
181
|
+
<Badge variant="outline" className="text-xs font-normal">
|
|
182
|
+
Dev mode
|
|
183
|
+
</Badge>
|
|
184
|
+
</div>
|
|
185
|
+
<p className="text-xs text-muted-foreground">
|
|
186
|
+
Running on the main dev repo. Instance upgrade features are disabled.
|
|
187
|
+
Set{" "}
|
|
188
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
189
|
+
STAGENT_INSTANCE_MODE=true
|
|
190
|
+
</code>{" "}
|
|
191
|
+
to test.
|
|
192
|
+
</p>
|
|
193
|
+
</section>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const config = state?.config ?? null;
|
|
198
|
+
const guardrails = state?.guardrails ?? null;
|
|
199
|
+
const upgrade = state?.upgrade ?? null;
|
|
200
|
+
const hasConfig = config !== null;
|
|
201
|
+
|
|
202
|
+
// Not-initialized state
|
|
203
|
+
if (!hasConfig) {
|
|
204
|
+
return (
|
|
205
|
+
<section className="rounded-xl border bg-card px-5 py-4 space-y-3">
|
|
206
|
+
<div className="flex items-center justify-between gap-4 flex-wrap">
|
|
207
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
208
|
+
<Button
|
|
209
|
+
variant="default"
|
|
210
|
+
size="sm"
|
|
211
|
+
onClick={reinit}
|
|
212
|
+
disabled={busy !== null}
|
|
213
|
+
>
|
|
214
|
+
{busy === "init" ? "Running…" : "Run setup"}
|
|
215
|
+
</Button>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs">
|
|
218
|
+
Instance setup incomplete. Run setup to initialize this workspace.
|
|
219
|
+
</div>
|
|
220
|
+
{message && (
|
|
221
|
+
<div className="text-xs text-muted-foreground">{message}</div>
|
|
222
|
+
)}
|
|
223
|
+
</section>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const shortId = config!.instanceId.slice(0, 8) + "…";
|
|
228
|
+
const consentLabel = guardrails?.consentStatus ?? "unknown";
|
|
229
|
+
const hookLabel = guardrails?.prePushHookInstalled
|
|
230
|
+
? `v${guardrails.prePushHookVersion}`
|
|
231
|
+
: "not installed";
|
|
232
|
+
const blockedLabel = guardrails?.pushRemoteBlocked.length
|
|
233
|
+
? guardrails.pushRemoteBlocked.join(", ")
|
|
234
|
+
: "none";
|
|
235
|
+
const lastCheck = upgrade?.lastPolledAt
|
|
236
|
+
? new Date(upgrade.lastPolledAt * 1000).toLocaleString()
|
|
237
|
+
: "never";
|
|
238
|
+
const lastUpgrade = upgrade?.lastSuccessfulUpgradeAt
|
|
239
|
+
? new Date(upgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
|
|
240
|
+
: "never";
|
|
241
|
+
const pollFailing = (upgrade?.pollFailureCount ?? 0) > 0;
|
|
242
|
+
|
|
243
|
+
const upgradeAvailable = upgrade?.upgradeAvailable ?? false;
|
|
244
|
+
const upgradeCount = upgrade?.commitsBehind ?? 0;
|
|
245
|
+
const startUpgradeDisabled = busy !== null || !upgradeAvailable;
|
|
246
|
+
const startUpgradeTitle = upgradeAvailable
|
|
247
|
+
? `Merge ${upgradeCount} upstream commit${upgradeCount === 1 ? "" : "s"} into ${config!.branchName}`
|
|
248
|
+
: "No upgrades available — click 'Check for upgrades' to refresh";
|
|
249
|
+
const statusMessage = pollFailing && upgrade?.lastPollError
|
|
250
|
+
? upgrade.lastPollError
|
|
251
|
+
: message;
|
|
252
|
+
const statusToneClass = pollFailing
|
|
253
|
+
? "text-amber-700 dark:text-amber-400"
|
|
254
|
+
: "text-muted-foreground";
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<section className="rounded-xl border bg-card">
|
|
258
|
+
<header className="flex items-start justify-between gap-4 px-5 py-3 border-b flex-wrap">
|
|
259
|
+
<div className="min-w-0 space-y-2">
|
|
260
|
+
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
|
261
|
+
<h2 className="text-base font-semibold">Instance</h2>
|
|
262
|
+
{upgradeAvailable && (
|
|
263
|
+
<Badge
|
|
264
|
+
variant="outline"
|
|
265
|
+
className="text-xs font-normal border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-400"
|
|
266
|
+
>
|
|
267
|
+
{upgradeCount} update{upgradeCount === 1 ? "" : "s"} available
|
|
268
|
+
</Badge>
|
|
269
|
+
)}
|
|
270
|
+
{pollFailing && (
|
|
271
|
+
<Badge variant="destructive" className="text-xs font-normal">
|
|
272
|
+
Poll failing ({upgrade?.pollFailureCount})
|
|
273
|
+
</Badge>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
<p className="text-xs text-muted-foreground leading-relaxed max-w-prose">
|
|
277
|
+
Pull latest changes from{" "}
|
|
278
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
279
|
+
main
|
|
280
|
+
</code>{" "}
|
|
281
|
+
into{" "}
|
|
282
|
+
<code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
|
|
283
|
+
{config!.branchName}
|
|
284
|
+
</code>
|
|
285
|
+
. Nothing is pushed automatically.
|
|
286
|
+
</p>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
|
289
|
+
<Button
|
|
290
|
+
variant="outline"
|
|
291
|
+
size="sm"
|
|
292
|
+
onClick={checkNow}
|
|
293
|
+
disabled={busy !== null}
|
|
294
|
+
>
|
|
295
|
+
{busy === "check" ? "Checking…" : "Check"}
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
variant="default"
|
|
299
|
+
size="sm"
|
|
300
|
+
onClick={startUpgrade}
|
|
301
|
+
disabled={startUpgradeDisabled}
|
|
302
|
+
title={startUpgradeTitle}
|
|
303
|
+
>
|
|
304
|
+
{busy === "upgrade"
|
|
305
|
+
? "Starting…"
|
|
306
|
+
: upgradeAvailable
|
|
307
|
+
? `Upgrade (${upgradeCount})`
|
|
308
|
+
: "Upgrade"}
|
|
309
|
+
</Button>
|
|
310
|
+
<Button
|
|
311
|
+
variant="ghost"
|
|
312
|
+
size="sm"
|
|
313
|
+
onClick={reinit}
|
|
314
|
+
disabled={busy !== null}
|
|
315
|
+
>
|
|
316
|
+
{busy === "init" ? "Running…" : "Repair setup"}
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
</header>
|
|
320
|
+
|
|
321
|
+
<dl className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 px-5 py-3 text-sm">
|
|
322
|
+
<Field label="Branch" mono>
|
|
323
|
+
{config!.branchName}
|
|
324
|
+
</Field>
|
|
325
|
+
<Field
|
|
326
|
+
label="Instance ID"
|
|
327
|
+
mono
|
|
328
|
+
title={config!.instanceId}
|
|
329
|
+
>
|
|
330
|
+
{shortId}
|
|
331
|
+
</Field>
|
|
332
|
+
<Field label="Last check">{lastCheck}</Field>
|
|
333
|
+
<Field label="Last upgrade">{lastUpgrade}</Field>
|
|
334
|
+
</dl>
|
|
335
|
+
|
|
336
|
+
<div className="flex items-start justify-between gap-3 border-t px-5 py-2.5 text-[11px]">
|
|
337
|
+
<p className={`leading-relaxed ${statusToneClass}`}>
|
|
338
|
+
{statusMessage ?? (
|
|
339
|
+
upgradeAvailable
|
|
340
|
+
? `Ready to merge ${upgradeCount} upstream update${upgradeCount === 1 ? "" : "s"}.`
|
|
341
|
+
: `Up to date. Last checked: ${lastCheck}.`
|
|
342
|
+
)}
|
|
343
|
+
</p>
|
|
344
|
+
<p className="shrink-0 text-muted-foreground">
|
|
345
|
+
Repairs local setup without changing data or commits.
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
</section>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function Field({
|
|
353
|
+
label,
|
|
354
|
+
children,
|
|
355
|
+
mono,
|
|
356
|
+
truncate,
|
|
357
|
+
title,
|
|
358
|
+
}: {
|
|
359
|
+
label: string;
|
|
360
|
+
children: ReactNode;
|
|
361
|
+
mono?: boolean;
|
|
362
|
+
truncate?: boolean;
|
|
363
|
+
title?: string;
|
|
364
|
+
}) {
|
|
365
|
+
return (
|
|
366
|
+
<div className="min-w-0">
|
|
367
|
+
<dt className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
368
|
+
{label}
|
|
369
|
+
</dt>
|
|
370
|
+
<dd
|
|
371
|
+
title={title}
|
|
372
|
+
className={
|
|
373
|
+
"mt-0.5 " +
|
|
374
|
+
(mono ? "font-mono text-xs " : "") +
|
|
375
|
+
(truncate ? "truncate" : "")
|
|
376
|
+
}
|
|
377
|
+
>
|
|
378
|
+
{children}
|
|
379
|
+
</dd>
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { ArrowUpCircle } from "lucide-react";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from "@/components/ui/dialog";
|
|
15
|
+
import { Button } from "@/components/ui/button";
|
|
16
|
+
import type { UpgradeState } from "@/lib/instance/types";
|
|
17
|
+
|
|
18
|
+
interface InstanceConfig {
|
|
19
|
+
instanceId: string;
|
|
20
|
+
branchName: string;
|
|
21
|
+
isPrivateInstance: boolean;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ConfigResponse {
|
|
26
|
+
devMode?: boolean;
|
|
27
|
+
config: InstanceConfig | null;
|
|
28
|
+
upgrade: UpgradeState | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type StatusResponse = UpgradeState & { devMode?: boolean };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sidebar upgrade badge + pre-flight modal combined into a single Client
|
|
35
|
+
* Component. Fetches status on mount and every 5 minutes; renders nothing
|
|
36
|
+
* when no upgrade is available. When clicked, opens the pre-flight modal
|
|
37
|
+
* and loads the full config for the fact panel.
|
|
38
|
+
*
|
|
39
|
+
* Combined into one component because Next.js 16's stricter client/server
|
|
40
|
+
* boundary rules reject passing callback props between separately-imported
|
|
41
|
+
* client components unless they're Server Actions. Bundling the two here
|
|
42
|
+
* preserves the spec's separation of concerns at the design level while
|
|
43
|
+
* satisfying the framework.
|
|
44
|
+
*/
|
|
45
|
+
export function UpgradeBadge() {
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const [state, setState] = useState<StatusResponse | null>(null);
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
const [config, setConfig] = useState<ConfigResponse | null>(null);
|
|
50
|
+
const [loading, setLoading] = useState(false);
|
|
51
|
+
const [starting, setStarting] = useState(false);
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
|
|
57
|
+
async function fetchStatus() {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch("/api/instance/upgrade/status", {
|
|
60
|
+
cache: "no-store",
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) return;
|
|
63
|
+
const data = (await res.json()) as StatusResponse;
|
|
64
|
+
if (!cancelled) setState(data);
|
|
65
|
+
} catch {
|
|
66
|
+
// Silent — the badge is ambient; status fetch failures should not
|
|
67
|
+
// produce UI noise. Persistent poll failures surface as a warning
|
|
68
|
+
// variant via state.pollFailureCount >= 3.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fetchStatus();
|
|
73
|
+
const interval = setInterval(fetchStatus, 5 * 60 * 1000);
|
|
74
|
+
// Refetch when the tab regains focus — picks up DB changes made by the
|
|
75
|
+
// hourly poller or by a manual "Check for upgrades" click while the user
|
|
76
|
+
// was running git commands in the terminal.
|
|
77
|
+
window.addEventListener("focus", fetchStatus);
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true;
|
|
80
|
+
clearInterval(interval);
|
|
81
|
+
window.removeEventListener("focus", fetchStatus);
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!open) return;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
setLoading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
(async () => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch("/api/instance/config", { cache: "no-store" });
|
|
93
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
94
|
+
const data = (await res.json()) as ConfigResponse;
|
|
95
|
+
if (!cancelled) setConfig(data);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
|
98
|
+
} finally {
|
|
99
|
+
if (!cancelled) setLoading(false);
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, [open]);
|
|
106
|
+
|
|
107
|
+
async function startUpgrade() {
|
|
108
|
+
setStarting(true);
|
|
109
|
+
setError(null);
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch("/api/instance/upgrade", { method: "POST" });
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const body = await res.json().catch(() => ({}));
|
|
114
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
115
|
+
}
|
|
116
|
+
const data = (await res.json()) as { taskId: string };
|
|
117
|
+
setOpen(false);
|
|
118
|
+
router.push(`/tasks/${data.taskId}`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
121
|
+
} finally {
|
|
122
|
+
setStarting(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!state || state.devMode || !state.upgradeAvailable) return null;
|
|
127
|
+
|
|
128
|
+
const failing = state.pollFailureCount >= 3;
|
|
129
|
+
const count = state.commitsBehind;
|
|
130
|
+
const label = failing
|
|
131
|
+
? "Check failing"
|
|
132
|
+
: `${count} update${count === 1 ? "" : "s"}`;
|
|
133
|
+
const tooltip = failing
|
|
134
|
+
? "Upgrade check failing — click to retry"
|
|
135
|
+
: `${count} upstream update${count === 1 ? "" : "s"} ready to merge`;
|
|
136
|
+
const buttonClass = failing
|
|
137
|
+
? "h-7 px-2 rounded-md border border-amber-500/40 bg-amber-500/10 text-[11px] font-medium text-amber-700 dark:text-amber-400 hover:bg-amber-500/20 transition-colors cursor-pointer inline-flex items-center gap-1.5 group-data-[collapsible=icon]:hidden"
|
|
138
|
+
: "h-7 px-2 rounded-md border border-blue-500/40 bg-blue-500/10 text-[11px] font-medium text-blue-700 dark:text-blue-400 hover:bg-blue-500/20 transition-colors cursor-pointer inline-flex items-center gap-1.5 group-data-[collapsible=icon]:hidden";
|
|
139
|
+
|
|
140
|
+
const modalUpgrade = config?.upgrade ?? null;
|
|
141
|
+
const modalCount = modalUpgrade?.commitsBehind ?? count;
|
|
142
|
+
const lastUpgradeText = modalUpgrade?.lastSuccessfulUpgradeAt
|
|
143
|
+
? new Date(modalUpgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
|
|
144
|
+
: "never";
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
148
|
+
<DialogTrigger asChild>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
aria-label={tooltip}
|
|
152
|
+
title={tooltip}
|
|
153
|
+
className={buttonClass}
|
|
154
|
+
onClick={(e) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
setOpen(true);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<ArrowUpCircle className="h-3 w-3" aria-hidden />
|
|
161
|
+
<span>{label}</span>
|
|
162
|
+
</button>
|
|
163
|
+
</DialogTrigger>
|
|
164
|
+
<DialogContent className="sm:max-w-lg">
|
|
165
|
+
<DialogHeader>
|
|
166
|
+
<DialogTitle>Upgrade available</DialogTitle>
|
|
167
|
+
<DialogDescription>
|
|
168
|
+
{modalCount} commit{modalCount === 1 ? "" : "s"} ready to merge into{" "}
|
|
169
|
+
<code className="font-mono text-xs">
|
|
170
|
+
{config?.config?.branchName ?? "…"}
|
|
171
|
+
</code>
|
|
172
|
+
</DialogDescription>
|
|
173
|
+
</DialogHeader>
|
|
174
|
+
|
|
175
|
+
{loading && (
|
|
176
|
+
<div className="py-4 text-sm text-muted-foreground">Loading instance state…</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{error && (
|
|
180
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
|
|
181
|
+
{error}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{config && !loading && (
|
|
186
|
+
<div className="space-y-3 py-2">
|
|
187
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
|
|
188
|
+
<span className="text-muted-foreground">Branch</span>
|
|
189
|
+
<code className="font-mono text-xs">{config.config?.branchName ?? "—"}</code>
|
|
190
|
+
<span className="text-muted-foreground">Data directory</span>
|
|
191
|
+
<code className="font-mono text-xs break-all">
|
|
192
|
+
{config.config?.isPrivateInstance ? "custom" : "default"}
|
|
193
|
+
</code>
|
|
194
|
+
<span className="text-muted-foreground">Commits behind</span>
|
|
195
|
+
<span>{modalCount}</span>
|
|
196
|
+
<span className="text-muted-foreground">Last successful upgrade</span>
|
|
197
|
+
<span>{lastUpgradeText}</span>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
201
|
+
Stagent will stash any uncommitted work, merge <code className="font-mono">main</code> into{" "}
|
|
202
|
+
<code className="font-mono">{config.config?.branchName ?? "your branch"}</code>, install any new
|
|
203
|
+
dependencies, and ask you to resolve conflicts if any appear.
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<DialogFooter>
|
|
209
|
+
<Button variant="ghost" onClick={() => setOpen(false)} disabled={starting}>
|
|
210
|
+
Cancel
|
|
211
|
+
</Button>
|
|
212
|
+
<Button onClick={startUpgrade} disabled={loading || starting || !config?.config}>
|
|
213
|
+
{starting ? "Starting…" : "Start upgrade"}
|
|
214
|
+
</Button>
|
|
215
|
+
</DialogFooter>
|
|
216
|
+
</DialogContent>
|
|
217
|
+
</Dialog>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { BatchProposalReview } from "@/components/notifications/batch-proposal-review";
|
|
5
|
+
|
|
6
|
+
const { toastError } = vi.hoisted(() => ({
|
|
7
|
+
toastError: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("sonner", () => ({
|
|
11
|
+
toast: {
|
|
12
|
+
error: toastError,
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("batch proposal review", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.unstubAllGlobals();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("optimistically resolves the batch before the request finishes", async () => {
|
|
26
|
+
const onResponded = vi.fn();
|
|
27
|
+
let resolveFetch: ((value: Response) => void) | null = null;
|
|
28
|
+
|
|
29
|
+
vi.stubGlobal(
|
|
30
|
+
"fetch",
|
|
31
|
+
vi.fn().mockImplementation(
|
|
32
|
+
() =>
|
|
33
|
+
new Promise<Response>((resolve) => {
|
|
34
|
+
resolveFetch = resolve;
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
render(
|
|
40
|
+
<BatchProposalReview
|
|
41
|
+
proposalIds={["p1", "p2"]}
|
|
42
|
+
profileIds={["general"]}
|
|
43
|
+
body="Batch summary"
|
|
44
|
+
onResponded={onResponded}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
fireEvent.click(screen.getByRole("button", { name: /approve all/i }));
|
|
49
|
+
|
|
50
|
+
expect(onResponded).toHaveBeenCalledTimes(1);
|
|
51
|
+
|
|
52
|
+
resolveFetch?.(
|
|
53
|
+
new Response(JSON.stringify({ action: "approve", count: 2 }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(screen.getByText("2 proposals approved")).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("restores server truth when the batch request fails", async () => {
|
|
65
|
+
const onResponded = vi.fn();
|
|
66
|
+
const onRequestFailed = vi.fn();
|
|
67
|
+
|
|
68
|
+
vi.stubGlobal(
|
|
69
|
+
"fetch",
|
|
70
|
+
vi.fn().mockRejectedValue(new Error("Batch approval failed"))
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<BatchProposalReview
|
|
75
|
+
proposalIds={["p1", "p2"]}
|
|
76
|
+
profileIds={["general"]}
|
|
77
|
+
body="Batch summary"
|
|
78
|
+
onResponded={onResponded}
|
|
79
|
+
onRequestFailed={onRequestFailed}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
fireEvent.click(screen.getByRole("button", { name: /approve all/i }));
|
|
84
|
+
|
|
85
|
+
expect(onResponded).toHaveBeenCalledTimes(1);
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(onRequestFailed).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
expect(toastError).toHaveBeenCalledWith("Batch approval failed");
|
|
91
|
+
expect(
|
|
92
|
+
screen.getByRole("button", { name: /approve all \(2\)/i })
|
|
93
|
+
).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|