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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { toast } from "sonner";
|
|
5
6
|
import {
|
|
6
7
|
Command,
|
|
7
8
|
CommandEmpty,
|
|
@@ -15,19 +16,40 @@ import {
|
|
|
15
16
|
ListTodo,
|
|
16
17
|
GitBranch,
|
|
17
18
|
FileText,
|
|
19
|
+
FileCode,
|
|
18
20
|
Bot,
|
|
19
21
|
Clock,
|
|
20
22
|
Loader2,
|
|
23
|
+
Pin,
|
|
24
|
+
PinOff,
|
|
25
|
+
Bookmark,
|
|
21
26
|
} from "lucide-react";
|
|
27
|
+
import { Button } from "@/components/ui/button";
|
|
22
28
|
import type { LucideIcon } from "lucide-react";
|
|
23
29
|
import {
|
|
24
30
|
getToolCatalogWithSkills,
|
|
25
31
|
groupToolCatalog,
|
|
26
32
|
TOOL_GROUP_ICONS,
|
|
27
|
-
TOOL_GROUP_ORDER,
|
|
28
|
-
type ToolCatalogEntry,
|
|
29
33
|
} from "@/lib/chat/tool-catalog";
|
|
30
34
|
import type { AutocompleteMode, EntitySearchResult } from "@/hooks/use-chat-autocomplete";
|
|
35
|
+
import { CommandTabBar } from "./command-tab-bar";
|
|
36
|
+
import { partitionCatalogByTab, type CommandTabId } from "@/lib/chat/command-tabs";
|
|
37
|
+
import { useEnrichedSkills } from "@/hooks/use-enriched-skills";
|
|
38
|
+
import { useRecentUserMessages } from "@/hooks/use-recent-user-messages";
|
|
39
|
+
import { SkillRow } from "./skill-row";
|
|
40
|
+
import { computeRecommendation } from "@/lib/environment/skill-recommendations";
|
|
41
|
+
import { browserLocalStore, activeDismissedIds, saveDismissal } from "@/lib/chat/dismissals";
|
|
42
|
+
import { useChatSession } from "@/components/chat/chat-session-provider";
|
|
43
|
+
import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
|
|
44
|
+
import { parseFilterInput, matchesClauses } from "@/lib/filters/parse";
|
|
45
|
+
import type { FilterClause } from "@/lib/filters/parse";
|
|
46
|
+
import { FilterHint } from "@/components/shared/filter-hint";
|
|
47
|
+
import { cleanFilterInput } from "@/lib/chat/clean-filter-input";
|
|
48
|
+
import { usePinnedEntries, type PinnedEntry } from "@/hooks/use-pinned-entries";
|
|
49
|
+
import { useSavedSearches, type SavedSearch, type SavedSearchSurface } from "@/hooks/use-saved-searches";
|
|
50
|
+
import { useActiveSkills } from "@/hooks/use-active-skills";
|
|
51
|
+
import { SkillCompositionConflictDialog } from "./skill-composition-conflict-dialog";
|
|
52
|
+
import type { SkillConflict } from "@/lib/chat/skill-conflict";
|
|
31
53
|
|
|
32
54
|
interface ChatCommandPopoverProps {
|
|
33
55
|
open: boolean;
|
|
@@ -37,6 +59,8 @@ interface ChatCommandPopoverProps {
|
|
|
37
59
|
entityResults: EntitySearchResult[];
|
|
38
60
|
entityLoading: boolean;
|
|
39
61
|
projectProfiles?: Array<{ id: string; name: string; description: string }>;
|
|
62
|
+
activeTab: CommandTabId;
|
|
63
|
+
onTabChange: (tab: CommandTabId) => void;
|
|
40
64
|
onSelect: (item: {
|
|
41
65
|
type: "slash" | "mention";
|
|
42
66
|
id: string;
|
|
@@ -46,6 +70,9 @@ interface ChatCommandPopoverProps {
|
|
|
46
70
|
entityId?: string;
|
|
47
71
|
}) => void;
|
|
48
72
|
onClose: () => void;
|
|
73
|
+
onApplySavedSearch?: (filterInput: string) => void;
|
|
74
|
+
/** Active conversation id — used for skill composition HTTP calls. */
|
|
75
|
+
conversationId?: string | null;
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
const ENTITY_ICONS: Record<string, LucideIcon> = {
|
|
@@ -55,6 +82,7 @@ const ENTITY_ICONS: Record<string, LucideIcon> = {
|
|
|
55
82
|
document: FileText,
|
|
56
83
|
profile: Bot,
|
|
57
84
|
schedule: Clock,
|
|
85
|
+
file: FileCode,
|
|
58
86
|
};
|
|
59
87
|
|
|
60
88
|
const ENTITY_LABELS: Record<string, string> = {
|
|
@@ -64,6 +92,7 @@ const ENTITY_LABELS: Record<string, string> = {
|
|
|
64
92
|
document: "Documents",
|
|
65
93
|
profile: "Profiles",
|
|
66
94
|
schedule: "Schedules",
|
|
95
|
+
file: "Files",
|
|
67
96
|
};
|
|
68
97
|
|
|
69
98
|
function groupByType(results: EntitySearchResult[]): Record<string, EntitySearchResult[]> {
|
|
@@ -83,8 +112,12 @@ export function ChatCommandPopover({
|
|
|
83
112
|
entityResults,
|
|
84
113
|
entityLoading,
|
|
85
114
|
projectProfiles,
|
|
115
|
+
activeTab,
|
|
116
|
+
onTabChange,
|
|
86
117
|
onSelect,
|
|
87
118
|
onClose,
|
|
119
|
+
onApplySavedSearch,
|
|
120
|
+
conversationId,
|
|
88
121
|
}: ChatCommandPopoverProps) {
|
|
89
122
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
90
123
|
|
|
@@ -100,6 +133,156 @@ export function ChatCommandPopover({
|
|
|
100
133
|
return () => document.removeEventListener("mousedown", handleClick);
|
|
101
134
|
}, [open, onClose]);
|
|
102
135
|
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// Skill composition state
|
|
138
|
+
// -----------------------------------------------------------------------
|
|
139
|
+
const { activeIds, supportsComposition, maxActive, refetch: refetchActive } =
|
|
140
|
+
useActiveSkills(conversationId ?? null);
|
|
141
|
+
|
|
142
|
+
// When activate returns requiresConfirmation, we stash the pending request
|
|
143
|
+
// and open the conflict dialog for the user to decide.
|
|
144
|
+
const [pendingAdd, setPendingAdd] = useState<{
|
|
145
|
+
skillId: string;
|
|
146
|
+
skillName: string;
|
|
147
|
+
conflicts: SkillConflict[];
|
|
148
|
+
} | null>(null);
|
|
149
|
+
|
|
150
|
+
const callActivate = useCallback(
|
|
151
|
+
async (skillId: string, skillName: string, mode: "replace" | "add", force = false) => {
|
|
152
|
+
if (!conversationId) return;
|
|
153
|
+
const r = await fetch(
|
|
154
|
+
`/api/chat/conversations/${conversationId}/skills/activate`,
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({ skillId, mode, force }),
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
if (!r.ok) {
|
|
162
|
+
const body = await r.json().catch(() => ({})) as Record<string, unknown>;
|
|
163
|
+
toast.error(typeof body.error === "string" ? body.error : "Failed to add skill");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const body = await r.json() as Record<string, unknown>;
|
|
167
|
+
if (body.requiresConfirmation) {
|
|
168
|
+
setPendingAdd({
|
|
169
|
+
skillId,
|
|
170
|
+
skillName,
|
|
171
|
+
conflicts: (body.conflicts as SkillConflict[]) ?? [],
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await refetchActive();
|
|
176
|
+
const activeCount = Array.isArray(body.activeSkillIds)
|
|
177
|
+
? (body.activeSkillIds as string[]).length
|
|
178
|
+
: 1;
|
|
179
|
+
toast.success(`Added ${skillName} — ${activeCount} skill${activeCount !== 1 ? "s" : ""} active`);
|
|
180
|
+
},
|
|
181
|
+
[conversationId, refetchActive]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const callDeactivate = useCallback(async () => {
|
|
185
|
+
if (!conversationId) return;
|
|
186
|
+
await fetch(`/api/chat/conversations/${conversationId}/skills/deactivate`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
});
|
|
189
|
+
await refetchActive();
|
|
190
|
+
}, [conversationId, refetchActive]);
|
|
191
|
+
|
|
192
|
+
// Enriched skills — only fetch when popover is open in slash mode
|
|
193
|
+
const enrichedSkills = useEnrichedSkills(open && mode === "slash");
|
|
194
|
+
|
|
195
|
+
// Session context for recommendation
|
|
196
|
+
const { activeId } = useChatSession();
|
|
197
|
+
const recentMessages = useRecentUserMessages(activeId, 20);
|
|
198
|
+
|
|
199
|
+
const dismissStore = useMemo(
|
|
200
|
+
() => browserLocalStore("stagent.chat.dismissed-suggestions"),
|
|
201
|
+
[]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const [dismissTick, setDismissTick] = useState(0);
|
|
205
|
+
|
|
206
|
+
const dismissedIds = useMemo(
|
|
207
|
+
() =>
|
|
208
|
+
activeId
|
|
209
|
+
? activeDismissedIds(dismissStore, activeId)
|
|
210
|
+
: new Set<string>(),
|
|
211
|
+
[dismissStore, activeId, dismissTick]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const recommended = useMemo(
|
|
215
|
+
() =>
|
|
216
|
+
computeRecommendation(enrichedSkills, recentMessages, {
|
|
217
|
+
dismissedIds,
|
|
218
|
+
}),
|
|
219
|
+
[enrichedSkills, recentMessages, dismissedIds]
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Pinned entries persist under settings.chat.pinnedEntries. Hook self-
|
|
223
|
+
// fetches on mount and sends optimistic PUTs on mutation.
|
|
224
|
+
const { pins, isPinned, pin, unpin } = usePinnedEntries();
|
|
225
|
+
|
|
226
|
+
// Parse `#key:value` filter clauses from the query. Relevant for mention
|
|
227
|
+
// mode — slash mode does its own tab-based grouping and doesn't currently
|
|
228
|
+
// consume free-text filters.
|
|
229
|
+
const parsed = useMemo(() => parseFilterInput(query), [query]);
|
|
230
|
+
|
|
231
|
+
// Pre-filter entity results by known filter keys. Unknown keys pass through
|
|
232
|
+
// per the parser contract (silently skipped). cmdk still runs its own
|
|
233
|
+
// fuzzy match on top using `parsed.rawQuery`.
|
|
234
|
+
const filteredEntityResults = useMemo(() => {
|
|
235
|
+
if (parsed.clauses.length === 0) return entityResults;
|
|
236
|
+
return entityResults.filter((r) =>
|
|
237
|
+
matchesClauses(r, parsed.clauses, {
|
|
238
|
+
// `#status:blocked` — case-insensitive substring match so partial
|
|
239
|
+
// values like `#status:block` also hit (helps while typing).
|
|
240
|
+
status: (item, value) =>
|
|
241
|
+
typeof item.status === "string" &&
|
|
242
|
+
item.status.toLowerCase().includes(value.toLowerCase()),
|
|
243
|
+
// `#type:task` — exact match on the entity-type discriminator.
|
|
244
|
+
type: (item, value) =>
|
|
245
|
+
item.entityType.toLowerCase() === value.toLowerCase(),
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
}, [entityResults, parsed.clauses]);
|
|
249
|
+
|
|
250
|
+
const { forSurface, save } = useSavedSearches();
|
|
251
|
+
|
|
252
|
+
// Surface inference for saved-search scoping. In mention mode, look at
|
|
253
|
+
// the first filtered entity result's type; fall back to "task" when the
|
|
254
|
+
// list is empty so the "Save this view" button still has a valid target.
|
|
255
|
+
// Slash-mode surface inference is deferred — Saved group renders in
|
|
256
|
+
// mention mode only in v2.
|
|
257
|
+
const currentSurface: SavedSearchSurface = useMemo(() => {
|
|
258
|
+
if (mode !== "mention") return "task";
|
|
259
|
+
const firstType = filteredEntityResults[0]?.entityType as SavedSearchSurface | undefined;
|
|
260
|
+
if (firstType && ["task", "project", "workflow", "document", "skill", "profile"].includes(firstType)) {
|
|
261
|
+
return firstType;
|
|
262
|
+
}
|
|
263
|
+
return "task";
|
|
264
|
+
}, [mode, filteredEntityResults]);
|
|
265
|
+
|
|
266
|
+
const savedForSurface = useMemo(
|
|
267
|
+
() => forSurface(currentSurface),
|
|
268
|
+
[forSurface, currentSurface]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Filter enriched skills by `#scope:` and `#type:` clauses so users can
|
|
272
|
+
// narrow the skills tab with e.g. `/skills #scope:project` or
|
|
273
|
+
// `/skills #type:claude-agent-sdk`. Unknown clauses pass through silently.
|
|
274
|
+
const filteredEnrichedSkills = useMemo(() => {
|
|
275
|
+
if (parsed.clauses.length === 0) return enrichedSkills;
|
|
276
|
+
return enrichedSkills.filter((skill) =>
|
|
277
|
+
matchesClauses(skill, parsed.clauses, {
|
|
278
|
+
// `#scope:project` / `#scope:user` — exact case-insensitive match.
|
|
279
|
+
scope: (s, v) => s.scope.toLowerCase() === v.toLowerCase(),
|
|
280
|
+
// `#type:skill-name` — substring match on the tool name.
|
|
281
|
+
type: (s, v) => (s.tool ?? "").toLowerCase().includes(v.toLowerCase()),
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
}, [enrichedSkills, parsed.clauses]);
|
|
285
|
+
|
|
103
286
|
if (!open || !anchorRect || !mode) return null;
|
|
104
287
|
|
|
105
288
|
// Position above the caret
|
|
@@ -116,60 +299,281 @@ export function ChatCommandPopover({
|
|
|
116
299
|
ref={containerRef}
|
|
117
300
|
style={style}
|
|
118
301
|
data-chat-autocomplete=""
|
|
119
|
-
className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0
|
|
302
|
+
className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0"
|
|
120
303
|
>
|
|
121
304
|
<Command shouldFilter loop>
|
|
122
|
-
{/* Hidden input for cmdk filtering
|
|
305
|
+
{/* Hidden input for cmdk filtering. In mention mode we pass the
|
|
306
|
+
filter-stripped `rawQuery` so cmdk's fuzzy match doesn't see
|
|
307
|
+
`#key:value` tokens and score every entity to zero. */}
|
|
123
308
|
<div className="sr-only">
|
|
124
|
-
<CommandInput value={query} />
|
|
309
|
+
<CommandInput value={mode === "mention" ? parsed.rawQuery : query} />
|
|
125
310
|
</div>
|
|
126
311
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
312
|
+
{mode === "slash" ? (
|
|
313
|
+
<>
|
|
314
|
+
<CommandTabBar activeTab={activeTab} onChange={onTabChange} />
|
|
315
|
+
{/* Active skill count indicator — shown only on the skills tab. */}
|
|
316
|
+
{activeTab === "skills" && conversationId && (
|
|
317
|
+
<div className="px-3 py-1.5 text-xs text-muted-foreground border-b">
|
|
318
|
+
{activeIds.length} of {maxActive} active
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
<CommandList className="max-h-[320px]">
|
|
322
|
+
<FilterHint inputValue={query} storageKey="stagent.filter-hint.dismissed" />
|
|
323
|
+
{activeTab !== "entities" && (
|
|
324
|
+
<CommandEmpty>No matching tools</CommandEmpty>
|
|
325
|
+
)}
|
|
326
|
+
<div
|
|
327
|
+
role="tabpanel"
|
|
328
|
+
id={`command-tabpanel-${activeTab}`}
|
|
329
|
+
aria-labelledby={`command-tab-${activeTab}`}
|
|
330
|
+
>
|
|
331
|
+
<ToolCatalogItems
|
|
332
|
+
onSelect={onSelect}
|
|
333
|
+
projectProfiles={projectProfiles}
|
|
334
|
+
activeTab={activeTab}
|
|
335
|
+
enrichedSkills={filteredEnrichedSkills}
|
|
336
|
+
totalSkillCount={enrichedSkills.length}
|
|
337
|
+
recommendedId={recommended?.id ?? null}
|
|
338
|
+
onDismissRecommendation={
|
|
339
|
+
activeId
|
|
340
|
+
? (skillId) => {
|
|
341
|
+
saveDismissal(dismissStore, activeId, skillId);
|
|
342
|
+
setDismissTick((t) => t + 1);
|
|
343
|
+
}
|
|
344
|
+
: undefined
|
|
345
|
+
}
|
|
346
|
+
activeSkillIds={activeIds}
|
|
347
|
+
supportsComposition={supportsComposition}
|
|
348
|
+
maxActive={maxActive}
|
|
349
|
+
onAddSkill={
|
|
350
|
+
conversationId
|
|
351
|
+
? (skillId, skillName) =>
|
|
352
|
+
callActivate(skillId, skillName, "add")
|
|
353
|
+
: undefined
|
|
354
|
+
}
|
|
355
|
+
onDeactivate={conversationId ? callDeactivate : undefined}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
</CommandList>
|
|
359
|
+
</>
|
|
360
|
+
) : (
|
|
361
|
+
<CommandList className="max-h-[320px]">
|
|
362
|
+
<CommandEmpty>No matching entities</CommandEmpty>
|
|
140
363
|
<MentionItems
|
|
141
|
-
results={
|
|
364
|
+
results={filteredEntityResults}
|
|
142
365
|
loading={entityLoading}
|
|
143
366
|
onSelect={onSelect}
|
|
367
|
+
pins={pins}
|
|
368
|
+
isPinned={isPinned}
|
|
369
|
+
onPin={pin}
|
|
370
|
+
onUnpin={unpin}
|
|
371
|
+
rawQuery={parsed.rawQuery}
|
|
372
|
+
savedSearches={savedForSurface}
|
|
373
|
+
onApplySavedSearch={(filterInput) => onApplySavedSearch?.(filterInput)}
|
|
374
|
+
clauses={parsed.clauses}
|
|
144
375
|
/>
|
|
145
|
-
|
|
146
|
-
|
|
376
|
+
{parsed.clauses.length > 0 && (() => {
|
|
377
|
+
// Persist the cleaned filterInput so saved searches don't
|
|
378
|
+
// carry the mention-trigger residue (e.g. `task: `) into
|
|
379
|
+
// their stored value. See features/saved-search-polish-v1.md.
|
|
380
|
+
const persistedFilterInput = cleanFilterInput(
|
|
381
|
+
parsed.clauses,
|
|
382
|
+
parsed.rawQuery
|
|
383
|
+
);
|
|
384
|
+
return (
|
|
385
|
+
<SaveViewFooter
|
|
386
|
+
surface={currentSurface}
|
|
387
|
+
clauses={parsed.clauses}
|
|
388
|
+
filterInput={persistedFilterInput}
|
|
389
|
+
onSave={(label) =>
|
|
390
|
+
save({
|
|
391
|
+
surface: currentSurface,
|
|
392
|
+
label:
|
|
393
|
+
label ||
|
|
394
|
+
parsed.clauses
|
|
395
|
+
.map((c) => `#${c.key}:${c.value}`)
|
|
396
|
+
.join(" "),
|
|
397
|
+
filterInput: persistedFilterInput,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
/>
|
|
401
|
+
);
|
|
402
|
+
})()}
|
|
403
|
+
</CommandList>
|
|
404
|
+
)}
|
|
147
405
|
</Command>
|
|
148
406
|
</div>
|
|
149
407
|
);
|
|
150
408
|
|
|
151
|
-
return
|
|
409
|
+
return (
|
|
410
|
+
<>
|
|
411
|
+
{createPortal(content, document.body)}
|
|
412
|
+
{pendingAdd && (
|
|
413
|
+
<SkillCompositionConflictDialog
|
|
414
|
+
open={!!pendingAdd}
|
|
415
|
+
onOpenChange={(o) => { if (!o) setPendingAdd(null); }}
|
|
416
|
+
newSkillName={pendingAdd.skillName}
|
|
417
|
+
conflicts={pendingAdd.conflicts}
|
|
418
|
+
onConfirm={() => {
|
|
419
|
+
void callActivate(pendingAdd.skillId, pendingAdd.skillName, "add", true);
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
)}
|
|
423
|
+
</>
|
|
424
|
+
);
|
|
152
425
|
}
|
|
153
426
|
|
|
154
427
|
function ToolCatalogItems({
|
|
155
428
|
onSelect,
|
|
156
429
|
projectProfiles,
|
|
430
|
+
activeTab,
|
|
431
|
+
enrichedSkills,
|
|
432
|
+
totalSkillCount,
|
|
433
|
+
recommendedId,
|
|
434
|
+
onDismissRecommendation,
|
|
435
|
+
activeSkillIds,
|
|
436
|
+
supportsComposition,
|
|
437
|
+
maxActive,
|
|
438
|
+
onAddSkill,
|
|
439
|
+
onDeactivate,
|
|
157
440
|
}: {
|
|
158
441
|
onSelect: ChatCommandPopoverProps["onSelect"];
|
|
159
442
|
projectProfiles?: ChatCommandPopoverProps["projectProfiles"];
|
|
443
|
+
activeTab: CommandTabId;
|
|
444
|
+
/** Filtered list of skills to render (may be a subset of all skills). */
|
|
445
|
+
enrichedSkills: EnrichedSkill[];
|
|
446
|
+
/** Total number of skills before any filter is applied — used for empty-state copy. */
|
|
447
|
+
totalSkillCount: number;
|
|
448
|
+
recommendedId?: string | null;
|
|
449
|
+
onDismissRecommendation?: (skillId: string) => void;
|
|
450
|
+
/** Currently active skill IDs for this conversation. */
|
|
451
|
+
activeSkillIds?: string[];
|
|
452
|
+
/** Whether the current runtime supports composing 2+ skills. */
|
|
453
|
+
supportsComposition?: boolean;
|
|
454
|
+
/** Max simultaneously-active skills for this runtime. */
|
|
455
|
+
maxActive?: number;
|
|
456
|
+
/** Called when the user clicks "+ Add" on an inactive skill. */
|
|
457
|
+
onAddSkill?: (skillId: string, skillName: string) => void;
|
|
458
|
+
/** Called when the user deactivates the current skill. */
|
|
459
|
+
onDeactivate?: () => void;
|
|
160
460
|
}) {
|
|
161
461
|
const catalog = getToolCatalogWithSkills({
|
|
162
462
|
includeBrowser: true,
|
|
163
463
|
projectProfiles,
|
|
164
464
|
});
|
|
165
|
-
const
|
|
465
|
+
const parts = partitionCatalogByTab(catalog);
|
|
466
|
+
const entries = parts[activeTab];
|
|
467
|
+
|
|
468
|
+
if (activeTab === "entities") {
|
|
469
|
+
return (
|
|
470
|
+
<div className="px-4 py-6 text-sm text-muted-foreground text-center">
|
|
471
|
+
Type <span className="font-mono text-foreground">@</span> to reference projects, tasks, documents, or files.
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// When the skills tab has enriched data, render the enriched list
|
|
477
|
+
if (activeTab === "skills" && enrichedSkills.length > 0) {
|
|
478
|
+
const activeSet = new Set(activeSkillIds ?? []);
|
|
479
|
+
const resolvedMax = maxActive ?? 1;
|
|
480
|
+
const atCapacity = (activeSkillIds?.length ?? 0) >= resolvedMax;
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<CommandGroup heading="Skills">
|
|
484
|
+
{enrichedSkills.map((skill) => {
|
|
485
|
+
const isActive = activeSet.has(skill.id);
|
|
486
|
+
// Show "+ Add" only when composition is available, slot is free, and
|
|
487
|
+
// we have a conversationId to POST to.
|
|
488
|
+
const canAdd =
|
|
489
|
+
!isActive &&
|
|
490
|
+
supportsComposition &&
|
|
491
|
+
!atCapacity &&
|
|
492
|
+
!!onAddSkill;
|
|
493
|
+
// Show disabled "+" when at capacity or runtime doesn't support composition.
|
|
494
|
+
const showDisabled = !isActive && !canAdd && !!onAddSkill;
|
|
495
|
+
const disabledReason = atCapacity
|
|
496
|
+
? `Max ${resolvedMax} skills active`
|
|
497
|
+
: "Single skill only on this runtime — switch runtime to compose";
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<SkillRow
|
|
501
|
+
key={skill.id}
|
|
502
|
+
skill={skill}
|
|
503
|
+
recommended={recommendedId === skill.id}
|
|
504
|
+
onDismissRecommendation={
|
|
505
|
+
recommendedId === skill.id
|
|
506
|
+
? () => onDismissRecommendation?.(skill.id)
|
|
507
|
+
: undefined
|
|
508
|
+
}
|
|
509
|
+
onSelect={() =>
|
|
510
|
+
onSelect({
|
|
511
|
+
type: "slash",
|
|
512
|
+
id: skill.name,
|
|
513
|
+
label: skill.name,
|
|
514
|
+
text: `Use the ${skill.name} profile: `,
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
isActive={isActive}
|
|
518
|
+
addButton={
|
|
519
|
+
canAdd ? (
|
|
520
|
+
<Button
|
|
521
|
+
variant="ghost"
|
|
522
|
+
size="sm"
|
|
523
|
+
className="ml-auto h-6 px-2 text-[10px] shrink-0"
|
|
524
|
+
onMouseDown={(e) => {
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
e.stopPropagation();
|
|
527
|
+
}}
|
|
528
|
+
onClick={(e) => {
|
|
529
|
+
e.stopPropagation();
|
|
530
|
+
onAddSkill(skill.id, skill.name);
|
|
531
|
+
}}
|
|
532
|
+
>
|
|
533
|
+
+ Add
|
|
534
|
+
</Button>
|
|
535
|
+
) : showDisabled ? (
|
|
536
|
+
<Button
|
|
537
|
+
variant="ghost"
|
|
538
|
+
size="sm"
|
|
539
|
+
disabled
|
|
540
|
+
aria-label={disabledReason}
|
|
541
|
+
title={disabledReason}
|
|
542
|
+
className="ml-auto h-6 px-2 text-[10px] shrink-0"
|
|
543
|
+
>
|
|
544
|
+
+ Add
|
|
545
|
+
</Button>
|
|
546
|
+
) : undefined
|
|
547
|
+
}
|
|
548
|
+
onDeactivate={isActive && onDeactivate ? onDeactivate : undefined}
|
|
549
|
+
/>
|
|
550
|
+
);
|
|
551
|
+
})}
|
|
552
|
+
</CommandGroup>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (entries.length === 0) {
|
|
557
|
+
return (
|
|
558
|
+
<div className="px-4 py-6 text-sm text-muted-foreground text-center">
|
|
559
|
+
{activeTab === "skills"
|
|
560
|
+
? totalSkillCount > 0
|
|
561
|
+
? "No skills match these filters."
|
|
562
|
+
: "No skills available yet."
|
|
563
|
+
: "Nothing here."}
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const groups = groupToolCatalog(entries);
|
|
569
|
+
const groupNames = Object.keys(groups);
|
|
166
570
|
|
|
167
571
|
return (
|
|
168
572
|
<>
|
|
169
|
-
{
|
|
573
|
+
{groupNames.map((groupName) => {
|
|
170
574
|
const items = groups[groupName];
|
|
171
575
|
if (!items?.length) return null;
|
|
172
|
-
const GroupIcon = TOOL_GROUP_ICONS[groupName];
|
|
576
|
+
const GroupIcon = TOOL_GROUP_ICONS[groupName as keyof typeof TOOL_GROUP_ICONS] ?? FileText;
|
|
173
577
|
return (
|
|
174
578
|
<CommandGroup key={groupName} heading={groupName}>
|
|
175
579
|
{items.map((entry) => (
|
|
@@ -184,8 +588,8 @@ function ToolCatalogItems({
|
|
|
184
588
|
text: entry.behavior === "execute_immediately"
|
|
185
589
|
? entry.name
|
|
186
590
|
: entry.group === "Skills"
|
|
187
|
-
|
|
188
|
-
|
|
591
|
+
? `Use the ${entry.name} profile: `
|
|
592
|
+
: `Use ${entry.name} to `,
|
|
189
593
|
})
|
|
190
594
|
}
|
|
191
595
|
>
|
|
@@ -214,10 +618,26 @@ function MentionItems({
|
|
|
214
618
|
results,
|
|
215
619
|
loading,
|
|
216
620
|
onSelect,
|
|
621
|
+
pins,
|
|
622
|
+
isPinned,
|
|
623
|
+
onPin,
|
|
624
|
+
onUnpin,
|
|
625
|
+
rawQuery,
|
|
626
|
+
savedSearches,
|
|
627
|
+
onApplySavedSearch,
|
|
628
|
+
clauses,
|
|
217
629
|
}: {
|
|
218
630
|
results: EntitySearchResult[];
|
|
219
631
|
loading: boolean;
|
|
220
632
|
onSelect: ChatCommandPopoverProps["onSelect"];
|
|
633
|
+
pins: PinnedEntry[];
|
|
634
|
+
isPinned: (id: string) => boolean;
|
|
635
|
+
onPin: (entry: Omit<PinnedEntry, "pinnedAt">) => void;
|
|
636
|
+
onUnpin: (id: string) => void;
|
|
637
|
+
rawQuery: string;
|
|
638
|
+
savedSearches: SavedSearch[];
|
|
639
|
+
onApplySavedSearch?: (filterInput: string) => void;
|
|
640
|
+
clauses: FilterClause[];
|
|
221
641
|
}) {
|
|
222
642
|
if (loading && results.length === 0) {
|
|
223
643
|
return (
|
|
@@ -228,53 +648,254 @@ function MentionItems({
|
|
|
228
648
|
);
|
|
229
649
|
}
|
|
230
650
|
|
|
231
|
-
|
|
651
|
+
// Pins render from the standalone pin records (denormalized label/status),
|
|
652
|
+
// so they surface even when outside the current entities/search window.
|
|
653
|
+
// Filter pins by `rawQuery` so typing a query narrows pins too.
|
|
654
|
+
const q = rawQuery.toLowerCase();
|
|
655
|
+
const visiblePins =
|
|
656
|
+
q.length === 0
|
|
657
|
+
? pins
|
|
658
|
+
: pins.filter(
|
|
659
|
+
(p) =>
|
|
660
|
+
p.label.toLowerCase().includes(q) ||
|
|
661
|
+
p.description?.toLowerCase().includes(q)
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Hide pinned items from their regular type group so they don't render
|
|
665
|
+
// twice on the same popover open.
|
|
666
|
+
const unpinnedResults = results.filter((r) => !isPinned(r.entityId));
|
|
667
|
+
const grouped = groupByType(unpinnedResults);
|
|
232
668
|
const entityTypes = Object.keys(grouped);
|
|
233
669
|
|
|
234
|
-
if (entityTypes.length === 0) {
|
|
235
|
-
|
|
670
|
+
if (savedSearches.length === 0 && visiblePins.length === 0 && entityTypes.length === 0) {
|
|
671
|
+
if (clauses.length > 0) {
|
|
672
|
+
return (
|
|
673
|
+
<CommandEmpty>
|
|
674
|
+
No matches for{" "}
|
|
675
|
+
<span className="font-mono">
|
|
676
|
+
{clauses.map((c) => `#${c.key}:${c.value}`).join(" ")}
|
|
677
|
+
</span>
|
|
678
|
+
</CommandEmpty>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
return null; // Generic "No results" handled by parent CommandList
|
|
236
682
|
}
|
|
237
683
|
|
|
238
684
|
return (
|
|
239
685
|
<>
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
686
|
+
{savedSearches.length > 0 && (
|
|
687
|
+
<CommandGroup heading="Saved">
|
|
688
|
+
{savedSearches.map((s) => (
|
|
689
|
+
<CommandItem
|
|
690
|
+
key={`saved-${s.id}`}
|
|
691
|
+
value={`saved ${s.label} ${s.filterInput}`}
|
|
692
|
+
onSelect={() => onApplySavedSearch?.(s.filterInput)}
|
|
693
|
+
>
|
|
694
|
+
<Bookmark className="h-4 w-4 shrink-0" />
|
|
695
|
+
<span className="flex-1 truncate">{s.label}</span>
|
|
696
|
+
<span className="ml-auto shrink-0 text-xs font-mono text-muted-foreground">
|
|
697
|
+
{s.filterInput}
|
|
698
|
+
</span>
|
|
699
|
+
</CommandItem>
|
|
700
|
+
))}
|
|
701
|
+
</CommandGroup>
|
|
702
|
+
)}
|
|
703
|
+
{visiblePins.length > 0 && (
|
|
704
|
+
<CommandGroup heading="Pinned">
|
|
705
|
+
{visiblePins.map((p) => {
|
|
706
|
+
const Icon = ENTITY_ICONS[p.type] ?? FileText;
|
|
707
|
+
return (
|
|
246
708
|
<CommandItem
|
|
247
|
-
key={
|
|
248
|
-
value={
|
|
709
|
+
key={`pin-${p.id}`}
|
|
710
|
+
value={`pinned ${p.type} ${p.label} ${p.description ?? ""} ${p.status ?? ""}`}
|
|
249
711
|
onSelect={() =>
|
|
250
712
|
onSelect({
|
|
251
713
|
type: "mention",
|
|
252
|
-
id:
|
|
253
|
-
label:
|
|
254
|
-
entityType:
|
|
255
|
-
entityId:
|
|
714
|
+
id: p.type,
|
|
715
|
+
label: p.label,
|
|
716
|
+
entityType: p.type,
|
|
717
|
+
entityId: p.id,
|
|
256
718
|
})
|
|
257
719
|
}
|
|
258
720
|
>
|
|
259
721
|
<Icon className="h-4 w-4 shrink-0" />
|
|
260
722
|
<div className="flex flex-col min-w-0">
|
|
261
|
-
<span className="flex-1 truncate">{
|
|
262
|
-
{
|
|
723
|
+
<span className="flex-1 truncate">{p.label}</span>
|
|
724
|
+
{p.description && (
|
|
263
725
|
<span className="truncate text-xs text-muted-foreground">
|
|
264
|
-
{
|
|
726
|
+
{p.description}
|
|
265
727
|
</span>
|
|
266
728
|
)}
|
|
267
729
|
</div>
|
|
268
|
-
{
|
|
730
|
+
{p.status && (
|
|
269
731
|
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
|
|
270
|
-
{
|
|
732
|
+
{p.status}
|
|
271
733
|
</span>
|
|
272
734
|
)}
|
|
735
|
+
<button
|
|
736
|
+
type="button"
|
|
737
|
+
aria-label={`Unpin ${p.label}`}
|
|
738
|
+
className="ml-2 shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
739
|
+
// Stop cmdk's parent row selection on pin-button click —
|
|
740
|
+
// otherwise the unpin fires AND the item is inserted.
|
|
741
|
+
onMouseDown={(e) => {
|
|
742
|
+
e.preventDefault();
|
|
743
|
+
e.stopPropagation();
|
|
744
|
+
}}
|
|
745
|
+
onClick={(e) => {
|
|
746
|
+
e.stopPropagation();
|
|
747
|
+
onUnpin(p.id);
|
|
748
|
+
}}
|
|
749
|
+
>
|
|
750
|
+
<PinOff className="h-3.5 w-3.5" />
|
|
751
|
+
</button>
|
|
273
752
|
</CommandItem>
|
|
274
|
-
)
|
|
753
|
+
);
|
|
754
|
+
})}
|
|
755
|
+
</CommandGroup>
|
|
756
|
+
)}
|
|
757
|
+
{entityTypes.map((type) => {
|
|
758
|
+
const Icon = ENTITY_ICONS[type] ?? FileText;
|
|
759
|
+
const groupLabel = ENTITY_LABELS[type] ?? type;
|
|
760
|
+
const isFile = type === "file";
|
|
761
|
+
return (
|
|
762
|
+
<CommandGroup key={type} heading={groupLabel}>
|
|
763
|
+
{grouped[type].map((entity) => {
|
|
764
|
+
const pinnable = entity.entityType !== "file";
|
|
765
|
+
return (
|
|
766
|
+
<CommandItem
|
|
767
|
+
key={`${entity.entityType}-${entity.entityId}`}
|
|
768
|
+
value={`${entity.entityType} ${entity.label} ${entity.description ?? ""} ${entity.status ?? ""}`}
|
|
769
|
+
onSelect={() =>
|
|
770
|
+
onSelect({
|
|
771
|
+
type: "mention",
|
|
772
|
+
id: entity.entityType,
|
|
773
|
+
label: entity.label,
|
|
774
|
+
entityType: entity.entityType,
|
|
775
|
+
entityId: entity.entityId,
|
|
776
|
+
})
|
|
777
|
+
}
|
|
778
|
+
>
|
|
779
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
780
|
+
<div className="flex flex-col min-w-0">
|
|
781
|
+
<span
|
|
782
|
+
className={
|
|
783
|
+
isFile
|
|
784
|
+
? "flex-1 truncate font-mono text-xs"
|
|
785
|
+
: "flex-1 truncate"
|
|
786
|
+
}
|
|
787
|
+
>
|
|
788
|
+
{entity.label}
|
|
789
|
+
</span>
|
|
790
|
+
{entity.description && (
|
|
791
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
792
|
+
{entity.description}
|
|
793
|
+
</span>
|
|
794
|
+
)}
|
|
795
|
+
</div>
|
|
796
|
+
{entity.status && (
|
|
797
|
+
<span className="ml-auto shrink-0 text-xs text-muted-foreground">
|
|
798
|
+
{entity.status}
|
|
799
|
+
</span>
|
|
800
|
+
)}
|
|
801
|
+
{pinnable && (
|
|
802
|
+
<button
|
|
803
|
+
type="button"
|
|
804
|
+
aria-label={`Pin ${entity.label}`}
|
|
805
|
+
className="ml-2 shrink-0 rounded p-0.5 text-muted-foreground opacity-0 group-hover:opacity-100 data-[selected=true]:opacity-100 hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring transition-opacity"
|
|
806
|
+
onMouseDown={(e) => {
|
|
807
|
+
e.preventDefault();
|
|
808
|
+
e.stopPropagation();
|
|
809
|
+
}}
|
|
810
|
+
onClick={(e) => {
|
|
811
|
+
e.stopPropagation();
|
|
812
|
+
onPin({
|
|
813
|
+
id: entity.entityId,
|
|
814
|
+
type: entity.entityType,
|
|
815
|
+
label: entity.label,
|
|
816
|
+
description: entity.description,
|
|
817
|
+
status: entity.status,
|
|
818
|
+
});
|
|
819
|
+
}}
|
|
820
|
+
>
|
|
821
|
+
<Pin className="h-3.5 w-3.5" />
|
|
822
|
+
</button>
|
|
823
|
+
)}
|
|
824
|
+
</CommandItem>
|
|
825
|
+
);
|
|
826
|
+
})}
|
|
275
827
|
</CommandGroup>
|
|
276
828
|
);
|
|
277
829
|
})}
|
|
278
830
|
</>
|
|
279
831
|
);
|
|
280
832
|
}
|
|
833
|
+
|
|
834
|
+
function SaveViewFooter({
|
|
835
|
+
surface,
|
|
836
|
+
clauses,
|
|
837
|
+
filterInput,
|
|
838
|
+
onSave,
|
|
839
|
+
}: {
|
|
840
|
+
surface: SavedSearchSurface;
|
|
841
|
+
clauses: FilterClause[];
|
|
842
|
+
filterInput: string;
|
|
843
|
+
onSave: (label: string) => void;
|
|
844
|
+
}) {
|
|
845
|
+
const [renaming, setRenaming] = useState(false);
|
|
846
|
+
const [draft, setDraft] = useState("");
|
|
847
|
+
|
|
848
|
+
const defaultLabel = clauses.map((c) => `#${c.key}:${c.value}`).join(" ");
|
|
849
|
+
|
|
850
|
+
if (!renaming) {
|
|
851
|
+
return (
|
|
852
|
+
<div className="border-t px-2 py-1.5">
|
|
853
|
+
<button
|
|
854
|
+
type="button"
|
|
855
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-1 py-0.5 rounded transition-colors"
|
|
856
|
+
onClick={() => setRenaming(true)}
|
|
857
|
+
>
|
|
858
|
+
<Bookmark className="h-3.5 w-3.5" />
|
|
859
|
+
Save this view ({surface})
|
|
860
|
+
</button>
|
|
861
|
+
</div>
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return (
|
|
866
|
+
<form
|
|
867
|
+
className="border-t px-2 py-1.5 flex items-center gap-2"
|
|
868
|
+
onSubmit={(e) => {
|
|
869
|
+
e.preventDefault();
|
|
870
|
+
onSave(draft.trim());
|
|
871
|
+
setRenaming(false);
|
|
872
|
+
setDraft("");
|
|
873
|
+
}}
|
|
874
|
+
>
|
|
875
|
+
<input
|
|
876
|
+
autoFocus
|
|
877
|
+
type="text"
|
|
878
|
+
value={draft}
|
|
879
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
880
|
+
placeholder={defaultLabel}
|
|
881
|
+
className="flex-1 h-7 px-2 text-xs rounded border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
|
|
882
|
+
/>
|
|
883
|
+
<button
|
|
884
|
+
type="submit"
|
|
885
|
+
className="h-7 px-2 text-xs rounded bg-primary text-primary-foreground hover:bg-primary/90"
|
|
886
|
+
>
|
|
887
|
+
Save
|
|
888
|
+
</button>
|
|
889
|
+
<button
|
|
890
|
+
type="button"
|
|
891
|
+
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
892
|
+
onClick={() => {
|
|
893
|
+
setRenaming(false);
|
|
894
|
+
setDraft("");
|
|
895
|
+
}}
|
|
896
|
+
>
|
|
897
|
+
Cancel
|
|
898
|
+
</button>
|
|
899
|
+
</form>
|
|
900
|
+
);
|
|
901
|
+
}
|