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,47 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { useChatAutocomplete } from "../use-chat-autocomplete";
|
|
4
|
+
|
|
5
|
+
const TAB_KEY = "stagent.command-tab";
|
|
6
|
+
|
|
7
|
+
describe("useChatAutocomplete — activeTab persistence", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
localStorage.clear();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("defaults to 'actions' when localStorage empty", () => {
|
|
13
|
+
const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
|
|
14
|
+
expect(result.current.activeTab).toBe("actions");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("reads persisted tab from localStorage on mount", () => {
|
|
18
|
+
localStorage.setItem(TAB_KEY, "skills");
|
|
19
|
+
const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
|
|
20
|
+
expect(result.current.activeTab).toBe("skills");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("ignores corrupt localStorage values", () => {
|
|
24
|
+
localStorage.setItem(TAB_KEY, "bogus");
|
|
25
|
+
const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
|
|
26
|
+
expect(result.current.activeTab).toBe("actions");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("persists tab on setActiveTab", () => {
|
|
30
|
+
const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
|
|
31
|
+
act(() => result.current.setActiveTab("tools"));
|
|
32
|
+
expect(result.current.activeTab).toBe("tools");
|
|
33
|
+
expect(localStorage.getItem(TAB_KEY)).toBe("tools");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("survives localStorage throwing on write", () => {
|
|
37
|
+
const setSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
38
|
+
throw new Error("QuotaExceeded");
|
|
39
|
+
});
|
|
40
|
+
const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
|
|
41
|
+
expect(() => {
|
|
42
|
+
act(() => result.current.setActiveTab("tools"));
|
|
43
|
+
}).not.toThrow();
|
|
44
|
+
expect(result.current.activeTab).toBe("tools");
|
|
45
|
+
setSpy.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
2
|
+
import { useSavedSearches } from "../use-saved-searches";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
describe("useSavedSearches — rename", () => {
|
|
6
|
+
const originalFetch = global.fetch;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
global.fetch = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
10
|
+
const u = String(url);
|
|
11
|
+
if (u.endsWith("/api/settings/chat/saved-searches") && (!init || init.method === undefined || init.method === "GET")) {
|
|
12
|
+
return new Response(
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
searches: [
|
|
15
|
+
{
|
|
16
|
+
id: "s1",
|
|
17
|
+
surface: "task",
|
|
18
|
+
label: "Old label",
|
|
19
|
+
filterInput: "#status:blocked",
|
|
20
|
+
createdAt: "2026-04-14T00:00:00.000Z",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (init?.method === "PUT") {
|
|
28
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
29
|
+
}
|
|
30
|
+
return new Response("{}", { status: 200 });
|
|
31
|
+
}) as unknown as typeof fetch;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
global.fetch = originalFetch;
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renames a saved search optimistically and persists via PUT", async () => {
|
|
40
|
+
const { result } = renderHook(() => useSavedSearches());
|
|
41
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
42
|
+
expect(result.current.searches[0].label).toBe("Old label");
|
|
43
|
+
|
|
44
|
+
await act(async () => {
|
|
45
|
+
result.current.rename("s1", "New label");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.current.searches[0].label).toBe("New label");
|
|
49
|
+
|
|
50
|
+
const putCall = (global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.find(
|
|
51
|
+
([, init]) => init?.method === "PUT"
|
|
52
|
+
);
|
|
53
|
+
expect(putCall).toBeDefined();
|
|
54
|
+
const body = JSON.parse((putCall![1] as RequestInit).body as string);
|
|
55
|
+
expect(body.searches[0].label).toBe("New label");
|
|
56
|
+
expect(body.searches[0].id).toBe("s1");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("no-ops when id is not found", async () => {
|
|
60
|
+
const { result } = renderHook(() => useSavedSearches());
|
|
61
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
62
|
+
const before = result.current.searches;
|
|
63
|
+
|
|
64
|
+
await act(async () => {
|
|
65
|
+
result.current.rename("does-not-exist", "Whatever");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.current.searches).toEqual(before);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `useActiveSkills` — surfaces the current conversation's composition
|
|
5
|
+
* state for UI affordances on the chat popover Skills tab.
|
|
6
|
+
*
|
|
7
|
+
* Returns the merged active skill IDs (legacy + composed), the runtime
|
|
8
|
+
* id, and the runtime's `supportsSkillComposition` + `maxActiveSkills`
|
|
9
|
+
* capability flags. Used by the `+ Add` action and active-count badge
|
|
10
|
+
* in `chat-command-popover.tsx`.
|
|
11
|
+
*
|
|
12
|
+
* See `features/chat-composition-ui-v1.md`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useState } from "react";
|
|
16
|
+
import { mergeActiveSkillIds } from "@/lib/chat/active-skills";
|
|
17
|
+
import { getRuntimeFeatures, type AgentRuntimeId } from "@/lib/agents/runtime/catalog";
|
|
18
|
+
|
|
19
|
+
interface ActiveSkillsState {
|
|
20
|
+
loading: boolean;
|
|
21
|
+
/** Resolved active skill IDs (legacy + composed, deduped, in order). */
|
|
22
|
+
activeIds: string[];
|
|
23
|
+
/** The conversation's runtime id, or null if not yet loaded / no conversation. */
|
|
24
|
+
runtimeId: AgentRuntimeId | null;
|
|
25
|
+
/** True iff the runtime supports composing 2+ skills concurrently. */
|
|
26
|
+
supportsComposition: boolean;
|
|
27
|
+
/** Max simultaneously-active skills for this runtime (1 for Ollama, 3 elsewhere). */
|
|
28
|
+
maxActive: number;
|
|
29
|
+
/** Re-fetch the conversation row. Call after a successful add/remove. */
|
|
30
|
+
refetch: () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const KNOWN_RUNTIMES = new Set<AgentRuntimeId>([
|
|
34
|
+
"claude-code",
|
|
35
|
+
"openai-codex-app-server",
|
|
36
|
+
"anthropic-direct",
|
|
37
|
+
"openai-direct",
|
|
38
|
+
"ollama",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function useActiveSkills(
|
|
42
|
+
conversationId: string | null
|
|
43
|
+
): ActiveSkillsState {
|
|
44
|
+
const [activeIds, setActiveIds] = useState<string[]>([]);
|
|
45
|
+
const [runtimeId, setRuntimeId] = useState<AgentRuntimeId | null>(null);
|
|
46
|
+
const [loading, setLoading] = useState<boolean>(!!conversationId);
|
|
47
|
+
|
|
48
|
+
const fetchOnce = useCallback(async (): Promise<void> => {
|
|
49
|
+
if (!conversationId) {
|
|
50
|
+
setActiveIds([]);
|
|
51
|
+
setRuntimeId(null);
|
|
52
|
+
setLoading(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const r = await fetch(`/api/chat/conversations/${conversationId}`);
|
|
57
|
+
if (!r.ok) {
|
|
58
|
+
setActiveIds([]);
|
|
59
|
+
setRuntimeId(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const data: {
|
|
63
|
+
activeSkillId?: string | null;
|
|
64
|
+
activeSkillIds?: string[] | null;
|
|
65
|
+
runtimeId?: string | null;
|
|
66
|
+
} = await r.json();
|
|
67
|
+
setActiveIds(
|
|
68
|
+
mergeActiveSkillIds(data.activeSkillId, data.activeSkillIds)
|
|
69
|
+
);
|
|
70
|
+
const rid = data.runtimeId as AgentRuntimeId | null | undefined;
|
|
71
|
+
setRuntimeId(rid && KNOWN_RUNTIMES.has(rid) ? rid : null);
|
|
72
|
+
} catch {
|
|
73
|
+
setActiveIds([]);
|
|
74
|
+
setRuntimeId(null);
|
|
75
|
+
} finally {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
}, [conversationId]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setLoading(true);
|
|
82
|
+
void fetchOnce();
|
|
83
|
+
}, [fetchOnce]);
|
|
84
|
+
|
|
85
|
+
// Derive capability flags from the catalog. Defaults match Ollama
|
|
86
|
+
// (most conservative) when the runtime is unknown — better to refuse
|
|
87
|
+
// composition than to crash on an unrecognized id.
|
|
88
|
+
const features = runtimeId
|
|
89
|
+
? safeGetRuntimeFeatures(runtimeId)
|
|
90
|
+
: null;
|
|
91
|
+
const supportsComposition = features?.supportsSkillComposition ?? false;
|
|
92
|
+
const maxActive = features?.maxActiveSkills ?? 1;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
loading,
|
|
96
|
+
activeIds,
|
|
97
|
+
runtimeId,
|
|
98
|
+
supportsComposition,
|
|
99
|
+
maxActive,
|
|
100
|
+
refetch: fetchOnce,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function safeGetRuntimeFeatures(rid: AgentRuntimeId) {
|
|
105
|
+
try {
|
|
106
|
+
return getRuntimeFeatures(rid);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
4
|
import { useCaretPosition } from "./use-caret-position";
|
|
5
|
+
import {
|
|
6
|
+
isCommandTabId,
|
|
7
|
+
DEFAULT_COMMAND_TAB,
|
|
8
|
+
type CommandTabId,
|
|
9
|
+
} from "@/lib/chat/command-tabs";
|
|
5
10
|
|
|
6
11
|
export type AutocompleteMode = "slash" | "mention" | null;
|
|
7
12
|
|
|
@@ -32,6 +37,8 @@ export interface ChatAutocompleteReturn {
|
|
|
32
37
|
entityResults: EntitySearchResult[];
|
|
33
38
|
entityLoading: boolean;
|
|
34
39
|
mentions: MentionReference[];
|
|
40
|
+
activeTab: CommandTabId;
|
|
41
|
+
setActiveTab: (tab: CommandTabId) => void;
|
|
35
42
|
handleChange: (value: string, textarea: HTMLTextAreaElement | null) => void;
|
|
36
43
|
handleKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => boolean;
|
|
37
44
|
handleSelect: (item: { type: "slash" | "mention"; id: string; label: string; text?: string }) => string;
|
|
@@ -39,6 +46,19 @@ export interface ChatAutocompleteReturn {
|
|
|
39
46
|
setTextareaRef: (el: HTMLTextAreaElement | null) => void;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
const TAB_STORAGE_KEY = "stagent.command-tab";
|
|
50
|
+
|
|
51
|
+
function readInitialTab(): CommandTabId {
|
|
52
|
+
if (typeof window === "undefined") return DEFAULT_COMMAND_TAB;
|
|
53
|
+
try {
|
|
54
|
+
const raw = window.localStorage.getItem(TAB_STORAGE_KEY);
|
|
55
|
+
if (raw && isCommandTabId(raw)) return raw;
|
|
56
|
+
} catch {
|
|
57
|
+
// localStorage unavailable — fall through
|
|
58
|
+
}
|
|
59
|
+
return DEFAULT_COMMAND_TAB;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
const CLOSED_STATE: AutocompleteState = {
|
|
43
63
|
open: false,
|
|
44
64
|
mode: null,
|
|
@@ -47,21 +67,45 @@ const CLOSED_STATE: AutocompleteState = {
|
|
|
47
67
|
anchorRect: null,
|
|
48
68
|
};
|
|
49
69
|
|
|
70
|
+
/** Compact size label for file popover rows (e.g., "1.4 KB", "23 B"). */
|
|
71
|
+
function humanSize(bytes: number): string {
|
|
72
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
73
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
74
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
/**
|
|
51
78
|
* Detects "/" and "@" triggers in a textarea and manages autocomplete state.
|
|
52
79
|
*
|
|
53
80
|
* "/" triggers at position 0 or after a newline.
|
|
54
81
|
* "@" triggers at position 0 or after whitespace.
|
|
55
82
|
*/
|
|
56
|
-
export function useChatAutocomplete(
|
|
83
|
+
export function useChatAutocomplete(
|
|
84
|
+
options: { projectId?: string | null } = {}
|
|
85
|
+
): ChatAutocompleteReturn {
|
|
57
86
|
const [state, setState] = useState<AutocompleteState>(CLOSED_STATE);
|
|
58
87
|
const [entityResults, setEntityResults] = useState<EntitySearchResult[]>([]);
|
|
88
|
+
const [fileResults, setFileResults] = useState<EntitySearchResult[]>([]);
|
|
59
89
|
const [entityLoading, setEntityLoading] = useState(false);
|
|
60
90
|
const [mentions, setMentions] = useState<MentionReference[]>([]);
|
|
91
|
+
const [activeTab, setActiveTabState] = useState<CommandTabId>(readInitialTab);
|
|
92
|
+
|
|
93
|
+
const setActiveTab = useCallback((tab: CommandTabId) => {
|
|
94
|
+
setActiveTabState(tab);
|
|
95
|
+
try {
|
|
96
|
+
window.localStorage.setItem(TAB_STORAGE_KEY, tab);
|
|
97
|
+
} catch {
|
|
98
|
+
// quota / disabled — silent, in-memory only
|
|
99
|
+
}
|
|
100
|
+
}, []);
|
|
61
101
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
62
102
|
const abortRef = useRef<AbortController | null>(null);
|
|
103
|
+
const fileAbortRef = useRef<AbortController | null>(null);
|
|
104
|
+
const fileDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
63
105
|
const entityCacheRef = useRef<EntitySearchResult[] | null>(null);
|
|
64
106
|
const getCaretCoordinates = useCaretPosition();
|
|
107
|
+
const projectIdRef = useRef(options.projectId ?? null);
|
|
108
|
+
projectIdRef.current = options.projectId ?? null;
|
|
65
109
|
|
|
66
110
|
// Ref to let the keyboard handler access current state synchronously
|
|
67
111
|
const stateRef = useRef(state);
|
|
@@ -74,9 +118,51 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
74
118
|
const close = useCallback(() => {
|
|
75
119
|
setState(CLOSED_STATE);
|
|
76
120
|
setEntityResults([]);
|
|
121
|
+
setFileResults([]);
|
|
77
122
|
setEntityLoading(false);
|
|
78
123
|
entityCacheRef.current = null;
|
|
79
124
|
if (abortRef.current) abortRef.current.abort();
|
|
125
|
+
if (fileAbortRef.current) fileAbortRef.current.abort();
|
|
126
|
+
if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Query the file search API with debounce + abort. Results are stored
|
|
131
|
+
* in `fileResults` and merged into the popover stream alongside
|
|
132
|
+
* entity results. Query is bound to the active "@" typeahead text.
|
|
133
|
+
*/
|
|
134
|
+
const loadFiles = useCallback((query: string) => {
|
|
135
|
+
// Debounce: wait 150ms after the last keystroke before firing.
|
|
136
|
+
if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
|
|
137
|
+
fileDebounceRef.current = setTimeout(() => {
|
|
138
|
+
if (fileAbortRef.current) fileAbortRef.current.abort();
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
fileAbortRef.current = controller;
|
|
141
|
+
|
|
142
|
+
const params = new URLSearchParams({ q: query, limit: "20" });
|
|
143
|
+
const projectId = projectIdRef.current;
|
|
144
|
+
if (projectId) params.set("projectId", projectId);
|
|
145
|
+
|
|
146
|
+
fetch(`/api/chat/files/search?${params}`, { signal: controller.signal })
|
|
147
|
+
.then((res) => (res.ok ? res.json() : { results: [] }))
|
|
148
|
+
.then(
|
|
149
|
+
(data: {
|
|
150
|
+
results?: Array<{ path: string; sizeBytes: number }>;
|
|
151
|
+
}) => {
|
|
152
|
+
const hits = data.results ?? [];
|
|
153
|
+
const mapped: EntitySearchResult[] = hits.map((h) => ({
|
|
154
|
+
entityType: "file",
|
|
155
|
+
entityId: h.path,
|
|
156
|
+
label: h.path,
|
|
157
|
+
description: humanSize(h.sizeBytes),
|
|
158
|
+
}));
|
|
159
|
+
setFileResults(mapped);
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
.catch(() => {
|
|
163
|
+
// Aborted or failed — leave previous results in place.
|
|
164
|
+
});
|
|
165
|
+
}, 150);
|
|
80
166
|
}, []);
|
|
81
167
|
|
|
82
168
|
// Fetch all recent entities once on "@" trigger, cache for cmdk client-side filtering
|
|
@@ -143,8 +229,15 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
143
229
|
return;
|
|
144
230
|
}
|
|
145
231
|
|
|
146
|
-
// Check for "@" trigger — must be at position 0 or after whitespace
|
|
147
|
-
|
|
232
|
+
// Check for "@" trigger — must be at position 0 or after whitespace.
|
|
233
|
+
// The mention body stops at whitespace or `#` so the filter-namespace
|
|
234
|
+
// (`#key:value`) can chain after a space: `@foo #status:blocked`.
|
|
235
|
+
// Filter tokens are part of the same trigger span so the popover stays
|
|
236
|
+
// open while the user types them. Partial tokens (`@foo #` or
|
|
237
|
+
// `@foo #sta`) are accepted — the parser handles incomplete input.
|
|
238
|
+
const mentionMatch = textBeforeCursor.match(
|
|
239
|
+
/(?:^|\s)(@[^\s#]*(?:\s+#[A-Za-z]?[\w-]*:?[^\s#]*)*)\s*$/
|
|
240
|
+
);
|
|
148
241
|
if (mentionMatch) {
|
|
149
242
|
const triggerIndex = cursorPos - mentionMatch[1].length;
|
|
150
243
|
const query = mentionMatch[1].substring(1); // text after "@"
|
|
@@ -157,6 +250,14 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
157
250
|
anchorRect: coords,
|
|
158
251
|
});
|
|
159
252
|
loadEntities();
|
|
253
|
+
// File search fires only when the user has typed something after `@`
|
|
254
|
+
// — an empty query would return every tracked file in the repo, which
|
|
255
|
+
// is noisy and defeats the whole "type to narrow" interaction.
|
|
256
|
+
if (query.length > 0) {
|
|
257
|
+
loadFiles(query);
|
|
258
|
+
} else {
|
|
259
|
+
setFileResults([]);
|
|
260
|
+
}
|
|
160
261
|
return;
|
|
161
262
|
}
|
|
162
263
|
|
|
@@ -165,7 +266,7 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
165
266
|
close();
|
|
166
267
|
}
|
|
167
268
|
},
|
|
168
|
-
[getCaretCoordinates, loadEntities, close]
|
|
269
|
+
[getCaretCoordinates, loadEntities, loadFiles, close]
|
|
169
270
|
);
|
|
170
271
|
|
|
171
272
|
/**
|
|
@@ -245,10 +346,15 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
245
346
|
if (item.type === "slash") {
|
|
246
347
|
replacement = item.text ?? item.label;
|
|
247
348
|
} else {
|
|
248
|
-
// "@" mention —
|
|
349
|
+
// "@" mention — format depends on entity type:
|
|
350
|
+
// file: @<path> (CLI-style, matches what users type)
|
|
351
|
+
// other: @<type>:<label> (disambiguates entity types)
|
|
249
352
|
const eType = item.entityType ?? item.id;
|
|
250
353
|
const eId = item.entityId ?? item.id;
|
|
251
|
-
replacement =
|
|
354
|
+
replacement =
|
|
355
|
+
eType === "file"
|
|
356
|
+
? `@${item.label} `
|
|
357
|
+
: `@${eType}:${item.label} `;
|
|
252
358
|
// Track the mention
|
|
253
359
|
setMentions((prev) => {
|
|
254
360
|
if (prev.some((m) => m.entityId === eId)) return prev;
|
|
@@ -271,14 +377,21 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
|
|
|
271
377
|
useEffect(() => {
|
|
272
378
|
return () => {
|
|
273
379
|
if (abortRef.current) abortRef.current.abort();
|
|
380
|
+
if (fileAbortRef.current) fileAbortRef.current.abort();
|
|
381
|
+
if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
|
|
274
382
|
};
|
|
275
383
|
}, []);
|
|
276
384
|
|
|
277
385
|
return {
|
|
278
386
|
state,
|
|
279
|
-
|
|
387
|
+
// Merge entity results with file results so the popover's single
|
|
388
|
+
// group-by-entityType render path covers both — no second props
|
|
389
|
+
// channel needed.
|
|
390
|
+
entityResults: [...entityResults, ...fileResults],
|
|
280
391
|
entityLoading,
|
|
281
392
|
mentions,
|
|
393
|
+
activeTab,
|
|
394
|
+
setActiveTab,
|
|
282
395
|
handleChange,
|
|
283
396
|
handleKeyDown,
|
|
284
397
|
handleSelect,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
|
|
4
|
+
|
|
5
|
+
export function useEnrichedSkills(open: boolean): EnrichedSkill[] {
|
|
6
|
+
const [skills, setSkills] = useState<EnrichedSkill[]>([]);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!open) return;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
fetch("/api/environment/skills", { signal: controller.signal })
|
|
11
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
12
|
+
.then((data) => {
|
|
13
|
+
if (Array.isArray(data)) setSkills(data);
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {});
|
|
16
|
+
return () => controller.abort();
|
|
17
|
+
}, [open]);
|
|
18
|
+
return skills;
|
|
19
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `usePinnedEntries` — client-side store for chat mention-popover pins.
|
|
5
|
+
*
|
|
6
|
+
* Fetches once on mount, keeps an in-memory list in React state, and writes
|
|
7
|
+
* back via PUT on every mutation (full-list replacement — see
|
|
8
|
+
* `src/app/api/settings/chat/pins/route.ts` for design rationale).
|
|
9
|
+
*
|
|
10
|
+
* Exposes:
|
|
11
|
+
* - `pins`: current pinned entries (stable identity per mount)
|
|
12
|
+
* - `isPinned(id)`: fast membership check
|
|
13
|
+
* - `pin(entry)` / `unpin(id)`: optimistic mutations with background sync
|
|
14
|
+
* - `loading`: true while the initial GET is in flight
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
18
|
+
|
|
19
|
+
export interface PinnedEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
pinnedAt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UsePinnedEntriesReturn {
|
|
29
|
+
pins: PinnedEntry[];
|
|
30
|
+
loading: boolean;
|
|
31
|
+
isPinned: (id: string) => boolean;
|
|
32
|
+
pin: (entry: Omit<PinnedEntry, "pinnedAt">) => void;
|
|
33
|
+
unpin: (id: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function usePinnedEntries(): UsePinnedEntriesReturn {
|
|
37
|
+
const [pins, setPins] = useState<PinnedEntry[]>([]);
|
|
38
|
+
const [loading, setLoading] = useState(true);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
fetch("/api/settings/chat/pins")
|
|
43
|
+
.then((r) => (r.ok ? r.json() : { pins: [] }))
|
|
44
|
+
.then((data: { pins?: PinnedEntry[] }) => {
|
|
45
|
+
if (!cancelled) setPins(data.pins ?? []);
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
// Network / parse failure: start with empty list. Subsequent writes
|
|
49
|
+
// will create the setting on first mutation.
|
|
50
|
+
if (!cancelled) setPins([]);
|
|
51
|
+
})
|
|
52
|
+
.finally(() => {
|
|
53
|
+
if (!cancelled) setLoading(false);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true;
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const pinnedIdSet = useMemo(() => new Set(pins.map((p) => p.id)), [pins]);
|
|
61
|
+
|
|
62
|
+
const isPinned = useCallback(
|
|
63
|
+
(id: string) => pinnedIdSet.has(id),
|
|
64
|
+
[pinnedIdSet]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const persist = useCallback(async (next: PinnedEntry[]) => {
|
|
68
|
+
try {
|
|
69
|
+
await fetch("/api/settings/chat/pins", {
|
|
70
|
+
method: "PUT",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ pins: next }),
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
// Optimistic update already applied; server sync failure is silently
|
|
76
|
+
// swallowed. A future follow-up can add a toast on persistent failure.
|
|
77
|
+
}
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const pin = useCallback(
|
|
81
|
+
(entry: Omit<PinnedEntry, "pinnedAt">) => {
|
|
82
|
+
if (pinnedIdSet.has(entry.id)) return;
|
|
83
|
+
const next: PinnedEntry[] = [
|
|
84
|
+
...pins,
|
|
85
|
+
{ ...entry, pinnedAt: new Date().toISOString() },
|
|
86
|
+
];
|
|
87
|
+
setPins(next);
|
|
88
|
+
void persist(next);
|
|
89
|
+
},
|
|
90
|
+
[pins, pinnedIdSet, persist]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const unpin = useCallback(
|
|
94
|
+
(id: string) => {
|
|
95
|
+
if (!pinnedIdSet.has(id)) return;
|
|
96
|
+
const next = pins.filter((p) => p.id !== id);
|
|
97
|
+
setPins(next);
|
|
98
|
+
void persist(next);
|
|
99
|
+
},
|
|
100
|
+
[pins, pinnedIdSet, persist]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return { pins, loading, isPinned, pin, unpin };
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useChatSession } from "@/components/chat/chat-session-provider";
|
|
4
|
+
|
|
5
|
+
export function useRecentUserMessages(
|
|
6
|
+
conversationId: string | null | undefined,
|
|
7
|
+
limit: number = 20
|
|
8
|
+
): string[] {
|
|
9
|
+
const { messages } = useChatSession();
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
if (!conversationId) return [];
|
|
12
|
+
return messages
|
|
13
|
+
.filter((m) => m.role === "user")
|
|
14
|
+
.slice(-limit)
|
|
15
|
+
.map((m) =>
|
|
16
|
+
typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
17
|
+
);
|
|
18
|
+
}, [messages, conversationId, limit]);
|
|
19
|
+
}
|