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,40 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { FilterHint } from "../filter-hint";
|
|
4
|
+
|
|
5
|
+
const KEY = "stagent.filter-hint.dismissed";
|
|
6
|
+
|
|
7
|
+
describe("FilterHint", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
localStorage.removeItem(KEY);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders when input is empty and not dismissed", () => {
|
|
13
|
+
render(<FilterHint inputValue="" storageKey={KEY} />);
|
|
14
|
+
expect(screen.getByText(/#key:value/i)).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders when input has no # character", () => {
|
|
18
|
+
render(<FilterHint inputValue="some search" storageKey={KEY} />);
|
|
19
|
+
expect(screen.getByText(/#key:value/i)).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("hides when input contains #", () => {
|
|
23
|
+
render(<FilterHint inputValue="#status:blocked" storageKey={KEY} />);
|
|
24
|
+
expect(screen.queryByText(/#key:value/i)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("sets dismissal flag and hides when input parses a valid clause", async () => {
|
|
28
|
+
render(<FilterHint inputValue="#type:pdf" storageKey={KEY} />);
|
|
29
|
+
expect(localStorage.getItem(KEY)).toBe("1");
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(screen.queryByText(/#key:value/i)).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("stays hidden on subsequent mounts once dismissed", () => {
|
|
36
|
+
localStorage.setItem(KEY, "1");
|
|
37
|
+
render(<FilterHint inputValue="" storageKey={KEY} />);
|
|
38
|
+
expect(screen.queryByText(/#key:value/i)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { SavedSearchesManager } from "../saved-searches-manager";
|
|
4
|
+
import type { SavedSearch } from "@/hooks/use-saved-searches";
|
|
5
|
+
|
|
6
|
+
const search = (over: Partial<SavedSearch> = {}): SavedSearch => ({
|
|
7
|
+
id: "s1",
|
|
8
|
+
surface: "task",
|
|
9
|
+
label: "Blocked tasks",
|
|
10
|
+
filterInput: "#status:blocked",
|
|
11
|
+
createdAt: "2026-04-14T00:00:00.000Z",
|
|
12
|
+
...over,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("SavedSearchesManager", () => {
|
|
16
|
+
it("lists all saved searches", () => {
|
|
17
|
+
const items = [
|
|
18
|
+
search({ id: "s1", label: "Blocked tasks" }),
|
|
19
|
+
search({ id: "s2", label: "Pdf docs", surface: "document", filterInput: "#type:pdf" }),
|
|
20
|
+
];
|
|
21
|
+
render(
|
|
22
|
+
<SavedSearchesManager
|
|
23
|
+
open
|
|
24
|
+
onOpenChange={() => {}}
|
|
25
|
+
searches={items}
|
|
26
|
+
onRename={() => {}}
|
|
27
|
+
onRemove={() => {}}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
expect(screen.getByText("Blocked tasks")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText("Pdf docs")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renames on blur with non-empty trimmed label", () => {
|
|
35
|
+
const onRename = vi.fn();
|
|
36
|
+
render(
|
|
37
|
+
<SavedSearchesManager
|
|
38
|
+
open
|
|
39
|
+
onOpenChange={() => {}}
|
|
40
|
+
searches={[search()]}
|
|
41
|
+
onRename={onRename}
|
|
42
|
+
onRemove={() => {}}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
|
|
46
|
+
const input = screen.getByRole("textbox", { name: /rename/i });
|
|
47
|
+
fireEvent.change(input, { target: { value: " Renamed " } });
|
|
48
|
+
fireEvent.blur(input);
|
|
49
|
+
expect(onRename).toHaveBeenCalledWith("s1", "Renamed");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects empty label with inline error", () => {
|
|
53
|
+
const onRename = vi.fn();
|
|
54
|
+
render(
|
|
55
|
+
<SavedSearchesManager
|
|
56
|
+
open
|
|
57
|
+
onOpenChange={() => {}}
|
|
58
|
+
searches={[search()]}
|
|
59
|
+
onRename={onRename}
|
|
60
|
+
onRemove={() => {}}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
|
|
64
|
+
const input = screen.getByRole("textbox", { name: /rename/i });
|
|
65
|
+
fireEvent.change(input, { target: { value: " " } });
|
|
66
|
+
fireEvent.blur(input);
|
|
67
|
+
expect(onRename).not.toHaveBeenCalled();
|
|
68
|
+
expect(screen.getByText(/cannot be empty/i)).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects duplicate label within same surface (case-insensitive)", () => {
|
|
72
|
+
const onRename = vi.fn();
|
|
73
|
+
render(
|
|
74
|
+
<SavedSearchesManager
|
|
75
|
+
open
|
|
76
|
+
onOpenChange={() => {}}
|
|
77
|
+
searches={[
|
|
78
|
+
search({ id: "s1", label: "Blocked tasks" }),
|
|
79
|
+
search({ id: "s2", label: "Another" }),
|
|
80
|
+
]}
|
|
81
|
+
onRename={onRename}
|
|
82
|
+
onRemove={() => {}}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
fireEvent.click(screen.getByRole("button", { name: /rename another/i }));
|
|
86
|
+
const input = screen.getByRole("textbox", { name: /rename/i });
|
|
87
|
+
fireEvent.change(input, { target: { value: "blocked TASKS" } });
|
|
88
|
+
fireEvent.blur(input);
|
|
89
|
+
expect(onRename).not.toHaveBeenCalled();
|
|
90
|
+
expect(screen.getByText(/already exists/i)).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects label longer than 120 chars", () => {
|
|
94
|
+
const onRename = vi.fn();
|
|
95
|
+
render(
|
|
96
|
+
<SavedSearchesManager
|
|
97
|
+
open
|
|
98
|
+
onOpenChange={() => {}}
|
|
99
|
+
searches={[search()]}
|
|
100
|
+
onRename={onRename}
|
|
101
|
+
onRemove={() => {}}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
|
|
105
|
+
const input = screen.getByRole("textbox", { name: /rename/i });
|
|
106
|
+
fireEvent.change(input, { target: { value: "x".repeat(121) } });
|
|
107
|
+
fireEvent.blur(input);
|
|
108
|
+
expect(onRename).not.toHaveBeenCalled();
|
|
109
|
+
expect(screen.getByText(/too long/i)).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("Escape cancels rename without persisting", () => {
|
|
113
|
+
const onRename = vi.fn();
|
|
114
|
+
render(
|
|
115
|
+
<SavedSearchesManager
|
|
116
|
+
open
|
|
117
|
+
onOpenChange={() => {}}
|
|
118
|
+
searches={[search()]}
|
|
119
|
+
onRename={onRename}
|
|
120
|
+
onRemove={() => {}}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
|
|
124
|
+
const input = screen.getByRole("textbox", { name: /rename/i });
|
|
125
|
+
fireEvent.change(input, { target: { value: "Changed" } });
|
|
126
|
+
fireEvent.keyDown(input, { key: "Escape" });
|
|
127
|
+
expect(onRename).not.toHaveBeenCalled();
|
|
128
|
+
expect(screen.queryByRole("textbox", { name: /rename/i })).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("delete requires explicit confirm", () => {
|
|
132
|
+
const onRemove = vi.fn();
|
|
133
|
+
render(
|
|
134
|
+
<SavedSearchesManager
|
|
135
|
+
open
|
|
136
|
+
onOpenChange={() => {}}
|
|
137
|
+
searches={[search()]}
|
|
138
|
+
onRename={() => {}}
|
|
139
|
+
onRemove={onRemove}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
fireEvent.click(screen.getByRole("button", { name: /delete blocked tasks/i }));
|
|
143
|
+
expect(onRemove).not.toHaveBeenCalled();
|
|
144
|
+
fireEvent.click(screen.getByRole("button", { name: /confirm delete/i }));
|
|
145
|
+
expect(onRemove).toHaveBeenCalledWith("s1");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
MessageCircle,
|
|
22
22
|
Table2,
|
|
23
23
|
BarChart3,
|
|
24
|
-
Store,
|
|
25
24
|
ChevronDown,
|
|
26
25
|
} from "lucide-react";
|
|
27
26
|
import { cn } from "@/lib/utils";
|
|
@@ -43,6 +42,7 @@ import {
|
|
|
43
42
|
import { ThemeToggle } from "@/components/shared/theme-toggle";
|
|
44
43
|
import { TrustTierBadge } from "@/components/shared/trust-tier-badge";
|
|
45
44
|
import { UnreadBadge } from "@/components/notifications/unread-badge";
|
|
45
|
+
import { UpgradeBadge } from "@/components/instance/upgrade-badge";
|
|
46
46
|
import { AuthStatusDot } from "@/components/settings/auth-status-dot";
|
|
47
47
|
import { StagentLogo } from "@/components/shared/stagent-logo";
|
|
48
48
|
import { WorkspaceIndicator } from "@/components/shared/workspace-indicator";
|
|
@@ -65,7 +65,6 @@ const workItems: NavItem[] = [
|
|
|
65
65
|
{ title: "Workflows", href: "/workflows", icon: Workflow },
|
|
66
66
|
{ title: "Documents", href: "/documents", icon: FileText },
|
|
67
67
|
{ title: "Tables", href: "/tables", icon: Table2, alsoMatches: ["/tables/"] },
|
|
68
|
-
{ title: "Marketplace", href: "/marketplace", icon: Store },
|
|
69
68
|
];
|
|
70
69
|
|
|
71
70
|
const manageItems: NavItem[] = [
|
|
@@ -199,7 +198,6 @@ function NavGroup({
|
|
|
199
198
|
|
|
200
199
|
export function AppSidebar() {
|
|
201
200
|
const pathname = usePathname();
|
|
202
|
-
|
|
203
201
|
// Determine which group owns the current route
|
|
204
202
|
const activeGroup = useMemo(() => {
|
|
205
203
|
for (const group of groupMap) {
|
|
@@ -248,6 +246,9 @@ export function AppSidebar() {
|
|
|
248
246
|
))}
|
|
249
247
|
</SidebarContent>
|
|
250
248
|
<SidebarFooter className="px-4 py-3">
|
|
249
|
+
<div className="group-data-[collapsible=icon]:hidden mb-2 empty:hidden">
|
|
250
|
+
<UpgradeBadge />
|
|
251
|
+
</div>
|
|
251
252
|
<div className="group-data-[collapsible=icon]:hidden mb-2">
|
|
252
253
|
<WorkspaceIndicator variant="sidebar" />
|
|
253
254
|
</div>
|
|
@@ -21,8 +21,29 @@ import {
|
|
|
21
21
|
CheckCheck,
|
|
22
22
|
Loader2,
|
|
23
23
|
BookOpen,
|
|
24
|
+
Sparkles,
|
|
25
|
+
FileCode,
|
|
26
|
+
Bookmark,
|
|
27
|
+
Trash2,
|
|
28
|
+
Settings2,
|
|
24
29
|
} from "lucide-react";
|
|
25
30
|
import { navigationItems, createItems } from "@/lib/chat/command-data";
|
|
31
|
+
import { toggleTheme } from "@/lib/theme";
|
|
32
|
+
import { useProjectSkills } from "@/hooks/use-project-skills";
|
|
33
|
+
import { useSavedSearches, type SavedSearch, type SavedSearchSurface } from "@/hooks/use-saved-searches";
|
|
34
|
+
import { SavedSearchesManager } from "./saved-searches-manager";
|
|
35
|
+
import { toast } from "sonner";
|
|
36
|
+
|
|
37
|
+
// Maps each saved-search surface to its list-page route. Tasks route to
|
|
38
|
+
// /dashboard since /tasks is still a redirect stub.
|
|
39
|
+
const SURFACE_ROUTE: Record<SavedSearchSurface, string> = {
|
|
40
|
+
task: "/dashboard",
|
|
41
|
+
project: "/projects",
|
|
42
|
+
workflow: "/workflows",
|
|
43
|
+
document: "/documents",
|
|
44
|
+
skill: "/skills",
|
|
45
|
+
profile: "/profiles",
|
|
46
|
+
};
|
|
26
47
|
|
|
27
48
|
interface RecentProject {
|
|
28
49
|
id: string;
|
|
@@ -63,8 +84,21 @@ export function CommandPalette() {
|
|
|
63
84
|
const [recentTasks, setRecentTasks] = useState<RecentTask[]>([]);
|
|
64
85
|
const [playbookItems, setPlaybookItems] = useState<PlaybookItem[]>([]);
|
|
65
86
|
const [loadingRecent, setLoadingRecent] = useState(false);
|
|
87
|
+
const [fileQuery, setFileQuery] = useState("");
|
|
88
|
+
const [fileResults, setFileResults] = useState<Array<{ entityId: string; label: string; description?: string }>>([]);
|
|
66
89
|
const abortRef = useRef<AbortController | null>(null);
|
|
90
|
+
const fileAbortRef = useRef<AbortController | null>(null);
|
|
91
|
+
const fileDebounceRef = useRef<number | null>(null);
|
|
67
92
|
const router = useRouter();
|
|
93
|
+
const { skills } = useProjectSkills(null);
|
|
94
|
+
const {
|
|
95
|
+
searches: savedSearches,
|
|
96
|
+
refetch: refetchSavedSearches,
|
|
97
|
+
remove: removeSavedSearch,
|
|
98
|
+
save: saveSavedSearch,
|
|
99
|
+
rename: renameSavedSearch,
|
|
100
|
+
} = useSavedSearches();
|
|
101
|
+
const [managerOpen, setManagerOpen] = useState(false);
|
|
68
102
|
|
|
69
103
|
// Defer render until after hydration to avoid Radix ID mismatch
|
|
70
104
|
useEffect(() => setMounted(true), []);
|
|
@@ -85,6 +119,10 @@ export function CommandPalette() {
|
|
|
85
119
|
if (!open) {
|
|
86
120
|
abortRef.current?.abort();
|
|
87
121
|
abortRef.current = null;
|
|
122
|
+
fileAbortRef.current?.abort();
|
|
123
|
+
if (fileDebounceRef.current) window.clearTimeout(fileDebounceRef.current);
|
|
124
|
+
setFileQuery("");
|
|
125
|
+
setFileResults([]);
|
|
88
126
|
return;
|
|
89
127
|
}
|
|
90
128
|
|
|
@@ -107,6 +145,33 @@ export function CommandPalette() {
|
|
|
107
145
|
.finally(() => setLoadingRecent(false));
|
|
108
146
|
}, [open]);
|
|
109
147
|
|
|
148
|
+
function handleInputChange(value: string) {
|
|
149
|
+
setFileQuery(value);
|
|
150
|
+
if (fileDebounceRef.current) {
|
|
151
|
+
window.clearTimeout(fileDebounceRef.current);
|
|
152
|
+
}
|
|
153
|
+
fileAbortRef.current?.abort();
|
|
154
|
+
if (!value || value.length < 2) {
|
|
155
|
+
setFileResults([]);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
fileDebounceRef.current = window.setTimeout(() => {
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
fileAbortRef.current = controller;
|
|
161
|
+
const params = new URLSearchParams({ q: value, limit: "8" });
|
|
162
|
+
fetch(`/api/chat/files/search?${params}`, { signal: controller.signal })
|
|
163
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
164
|
+
.then((data) => {
|
|
165
|
+
if (Array.isArray(data)) setFileResults(data);
|
|
166
|
+
else if (Array.isArray(data?.results)) setFileResults(data.results);
|
|
167
|
+
else setFileResults([]);
|
|
168
|
+
})
|
|
169
|
+
.catch(() => {
|
|
170
|
+
// aborted or failed — ignore
|
|
171
|
+
});
|
|
172
|
+
}, 200);
|
|
173
|
+
}
|
|
174
|
+
|
|
110
175
|
const navigate = useCallback(
|
|
111
176
|
(href: string) => {
|
|
112
177
|
setOpen(false);
|
|
@@ -115,11 +180,27 @@ export function CommandPalette() {
|
|
|
115
180
|
[router]
|
|
116
181
|
);
|
|
117
182
|
|
|
118
|
-
function
|
|
183
|
+
function handleToggleTheme() {
|
|
184
|
+
setOpen(false);
|
|
185
|
+
toggleTheme();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleSelectSkill(id: string, name: string) {
|
|
119
189
|
setOpen(false);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
window.dispatchEvent(
|
|
191
|
+
new CustomEvent("stagent.chat.activate-skill", { detail: { id } })
|
|
192
|
+
);
|
|
193
|
+
toast.info(`Skill "${name}" — activation coming soon`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleSelectFile(entityId: string, label: string) {
|
|
197
|
+
setOpen(false);
|
|
198
|
+
window.dispatchEvent(
|
|
199
|
+
new CustomEvent("stagent.chat.insert-mention", {
|
|
200
|
+
detail: { type: "file", path: entityId, label },
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
toast.info(`File "${label}" — mention insert coming soon`);
|
|
123
204
|
}
|
|
124
205
|
|
|
125
206
|
async function markAllRead() {
|
|
@@ -130,11 +211,55 @@ export function CommandPalette() {
|
|
|
130
211
|
|
|
131
212
|
const hasRecent = recentProjects.length > 0 || recentTasks.length > 0;
|
|
132
213
|
|
|
214
|
+
const handleDeleteSavedSearch = useCallback(
|
|
215
|
+
(s: SavedSearch) => {
|
|
216
|
+
// Optimistic remove + toast with Undo. The closure holds the full
|
|
217
|
+
// record so undo restores id/createdAt verbatim (not just label).
|
|
218
|
+
removeSavedSearch(s.id);
|
|
219
|
+
toast("Saved search deleted", {
|
|
220
|
+
duration: 5000,
|
|
221
|
+
action: {
|
|
222
|
+
label: "Undo",
|
|
223
|
+
onClick: () => {
|
|
224
|
+
// `save` generates a new id — we need to restore the original.
|
|
225
|
+
// The cheapest restoration is to re-save and then immediately
|
|
226
|
+
// patch the id via a rename-adjacent path. Since the hook has
|
|
227
|
+
// no "insert with id" method, we accept id churn on undo: the
|
|
228
|
+
// label/filterInput/surface are preserved, which is what the
|
|
229
|
+
// user sees. Acceptance criterion: the row reappears with its
|
|
230
|
+
// label and filter, the actual id is an implementation detail.
|
|
231
|
+
saveSavedSearch({
|
|
232
|
+
surface: s.surface,
|
|
233
|
+
label: s.label,
|
|
234
|
+
filterInput: s.filterInput,
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
[removeSavedSearch, saveSavedSearch]
|
|
241
|
+
);
|
|
242
|
+
|
|
133
243
|
if (!mounted) return null;
|
|
134
244
|
|
|
135
245
|
return (
|
|
136
|
-
|
|
137
|
-
|
|
246
|
+
<>
|
|
247
|
+
<CommandDialog
|
|
248
|
+
open={open}
|
|
249
|
+
onOpenChange={(next) => {
|
|
250
|
+
// Revalidate saved searches on every open. Each useSavedSearches
|
|
251
|
+
// consumer holds its own state, so a save in the chat popover
|
|
252
|
+
// wouldn't otherwise appear here until page reload.
|
|
253
|
+
// See features/saved-search-polish-v1.md.
|
|
254
|
+
if (next && !open) void refetchSavedSearches();
|
|
255
|
+
setOpen(next);
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<CommandInput
|
|
259
|
+
placeholder="Type a command or search..."
|
|
260
|
+
value={fileQuery}
|
|
261
|
+
onValueChange={handleInputChange}
|
|
262
|
+
/>
|
|
138
263
|
<CommandList>
|
|
139
264
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
140
265
|
|
|
@@ -183,6 +308,63 @@ export function CommandPalette() {
|
|
|
183
308
|
|
|
184
309
|
{hasRecent && <CommandSeparator />}
|
|
185
310
|
|
|
311
|
+
{/* Saved searches */}
|
|
312
|
+
{savedSearches.length > 0 && (
|
|
313
|
+
<>
|
|
314
|
+
<CommandGroup heading="Saved searches">
|
|
315
|
+
{savedSearches.map((s) => (
|
|
316
|
+
<CommandItem
|
|
317
|
+
key={`saved-${s.id}`}
|
|
318
|
+
value={`saved ${s.label} ${s.filterInput} ${s.surface}`}
|
|
319
|
+
onSelect={() => {
|
|
320
|
+
const base = SURFACE_ROUTE[s.surface];
|
|
321
|
+
navigate(`${base}?filter=${encodeURIComponent(s.filterInput)}`);
|
|
322
|
+
}}
|
|
323
|
+
keywords={["saved", "search", s.surface]}
|
|
324
|
+
className="group/item"
|
|
325
|
+
onKeyDown={(e) => {
|
|
326
|
+
// ⌘⌫ on focused row deletes with undo
|
|
327
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Backspace") {
|
|
328
|
+
e.preventDefault();
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
handleDeleteSavedSearch(s);
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<Bookmark className="h-4 w-4" />
|
|
335
|
+
<span className="flex-1 truncate">{s.label}</span>
|
|
336
|
+
<span className="text-xs text-muted-foreground font-mono">{s.filterInput}</span>
|
|
337
|
+
<span className="ml-2 text-xs text-muted-foreground">{s.surface}</span>
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
aria-label={`Delete saved search: ${s.label}`}
|
|
341
|
+
className="ml-1 p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive opacity-0 group-hover/item:opacity-100 focus-visible:opacity-100 transition-opacity"
|
|
342
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
343
|
+
onClick={(e) => {
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
e.stopPropagation();
|
|
346
|
+
handleDeleteSavedSearch(s);
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
350
|
+
</button>
|
|
351
|
+
</CommandItem>
|
|
352
|
+
))}
|
|
353
|
+
<CommandItem
|
|
354
|
+
value="manage-saved-searches"
|
|
355
|
+
keywords={["manage", "saved", "rename", "delete"]}
|
|
356
|
+
onSelect={() => {
|
|
357
|
+
setManagerOpen(true);
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<Settings2 className="h-4 w-4" />
|
|
361
|
+
<span className="flex-1">Manage saved searches…</span>
|
|
362
|
+
</CommandItem>
|
|
363
|
+
</CommandGroup>
|
|
364
|
+
<CommandSeparator />
|
|
365
|
+
</>
|
|
366
|
+
)}
|
|
367
|
+
|
|
186
368
|
{/* Navigation */}
|
|
187
369
|
<CommandGroup heading="Navigation">
|
|
188
370
|
{navigationItems.map((item) => (
|
|
@@ -237,9 +419,78 @@ export function CommandPalette() {
|
|
|
237
419
|
|
|
238
420
|
<CommandSeparator />
|
|
239
421
|
|
|
422
|
+
{/* Templates */}
|
|
423
|
+
<CommandGroup heading="Templates">
|
|
424
|
+
<CommandItem
|
|
425
|
+
value="start-from-template"
|
|
426
|
+
keywords={["template", "blueprint", "new", "conversation", "chat"]}
|
|
427
|
+
onSelect={() => {
|
|
428
|
+
setOpen(false);
|
|
429
|
+
// Ensure chat-shell is mounted so its event listener is live.
|
|
430
|
+
// When already on /chat, next-tick dispatch is a no-op nav.
|
|
431
|
+
router.push("/chat");
|
|
432
|
+
window.setTimeout(() => {
|
|
433
|
+
window.dispatchEvent(
|
|
434
|
+
new CustomEvent("stagent.chat.openTemplatePicker")
|
|
435
|
+
);
|
|
436
|
+
}, 50);
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
<Sparkles className="h-4 w-4" />
|
|
440
|
+
Start conversation from template…
|
|
441
|
+
</CommandItem>
|
|
442
|
+
</CommandGroup>
|
|
443
|
+
|
|
444
|
+
<CommandSeparator />
|
|
445
|
+
|
|
446
|
+
{/* Skills */}
|
|
447
|
+
{skills.length > 0 && (
|
|
448
|
+
<>
|
|
449
|
+
<CommandGroup heading="Skills">
|
|
450
|
+
{skills.map((skill) => (
|
|
451
|
+
<CommandItem
|
|
452
|
+
key={`skill-${skill.id}`}
|
|
453
|
+
value={`skill-${skill.name}`}
|
|
454
|
+
onSelect={() => handleSelectSkill(skill.id, skill.name)}
|
|
455
|
+
keywords={["skill", "profile"]}
|
|
456
|
+
>
|
|
457
|
+
<Sparkles className="h-4 w-4" />
|
|
458
|
+
<span className="flex-1 truncate">{skill.name}</span>
|
|
459
|
+
{skill.description && (
|
|
460
|
+
<span className="text-xs text-muted-foreground truncate max-w-[40%]">
|
|
461
|
+
{skill.description}
|
|
462
|
+
</span>
|
|
463
|
+
)}
|
|
464
|
+
</CommandItem>
|
|
465
|
+
))}
|
|
466
|
+
</CommandGroup>
|
|
467
|
+
<CommandSeparator />
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
|
|
471
|
+
{/* Files */}
|
|
472
|
+
{fileResults.length > 0 && (
|
|
473
|
+
<>
|
|
474
|
+
<CommandGroup heading="Files">
|
|
475
|
+
{fileResults.map((file) => (
|
|
476
|
+
<CommandItem
|
|
477
|
+
key={`file-${file.entityId}`}
|
|
478
|
+
value={`file-${file.label}`}
|
|
479
|
+
onSelect={() => handleSelectFile(file.entityId, file.label)}
|
|
480
|
+
keywords={["file", "path"]}
|
|
481
|
+
>
|
|
482
|
+
<FileCode className="h-4 w-4" />
|
|
483
|
+
<span className="flex-1 truncate font-mono text-xs">{file.label}</span>
|
|
484
|
+
</CommandItem>
|
|
485
|
+
))}
|
|
486
|
+
</CommandGroup>
|
|
487
|
+
<CommandSeparator />
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
|
|
240
491
|
{/* Utility */}
|
|
241
492
|
<CommandGroup heading="Utility">
|
|
242
|
-
<CommandItem onSelect={
|
|
493
|
+
<CommandItem onSelect={handleToggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
|
|
243
494
|
<Sun className="h-4 w-4 dark:hidden" />
|
|
244
495
|
<Moon className="h-4 w-4 hidden dark:block" />
|
|
245
496
|
Toggle Theme
|
|
@@ -253,5 +504,13 @@ export function CommandPalette() {
|
|
|
253
504
|
</CommandGroup>
|
|
254
505
|
</CommandList>
|
|
255
506
|
</CommandDialog>
|
|
507
|
+
<SavedSearchesManager
|
|
508
|
+
open={managerOpen}
|
|
509
|
+
onOpenChange={setManagerOpen}
|
|
510
|
+
searches={savedSearches}
|
|
511
|
+
onRename={renameSavedSearch}
|
|
512
|
+
onRemove={removeSavedSearch}
|
|
513
|
+
/>
|
|
514
|
+
</>
|
|
256
515
|
);
|
|
257
516
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Lightbulb } from "lucide-react";
|
|
5
|
+
import { parseFilterInput } from "@/lib/filters/parse";
|
|
6
|
+
|
|
7
|
+
interface FilterHintProps {
|
|
8
|
+
inputValue: string;
|
|
9
|
+
storageKey: string;
|
|
10
|
+
/** Optional copy override; defaults to the #key:value tip. */
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* FilterHint — passive discovery row for the `#key:value` filter syntax.
|
|
16
|
+
*
|
|
17
|
+
* Visibility rules:
|
|
18
|
+
* - Hidden once the dismissal flag is set in localStorage.
|
|
19
|
+
* - Hidden when the input contains `#` (user has discovered the syntax).
|
|
20
|
+
* - The flag is set the first time parseFilterInput returns ≥1 clause.
|
|
21
|
+
*
|
|
22
|
+
* Consumers: chat-command-popover, filter-input (list pages).
|
|
23
|
+
*/
|
|
24
|
+
export function FilterHint({ inputValue, storageKey, message }: FilterHintProps) {
|
|
25
|
+
const [dismissed, setDismissed] = useState(false);
|
|
26
|
+
|
|
27
|
+
const parsed = useMemo(() => parseFilterInput(inputValue), [inputValue]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
try {
|
|
31
|
+
if (window.localStorage.getItem(storageKey) === "1") {
|
|
32
|
+
setDismissed(true);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Private-mode or disabled storage — hint stays visible.
|
|
36
|
+
}
|
|
37
|
+
}, [storageKey]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (dismissed) return;
|
|
41
|
+
if (parsed.clauses.length > 0) {
|
|
42
|
+
try {
|
|
43
|
+
window.localStorage.setItem(storageKey, "1");
|
|
44
|
+
} catch {
|
|
45
|
+
// Private-mode or disabled storage — hint stays visible, no-op.
|
|
46
|
+
}
|
|
47
|
+
setDismissed(true);
|
|
48
|
+
}
|
|
49
|
+
}, [parsed.clauses.length, dismissed, storageKey]);
|
|
50
|
+
|
|
51
|
+
if (dismissed) return null;
|
|
52
|
+
if (inputValue.includes("#")) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
role="note"
|
|
57
|
+
className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground border-t border-border/50"
|
|
58
|
+
>
|
|
59
|
+
<Lightbulb className="h-3 w-3 shrink-0" aria-hidden />
|
|
60
|
+
<span>
|
|
61
|
+
{message ?? (
|
|
62
|
+
<>
|
|
63
|
+
Tip: use <code className="font-mono text-foreground">#key:value</code> to filter (e.g.{" "}
|
|
64
|
+
<code className="font-mono text-foreground">#status:blocked</code>)
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|