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,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
recordTermination,
|
|
4
|
+
readTerminations,
|
|
5
|
+
countTerminations,
|
|
6
|
+
__resetForTesting,
|
|
7
|
+
} from "../stream-telemetry";
|
|
8
|
+
|
|
9
|
+
describe("stream-telemetry ring buffer", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
__resetForTesting();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns [] before any events are recorded", () => {
|
|
15
|
+
expect(readTerminations()).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("records events in chronological order", () => {
|
|
19
|
+
recordTermination({
|
|
20
|
+
reason: "stream.completed",
|
|
21
|
+
conversationId: "c1",
|
|
22
|
+
messageId: "m1",
|
|
23
|
+
durationMs: 100,
|
|
24
|
+
});
|
|
25
|
+
recordTermination({
|
|
26
|
+
reason: "stream.aborted.client",
|
|
27
|
+
conversationId: "c2",
|
|
28
|
+
messageId: "m2",
|
|
29
|
+
durationMs: 50,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const events = readTerminations();
|
|
33
|
+
expect(events).toHaveLength(2);
|
|
34
|
+
expect(events[0].reason).toBe("stream.completed");
|
|
35
|
+
expect(events[1].reason).toBe("stream.aborted.client");
|
|
36
|
+
expect(events[0].timestamp).toBeLessThanOrEqual(events[1].timestamp);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("stamps each event with a timestamp", () => {
|
|
40
|
+
const before = Date.now();
|
|
41
|
+
recordTermination({
|
|
42
|
+
reason: "stream.completed",
|
|
43
|
+
conversationId: "c1",
|
|
44
|
+
messageId: "m1",
|
|
45
|
+
durationMs: 0,
|
|
46
|
+
});
|
|
47
|
+
const after = Date.now();
|
|
48
|
+
const events = readTerminations();
|
|
49
|
+
expect(events[0].timestamp).toBeGreaterThanOrEqual(before);
|
|
50
|
+
expect(events[0].timestamp).toBeLessThanOrEqual(after);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("wraps around after 500 events, preserving newest-500 in order", () => {
|
|
54
|
+
// Write 520 events — first 20 should be evicted.
|
|
55
|
+
for (let i = 0; i < 520; i++) {
|
|
56
|
+
recordTermination({
|
|
57
|
+
reason: "stream.completed",
|
|
58
|
+
conversationId: `c${i}`,
|
|
59
|
+
messageId: `m${i}`,
|
|
60
|
+
durationMs: i,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const events = readTerminations();
|
|
65
|
+
expect(events).toHaveLength(500);
|
|
66
|
+
// Oldest surviving event should be #20; newest should be #519.
|
|
67
|
+
expect(events[0].conversationId).toBe("c20");
|
|
68
|
+
expect(events[0].durationMs).toBe(20);
|
|
69
|
+
expect(events[499].conversationId).toBe("c519");
|
|
70
|
+
expect(events[499].durationMs).toBe(519);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("countTerminations groups by reason code across the full buffer", () => {
|
|
74
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
75
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
76
|
+
recordTermination({ reason: "stream.aborted.client", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
77
|
+
recordTermination({ reason: "stream.finalized.error", conversationId: "c", messageId: "m", durationMs: 1, error: "boom" });
|
|
78
|
+
recordTermination({ reason: "stream.abandoned", conversationId: "c", messageId: "m", durationMs: 42 });
|
|
79
|
+
|
|
80
|
+
const counts = countTerminations();
|
|
81
|
+
expect(counts["stream.completed"]).toBe(2);
|
|
82
|
+
expect(counts["stream.aborted.client"]).toBe(1);
|
|
83
|
+
expect(counts["stream.finalized.error"]).toBe(1);
|
|
84
|
+
expect(counts["stream.abandoned"]).toBe(1);
|
|
85
|
+
expect(counts["stream.aborted.signal"]).toBe(0);
|
|
86
|
+
expect(counts["stream.reconciled.stale"]).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("stream.abandoned is a valid reason code for iterator abandonment", () => {
|
|
90
|
+
// finalizeStreamingMessage records this when the engine's happy and
|
|
91
|
+
// catch paths both missed the termination — the canonical "gap"
|
|
92
|
+
// indicator. Make sure it round-trips through the buffer.
|
|
93
|
+
recordTermination({
|
|
94
|
+
reason: "stream.abandoned",
|
|
95
|
+
conversationId: "c1",
|
|
96
|
+
messageId: "m1",
|
|
97
|
+
durationMs: 100,
|
|
98
|
+
error: "no content streamed before abandonment",
|
|
99
|
+
});
|
|
100
|
+
const events = readTerminations();
|
|
101
|
+
expect(events[0].reason).toBe("stream.abandoned");
|
|
102
|
+
expect(events[0].error).toBe("no content streamed before abandonment");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("countTerminations honors the windowMs filter", async () => {
|
|
106
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
107
|
+
// Wait a few ms so the second event has a strictly later timestamp.
|
|
108
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
109
|
+
const midpoint = Date.now();
|
|
110
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
111
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
112
|
+
|
|
113
|
+
// Use a window that only includes the second event.
|
|
114
|
+
const windowMs = Date.now() - midpoint + 5;
|
|
115
|
+
const counts = countTerminations(windowMs);
|
|
116
|
+
expect(counts["stream.completed"]).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("readTerminations returns a copy, not a live reference", () => {
|
|
120
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
121
|
+
const first = readTerminations();
|
|
122
|
+
recordTermination({ reason: "stream.completed", conversationId: "c2", messageId: "m2", durationMs: 1 });
|
|
123
|
+
// first snapshot should still have only the initial event.
|
|
124
|
+
expect(first).toHaveLength(1);
|
|
125
|
+
expect(readTerminations()).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("records optional error strings on error events", () => {
|
|
129
|
+
recordTermination({
|
|
130
|
+
reason: "stream.finalized.error",
|
|
131
|
+
conversationId: "c",
|
|
132
|
+
messageId: "m",
|
|
133
|
+
durationMs: 42,
|
|
134
|
+
error: "boom",
|
|
135
|
+
});
|
|
136
|
+
expect(readTerminations()[0].error).toBe("boom");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("allows null conversationId / messageId / durationMs for edge cases", () => {
|
|
140
|
+
recordTermination({
|
|
141
|
+
reason: "stream.reconciled.stale",
|
|
142
|
+
conversationId: null,
|
|
143
|
+
messageId: null,
|
|
144
|
+
durationMs: null,
|
|
145
|
+
});
|
|
146
|
+
const events = readTerminations();
|
|
147
|
+
expect(events[0].conversationId).toBeNull();
|
|
148
|
+
expect(events[0].messageId).toBeNull();
|
|
149
|
+
expect(events[0].durationMs).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getFeaturesForModel, getRuntimeForModel } from "@/lib/chat/types";
|
|
3
|
+
|
|
4
|
+
describe("getFeaturesForModel", () => {
|
|
5
|
+
it("returns Claude features for a Claude model id", () => {
|
|
6
|
+
const features = getFeaturesForModel("sonnet");
|
|
7
|
+
expect(features.hasNativeSkills).toBe(true);
|
|
8
|
+
expect(features.autoLoadsInstructions).toBe("CLAUDE.md");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns Ollama features for an ollama-prefixed model id", () => {
|
|
12
|
+
const features = getFeaturesForModel("ollama:llama3");
|
|
13
|
+
expect(features.stagentInjectsSkills).toBe(true);
|
|
14
|
+
expect(features.hasNativeSkills).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns Codex features for a GPT model id", () => {
|
|
18
|
+
const features = getFeaturesForModel("gpt-5.4");
|
|
19
|
+
expect(features.autoLoadsInstructions).toBe("AGENTS.md");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("falls back to claude-code features for an unknown model id", () => {
|
|
23
|
+
// getRuntimeForModel's fallback chain lands on claude-code for unknown ids.
|
|
24
|
+
const features = getFeaturesForModel("totally-made-up-model");
|
|
25
|
+
expect(features.hasNativeSkills).toBe(true);
|
|
26
|
+
expect(getRuntimeForModel("totally-made-up-model")).toBe("claude-code");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for combining the legacy `conversations.active_skill_id`
|
|
3
|
+
* column with the new `conversations.active_skill_ids` JSON array
|
|
4
|
+
* (`features/chat-skill-composition.md`).
|
|
5
|
+
*
|
|
6
|
+
* Lives in its own module (no DB imports) so client components can use
|
|
7
|
+
* it without pulling server-only code into the bundle. The original
|
|
8
|
+
* lived alongside the chat-tool definition in `tools/skill-tools.ts`,
|
|
9
|
+
* which can only run server-side.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function mergeActiveSkillIds(
|
|
13
|
+
legacyId: string | null | undefined,
|
|
14
|
+
composed: string[] | null | undefined
|
|
15
|
+
): string[] {
|
|
16
|
+
const out: string[] = [];
|
|
17
|
+
const seen = new Set<string>();
|
|
18
|
+
if (legacyId) {
|
|
19
|
+
out.push(legacyId);
|
|
20
|
+
seen.add(legacyId);
|
|
21
|
+
}
|
|
22
|
+
if (composed) {
|
|
23
|
+
for (const id of composed) {
|
|
24
|
+
if (id && !seen.has(id)) {
|
|
25
|
+
out.push(id);
|
|
26
|
+
seen.add(id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory tracker for chat conversations that currently have an SSE stream
|
|
3
|
+
* in flight. Used by the scheduler tick loop to apply a soft pressure signal
|
|
4
|
+
* — when chat is active, new schedule firings are deferred by N seconds to
|
|
5
|
+
* keep the Node event loop responsive for the user's conversation.
|
|
6
|
+
*
|
|
7
|
+
* Module-level state; single-process (same Node instance as the scheduler).
|
|
8
|
+
* Must NOT be persisted — crash recovery relies on the set starting empty.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const activeStreams = new Set<string>();
|
|
12
|
+
|
|
13
|
+
export function registerChatStream(conversationId: string): void {
|
|
14
|
+
activeStreams.add(conversationId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function unregisterChatStream(conversationId: string): void {
|
|
18
|
+
activeStreams.delete(conversationId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getActiveChatStreamCount(): number {
|
|
22
|
+
return activeStreams.size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAnyChatStreaming(): boolean {
|
|
26
|
+
return activeStreams.size > 0;
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize the filterInput we persist for a saved search.
|
|
3
|
+
*
|
|
4
|
+
* The chat popover input may include the mention trigger prefix
|
|
5
|
+
* (e.g. `@task: ` or `task: ` depending on what the trigger regex
|
|
6
|
+
* stripped). When the user "Saves this view", we want the persisted
|
|
7
|
+
* filterInput to contain ONLY the meaningful filter expression —
|
|
8
|
+
* `#key:value` clauses plus any free-text search the user typed —
|
|
9
|
+
* not the trigger residue.
|
|
10
|
+
*
|
|
11
|
+
* Pure function. Tested in isolation; called from
|
|
12
|
+
* `chat-command-popover.tsx` at the SaveViewFooter call site.
|
|
13
|
+
*
|
|
14
|
+
* See `features/saved-search-polish-v1.md` for the bug history.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FilterClause } from "@/lib/filters/parse";
|
|
18
|
+
|
|
19
|
+
const TRIGGER_RESIDUE = /^@?[a-z]+:\s*/i;
|
|
20
|
+
|
|
21
|
+
export function cleanFilterInput(
|
|
22
|
+
clauses: FilterClause[],
|
|
23
|
+
rawQuery: string
|
|
24
|
+
): string {
|
|
25
|
+
const cleanRawQuery = rawQuery.replace(TRIGGER_RESIDUE, "").trim();
|
|
26
|
+
return [
|
|
27
|
+
...clauses.map((c) => `#${c.key}:${c.value}`),
|
|
28
|
+
...(cleanRawQuery ? [cleanRawQuery] : []),
|
|
29
|
+
].join(" ");
|
|
30
|
+
}
|
|
@@ -2,7 +2,10 @@ import { db } from "@/lib/db";
|
|
|
2
2
|
import { projects } from "@/lib/db/schema";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
4
|
import { CodexAppServerClient } from "@/lib/agents/runtime/codex-app-server-client";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ensureOpenAICodexClientAuthenticated,
|
|
7
|
+
resolveOpenAICodexAuthContext,
|
|
8
|
+
} from "@/lib/agents/runtime/openai-codex-auth";
|
|
6
9
|
import {
|
|
7
10
|
extractUsageSnapshot,
|
|
8
11
|
mergeUsageSnapshot,
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
cleanupConversation,
|
|
32
35
|
} from "./permission-bridge";
|
|
33
36
|
import { getWorkspaceContext } from "@/lib/environment/workspace-context";
|
|
37
|
+
import type { ResolvedExecutionTarget } from "@/lib/agents/runtime/execution-target";
|
|
34
38
|
|
|
35
39
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
36
40
|
|
|
@@ -54,7 +58,8 @@ function asString(v: unknown): string | null {
|
|
|
54
58
|
export async function* sendCodexMessage(
|
|
55
59
|
conversationId: string,
|
|
56
60
|
userContent: string,
|
|
57
|
-
signal?: AbortSignal
|
|
61
|
+
signal?: AbortSignal,
|
|
62
|
+
targetOverride?: ResolvedExecutionTarget
|
|
58
63
|
): AsyncGenerator<ChatStreamEvent> {
|
|
59
64
|
const conversation = await getConversation(conversationId);
|
|
60
65
|
if (!conversation) {
|
|
@@ -62,7 +67,7 @@ export async function* sendCodexMessage(
|
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
const runtimeId = conversation.runtimeId;
|
|
70
|
+
const runtimeId = targetOverride?.effectiveRuntimeId ?? conversation.runtimeId;
|
|
66
71
|
const providerId = getProviderForRuntime(runtimeId);
|
|
67
72
|
|
|
68
73
|
// Enforce budget
|
|
@@ -128,11 +133,17 @@ export async function* sendCodexMessage(
|
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
// Get OpenAI API key
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await
|
|
136
|
+
let auth;
|
|
137
|
+
try {
|
|
138
|
+
auth = await resolveOpenAICodexAuthContext();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message =
|
|
141
|
+
error instanceof Error
|
|
142
|
+
? error.message
|
|
143
|
+
: "OpenAI Codex authentication is not configured.";
|
|
144
|
+
await updateMessageContent(assistantMsg.id, message);
|
|
134
145
|
await updateMessageStatus(assistantMsg.id, "error");
|
|
135
|
-
yield { type: "error", message
|
|
146
|
+
yield { type: "error", message };
|
|
136
147
|
return;
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -164,20 +175,10 @@ export async function* sendCodexMessage(
|
|
|
164
175
|
}
|
|
165
176
|
|
|
166
177
|
try {
|
|
167
|
-
client = await
|
|
168
|
-
cwd: workspace.cwd,
|
|
169
|
-
env: { OPENAI_API_KEY: apiKey },
|
|
170
|
-
});
|
|
178
|
+
client = await auth.connect(workspace.cwd);
|
|
171
179
|
|
|
172
180
|
// Initialize and authenticate
|
|
173
|
-
await client
|
|
174
|
-
clientInfo: { name: "Stagent", version: "0.1.1" },
|
|
175
|
-
capabilities: null,
|
|
176
|
-
});
|
|
177
|
-
await client.request("account/login/start", {
|
|
178
|
-
type: "apiKey",
|
|
179
|
-
apiKey,
|
|
180
|
-
});
|
|
181
|
+
await ensureOpenAICodexClientAuthenticated(client, auth);
|
|
181
182
|
|
|
182
183
|
// Validate model availability against what the user's account supports
|
|
183
184
|
let validatedModel: string | undefined;
|
|
@@ -188,8 +189,10 @@ export async function* sendCodexMessage(
|
|
|
188
189
|
const availableIds = new Set(
|
|
189
190
|
(modelResponse.models ?? []).map((m: { id: string }) => m.id)
|
|
190
191
|
);
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
const requestedModelId =
|
|
193
|
+
targetOverride?.effectiveModelId ?? conversation.modelId;
|
|
194
|
+
if (requestedModelId && availableIds.has(requestedModelId)) {
|
|
195
|
+
validatedModel = requestedModelId;
|
|
193
196
|
}
|
|
194
197
|
// If not available, validatedModel stays undefined → Codex uses its default
|
|
195
198
|
} catch {
|
|
@@ -377,7 +380,18 @@ export async function* sendCodexMessage(
|
|
|
377
380
|
|
|
378
381
|
// Save usage metadata
|
|
379
382
|
const metadata = JSON.stringify({
|
|
380
|
-
modelId:
|
|
383
|
+
modelId:
|
|
384
|
+
usage.modelId ??
|
|
385
|
+
targetOverride?.effectiveModelId ??
|
|
386
|
+
conversation.modelId,
|
|
387
|
+
runtimeId,
|
|
388
|
+
requestedRuntimeId:
|
|
389
|
+
targetOverride?.requestedRuntimeId ?? conversation.runtimeId,
|
|
390
|
+
requestedModelId:
|
|
391
|
+
targetOverride?.requestedModelId ?? conversation.modelId,
|
|
392
|
+
...(targetOverride?.fallbackReason
|
|
393
|
+
? { fallbackReason: targetOverride.fallbackReason }
|
|
394
|
+
: {}),
|
|
381
395
|
inputTokens: usage.inputTokens,
|
|
382
396
|
outputTokens: usage.outputTokens,
|
|
383
397
|
...(quickAccess.length > 0 ? { quickAccess } : {}),
|
|
@@ -394,7 +408,11 @@ export async function* sendCodexMessage(
|
|
|
394
408
|
activityType: "chat_turn",
|
|
395
409
|
runtimeId,
|
|
396
410
|
providerId,
|
|
397
|
-
modelId:
|
|
411
|
+
modelId:
|
|
412
|
+
usage.modelId ??
|
|
413
|
+
targetOverride?.effectiveModelId ??
|
|
414
|
+
conversation.modelId ??
|
|
415
|
+
null,
|
|
398
416
|
inputTokens: usage.inputTokens ?? null,
|
|
399
417
|
outputTokens: usage.outputTokens ?? null,
|
|
400
418
|
totalTokens: usage.totalTokens ?? null,
|
|
@@ -414,7 +432,11 @@ export async function* sendCodexMessage(
|
|
|
414
432
|
activityType: "chat_turn",
|
|
415
433
|
runtimeId,
|
|
416
434
|
providerId,
|
|
417
|
-
modelId:
|
|
435
|
+
modelId:
|
|
436
|
+
usage.modelId ??
|
|
437
|
+
targetOverride?.effectiveModelId ??
|
|
438
|
+
conversation.modelId ??
|
|
439
|
+
null,
|
|
418
440
|
inputTokens: usage.inputTokens ?? null,
|
|
419
441
|
outputTokens: usage.outputTokens ?? null,
|
|
420
442
|
totalTokens: usage.totalTokens ?? null,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ToolCatalogEntry, ToolGroup } from "./tool-catalog";
|
|
2
|
+
|
|
3
|
+
export const COMMAND_TAB_IDS = ["actions", "skills", "tools", "entities"] as const;
|
|
4
|
+
export type CommandTabId = (typeof COMMAND_TAB_IDS)[number];
|
|
5
|
+
|
|
6
|
+
export interface CommandTab {
|
|
7
|
+
id: CommandTabId;
|
|
8
|
+
label: string;
|
|
9
|
+
shortcut: string; // ⌘1..⌘4
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const COMMAND_TABS: CommandTab[] = [
|
|
13
|
+
{ id: "actions", label: "Actions", shortcut: "⌘1" },
|
|
14
|
+
{ id: "skills", label: "Skills", shortcut: "⌘2" },
|
|
15
|
+
{ id: "tools", label: "Tools", shortcut: "⌘3" },
|
|
16
|
+
{ id: "entities", label: "Entities", shortcut: "⌘4" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_COMMAND_TAB: CommandTabId = "actions";
|
|
20
|
+
|
|
21
|
+
export const GROUP_TO_TAB = {
|
|
22
|
+
// Stagent actions / session primitives
|
|
23
|
+
Session: "actions",
|
|
24
|
+
Tasks: "actions",
|
|
25
|
+
Projects: "actions",
|
|
26
|
+
Workflows: "actions",
|
|
27
|
+
Schedules: "actions",
|
|
28
|
+
Documents: "actions",
|
|
29
|
+
Tables: "actions",
|
|
30
|
+
Notifications: "actions",
|
|
31
|
+
Profiles: "actions",
|
|
32
|
+
Usage: "actions",
|
|
33
|
+
Settings: "actions",
|
|
34
|
+
Chat: "actions",
|
|
35
|
+
// Skills
|
|
36
|
+
Skills: "skills",
|
|
37
|
+
// Tools (filesystem / system / utility)
|
|
38
|
+
Browser: "tools",
|
|
39
|
+
Utility: "tools",
|
|
40
|
+
} satisfies Record<ToolGroup, CommandTabId>;
|
|
41
|
+
|
|
42
|
+
export function isCommandTabId(value: string): value is CommandTabId {
|
|
43
|
+
return (COMMAND_TAB_IDS as readonly string[]).includes(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PartitionedCatalog {
|
|
47
|
+
actions: ToolCatalogEntry[];
|
|
48
|
+
skills: ToolCatalogEntry[];
|
|
49
|
+
tools: ToolCatalogEntry[];
|
|
50
|
+
entities: ToolCatalogEntry[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function partitionCatalogByTab(
|
|
54
|
+
catalog: ToolCatalogEntry[]
|
|
55
|
+
): PartitionedCatalog {
|
|
56
|
+
const out: PartitionedCatalog = { actions: [], skills: [], tools: [], entities: [] };
|
|
57
|
+
for (const entry of catalog) {
|
|
58
|
+
out[GROUP_TO_TAB[entry.group]].push(entry);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
@@ -5,6 +5,8 @@ import { getMessages } from "@/lib/data/chat";
|
|
|
5
5
|
import { getProfile } from "@/lib/agents/profiles/registry";
|
|
6
6
|
import { STAGENT_SYSTEM_PROMPT } from "./system-prompt";
|
|
7
7
|
import type { WorkspaceContext } from "@/lib/environment/workspace-context";
|
|
8
|
+
import { expandFileMention } from "./files/expand-mention";
|
|
9
|
+
import { conversations } from "@/lib/db/schema";
|
|
8
10
|
|
|
9
11
|
// ── Token budget constants ─────────────────────────────────────────────
|
|
10
12
|
|
|
@@ -50,6 +52,121 @@ function buildTier0(
|
|
|
50
52
|
return parts.join("\n");
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// ── Active skill injection (Ollama-first, runtime-agnostic) ────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Token budget for a conversation-bound skill's SKILL.md content.
|
|
59
|
+
*
|
|
60
|
+
* Per spec §7.1: 1000-4000 tokens typical, with 300 tokens of index/
|
|
61
|
+
* metadata on top. We cap at ~4000 tokens (≈16K chars) so a large skill
|
|
62
|
+
* can't blow out a small-context local model. Single-active-skill is
|
|
63
|
+
* enforced at the MCP-tool layer.
|
|
64
|
+
*/
|
|
65
|
+
const ACTIVE_SKILL_BUDGET = 4_000;
|
|
66
|
+
|
|
67
|
+
interface ActiveSkillSection {
|
|
68
|
+
name: string;
|
|
69
|
+
text: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderActiveSkillSections(
|
|
73
|
+
kept: ActiveSkillSection[],
|
|
74
|
+
omitted: ActiveSkillSection[]
|
|
75
|
+
): string {
|
|
76
|
+
if (kept.length === 0) return "";
|
|
77
|
+
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
if (omitted.length > 0) {
|
|
80
|
+
const label = omitted.length === 1 ? "skill" : "skills";
|
|
81
|
+
parts.push(
|
|
82
|
+
`## Active Skill Note\nOmitted ${omitted.length} older active ${label} to fit the prompt budget: ${omitted
|
|
83
|
+
.map((section) => section.name)
|
|
84
|
+
.join(", ")}.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
parts.push(...kept.map((section) => section.text));
|
|
88
|
+
return parts.join("\n\n---\n\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the "Active Skill" section of the system prompt, if one is bound
|
|
93
|
+
* to the conversation via `conversations.active_skill_id`. Returns "" for
|
|
94
|
+
* conversations without an active skill.
|
|
95
|
+
*
|
|
96
|
+
* Primary use case: Ollama has no SDK-native skill support, so this is
|
|
97
|
+
* how SKILL.md reaches a local model. Claude and Codex runtimes can
|
|
98
|
+
* also bind a skill via this path alongside their native Skill tools.
|
|
99
|
+
*
|
|
100
|
+
* See `features/chat-ollama-native-skills.md`.
|
|
101
|
+
*/
|
|
102
|
+
async function buildActiveSkill(conversationId: string): Promise<string> {
|
|
103
|
+
const row = await db
|
|
104
|
+
.select({
|
|
105
|
+
activeSkillId: conversations.activeSkillId,
|
|
106
|
+
activeSkillIds: conversations.activeSkillIds,
|
|
107
|
+
runtimeId: conversations.runtimeId,
|
|
108
|
+
})
|
|
109
|
+
.from(conversations)
|
|
110
|
+
.where(eq(conversations.id, conversationId))
|
|
111
|
+
.get();
|
|
112
|
+
|
|
113
|
+
// Merge legacy single-active + new composed array. Dynamic import to
|
|
114
|
+
// avoid loading the chat tools module on the hot path / risk import
|
|
115
|
+
// cycles per the runtime-catalog smoke-test budget rule in MEMORY.md.
|
|
116
|
+
const { mergeActiveSkillIds } = await import("@/lib/chat/active-skills");
|
|
117
|
+
const merged = mergeActiveSkillIds(row?.activeSkillId, row?.activeSkillIds);
|
|
118
|
+
if (merged.length === 0) return "";
|
|
119
|
+
|
|
120
|
+
// Composition (any entry in the new activeSkillIds column) is an
|
|
121
|
+
// explicit user opt-in to override the SDK-native default. Without
|
|
122
|
+
// this carve-out, composed skills would silently no-op on Claude/
|
|
123
|
+
// Codex where stagentInjectsSkills=false. When only the legacy
|
|
124
|
+
// activeSkillId is set, fall back to the original capability gate
|
|
125
|
+
// (Ollama-only injection).
|
|
126
|
+
const isComposed = (row?.activeSkillIds?.length ?? 0) > 0;
|
|
127
|
+
|
|
128
|
+
if (!isComposed && row?.runtimeId) {
|
|
129
|
+
try {
|
|
130
|
+
const { getRuntimeFeatures } = await import("@/lib/agents/runtime/catalog");
|
|
131
|
+
const features = getRuntimeFeatures(
|
|
132
|
+
row.runtimeId as Parameters<typeof getRuntimeFeatures>[0]
|
|
133
|
+
);
|
|
134
|
+
if (!features.stagentInjectsSkills) return "";
|
|
135
|
+
} catch {
|
|
136
|
+
// Unknown runtime — fall through and inject (safer default than
|
|
137
|
+
// silently dropping the skill on an unrecognized runtime id).
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Dynamic import keeps the scanner + fs dependency off the hot path for
|
|
142
|
+
// conversations that don't have an active skill (the common case).
|
|
143
|
+
const { getSkill } = await import("@/lib/environment/list-skills");
|
|
144
|
+
const sections: ActiveSkillSection[] = [];
|
|
145
|
+
for (const id of merged) {
|
|
146
|
+
const skill = getSkill(id);
|
|
147
|
+
if (!skill) continue;
|
|
148
|
+
sections.push({
|
|
149
|
+
name: skill.name,
|
|
150
|
+
text: `## Active Skill: ${skill.name}\n\n${skill.content}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (sections.length === 0) return "";
|
|
154
|
+
|
|
155
|
+
const kept = [...sections];
|
|
156
|
+
const omitted: ActiveSkillSection[] = [];
|
|
157
|
+
while (
|
|
158
|
+
kept.length > 1 &&
|
|
159
|
+
estimateTokens(renderActiveSkillSections(kept, omitted)) > ACTIVE_SKILL_BUDGET
|
|
160
|
+
) {
|
|
161
|
+
const oldest = kept.shift();
|
|
162
|
+
if (oldest) omitted.push(oldest);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const combined = renderActiveSkillSections(kept, omitted);
|
|
166
|
+
if (estimateTokens(combined) <= ACTIVE_SKILL_BUDGET) return combined;
|
|
167
|
+
return truncateToTokenBudget(combined, ACTIVE_SKILL_BUDGET);
|
|
168
|
+
}
|
|
169
|
+
|
|
53
170
|
// ── Tier 1: Conversation history ───────────────────────────────────────
|
|
54
171
|
|
|
55
172
|
interface HistoryMessage {
|
|
@@ -108,7 +225,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
108
225
|
if (recentTasks.length > 0) {
|
|
109
226
|
parts.push("\n### Recent Tasks");
|
|
110
227
|
for (const t of recentTasks) {
|
|
111
|
-
parts.push(`- [${t.status}] ${t.title} (id: ${t.id
|
|
228
|
+
parts.push(`- [${t.status}] ${t.title} (id: ${t.id})`);
|
|
112
229
|
}
|
|
113
230
|
}
|
|
114
231
|
|
|
@@ -123,7 +240,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
123
240
|
if (activeWorkflows.length > 0) {
|
|
124
241
|
parts.push("\n### Workflows");
|
|
125
242
|
for (const w of activeWorkflows) {
|
|
126
|
-
parts.push(`- [${w.status}] ${w.name} (id: ${w.id
|
|
243
|
+
parts.push(`- [${w.status}] ${w.name} (id: ${w.id})`);
|
|
127
244
|
}
|
|
128
245
|
}
|
|
129
246
|
|
|
@@ -137,7 +254,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
137
254
|
if (docs.length > 0) {
|
|
138
255
|
parts.push(`\n### Documents (${docs.length})`);
|
|
139
256
|
for (const d of docs) {
|
|
140
|
-
parts.push(`- ${d.filename} (id: ${d.id
|
|
257
|
+
parts.push(`- ${d.filename} (id: ${d.id})`);
|
|
141
258
|
}
|
|
142
259
|
}
|
|
143
260
|
|
|
@@ -278,6 +395,23 @@ async function buildTier3(mentions: MentionReference[]): Promise<string> {
|
|
|
278
395
|
}
|
|
279
396
|
break;
|
|
280
397
|
}
|
|
398
|
+
case "file": {
|
|
399
|
+
// `entityId` is a relative path scoped to the active project's
|
|
400
|
+
// workingDirectory (preferred) or the stagent launch cwd (fallback).
|
|
401
|
+
// Security is enforced inside expandFileMention — the caller cannot
|
|
402
|
+
// influence cwd.
|
|
403
|
+
const { getLaunchCwd } = await import("@/lib/environment/workspace-context");
|
|
404
|
+
let cwd = getLaunchCwd();
|
|
405
|
+
// If the mention has a known project context in scope, prefer the
|
|
406
|
+
// project's workingDirectory. We don't have it at this scope today,
|
|
407
|
+
// so launch cwd is the safe default — matches the API route.
|
|
408
|
+
// (Future: plumb projectId into buildTier3 so file expansion honors
|
|
409
|
+
// per-project cwds exactly the same way as the search API.)
|
|
410
|
+
void cwd;
|
|
411
|
+
cwd = getLaunchCwd();
|
|
412
|
+
parts.push(...expandFileMention(mention.entityId, cwd));
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
281
415
|
}
|
|
282
416
|
}
|
|
283
417
|
|
|
@@ -285,6 +419,7 @@ async function buildTier3(mentions: MentionReference[]): Promise<string> {
|
|
|
285
419
|
return truncateToTokenBudget(text, TIER_3_BUDGET);
|
|
286
420
|
}
|
|
287
421
|
|
|
422
|
+
|
|
288
423
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
289
424
|
|
|
290
425
|
export interface ChatContext {
|
|
@@ -303,15 +438,22 @@ export async function buildChatContext(opts: {
|
|
|
303
438
|
workspace?: WorkspaceContext | null;
|
|
304
439
|
mentions?: MentionReference[];
|
|
305
440
|
}): Promise<ChatContext> {
|
|
306
|
-
const [history, tier2, tier3] = await Promise.all([
|
|
441
|
+
const [history, tier2, tier3, activeSkill] = await Promise.all([
|
|
307
442
|
buildTier1(opts.conversationId),
|
|
308
443
|
buildTier2(opts.projectId),
|
|
309
444
|
buildTier3(opts.mentions ?? []),
|
|
445
|
+
buildActiveSkill(opts.conversationId),
|
|
310
446
|
]);
|
|
311
447
|
|
|
312
448
|
const tier0 = buildTier0(opts.projectName, opts.workspace);
|
|
313
449
|
|
|
314
450
|
const systemParts = [tier0];
|
|
451
|
+
|
|
452
|
+
// Active skill (from conversations.active_skill_id) sits right below
|
|
453
|
+
// Tier 0 so its instructions carry the most weight. Empty string when
|
|
454
|
+
// no skill is bound — common case.
|
|
455
|
+
if (activeSkill) systemParts.push(activeSkill);
|
|
456
|
+
|
|
315
457
|
if (tier3) systemParts.push(tier3);
|
|
316
458
|
if (tier2) systemParts.push(tier2);
|
|
317
459
|
|