manigent-server 0.1.0__py3-none-any.whl
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.
- manigent/_catalog/mcps/chess/README.md +38 -0
- manigent/_catalog/mcps/chess/mcp.json +9 -0
- manigent/_catalog/mcps/registry.json +9 -0
- manigent/_catalog/skills/brief/SKILL.md +339 -0
- manigent/_catalog/skills/broaden-options/SKILL.md +133 -0
- manigent/_catalog/skills/calendar/SKILL.md +44 -0
- manigent/_catalog/skills/calendar/starter_config.jsonc +25 -0
- manigent/_catalog/skills/gmail/SKILL.md +73 -0
- manigent/_catalog/skills/google-calendar/SKILL.md +44 -0
- manigent/_catalog/skills/google-drive/SKILL.md +42 -0
- manigent/_catalog/skills/google-drive-write/SKILL.md +45 -0
- manigent/_catalog/skills/google-drive-write/starter_config.jsonc +45 -0
- manigent/_catalog/skills/grill-me/SKILL.md +173 -0
- manigent/_catalog/skills/homekit/SKILL.md +37 -0
- manigent/_catalog/skills/http-api/SKILL.md +117 -0
- manigent/_catalog/skills/ib/SKILL.md +89 -0
- manigent/_catalog/skills/ib/starter_config.jsonc +35 -0
- manigent/_catalog/skills/mailbox/SKILL.md +47 -0
- manigent/_catalog/skills/mailbox/starter_config.jsonc +38 -0
- manigent/_catalog/skills/media/SKILL.md +93 -0
- manigent/_catalog/skills/media/starter_config.jsonc +63 -0
- manigent/_catalog/skills/osxphotos/SKILL.md +41 -0
- manigent/_catalog/skills/photo-story/SKILL.md +285 -0
- manigent/_catalog/skills/playwright-browser/SKILL.md +291 -0
- manigent/_catalog/skills/playwright-browser/starter_config.jsonc +44 -0
- manigent/_catalog/skills/playwright-save-skill/SKILL.md +149 -0
- manigent/_catalog/skills/registry.json +166 -0
- manigent/_catalog/skills/web-artifacts/SKILL.md +194 -0
- manigent/api/action_executor.py +404 -0
- manigent/api/action_validator.py +531 -0
- manigent/api/actions.py +221 -0
- manigent/api/client.py +558 -0
- manigent/api/control.py +806 -0
- manigent/api/edge_protocol.py +123 -0
- manigent/api/fs_protocol.py +450 -0
- manigent/api/host_protocol.py +298 -0
- manigent/api/managed_job_client.py +124 -0
- manigent/api/responses.py +342 -0
- manigent/api/retry_transport.py +178 -0
- manigent/api/turn_protocol.py +78 -0
- manigent/cli_lifecycle.py +467 -0
- manigent/cli_server.py +978 -0
- manigent/doctor.py +1167 -0
- manigent/hub_protocol/canonical.py +375 -0
- manigent/hub_protocol/codes.py +171 -0
- manigent/hub_protocol/constants.py +71 -0
- manigent/hub_protocol/envelope.py +218 -0
- manigent/hub_protocol/frames.py +788 -0
- manigent/hub_protocol/http.py +260 -0
- manigent/hub_protocol/ids.py +170 -0
- manigent/hub_protocol/keys.py +195 -0
- manigent/hub_protocol/text.py +75 -0
- manigent/llm/base.py +1072 -0
- manigent/llm/prompt_builder.py +2450 -0
- manigent/llm/triggers.py +68 -0
- manigent/models/agent.py +2905 -0
- manigent/models/agent_bundle.py +298 -0
- manigent/models/brief_tab.py +311 -0
- manigent/models/chat_message.py +231 -0
- manigent/models/chatroom.py +1108 -0
- manigent/models/context.py +1159 -0
- manigent/models/desk_label.py +836 -0
- manigent/models/desk_read_state.py +220 -0
- manigent/models/desk_sign_off.py +267 -0
- manigent/models/desk_sop.py +965 -0
- manigent/models/desk_todo.py +1603 -0
- manigent/models/desk_todo_link.py +404 -0
- manigent/models/file_cache.py +75 -0
- manigent/models/host.py +732 -0
- manigent/models/install_token.py +401 -0
- manigent/models/invitation_code.py +162 -0
- manigent/models/knowledge_pack.py +552 -0
- manigent/models/managed_job.py +295 -0
- manigent/models/model_config.py +564 -0
- manigent/models/model_defaults.py +238 -0
- manigent/models/onboarding_plan.py +161 -0
- manigent/models/participant.py +449 -0
- manigent/models/persistable.py +892 -0
- manigent/models/plan_markdown.py +1774 -0
- manigent/models/project.py +1343 -0
- manigent/models/project_plan.py +8104 -0
- manigent/models/project_report.py +145 -0
- manigent/models/provenance.py +261 -0
- manigent/models/provider_agents.py +101 -0
- manigent/models/public_request.py +108 -0
- manigent/models/reflection_schedule.py +191 -0
- manigent/models/scheduled_message.py +285 -0
- manigent/models/share_token.py +190 -0
- manigent/models/skill_version.py +31 -0
- manigent/models/sop_template.py +187 -0
- manigent/models/thinking_partner_plan.py +187 -0
- manigent/models/tunnel_binding.py +129 -0
- manigent/models/user.py +1276 -0
- manigent/models/work_tracker.py +510 -0
- manigent/runner/invocation_registry.py +187 -0
- manigent/server/agent_capabilities.py +119 -0
- manigent/server/agent_visibility.py +119 -0
- manigent/server/announcements.py +119 -0
- manigent/server/app.py +807 -0
- manigent/server/auth.py +355 -0
- manigent/server/batch_timeout.py +94 -0
- manigent/server/channels/access.py +194 -0
- manigent/server/channels/adapter.py +295 -0
- manigent/server/channels/config.py +693 -0
- manigent/server/channels/contacts.py +39 -0
- manigent/server/channels/email_adapter.py +436 -0
- manigent/server/channels/email_address.py +145 -0
- manigent/server/channels/email_form.py +242 -0
- manigent/server/channels/email_format.py +285 -0
- manigent/server/channels/email_mail.py +427 -0
- manigent/server/channels/email_mailgun.py +164 -0
- manigent/server/channels/email_text.py +280 -0
- manigent/server/channels/file_links.py +127 -0
- manigent/server/channels/gateway.py +2998 -0
- manigent/server/channels/hub_delivery.py +211 -0
- manigent/server/channels/hub_store.py +1730 -0
- manigent/server/channels/line.py +645 -0
- manigent/server/channels/line_format.py +589 -0
- manigent/server/channels/log_redaction.py +145 -0
- manigent/server/channels/store.py +3316 -0
- manigent/server/channels/telegram.py +422 -0
- manigent/server/channels/telegram_format.py +312 -0
- manigent/server/channels/webchat.py +261 -0
- manigent/server/channels/webchat_widget.js +1310 -0
- manigent/server/channels/whatsapp.py +707 -0
- manigent/server/channels/whatsapp_format.py +511 -0
- manigent/server/chat_search_index.py +1203 -0
- manigent/server/config.py +61 -0
- manigent/server/context.py +121 -0
- manigent/server/coordinator_rule.py +115 -0
- manigent/server/desk_todo_notify.py +300 -0
- manigent/server/edge_hub.py +577 -0
- manigent/server/email.py +354 -0
- manigent/server/errors.py +83 -0
- manigent/server/home_fs_relay.py +392 -0
- manigent/server/hub/adapter.py +1395 -0
- manigent/server/hub/agent_send_guard.py +196 -0
- manigent/server/hub/avatar.py +342 -0
- manigent/server/hub/browser.py +778 -0
- manigent/server/hub/client.py +859 -0
- manigent/server/hub/demand.py +49 -0
- manigent/server/hub/directory.py +385 -0
- manigent/server/hub/enroll.py +400 -0
- manigent/server/hub/http.py +134 -0
- manigent/server/hub/identity.py +61 -0
- manigent/server/hub/keys.py +588 -0
- manigent/server/hub/mirror.py +802 -0
- manigent/server/hub/operator.py +817 -0
- manigent/server/hub/retain.py +361 -0
- manigent/server/hub/room_bridge.py +741 -0
- manigent/server/hub/runtime.py +691 -0
- manigent/server/hub/trust.py +182 -0
- manigent/server/hub/views.py +316 -0
- manigent/server/idempotency.py +205 -0
- manigent/server/in_flight_restore.py +181 -0
- manigent/server/inspection.py +816 -0
- manigent/server/isolation_gate.py +516 -0
- manigent/server/isolation_notice.py +310 -0
- manigent/server/managed_job_events.py +242 -0
- manigent/server/mcp_registry.py +118 -0
- manigent/server/message_forward.py +78 -0
- manigent/server/message_scheduler.py +176 -0
- manigent/server/notification_assets.py +164 -0
- manigent/server/oauth_login/cli_state.py +120 -0
- manigent/server/oauth_login/config.py +90 -0
- manigent/server/oauth_login/errors.py +78 -0
- manigent/server/oauth_login/flow.py +223 -0
- manigent/server/oauth_login/handoff.py +63 -0
- manigent/server/oauth_login/providers.py +45 -0
- manigent/server/oauth_state.py +81 -0
- manigent/server/owner_isolation.py +466 -0
- manigent/server/participant_liveness.py +82 -0
- manigent/server/public_profile.py +149 -0
- manigent/server/public_requests/candidates.py +116 -0
- manigent/server/public_requests/dispatch.py +121 -0
- manigent/server/public_requests/ranking.py +153 -0
- manigent/server/recent_activity.py +250 -0
- manigent/server/reflection_completion_subscriber.py +151 -0
- manigent/server/reflection_scheduler.py +239 -0
- manigent/server/requester_key_migration.py +261 -0
- manigent/server/room_flow.py +139 -0
- manigent/server/routes/_batch.py +218 -0
- manigent/server/routes/_fd_routing.py +155 -0
- manigent/server/routes/_mentions.py +70 -0
- manigent/server/routes/agent_env.py +188 -0
- manigent/server/routes/agent_fs.py +450 -0
- manigent/server/routes/agents.py +1206 -0
- manigent/server/routes/announcements.py +79 -0
- manigent/server/routes/auth.py +387 -0
- manigent/server/routes/brief_tabs.py +404 -0
- manigent/server/routes/changelog.py +237 -0
- manigent/server/routes/channels.py +993 -0
- manigent/server/routes/chatrooms.py +871 -0
- manigent/server/routes/desk_labels.py +298 -0
- manigent/server/routes/desk_latest_messages.py +116 -0
- manigent/server/routes/desk_read_state.py +110 -0
- manigent/server/routes/desk_sign_off.py +88 -0
- manigent/server/routes/desk_sops.py +216 -0
- manigent/server/routes/desk_todos.py +692 -0
- manigent/server/routes/desk_tour.py +44 -0
- manigent/server/routes/files.py +572 -0
- manigent/server/routes/hosts.py +484 -0
- manigent/server/routes/hub.py +534 -0
- manigent/server/routes/hub_channels.py +234 -0
- manigent/server/routes/inbox.py +581 -0
- manigent/server/routes/install.ps1 +436 -0
- manigent/server/routes/install.py +324 -0
- manigent/server/routes/install.sh +566 -0
- manigent/server/routes/isolation_check.py +145 -0
- manigent/server/routes/knowledge_packs.py +477 -0
- manigent/server/routes/managed_jobs.py +196 -0
- manigent/server/routes/mcps.py +271 -0
- manigent/server/routes/messages.py +1972 -0
- manigent/server/routes/model_defaults.py +279 -0
- manigent/server/routes/oauth.py +237 -0
- manigent/server/routes/oauth_login.py +351 -0
- manigent/server/routes/owner_isolation.py +91 -0
- manigent/server/routes/project_plans.py +1521 -0
- manigent/server/routes/project_reports.py +235 -0
- manigent/server/routes/projects.py +2047 -0
- manigent/server/routes/public_agents.py +151 -0
- manigent/server/routes/public_requests.py +181 -0
- manigent/server/routes/reflection.py +93 -0
- manigent/server/routes/scheduled_messages.py +263 -0
- manigent/server/routes/search.py +94 -0
- manigent/server/routes/sharing.py +156 -0
- manigent/server/routes/skills.py +406 -0
- manigent/server/routes/templates.py +219 -0
- manigent/server/routes/turn_tokens.py +119 -0
- manigent/server/routes/webchat.py +406 -0
- manigent/server/routes/websocket.py +431 -0
- manigent/server/serving_only.py +200 -0
- manigent/server/signup_policy.py +120 -0
- manigent/server/skill_registry.py +164 -0
- manigent/server/socket_events.py +858 -0
- manigent/server/timeout_checker.py +446 -0
- manigent/server/tunnel_subscriber.py +1000 -0
- manigent/server/turn_tokens.py +285 -0
- manigent/server/ws_hub.py +556 -0
- manigent/server_cli.py +31 -0
- manigent/sync/changelog.py +885 -0
- manigent/sync/console_subscriber.py +222 -0
- manigent/sync/runloop.py +619 -0
- manigent/sync/runloop_manager.py +197 -0
- manigent/sync/subscriber.py +98 -0
- manigent/utils/agent_processes.py +276 -0
- manigent/utils/agent_storage.py +125 -0
- manigent/utils/email.py +563 -0
- manigent/utils/env_store.py +174 -0
- manigent/utils/extra_dirs.py +85 -0
- manigent/utils/file_io.py +485 -0
- manigent/utils/invoke_timeout.py +87 -0
- manigent/utils/jsonc.py +78 -0
- manigent/utils/knowledge_index.py +148 -0
- manigent/utils/nofollow_fs.py +279 -0
- manigent/utils/notification_center.py +67 -0
- manigent/utils/version.py +57 -0
- manigent/web/frontend/.design-sync-entry.tsx +74 -0
- manigent/web/frontend/.design-sync-provider.tsx +46 -0
- manigent/web/frontend/.eslintrc.cjs +183 -0
- manigent/web/frontend/.gitignore +6 -0
- manigent/web/frontend/app/page.tsx +39 -0
- manigent/web/frontend/hub-terms.html +192 -0
- manigent/web/frontend/index.html +15 -0
- manigent/web/frontend/landing.html +15 -0
- manigent/web/frontend/package-lock.json +8028 -0
- manigent/web/frontend/package.json +55 -0
- manigent/web/frontend/postcss.config.js +6 -0
- manigent/web/frontend/privacy.html +217 -0
- manigent/web/frontend/private-beta-announcement.html +19 -0
- manigent/web/frontend/public/apple-touch-icon.png +0 -0
- manigent/web/frontend/public/brand-hero-banner.png +0 -0
- manigent/web/frontend/public/brand-hero.png +0 -0
- manigent/web/frontend/public/favicon.png +0 -0
- manigent/web/frontend/public/favicon.svg +8 -0
- manigent/web/frontend/public/icon-transparent-sm.png +0 -0
- manigent/web/frontend/public/icon.png +0 -0
- manigent/web/frontend/public/manigent-mark.svg +7 -0
- manigent/web/frontend/public/vite.svg +5 -0
- manigent/web/frontend/public.html +15 -0
- manigent/web/frontend/src/App.activeAccountRedirect.test.tsx +236 -0
- manigent/web/frontend/src/App.hubHostDm.test.tsx +119 -0
- manigent/web/frontend/src/App.hubHostProject.test.tsx +142 -0
- manigent/web/frontend/src/App.hubRoutes.test.tsx +187 -0
- manigent/web/frontend/src/App.oauthAddCallback.test.tsx +263 -0
- manigent/web/frontend/src/App.tsx +246 -0
- manigent/web/frontend/src/api/attachments.ts +32 -0
- manigent/web/frontend/src/api/batchLoader.test.ts +212 -0
- manigent/web/frontend/src/api/batchLoader.ts +124 -0
- manigent/web/frontend/src/api/chatroomsBatch.test.tsx +392 -0
- manigent/web/frontend/src/api/client.ts +321 -0
- manigent/web/frontend/src/api/crossAccountAuth.test.ts +388 -0
- manigent/web/frontend/src/api/endpoints/agentEnv.ts +24 -0
- manigent/web/frontend/src/api/endpoints/agentFs.ts +125 -0
- manigent/web/frontend/src/api/endpoints/agentSearch.ts +19 -0
- manigent/web/frontend/src/api/endpoints/agents.ts +66 -0
- manigent/web/frontend/src/api/endpoints/announcements.ts +26 -0
- manigent/web/frontend/src/api/endpoints/auth.ts +361 -0
- manigent/web/frontend/src/api/endpoints/authLogin.test.ts +236 -0
- manigent/web/frontend/src/api/endpoints/batch.ts +64 -0
- manigent/web/frontend/src/api/endpoints/briefTabs.ts +81 -0
- manigent/web/frontend/src/api/endpoints/channels.ts +285 -0
- manigent/web/frontend/src/api/endpoints/computers.ts +212 -0
- manigent/web/frontend/src/api/endpoints/deskLabels.ts +151 -0
- manigent/web/frontend/src/api/endpoints/deskLatestMessages.ts +23 -0
- manigent/web/frontend/src/api/endpoints/deskReadState.ts +41 -0
- manigent/web/frontend/src/api/endpoints/deskSignOffs.ts +74 -0
- manigent/web/frontend/src/api/endpoints/deskSops.ts +53 -0
- manigent/web/frontend/src/api/endpoints/deskTodoAttachments.ts +69 -0
- manigent/web/frontend/src/api/endpoints/deskTodoProjects.test.ts +99 -0
- manigent/web/frontend/src/api/endpoints/deskTodoProjects.ts +114 -0
- manigent/web/frontend/src/api/endpoints/deskTodos.ts +164 -0
- manigent/web/frontend/src/api/endpoints/deskTour.ts +16 -0
- manigent/web/frontend/src/api/endpoints/dm.ts +137 -0
- manigent/web/frontend/src/api/endpoints/files.ts +55 -0
- manigent/web/frontend/src/api/endpoints/hub.contract.test.ts +94 -0
- manigent/web/frontend/src/api/endpoints/hub.keys.ts +260 -0
- manigent/web/frontend/src/api/endpoints/hub.test.ts +203 -0
- manigent/web/frontend/src/api/endpoints/hub.ts +654 -0
- manigent/web/frontend/src/api/endpoints/index.ts +15 -0
- manigent/web/frontend/src/api/endpoints/install.ts +74 -0
- manigent/web/frontend/src/api/endpoints/isolationCheck.ts +36 -0
- manigent/web/frontend/src/api/endpoints/knowledgePacks.ts +179 -0
- manigent/web/frontend/src/api/endpoints/mcps.ts +36 -0
- manigent/web/frontend/src/api/endpoints/messages.held.test.ts +43 -0
- manigent/web/frontend/src/api/endpoints/messages.ts +95 -0
- manigent/web/frontend/src/api/endpoints/modelConfigs.ts +232 -0
- manigent/web/frontend/src/api/endpoints/oauthApi.test.ts +110 -0
- manigent/web/frontend/src/api/endpoints/projectPlans.ts +608 -0
- manigent/web/frontend/src/api/endpoints/projectReports.ts +36 -0
- manigent/web/frontend/src/api/endpoints/projects.ts +124 -0
- manigent/web/frontend/src/api/endpoints/publicRequests.ts +75 -0
- manigent/web/frontend/src/api/endpoints/reflection.ts +32 -0
- manigent/web/frontend/src/api/endpoints/roomFlow.ts +14 -0
- manigent/web/frontend/src/api/endpoints/scheduledMessages.ts +60 -0
- manigent/web/frontend/src/api/endpoints/search.ts +27 -0
- manigent/web/frontend/src/api/endpoints/sharing.ts +42 -0
- manigent/web/frontend/src/api/endpoints/skills.ts +67 -0
- manigent/web/frontend/src/api/endpoints/templates.ts +72 -0
- manigent/web/frontend/src/api/loaders.ts +22 -0
- manigent/web/frontend/src/api/singleIssuer.test.ts +148 -0
- manigent/web/frontend/src/api/transientRetry.test.ts +176 -0
- manigent/web/frontend/src/api/transientRetry.ts +140 -0
- manigent/web/frontend/src/api/websocket.resync.test.ts +111 -0
- manigent/web/frontend/src/api/websocket.ts +335 -0
- manigent/web/frontend/src/components/agent/AgentNameLink.tsx +24 -0
- manigent/web/frontend/src/components/agent/AgentViewActions.test.tsx +41 -0
- manigent/web/frontend/src/components/agent/AgentViewActions.tsx +89 -0
- manigent/web/frontend/src/components/agent/CreateAgentPage.tsx +535 -0
- manigent/web/frontend/src/components/agent/JsonConfigEditor.tsx +33 -0
- manigent/web/frontend/src/components/agent/KnowledgePackList.tsx +103 -0
- manigent/web/frontend/src/components/agent/McpCatalog.tsx +261 -0
- manigent/web/frontend/src/components/agent/McpConfigModal.tsx +224 -0
- manigent/web/frontend/src/components/agent/PreConsentModal.tsx +172 -0
- manigent/web/frontend/src/components/agent/SkillCatalog.tsx +364 -0
- manigent/web/frontend/src/components/agent/SkillConfigModal.tsx +221 -0
- manigent/web/frontend/src/components/auth/AddAccountPage.test.tsx +181 -0
- manigent/web/frontend/src/components/auth/AddAccountPage.tsx +201 -0
- manigent/web/frontend/src/components/auth/AuthShell.tsx +462 -0
- manigent/web/frontend/src/components/auth/LoginPage.test.tsx +384 -0
- manigent/web/frontend/src/components/auth/LoginPage.tsx +129 -0
- manigent/web/frontend/src/components/auth/OAuthCallbackPage.test.tsx +185 -0
- manigent/web/frontend/src/components/auth/OAuthCallbackPage.tsx +154 -0
- manigent/web/frontend/src/components/auth/OAuthLinkedBanner.tsx +44 -0
- manigent/web/frontend/src/components/auth/ProviderButtons.tsx +94 -0
- manigent/web/frontend/src/components/auth/SignupPage.tsx +437 -0
- manigent/web/frontend/src/components/auth/StepProgress.tsx +60 -0
- manigent/web/frontend/src/components/auth/VerifyEmailPage.test.tsx +155 -0
- manigent/web/frontend/src/components/auth/VerifyEmailPage.tsx +211 -0
- manigent/web/frontend/src/components/auth/index.ts +3 -0
- manigent/web/frontend/src/components/auth/oauthErrors.test.ts +107 -0
- manigent/web/frontend/src/components/auth/oauthErrors.ts +82 -0
- manigent/web/frontend/src/components/auth/useUsernameCheck.test.tsx +68 -0
- manigent/web/frontend/src/components/auth/useUsernameCheck.ts +86 -0
- manigent/web/frontend/src/components/beta/BetaPage.tsx +21 -0
- manigent/web/frontend/src/components/beta/DayAtMyDesk.tsx +120 -0
- manigent/web/frontend/src/components/beta/Footer.tsx +82 -0
- manigent/web/frontend/src/components/beta/Hero.tsx +224 -0
- manigent/web/frontend/src/components/beta/Nav.tsx +51 -0
- manigent/web/frontend/src/components/beta/Pillars.tsx +172 -0
- manigent/web/frontend/src/components/beta/TwoModels.tsx +146 -0
- manigent/web/frontend/src/components/beta/constants.ts +3 -0
- manigent/web/frontend/src/components/briefing/BriefingFrame.test.tsx +208 -0
- manigent/web/frontend/src/components/briefing/BriefingFrame.tsx +151 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.test.tsx +176 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.tsx +157 -0
- manigent/web/frontend/src/components/briefing/BriefingPage.wsDelete.test.tsx +177 -0
- manigent/web/frontend/src/components/briefing/briefing.css +147 -0
- manigent/web/frontend/src/components/briefing/heightBootstrap.test.ts +121 -0
- manigent/web/frontend/src/components/briefing/heightBootstrap.ts +224 -0
- manigent/web/frontend/src/components/briefing/parseBriefMessage.test.ts +90 -0
- manigent/web/frontend/src/components/briefing/sampleBriefing.ts +148 -0
- manigent/web/frontend/src/components/channels/ChannelBadge.tsx +35 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadControls.tsx +127 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.test.tsx +207 -0
- manigent/web/frontend/src/components/chatroom/ChannelThreadNotice.tsx +80 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.channel.test.tsx +550 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.flow.test.tsx +186 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.isolation.test.tsx +70 -0
- manigent/web/frontend/src/components/chatroom/ChatroomView.tsx +450 -0
- manigent/web/frontend/src/components/chatroom/DelegationRow.test.tsx +170 -0
- manigent/web/frontend/src/components/chatroom/DelegationRow.tsx +217 -0
- manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.test.tsx +33 -0
- manigent/web/frontend/src/components/chatroom/DeliveryStatusLine.tsx +46 -0
- manigent/web/frontend/src/components/chatroom/ForwardModal.test.tsx +109 -0
- manigent/web/frontend/src/components/chatroom/ForwardModal.tsx +304 -0
- manigent/web/frontend/src/components/chatroom/HeldNotice.tsx +77 -0
- manigent/web/frontend/src/components/chatroom/MentionDropdown.tsx +97 -0
- manigent/web/frontend/src/components/chatroom/MessageInput.tsx +408 -0
- manigent/web/frontend/src/components/chatroom/MessageItem.tsx +425 -0
- manigent/web/frontend/src/components/chatroom/MessageList.outside.test.tsx +98 -0
- manigent/web/frontend/src/components/chatroom/MessageList.test.tsx +245 -0
- manigent/web/frontend/src/components/chatroom/MessageList.tsx +406 -0
- manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.test.tsx +211 -0
- manigent/web/frontend/src/components/chatroom/OfflineRemedyNote.tsx +182 -0
- manigent/web/frontend/src/components/chatroom/OutsideBadge.tsx +18 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowControl.tsx +49 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowStrip.test.tsx +47 -0
- manigent/web/frontend/src/components/chatroom/RoomFlowStrip.tsx +25 -0
- manigent/web/frontend/src/components/chatroom/SaveToPackModal.tsx +350 -0
- manigent/web/frontend/src/components/chatroom/StagedAttachments.test.tsx +56 -0
- manigent/web/frontend/src/components/chatroom/StagedAttachments.tsx +160 -0
- manigent/web/frontend/src/components/chatroom/SystemNote.tsx +18 -0
- manigent/web/frontend/src/components/chatroom/contactSender.test.tsx +130 -0
- manigent/web/frontend/src/components/chatroom/index.ts +3 -0
- manigent/web/frontend/src/components/chatroom/recipientStatus.test.ts +344 -0
- manigent/web/frontend/src/components/chatroom/recipientStatus.ts +197 -0
- manigent/web/frontend/src/components/common/AnnouncementBanners.test.tsx +221 -0
- manigent/web/frontend/src/components/common/AnnouncementBanners.tsx +180 -0
- manigent/web/frontend/src/components/common/Avatar.test.tsx +50 -0
- manigent/web/frontend/src/components/common/Avatar.tsx +102 -0
- manigent/web/frontend/src/components/common/Badge.tsx +30 -0
- manigent/web/frontend/src/components/common/BrandName.tsx +16 -0
- manigent/web/frontend/src/components/common/ClearHistoryButton.tsx +74 -0
- manigent/web/frontend/src/components/common/CollapsibleHeader.tsx +64 -0
- manigent/web/frontend/src/components/common/ConnectionStatus.test.tsx +56 -0
- manigent/web/frontend/src/components/common/ConnectionStatus.tsx +28 -0
- manigent/web/frontend/src/components/common/DropOverlay.tsx +27 -0
- manigent/web/frontend/src/components/common/InlineEditableTitle.test.tsx +369 -0
- manigent/web/frontend/src/components/common/InlineEditableTitle.tsx +287 -0
- manigent/web/frontend/src/components/common/IsolationUnavailablePanel.tsx +19 -0
- manigent/web/frontend/src/components/common/Loading.tsx +22 -0
- manigent/web/frontend/src/components/common/ModelSelector.tsx +248 -0
- manigent/web/frontend/src/components/common/Overlay.tsx +15 -0
- manigent/web/frontend/src/components/common/ResizeHandle.tsx +52 -0
- manigent/web/frontend/src/components/common/TagAutocompleteInput.test.tsx +92 -0
- manigent/web/frontend/src/components/common/TagAutocompleteInput.tsx +246 -0
- manigent/web/frontend/src/components/common/TagPillInput.tsx +181 -0
- manigent/web/frontend/src/components/common/TeamAndAgentPicker.tsx +83 -0
- manigent/web/frontend/src/components/common/Timestamp.tsx +61 -0
- manigent/web/frontend/src/components/common/VerifiedBadge.tsx +20 -0
- manigent/web/frontend/src/components/common/index.ts +8 -0
- manigent/web/frontend/src/components/composer/PhPopover.tsx +239 -0
- manigent/web/frontend/src/components/composer/PromptEditor.test.tsx +472 -0
- manigent/web/frontend/src/components/composer/PromptEditor.tsx +540 -0
- manigent/web/frontend/src/components/composer/caret.ts +159 -0
- manigent/web/frontend/src/components/composer/placeholders.test.ts +352 -0
- manigent/web/frontend/src/components/composer/placeholders.ts +354 -0
- manigent/web/frontend/src/components/composer/promptEditor.css +161 -0
- manigent/web/frontend/src/components/computer/ComputerPage.test.tsx +452 -0
- manigent/web/frontend/src/components/computer/ComputerPage.tsx +811 -0
- manigent/web/frontend/src/components/computer/ConnectComputerModal.tsx +135 -0
- manigent/web/frontend/src/components/computer/CopyCommand.tsx +77 -0
- manigent/web/frontend/src/components/computer/DisconnectComputerModal.tsx +121 -0
- manigent/web/frontend/src/components/computer/SoftwareCard.tsx +145 -0
- manigent/web/frontend/src/components/computer/TerminalCard.test.tsx +114 -0
- manigent/web/frontend/src/components/computer/TerminalCard.tsx +131 -0
- manigent/web/frontend/src/components/computer/TerminalPanel.tsx +226 -0
- manigent/web/frontend/src/components/computer/TrustPanel.test.tsx +58 -0
- manigent/web/frontend/src/components/computer/TrustPanel.tsx +143 -0
- manigent/web/frontend/src/components/computer/computerCopy.test.ts +322 -0
- manigent/web/frontend/src/components/computer/computerCopy.ts +413 -0
- manigent/web/frontend/src/components/computer/sections.ts +21 -0
- manigent/web/frontend/src/components/computer/terminalProtocol.test.ts +135 -0
- manigent/web/frontend/src/components/computer/terminalProtocol.ts +170 -0
- manigent/web/frontend/src/components/context/ContextGuide.test.tsx +26 -0
- manigent/web/frontend/src/components/context/ContextGuide.tsx +144 -0
- manigent/web/frontend/src/components/context/ContextLanding.tsx +46 -0
- manigent/web/frontend/src/components/desk/CardBand.a11y.test.tsx +138 -0
- manigent/web/frontend/src/components/desk/CardBand.tsx +132 -0
- manigent/web/frontend/src/components/desk/DashTile.tsx +81 -0
- manigent/web/frontend/src/components/desk/DeskAgentPicker.retained.test.tsx +79 -0
- manigent/web/frontend/src/components/desk/DeskAgentPicker.tsx +151 -0
- manigent/web/frontend/src/components/desk/DeskComposer.detachTodo.test.tsx +233 -0
- manigent/web/frontend/src/components/desk/DeskComposer.dispatch.test.tsx +208 -0
- manigent/web/frontend/src/components/desk/DeskComposer.loadTodoDraft.test.tsx +242 -0
- manigent/web/frontend/src/components/desk/DeskComposer.restage.test.tsx +258 -0
- manigent/web/frontend/src/components/desk/DeskComposer.saveTodo.test.tsx +265 -0
- manigent/web/frontend/src/components/desk/DeskComposer.tsx +828 -0
- manigent/web/frontend/src/components/desk/DeskPage.crossHighlight.test.tsx +427 -0
- manigent/web/frontend/src/components/desk/DeskPage.link.test.tsx +271 -0
- manigent/web/frontend/src/components/desk/DeskPage.tsx +1066 -0
- manigent/web/frontend/src/components/desk/DeskPage.welcomeTour.test.tsx +262 -0
- manigent/web/frontend/src/components/desk/DeskSideRail.tsx +100 -0
- manigent/web/frontend/src/components/desk/DeskWelcomeTour.tsx +513 -0
- manigent/web/frontend/src/components/desk/DeskZeroState.tsx +45 -0
- manigent/web/frontend/src/components/desk/LabelChip.tsx +147 -0
- manigent/web/frontend/src/components/desk/LabelFilter.tsx +474 -0
- manigent/web/frontend/src/components/desk/LabelInput.test.tsx +439 -0
- manigent/web/frontend/src/components/desk/LabelInput.tsx +477 -0
- manigent/web/frontend/src/components/desk/LabelManageSheet.test.tsx +384 -0
- manigent/web/frontend/src/components/desk/LabelManageSheet.tsx +498 -0
- manigent/web/frontend/src/components/desk/PhaseRail.test.tsx +170 -0
- manigent/web/frontend/src/components/desk/PhaseRail.tsx +120 -0
- manigent/web/frontend/src/components/desk/PlanBand.test.tsx +655 -0
- manigent/web/frontend/src/components/desk/PlanBand.tsx +216 -0
- manigent/web/frontend/src/components/desk/SaveTodoModal.tsx +126 -0
- manigent/web/frontend/src/components/desk/SopEditor.tsx +280 -0
- manigent/web/frontend/src/components/desk/SopItem.tsx +171 -0
- manigent/web/frontend/src/components/desk/SopRail.test.tsx +517 -0
- manigent/web/frontend/src/components/desk/SopRail.tsx +250 -0
- manigent/web/frontend/src/components/desk/TicketChip.tsx +51 -0
- manigent/web/frontend/src/components/desk/TodoItem.attachments.test.tsx +96 -0
- manigent/web/frontend/src/components/desk/TodoItem.menu.test.tsx +142 -0
- manigent/web/frontend/src/components/desk/TodoItem.tsx +573 -0
- manigent/web/frontend/src/components/desk/TodoRail.longpress.test.tsx +396 -0
- manigent/web/frontend/src/components/desk/TodoRail.test.tsx +1155 -0
- manigent/web/frontend/src/components/desk/TodoRail.tsx +918 -0
- manigent/web/frontend/src/components/desk/TodoRowMenu.tsx +147 -0
- manigent/web/frontend/src/components/desk/UpdateCard.bands.test.tsx +308 -0
- manigent/web/frontend/src/components/desk/UpdateCard.sendBack.test.tsx +444 -0
- manigent/web/frontend/src/components/desk/UpdateCard.tsx +943 -0
- manigent/web/frontend/src/components/desk/UpdateCard.untrusted.test.tsx +161 -0
- manigent/web/frontend/src/components/desk/desk.css +1921 -0
- manigent/web/frontend/src/components/desk/desk.css.test.ts +582 -0
- manigent/web/frontend/src/components/desk/deskBands.test.ts +389 -0
- manigent/web/frontend/src/components/desk/deskBands.ts +226 -0
- manigent/web/frontend/src/components/desk/m16Surfaces.test.ts +134 -0
- manigent/web/frontend/src/components/desk/sopSyntax.ts +73 -0
- manigent/web/frontend/src/components/discovery/AgentDirectory.tsx +71 -0
- manigent/web/frontend/src/components/discovery/AgentFilterBar.tsx +46 -0
- manigent/web/frontend/src/components/discovery/AgentSearchBar.tsx +56 -0
- manigent/web/frontend/src/components/dm/AgentSummaryCard.tsx +154 -0
- manigent/web/frontend/src/components/dm/ComposeFields.tsx +145 -0
- manigent/web/frontend/src/components/dm/DMChatView.flow.test.tsx +374 -0
- manigent/web/frontend/src/components/dm/DMChatView.tsx +841 -0
- manigent/web/frontend/src/components/dm/DMZeroState.tsx +204 -0
- manigent/web/frontend/src/components/dm/DmComposeModal.tsx +301 -0
- manigent/web/frontend/src/components/dm/PersonalizeModal.tsx +366 -0
- manigent/web/frontend/src/components/dm/index.ts +1 -0
- manigent/web/frontend/src/components/files/FileItem.tsx +138 -0
- manigent/web/frontend/src/components/files/FileList.outside.test.tsx +29 -0
- manigent/web/frontend/src/components/files/FileList.tsx +48 -0
- manigent/web/frontend/src/components/files/FilePreview.test.tsx +75 -0
- manigent/web/frontend/src/components/files/FilePreview.tsx +325 -0
- manigent/web/frontend/src/components/files/FileViewerModal.test.tsx +157 -0
- manigent/web/frontend/src/components/files/FileViewerModal.tsx +261 -0
- manigent/web/frontend/src/components/files/HighlightedCode.tsx +39 -0
- manigent/web/frontend/src/components/files/SaveToAgentPanel.test.tsx +156 -0
- manigent/web/frontend/src/components/files/SaveToAgentPanel.tsx +545 -0
- manigent/web/frontend/src/components/files/index.ts +3 -0
- manigent/web/frontend/src/components/hero/AgentAvatar.tsx +66 -0
- manigent/web/frontend/src/components/hero/Button.tsx +66 -0
- manigent/web/frontend/src/components/hero/Hero.tsx +133 -0
- manigent/web/frontend/src/components/homefs/HomeFolderBrowser.test.tsx +393 -0
- manigent/web/frontend/src/components/homefs/HomeFolderBrowser.tsx +792 -0
- manigent/web/frontend/src/components/hub/BridgeDeliveryLine.test.tsx +40 -0
- manigent/web/frontend/src/components/hub/BridgeDeliveryLine.tsx +36 -0
- manigent/web/frontend/src/components/hub/HubAvatar.test.tsx +93 -0
- manigent/web/frontend/src/components/hub/HubAvatar.tsx +33 -0
- manigent/web/frontend/src/components/hub/HubChatView.test.tsx +695 -0
- manigent/web/frontend/src/components/hub/HubChatView.tsx +504 -0
- manigent/web/frontend/src/components/hub/HubContactLabel.test.tsx +92 -0
- manigent/web/frontend/src/components/hub/HubContactLabel.tsx +47 -0
- manigent/web/frontend/src/components/hub/HubDeliveryLine.tsx +49 -0
- manigent/web/frontend/src/components/hub/HubDisclosure.tsx +49 -0
- manigent/web/frontend/src/components/hub/HubEnrollmentDialog.test.tsx +144 -0
- manigent/web/frontend/src/components/hub/HubEnrollmentDialog.tsx +244 -0
- manigent/web/frontend/src/components/hub/HubMessageBody.test.tsx +26 -0
- manigent/web/frontend/src/components/hub/HubMessageBody.tsx +14 -0
- manigent/web/frontend/src/components/hub/HubOfflineConfirm.tsx +45 -0
- manigent/web/frontend/src/components/hub/HubThreadBanner.tsx +33 -0
- manigent/web/frontend/src/components/hub/HubThreadGuard.tsx +59 -0
- manigent/web/frontend/src/components/hub/RemoteAgentCard.test.tsx +50 -0
- manigent/web/frontend/src/components/hub/RemoteAgentCard.tsx +174 -0
- manigent/web/frontend/src/components/hub/RemoteAgentSection.test.tsx +446 -0
- manigent/web/frontend/src/components/hub/RemoteAgentSection.tsx +219 -0
- manigent/web/frontend/src/components/hub/ReportDialog.test.tsx +96 -0
- manigent/web/frontend/src/components/hub/ReportDialog.tsx +162 -0
- manigent/web/frontend/src/components/hub/RetainButton.test.tsx +103 -0
- manigent/web/frontend/src/components/hub/RetainButton.tsx +114 -0
- manigent/web/frontend/src/components/hub/RetainDialog.test.tsx +68 -0
- manigent/web/frontend/src/components/hub/RetainDialog.tsx +115 -0
- manigent/web/frontend/src/components/hub/RetainedList.test.tsx +94 -0
- manigent/web/frontend/src/components/hub/RetainedList.tsx +32 -0
- manigent/web/frontend/src/components/hub/RetainedRow.tsx +129 -0
- manigent/web/frontend/src/components/hub/StandInChip.test.tsx +37 -0
- manigent/web/frontend/src/components/hub/StandInChip.tsx +43 -0
- manigent/web/frontend/src/components/hub/UnusualNameChip.test.tsx +81 -0
- manigent/web/frontend/src/components/hub/UnusualNameChip.tsx +22 -0
- manigent/web/frontend/src/components/hub/hubLint.test.ts +265 -0
- manigent/web/frontend/src/components/landing/ComparisonSection.tsx +92 -0
- manigent/web/frontend/src/components/landing/ConsoleSection.tsx +402 -0
- manigent/web/frontend/src/components/landing/CrewSection.tsx +224 -0
- manigent/web/frontend/src/components/landing/FAQ.tsx +68 -0
- manigent/web/frontend/src/components/landing/Footer.tsx +58 -0
- manigent/web/frontend/src/components/landing/HowItWorks.tsx +117 -0
- manigent/web/frontend/src/components/landing/KnowledgeSection.tsx +91 -0
- manigent/web/frontend/src/components/landing/LandingPage.test.tsx +134 -0
- manigent/web/frontend/src/components/landing/LearningSection.tsx +486 -0
- manigent/web/frontend/src/components/landing/Nav.tsx +68 -0
- manigent/web/frontend/src/components/landing/Pricing.tsx +263 -0
- manigent/web/frontend/src/components/landing/Section.tsx +98 -0
- manigent/web/frontend/src/components/landing/StaffingSection.tsx +122 -0
- manigent/web/frontend/src/components/landing/WhyNowSection.tsx +92 -0
- manigent/web/frontend/src/components/landing/WhyTeamSection.tsx +73 -0
- manigent/web/frontend/src/components/landing/earlyAccess.ts +23 -0
- manigent/web/frontend/src/components/layout/AppLayout.tsx +77 -0
- manigent/web/frontend/src/components/layout/Sidebar.tsx +172 -0
- manigent/web/frontend/src/components/layout/index.ts +2 -0
- manigent/web/frontend/src/components/project/ActivityFeed.tsx +247 -0
- manigent/web/frontend/src/components/project/CreateProjectPage.tsx +543 -0
- manigent/web/frontend/src/components/project/DeleteProjectButton.tsx +39 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.phase.test.tsx +107 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.test.tsx +994 -0
- manigent/web/frontend/src/components/project/ProjectPlanTab.tsx +166 -0
- manigent/web/frontend/src/components/project/ProjectReportTab.tsx +139 -0
- manigent/web/frontend/src/components/project/ProjectView.tsx +390 -0
- manigent/web/frontend/src/components/project/index.ts +2 -0
- manigent/web/frontend/src/components/project/plan/PlanAcceptance.test.tsx +460 -0
- manigent/web/frontend/src/components/project/plan/PlanAcceptance.tsx +111 -0
- manigent/web/frontend/src/components/project/plan/PlanBlockEdit.tsx +111 -0
- manigent/web/frontend/src/components/project/plan/PlanBlockEditing.test.tsx +1132 -0
- manigent/web/frontend/src/components/project/plan/PlanBody.test.tsx +495 -0
- manigent/web/frontend/src/components/project/plan/PlanBody.tsx +992 -0
- manigent/web/frontend/src/components/project/plan/PlanCollide.tsx +254 -0
- manigent/web/frontend/src/components/project/plan/PlanCommentList.test.tsx +420 -0
- manigent/web/frontend/src/components/project/plan/PlanCommentList.tsx +391 -0
- manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.test.tsx +226 -0
- manigent/web/frontend/src/components/project/plan/PlanDecisionNudge.tsx +128 -0
- manigent/web/frontend/src/components/project/plan/PlanDestructiveAccept.test.tsx +209 -0
- manigent/web/frontend/src/components/project/plan/PlanDiff.test.tsx +126 -0
- manigent/web/frontend/src/components/project/plan/PlanDiff.tsx +131 -0
- manigent/web/frontend/src/components/project/plan/PlanDiffSeries.test.tsx +500 -0
- manigent/web/frontend/src/components/project/plan/PlanDiffSeries.tsx +125 -0
- manigent/web/frontend/src/components/project/plan/PlanEditor.mounts.test.tsx +504 -0
- manigent/web/frontend/src/components/project/plan/PlanEditor.tsx +2159 -0
- manigent/web/frontend/src/components/project/plan/PlanHistory.tsx +69 -0
- manigent/web/frontend/src/components/project/plan/PlanInlineNotes.test.tsx +2398 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteBase.test.tsx +215 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteRow.test.tsx +562 -0
- manigent/web/frontend/src/components/project/plan/PlanNoteRow.tsx +908 -0
- manigent/web/frontend/src/components/project/plan/PlanQuotePop.tsx +254 -0
- manigent/web/frontend/src/components/project/plan/PlanStaleReconcile.test.tsx +244 -0
- manigent/web/frontend/src/components/project/plan/PlanSubmitWindow.test.tsx +281 -0
- manigent/web/frontend/src/components/project/plan/PlanTray.tsx +184 -0
- manigent/web/frontend/src/components/project/plan/PlanTrayMirror.test.tsx +242 -0
- manigent/web/frontend/src/components/project/plan/plan.css +1151 -0
- manigent/web/frontend/src/components/project/plan/planAnchor.test.tsx +459 -0
- manigent/web/frontend/src/components/project/plan/planAnchor.tsx +277 -0
- manigent/web/frontend/src/components/project/plan/planBlocks.test.tsx +470 -0
- manigent/web/frontend/src/components/project/plan/planBlocks.ts +242 -0
- manigent/web/frontend/src/components/project/plan/planContrast.test.ts +229 -0
- manigent/web/frontend/src/components/project/plan/planDiffs.test.ts +455 -0
- manigent/web/frontend/src/components/project/plan/planDiffs.ts +358 -0
- manigent/web/frontend/src/components/project/plan/planTestServer.ts +741 -0
- manigent/web/frontend/src/components/project/plan/planTransport.test.ts +160 -0
- manigent/web/frontend/src/components/project/plan/planTrayLogic.test.ts +1201 -0
- manigent/web/frontend/src/components/project/plan/planTrayLogic.ts +1164 -0
- manigent/web/frontend/src/components/publicAgents/PublicAgentsAccount.tsx +162 -0
- manigent/web/frontend/src/components/publicAgents/api.ts +55 -0
- manigent/web/frontend/src/components/publicAgents/parts.tsx +54 -0
- manigent/web/frontend/src/components/publicAgents/publicAgents.css +53 -0
- manigent/web/frontend/src/components/publicAgents/publicAgents.test.tsx +86 -0
- manigent/web/frontend/src/components/publicAgents/widget.test.ts +148 -0
- manigent/web/frontend/src/components/publicAgents/widget.ts +40 -0
- manigent/web/frontend/src/components/publicRequests/AgentDrawer.tsx +101 -0
- manigent/web/frontend/src/components/publicRequests/AgentPanel.tsx +173 -0
- manigent/web/frontend/src/components/publicRequests/AgentRankList.tsx +61 -0
- manigent/web/frontend/src/components/publicRequests/MatchRetainButton.tsx +67 -0
- manigent/web/frontend/src/components/publicRequests/ProgressBoard.tsx +74 -0
- manigent/web/frontend/src/components/publicRequests/PublicRequestsPage.tsx +57 -0
- manigent/web/frontend/src/components/publicRequests/RequestHome.tsx +99 -0
- manigent/web/frontend/src/components/publicRequests/RequestRoom.tsx +126 -0
- manigent/web/frontend/src/components/publicRequests/agentView.ts +65 -0
- manigent/web/frontend/src/components/publicRequests/publicRequests.css +209 -0
- manigent/web/frontend/src/components/publicRequests/publicRequests.test.tsx +172 -0
- manigent/web/frontend/src/components/publicRequests/requestCopy.ts +121 -0
- manigent/web/frontend/src/components/sample-request/SampleIdeaList.test.tsx +190 -0
- manigent/web/frontend/src/components/sample-request/SampleIdeaList.tsx +180 -0
- manigent/web/frontend/src/components/sample-request/SampleRequestBody.tsx +276 -0
- manigent/web/frontend/src/components/sample-request/assistantStarterSamples.ts +62 -0
- manigent/web/frontend/src/components/sample-request/sampleIdeaList.css +106 -0
- manigent/web/frontend/src/components/scheduled/CronPicker.tsx +80 -0
- manigent/web/frontend/src/components/scheduled/ScheduleMessageModal.tsx +341 -0
- manigent/web/frontend/src/components/scheduled/ScheduledMessagesList.tsx +157 -0
- manigent/web/frontend/src/components/settings/AgentSettingsPage.tsx +366 -0
- manigent/web/frontend/src/components/settings/ConnectedAccounts.tsx +277 -0
- manigent/web/frontend/src/components/settings/DefaultModelSection.test.tsx +96 -0
- manigent/web/frontend/src/components/settings/DefaultModelSection.tsx +212 -0
- manigent/web/frontend/src/components/settings/DisconnectConfirmModal.tsx +93 -0
- manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.isolation.test.tsx +66 -0
- manigent/web/frontend/src/components/settings/FrontDeskSettingsPage.tsx +134 -0
- manigent/web/frontend/src/components/settings/InvitableAgentsPicker.tsx +182 -0
- manigent/web/frontend/src/components/settings/KnowledgePacksSection.tsx +867 -0
- manigent/web/frontend/src/components/settings/ModelConfigDialog.catalogDriven.test.tsx +79 -0
- manigent/web/frontend/src/components/settings/ModelConfigDialog.tsx +343 -0
- manigent/web/frontend/src/components/settings/ModelConfigSection.smartRouting.test.tsx +109 -0
- manigent/web/frontend/src/components/settings/ModelConfigSection.tsx +599 -0
- manigent/web/frontend/src/components/settings/ReflectionScheduleSection.tsx +170 -0
- manigent/web/frontend/src/components/settings/UserSettingsPage.tsx +168 -0
- manigent/web/frontend/src/components/settings/agent/AgentStatusStrip.tsx +71 -0
- manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.test.tsx +81 -0
- manigent/web/frontend/src/components/settings/agent/BlockedDeploymentsCard.tsx +62 -0
- manigent/web/frontend/src/components/settings/agent/ChannelIsolationLine.tsx +21 -0
- manigent/web/frontend/src/components/settings/agent/ChannelsSection.test.tsx +879 -0
- manigent/web/frontend/src/components/settings/agent/ChannelsSection.tsx +1124 -0
- manigent/web/frontend/src/components/settings/agent/EnvVarsCard.test.ts +15 -0
- manigent/web/frontend/src/components/settings/agent/EnvVarsCard.tsx +301 -0
- manigent/web/frontend/src/components/settings/agent/ExtraDirsList.tsx +85 -0
- manigent/web/frontend/src/components/settings/agent/GeneralSection.tsx +88 -0
- manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.test.tsx +32 -0
- manigent/web/frontend/src/components/settings/agent/HostDisclaimerSlot.tsx +30 -0
- manigent/web/frontend/src/components/settings/agent/HubChannelCard.test.tsx +449 -0
- manigent/web/frontend/src/components/settings/agent/HubChannelCard.tsx +312 -0
- manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.test.tsx +83 -0
- manigent/web/frontend/src/components/settings/agent/HubInvitableAgentsCard.tsx +64 -0
- manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.test.tsx +69 -0
- manigent/web/frontend/src/components/settings/agent/HubSendUsageCard.tsx +42 -0
- manigent/web/frontend/src/components/settings/agent/HubTurnOffDialog.tsx +88 -0
- manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.test.tsx +26 -0
- manigent/web/frontend/src/components/settings/agent/IsolationRecheckButton.tsx +68 -0
- manigent/web/frontend/src/components/settings/agent/KnowledgeSection.tsx +68 -0
- manigent/web/frontend/src/components/settings/agent/LineChannelCard.test.tsx +428 -0
- manigent/web/frontend/src/components/settings/agent/LineChannelCard.tsx +343 -0
- manigent/web/frontend/src/components/settings/agent/RuntimeSection.test.tsx +164 -0
- manigent/web/frontend/src/components/settings/agent/RuntimeSection.tsx +118 -0
- manigent/web/frontend/src/components/settings/agent/StorageSection.tsx +78 -0
- manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.test.tsx +253 -0
- manigent/web/frontend/src/components/settings/agent/WebChatChannelCard.tsx +401 -0
- manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.test.tsx +462 -0
- manigent/web/frontend/src/components/settings/agent/WhatsAppChannelCard.tsx +289 -0
- manigent/web/frontend/src/components/settings/agent/agentSettings.test.tsx +109 -0
- manigent/web/frontend/src/components/settings/agent/channelSetupGuides.test.tsx +162 -0
- manigent/web/frontend/src/components/settings/agent/channelSetupGuides.tsx +119 -0
- manigent/web/frontend/src/components/settings/agent/channelsShared.ts +83 -0
- manigent/web/frontend/src/components/settings/agent/fields.tsx +67 -0
- manigent/web/frontend/src/components/settings/agent/sections.ts +36 -0
- manigent/web/frontend/src/components/settings/agent/useAgentSettingsDraft.ts +129 -0
- manigent/web/frontend/src/components/settings/agent/useInstallSelection.ts +38 -0
- manigent/web/frontend/src/components/settings/primitives.tsx +98 -0
- manigent/web/frontend/src/components/shared/ExpandToggle.tsx +45 -0
- manigent/web/frontend/src/components/shared/MarkdownBody.tsx +218 -0
- manigent/web/frontend/src/components/shared/MarkdownBody.untrusted.test.tsx +97 -0
- manigent/web/frontend/src/components/shared/RenderBody.tsx +90 -0
- manigent/web/frontend/src/components/shared/rehypeHideComments.test.tsx +115 -0
- manigent/web/frontend/src/components/shared/rehypeHideComments.ts +124 -0
- manigent/web/frontend/src/components/sharing/AcceptSharePage.tsx +141 -0
- manigent/web/frontend/src/components/sharing/ShareLandingPage.tsx +89 -0
- manigent/web/frontend/src/components/sidebar/AccountChip.test.tsx +337 -0
- manigent/web/frontend/src/components/sidebar/AccountChip.tsx +407 -0
- manigent/web/frontend/src/components/sidebar/ActiveWorkDot.tsx +9 -0
- manigent/web/frontend/src/components/sidebar/AgentProfile.tsx +44 -0
- manigent/web/frontend/src/components/sidebar/AgentRow.tsx +71 -0
- manigent/web/frontend/src/components/sidebar/AgentsPanel.retained.test.tsx +105 -0
- manigent/web/frontend/src/components/sidebar/AgentsPanel.tsx +231 -0
- manigent/web/frontend/src/components/sidebar/AssistantHero.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/ChatroomItem.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.channelThreads.test.tsx +76 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.reopen.test.tsx +206 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.test.tsx +725 -0
- manigent/web/frontend/src/components/sidebar/CommandPalette.tsx +774 -0
- manigent/web/frontend/src/components/sidebar/ComputersPanel.tsx +144 -0
- manigent/web/frontend/src/components/sidebar/DmIcon.tsx +46 -0
- manigent/web/frontend/src/components/sidebar/DmsPanel.tsx +226 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.channels.test.tsx +582 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.test.tsx +151 -0
- manigent/web/frontend/src/components/sidebar/FrontDeskPanel.tsx +454 -0
- manigent/web/frontend/src/components/sidebar/HotkeysHelp.tsx +101 -0
- manigent/web/frontend/src/components/sidebar/MessageHitRow.test.tsx +84 -0
- manigent/web/frontend/src/components/sidebar/MessageHitRow.tsx +57 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.crossHighlight.test.tsx +247 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.delete.test.tsx +273 -0
- manigent/web/frontend/src/components/sidebar/MyDeskRailPanel.tsx +351 -0
- manigent/web/frontend/src/components/sidebar/PanelShell.tsx +48 -0
- manigent/web/frontend/src/components/sidebar/ProjectItem.tsx +161 -0
- manigent/web/frontend/src/components/sidebar/ProjectRow.tsx +151 -0
- manigent/web/frontend/src/components/sidebar/ProjectsPanel.tsx +80 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.counterpart.test.tsx +85 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.hub.test.tsx +147 -0
- manigent/web/frontend/src/components/sidebar/PublicPanel.tsx +271 -0
- manigent/web/frontend/src/components/sidebar/RecencyHeader.tsx +17 -0
- manigent/web/frontend/src/components/sidebar/ScheduledPanel.tsx +58 -0
- manigent/web/frontend/src/components/sidebar/SidebarAvatar.tsx +128 -0
- manigent/web/frontend/src/components/sidebar/SidebarRail.tsx +163 -0
- manigent/web/frontend/src/components/sidebar/TeamGroup.tsx +67 -0
- manigent/web/frontend/src/components/sidebar/dmPrefill.test.ts +69 -0
- manigent/web/frontend/src/components/sidebar/dmPrefill.ts +80 -0
- manigent/web/frontend/src/components/sidebar/index.ts +18 -0
- manigent/web/frontend/src/components/sidebar/railShortcuts.ts +18 -0
- manigent/web/frontend/src/components/today/lib.test.ts +70 -0
- manigent/web/frontend/src/components/today/lib.ts +296 -0
- manigent/web/frontend/src/components/today/today.css +113 -0
- manigent/web/frontend/src/components/welcome/RunnerSetup.css +253 -0
- manigent/web/frontend/src/components/welcome/RunnerSetup.tsx +148 -0
- manigent/web/frontend/src/components/welcome/WelcomePage.css +383 -0
- manigent/web/frontend/src/components/welcome/WelcomePage.tsx +329 -0
- manigent/web/frontend/src/components/welcome/welcomeCopy.test.ts +382 -0
- manigent/web/frontend/src/components/welcome/welcomeCopy.ts +313 -0
- manigent/web/frontend/src/config/llmProviders.test.ts +161 -0
- manigent/web/frontend/src/config/llmProviders.ts +109 -0
- manigent/web/frontend/src/config/providerIds.fixture.json +13 -0
- manigent/web/frontend/src/hooks/accountNavigation.test.tsx +138 -0
- manigent/web/frontend/src/hooks/index.ts +10 -0
- manigent/web/frontend/src/hooks/useActiveWorkBootstrap.ts +86 -0
- manigent/web/frontend/src/hooks/useAgentAccess.test.tsx +30 -0
- manigent/web/frontend/src/hooks/useAgentAccess.ts +204 -0
- manigent/web/frontend/src/hooks/useAgentSampleRequests.ts +27 -0
- manigent/web/frontend/src/hooks/useAgentSearch.ts +10 -0
- manigent/web/frontend/src/hooks/useAgents.ts +46 -0
- manigent/web/frontend/src/hooks/useAnnouncements.test.ts +112 -0
- manigent/web/frontend/src/hooks/useAnnouncements.ts +100 -0
- manigent/web/frontend/src/hooks/useAssistantAgent.ts +24 -0
- manigent/web/frontend/src/hooks/useAutoResizeTextarea.ts +18 -0
- manigent/web/frontend/src/hooks/useBridgeDeliveries.test.tsx +79 -0
- manigent/web/frontend/src/hooks/useBriefTabs.ts +122 -0
- manigent/web/frontend/src/hooks/useChannelThreads.ts +69 -0
- manigent/web/frontend/src/hooks/useComposeForm.ts +80 -0
- manigent/web/frontend/src/hooks/useComputers.ts +123 -0
- manigent/web/frontend/src/hooks/useContextFirstItem.ts +118 -0
- manigent/web/frontend/src/hooks/useCreateProject.ts +78 -0
- manigent/web/frontend/src/hooks/useDebouncedValue.ts +16 -0
- manigent/web/frontend/src/hooks/useDeleteProject.ts +27 -0
- manigent/web/frontend/src/hooks/useDeskDispatch.test.tsx +170 -0
- manigent/web/frontend/src/hooks/useDeskDispatch.ts +135 -0
- manigent/web/frontend/src/hooks/useDeskReadState.test.tsx +85 -0
- manigent/web/frontend/src/hooks/useDeskReadState.ts +150 -0
- manigent/web/frontend/src/hooks/useDeskRecentlyDone.test.tsx +222 -0
- manigent/web/frontend/src/hooks/useDeskRecentlyDone.ts +225 -0
- manigent/web/frontend/src/hooks/useDeskTour.ts +37 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.channelThreads.test.tsx +91 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.hubThreads.test.tsx +93 -0
- manigent/web/frontend/src/hooks/useDeskUpdates.ts +353 -0
- manigent/web/frontend/src/hooks/useDisconnectIntegration.ts +23 -0
- manigent/web/frontend/src/hooks/useDmThreads.ts +99 -0
- manigent/web/frontend/src/hooks/useFileDropzone.ts +93 -0
- manigent/web/frontend/src/hooks/useFileUpload.ts +38 -0
- manigent/web/frontend/src/hooks/useFiles.ts +98 -0
- manigent/web/frontend/src/hooks/useHub.test.tsx +237 -0
- manigent/web/frontend/src/hooks/useHub.ts +502 -0
- manigent/web/frontend/src/hooks/useInstall.ts +82 -0
- manigent/web/frontend/src/hooks/useLabels.test.tsx +294 -0
- manigent/web/frontend/src/hooks/useLabels.ts +287 -0
- manigent/web/frontend/src/hooks/useLogout.ts +49 -0
- manigent/web/frontend/src/hooks/useMentionDetection.ts +90 -0
- manigent/web/frontend/src/hooks/useMessageAttachments.deskLimit.test.ts +264 -0
- manigent/web/frontend/src/hooks/useMessageAttachments.ts +283 -0
- manigent/web/frontend/src/hooks/useMessageDeepLink.test.tsx +70 -0
- manigent/web/frontend/src/hooks/useMessageDeepLink.ts +32 -0
- manigent/web/frontend/src/hooks/useMessageSearch.ts +78 -0
- manigent/web/frontend/src/hooks/useModelConfigs.ts +178 -0
- manigent/web/frontend/src/hooks/useMyAgents.ts +18 -0
- manigent/web/frontend/src/hooks/usePointerReorder.ts +270 -0
- manigent/web/frontend/src/hooks/usePrefersReducedMotion.ts +18 -0
- manigent/web/frontend/src/hooks/useProjectPlan.ts +36 -0
- manigent/web/frontend/src/hooks/useProjectReport.ts +19 -0
- manigent/web/frontend/src/hooks/useProjects.ts +104 -0
- manigent/web/frontend/src/hooks/useProjectsWithChatrooms.ts +97 -0
- manigent/web/frontend/src/hooks/usePublicRequests.ts +89 -0
- manigent/web/frontend/src/hooks/useRecentAgents.ts +38 -0
- manigent/web/frontend/src/hooks/useRenameProject.test.tsx +185 -0
- manigent/web/frontend/src/hooks/useRenameProject.ts +192 -0
- manigent/web/frontend/src/hooks/useRoomTrust.ts +56 -0
- manigent/web/frontend/src/hooks/useSharing.ts +46 -0
- manigent/web/frontend/src/hooks/useSopRecents.ts +55 -0
- manigent/web/frontend/src/hooks/useSopSchedules.ts +25 -0
- manigent/web/frontend/src/hooks/useSops.ts +99 -0
- manigent/web/frontend/src/hooks/useSwitchAccount.ts +41 -0
- manigent/web/frontend/src/hooks/useTeamGroups.ts +48 -0
- manigent/web/frontend/src/hooks/useThreadOutbox.ts +68 -0
- manigent/web/frontend/src/hooks/useTodos.add.test.tsx +89 -0
- manigent/web/frontend/src/hooks/useTodos.linkProject.test.tsx +115 -0
- manigent/web/frontend/src/hooks/useTodos.saveDraft.test.tsx +188 -0
- manigent/web/frontend/src/hooks/useTodos.ts +396 -0
- manigent/web/frontend/src/hooks/useUntrustedBodies.test.tsx +87 -0
- manigent/web/frontend/src/hooks/useUntrustedBodies.ts +51 -0
- manigent/web/frontend/src/hooks/useWebSocket.briefTab.test.tsx +148 -0
- manigent/web/frontend/src/hooks/useWebSocket.channelThreads.test.tsx +82 -0
- manigent/web/frontend/src/hooks/useWebSocket.hostSync.test.tsx +92 -0
- manigent/web/frontend/src/hooks/useWebSocket.hub.test.tsx +114 -0
- manigent/web/frontend/src/hooks/useWebSocket.labelSync.test.tsx +168 -0
- manigent/web/frontend/src/hooks/useWebSocket.plan.test.tsx +168 -0
- manigent/web/frontend/src/hooks/useWebSocket.reconnect.test.tsx +166 -0
- manigent/web/frontend/src/hooks/useWebSocket.runnerVersions.test.tsx +167 -0
- manigent/web/frontend/src/hooks/useWebSocket.ts +495 -0
- manigent/web/frontend/src/hooks/useWindowFileDropGuard.ts +33 -0
- manigent/web/frontend/src/landing.tsx +11 -0
- manigent/web/frontend/src/main.tsx +17 -0
- manigent/web/frontend/src/private-beta.tsx +10 -0
- manigent/web/frontend/src/public.tsx +42 -0
- manigent/web/frontend/src/queryClient.ts +16 -0
- manigent/web/frontend/src/stores/activeWorkStore.ts +88 -0
- manigent/web/frontend/src/stores/announcementStore.test.ts +217 -0
- manigent/web/frontend/src/stores/announcementStore.ts +169 -0
- manigent/web/frontend/src/stores/authStore.multiSession.test.ts +304 -0
- manigent/web/frontend/src/stores/authStore.test.ts +235 -0
- manigent/web/frontend/src/stores/authStore.ts +693 -0
- manigent/web/frontend/src/stores/deskLabelViewStore.test.ts +162 -0
- manigent/web/frontend/src/stores/deskLabelViewStore.ts +234 -0
- manigent/web/frontend/src/stores/deskLinkStore.ts +54 -0
- manigent/web/frontend/src/stores/deskStore.test.ts +53 -0
- manigent/web/frontend/src/stores/deskStore.ts +93 -0
- manigent/web/frontend/src/stores/heldPostsStore.ts +78 -0
- manigent/web/frontend/src/stores/mcpAuthStore.ts +52 -0
- manigent/web/frontend/src/stores/modelSelectionStore.ts +33 -0
- manigent/web/frontend/src/stores/perUserStores.ts +79 -0
- manigent/web/frontend/src/stores/runnerVersionStore.test.ts +81 -0
- manigent/web/frontend/src/stores/runnerVersionStore.ts +60 -0
- manigent/web/frontend/src/stores/skillAuthStore.ts +54 -0
- manigent/web/frontend/src/stores/switchReset.test.ts +236 -0
- manigent/web/frontend/src/stores/tokenExp.ts +32 -0
- manigent/web/frontend/src/stores/uiStore.ts +186 -0
- manigent/web/frontend/src/stores/unreadStore.ts +204 -0
- manigent/web/frontend/src/stores/unsavedGuard.ts +22 -0
- manigent/web/frontend/src/styles/design/fonts/JetBrainsMono-Latin.woff2 +0 -0
- manigent/web/frontend/src/styles/design/fonts/Nunito-Latin.woff2 +0 -0
- manigent/web/frontend/src/styles/design/fonts/Nunito-LatinExt.woff2 +0 -0
- manigent/web/frontend/src/styles/design/tokens.css +160 -0
- manigent/web/frontend/src/styles/homepage.css +242 -0
- manigent/web/frontend/src/styles/index.css +126 -0
- manigent/web/frontend/src/styles/landing.css +52 -0
- manigent/web/frontend/src/test/authFixtures.ts +154 -0
- manigent/web/frontend/src/test/fetchHarness.ts +203 -0
- manigent/web/frontend/src/test/fixtures/channelThreads.smoke.json +42 -0
- manigent/web/frontend/src/test/hubFixtures.ts +212 -0
- manigent/web/frontend/src/test/queryWrapper.tsx +9 -0
- manigent/web/frontend/src/test/suiteHygiene.test.ts +212 -0
- manigent/web/frontend/src/types/channelThreadName.test.ts +16 -0
- manigent/web/frontend/src/types/index.ts +903 -0
- manigent/web/frontend/src/utils/agentAccessCopy.test.ts +53 -0
- manigent/web/frontend/src/utils/agentAccessCopy.ts +231 -0
- manigent/web/frontend/src/utils/agentDisplay.ts +81 -0
- manigent/web/frontend/src/utils/briefSchedule.ts +25 -0
- manigent/web/frontend/src/utils/channelThreads.test.ts +109 -0
- manigent/web/frontend/src/utils/channelThreads.ts +179 -0
- manigent/web/frontend/src/utils/chatroomDisplay.ts +11 -0
- manigent/web/frontend/src/utils/deskLinks.test.ts +155 -0
- manigent/web/frontend/src/utils/deskLinks.ts +115 -0
- manigent/web/frontend/src/utils/deskRecipient.ts +29 -0
- manigent/web/frontend/src/utils/formatDate.ts +102 -0
- manigent/web/frontend/src/utils/frontDesk.ts +65 -0
- manigent/web/frontend/src/utils/hubChannelView.test.ts +178 -0
- manigent/web/frontend/src/utils/hubChannelView.ts +307 -0
- manigent/web/frontend/src/utils/hubDelivery.test.ts +105 -0
- manigent/web/frontend/src/utils/hubDelivery.ts +137 -0
- manigent/web/frontend/src/utils/hubDisplay.test.ts +157 -0
- manigent/web/frontend/src/utils/hubDisplay.ts +221 -0
- manigent/web/frontend/src/utils/hubThread.test.ts +184 -0
- manigent/web/frontend/src/utils/hubThread.ts +247 -0
- manigent/web/frontend/src/utils/labelErrors.test.ts +127 -0
- manigent/web/frontend/src/utils/labelErrors.ts +123 -0
- manigent/web/frontend/src/utils/labelView.test.ts +846 -0
- manigent/web/frontend/src/utils/labelView.ts +760 -0
- manigent/web/frontend/src/utils/legalUrls.ts +11 -0
- manigent/web/frontend/src/utils/messageSender.test.ts +32 -0
- manigent/web/frontend/src/utils/messageSender.ts +29 -0
- manigent/web/frontend/src/utils/oauthScopeCopy.ts +73 -0
- manigent/web/frontend/src/utils/personalize.ts +49 -0
- manigent/web/frontend/src/utils/reachableAgents.test.ts +36 -0
- manigent/web/frontend/src/utils/reachableAgents.ts +24 -0
- manigent/web/frontend/src/utils/recencyBuckets.ts +61 -0
- manigent/web/frontend/src/utils/roomTrust.test.ts +94 -0
- manigent/web/frontend/src/utils/roomTrust.ts +111 -0
- manigent/web/frontend/src/utils/snippetHighlight.test.ts +63 -0
- manigent/web/frontend/src/utils/snippetHighlight.ts +42 -0
- manigent/web/frontend/src/utils/sopRecipient.ts +37 -0
- manigent/web/frontend/src/utils/sopRequest.test.ts +91 -0
- manigent/web/frontend/src/utils/sopRequest.ts +68 -0
- manigent/web/frontend/src/utils/sopSchedule.ts +26 -0
- manigent/web/frontend/src/utils/sopSection.test.ts +148 -0
- manigent/web/frontend/src/utils/sopSection.ts +143 -0
- manigent/web/frontend/src/utils/textSearch.test.ts +60 -0
- manigent/web/frontend/src/utils/textSearch.ts +43 -0
- manigent/web/frontend/src/utils/thoughtPartner.ts +26 -0
- manigent/web/frontend/src/utils/threadName.test.ts +131 -0
- manigent/web/frontend/src/utils/threadName.ts +122 -0
- manigent/web/frontend/src/utils/ticketView.test.ts +296 -0
- manigent/web/frontend/src/utils/ticketView.ts +175 -0
- manigent/web/frontend/src/utils/todoAttachments.desiredState.test.ts +180 -0
- manigent/web/frontend/src/utils/todoAttachments.ts +126 -0
- manigent/web/frontend/src/utils/todoDraft.test.ts +84 -0
- manigent/web/frontend/src/utils/todoDraft.ts +46 -0
- manigent/web/frontend/src/utils/todoSearch.test.ts +82 -0
- manigent/web/frontend/src/utils/todoSearch.ts +43 -0
- manigent/web/frontend/src/utils/versionMatch.test.ts +295 -0
- manigent/web/frontend/src/utils/versionMatch.ts +245 -0
- manigent/web/frontend/src/vite-env.d.ts +1 -0
- manigent/web/frontend/tailwind.config.js +86 -0
- manigent/web/frontend/tos.html +188 -0
- manigent/web/frontend/tsconfig.dts.json +25 -0
- manigent/web/frontend/tsconfig.json +26 -0
- manigent/web/frontend/tsconfig.node.json +11 -0
- manigent/web/frontend/vite.config.ts +41 -0
- manigent/web/frontend/vitest.config.ts +16 -0
- manigent/ws_edge/app.py +514 -0
- manigent/ws_edge/auth.py +104 -0
- manigent/ws_edge/link.py +282 -0
- manigent/ws_edge/registry.py +206 -0
- manigent/ws_edge/terminal_relay.py +269 -0
- manigent_server-0.1.0.dist-info/METADATA +138 -0
- manigent_server-0.1.0.dist-info/RECORD +1004 -0
- manigent_server-0.1.0.dist-info/WHEEL +4 -0
- manigent_server-0.1.0.dist-info/entry_points.txt +2 -0
- manigent_server-0.1.0.dist-info/licenses/LICENSE +68 -0
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
"""
|
|
3
|
+
manigent/server/chat_search_index.py
|
|
4
|
+
|
|
5
|
+
Derived FTS5 search index over chat message bodies, plus the changelog
|
|
6
|
+
subscriber that keeps it current.
|
|
7
|
+
|
|
8
|
+
The index is a single SQLite file at ``{data_dir}/search/chat_index.db``. It
|
|
9
|
+
holds no authoritative data: every row is derived from a room's
|
|
10
|
+
``CHATS.ndjson``, so the file can be deleted at any time and is rebuilt on the
|
|
11
|
+
next boot by :meth:`ChatSearchIndex.reconcile_all`.
|
|
12
|
+
|
|
13
|
+
Three tables and one FTS5 virtual table (see ``_SCHEMA_SQL``):
|
|
14
|
+
|
|
15
|
+
- ``projects`` mirrors the ACL-relevant columns of a :class:`Project`
|
|
16
|
+
(``created_by``, ``fd_requester_name``, ``surface``) so permission scoping is
|
|
17
|
+
a SQL predicate on the same statement as the match. This is the decision that
|
|
18
|
+
buys the latency target: recomputing the ACL from the filesystem per query
|
|
19
|
+
means ``Project.list_all()`` — 540 ``meta.json`` reads — which costs 30-60x
|
|
20
|
+
the rest of the query. Any change that reintroduces a filesystem walk into
|
|
21
|
+
:meth:`ChatSearchIndex.search` should be rejected on latency grounds alone.
|
|
22
|
+
- ``rooms`` carries the pre-computed ``room_type`` / ``room_label`` the wire
|
|
23
|
+
contract returns, a mirrored ``is_user_communication`` boolean (see below),
|
|
24
|
+
and a ``(size, mtime_ns)`` fingerprint of ``CHATS.ndjson`` so reconcile can
|
|
25
|
+
skip an unchanged room without opening it.
|
|
26
|
+
- ``messages`` + ``messages_fts`` hold the corpus. ``messages_fts`` is an
|
|
27
|
+
external-content table over ``messages``, kept in sync by triggers.
|
|
28
|
+
|
|
29
|
+
The wire format this module produces is pinned by ``SEARCH_API_CONTRACT.md``
|
|
30
|
+
v1.0 — do not change a field name or shape here without updating that document
|
|
31
|
+
and announcing the change.
|
|
32
|
+
|
|
33
|
+
Permission model (contract §5), enforced in :meth:`ChatSearchIndex.search`:
|
|
34
|
+
a message is searchable by user U iff its project P satisfies one of
|
|
35
|
+
|
|
36
|
+
1. ``P.created_by == U.id``;
|
|
37
|
+
2. ``P.fd_requester_name == U.username`` AND the room is ``user-communication``
|
|
38
|
+
(the room restriction is load-bearing — a Front Desk requester can read that
|
|
39
|
+
one room and nothing else in the host's project);
|
|
40
|
+
3. U is a share-token viewer of P.
|
|
41
|
+
|
|
42
|
+
Clause 2's room test is **not** restated in SQL. ``Chatroom.is_user_communication_name``
|
|
43
|
+
is called once per room at index-WRITE time and mirrored into
|
|
44
|
+
``rooms.is_user_communication``, so the query reads one boolean column and the
|
|
45
|
+
model keeps the single definition of the rule. The first cut of this module
|
|
46
|
+
wrote the clause as ``chatroom_name LIKE 'user-communication%'``, which is
|
|
47
|
+
ASCII-case-INSENSITIVE in SQLite while ``startswith`` is not: a room named
|
|
48
|
+
``User-communication-notes`` satisfied search's clause 2 but failed the
|
|
49
|
+
room-read endpoint's, leaking snippets across a user boundary. Do not
|
|
50
|
+
reintroduce a room-name predicate in SQL.
|
|
51
|
+
|
|
52
|
+
There is deliberately **no bypass** parameter on the query path: every caller
|
|
53
|
+
is scoped to their own plate. ``is_ack`` rows never enter the corpus.
|
|
54
|
+
"""
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import logging
|
|
58
|
+
import os
|
|
59
|
+
import sqlite3
|
|
60
|
+
import threading
|
|
61
|
+
import unicodedata
|
|
62
|
+
from datetime import UTC, datetime
|
|
63
|
+
from functools import lru_cache
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
from typing import TYPE_CHECKING, Iterable, Literal
|
|
66
|
+
from urllib.parse import quote
|
|
67
|
+
|
|
68
|
+
from pydantic import BaseModel, field_serializer
|
|
69
|
+
|
|
70
|
+
from manigent.models.chat_message import ChatMessage
|
|
71
|
+
from manigent.models.chatroom import Chatroom
|
|
72
|
+
from manigent.models.project import Project
|
|
73
|
+
from manigent.sync.changelog import ChangelogEntryType
|
|
74
|
+
from manigent.sync.subscriber import ChangelogSubscriber
|
|
75
|
+
|
|
76
|
+
if TYPE_CHECKING:
|
|
77
|
+
from manigent.models.context import ModelContext
|
|
78
|
+
from manigent.sync.changelog import ChangelogEntry
|
|
79
|
+
|
|
80
|
+
logger = logging.getLogger("manigent.server")
|
|
81
|
+
|
|
82
|
+
SCHEMA_VERSION = 2 # 2: rooms.is_user_communication (mirrors the model predicate)
|
|
83
|
+
CANDIDATE_POOL = 200 # rows BM25 hands to the Python re-ranker
|
|
84
|
+
RECENCY_HALF_LIFE_D = 30.0 # days; recency weight halves every 30d
|
|
85
|
+
RECENCY_FLOOR = 0.25 # oldest possible weight; recency scales 0.25..1.0
|
|
86
|
+
SNIPPET_CHARS = 240 # hard cap on the snippet window (contract §3.5)
|
|
87
|
+
SNIPPET_CORE_CHARS = 200 # initial window before outward word-boundary snapping
|
|
88
|
+
MIN_QUERY_CHARS = 2
|
|
89
|
+
MAX_LIMIT = 50
|
|
90
|
+
MAX_OFFSET = 200
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SearchIndexError(RuntimeError):
|
|
94
|
+
"""A query-path failure the caller must surface as 503, not as zero hits.
|
|
95
|
+
|
|
96
|
+
The write path deliberately swallows ``sqlite3.Error`` — a broken search
|
|
97
|
+
index must never take chat down. The QUERY path must not: a locked or
|
|
98
|
+
corrupted index that answered ``hits: []`` would be indistinguishable from
|
|
99
|
+
"nothing matched", and the UI's ``unavailable`` state could never be
|
|
100
|
+
reached (contract §2.3).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_SCHEMA_SQL = """
|
|
105
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
106
|
+
project_id TEXT PRIMARY KEY,
|
|
107
|
+
project_name TEXT NOT NULL,
|
|
108
|
+
display_name TEXT,
|
|
109
|
+
surface TEXT NOT NULL, -- 'regular' | 'dm' | 'frontdesk'
|
|
110
|
+
created_by TEXT NOT NULL, -- owner user_id
|
|
111
|
+
fd_requester_name TEXT, -- FD requester username, else NULL
|
|
112
|
+
coordinator_name TEXT NOT NULL -- the DM deep link's :agentName
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE TABLE IF NOT EXISTS rooms (
|
|
116
|
+
project_id TEXT NOT NULL,
|
|
117
|
+
chatroom_name TEXT NOT NULL,
|
|
118
|
+
room_type TEXT NOT NULL, -- 'dm' | 'project'
|
|
119
|
+
room_label TEXT NOT NULL, -- group header the palette renders
|
|
120
|
+
-- Chatroom.is_user_communication_name(chatroom_name), resolved at write time.
|
|
121
|
+
-- The ACL reads THIS, never a room-name pattern; defaults closed (0) so a
|
|
122
|
+
-- writer that forgets it narrows the scope rather than widening it.
|
|
123
|
+
is_user_communication INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
fingerprint TEXT, -- "{size}:{mtime_ns}" of CHATS.ndjson
|
|
125
|
+
PRIMARY KEY (project_id, chatroom_name)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
129
|
+
rowid INTEGER PRIMARY KEY,
|
|
130
|
+
message_id TEXT NOT NULL,
|
|
131
|
+
project_id TEXT NOT NULL,
|
|
132
|
+
chatroom_name TEXT NOT NULL,
|
|
133
|
+
ts REAL NOT NULL, -- epoch seconds, UTC, sub-second kept
|
|
134
|
+
author_id TEXT NOT NULL,
|
|
135
|
+
author_name TEXT NOT NULL,
|
|
136
|
+
content TEXT NOT NULL,
|
|
137
|
+
UNIQUE (project_id, chatroom_name, message_id) -- makes replay idempotent
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_messages_room ON messages(project_id, chatroom_name);
|
|
140
|
+
|
|
141
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
142
|
+
content,
|
|
143
|
+
content = 'messages', -- external content: no duplicate text blob
|
|
144
|
+
content_rowid = 'rowid',
|
|
145
|
+
tokenize = "unicode61 remove_diacritics 2",
|
|
146
|
+
prefix = '2 3' -- typeahead: 2- and 3-char prefixes are O(1)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
-- External-content FTS5 is NOT auto-maintained; these triggers are what keep
|
|
150
|
+
-- the index and the content table from diverging on any write path.
|
|
151
|
+
CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
|
|
152
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
153
|
+
END;
|
|
154
|
+
CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
|
|
155
|
+
INSERT INTO messages_fts(messages_fts, rowid, content)
|
|
156
|
+
VALUES ('delete', old.rowid, old.content);
|
|
157
|
+
END;
|
|
158
|
+
CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
|
|
159
|
+
INSERT INTO messages_fts(messages_fts, rowid, content)
|
|
160
|
+
VALUES ('delete', old.rowid, old.content);
|
|
161
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
162
|
+
END;
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS index_meta (key TEXT PRIMARY KEY, value TEXT);
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Wire models — SEARCH_API_CONTRACT.md v1.0, serialized verbatim by the route
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
class DeepLink(BaseModel):
|
|
173
|
+
"""The click-through target of a hit (contract §4).
|
|
174
|
+
|
|
175
|
+
``path`` is a complete, URL-encoded, application-relative route the client
|
|
176
|
+
hands straight to ``navigate()``; the structured parts are provided so a
|
|
177
|
+
caller never has to parse it back apart.
|
|
178
|
+
"""
|
|
179
|
+
path: str
|
|
180
|
+
kind: Literal["dm", "project_room"]
|
|
181
|
+
project_id: str
|
|
182
|
+
chatroom_name: str
|
|
183
|
+
agent_name: str | None # non-null iff kind == "dm"
|
|
184
|
+
message_id: str
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class SearchHit(BaseModel):
|
|
188
|
+
"""One ranked message hit (contract §3.2). Every field is always present."""
|
|
189
|
+
message_id: str
|
|
190
|
+
project_id: str
|
|
191
|
+
project_name: str
|
|
192
|
+
room_id: str # == chatroom_name (the room's primary key)
|
|
193
|
+
room_type: Literal["dm", "project"]
|
|
194
|
+
room_label: str
|
|
195
|
+
ts: datetime
|
|
196
|
+
author_id: str
|
|
197
|
+
author_name: str
|
|
198
|
+
snippet: str
|
|
199
|
+
snippet_truncated_start: bool
|
|
200
|
+
snippet_truncated_end: bool
|
|
201
|
+
match_offsets: list[tuple[int, int]] # UTF-16 [start, end) into `snippet`
|
|
202
|
+
score: float
|
|
203
|
+
deep_link: DeepLink
|
|
204
|
+
|
|
205
|
+
@field_serializer("ts")
|
|
206
|
+
def _ser_ts(self, value: datetime) -> str:
|
|
207
|
+
"""Emit the contract's exact timestamp shape: ISO-8601, UTC, Z-suffixed,
|
|
208
|
+
microsecond precision (``2026-07-26T14:03:11.482000Z``). Pinned here
|
|
209
|
+
rather than left to Pydantic's default so the wire string cannot drift
|
|
210
|
+
with a library upgrade."""
|
|
211
|
+
return value.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class SearchResponse(BaseModel):
|
|
215
|
+
"""The response envelope (contract §3.1)."""
|
|
216
|
+
query: str
|
|
217
|
+
hits: list[SearchHit]
|
|
218
|
+
limit: int
|
|
219
|
+
offset: int
|
|
220
|
+
has_more: bool
|
|
221
|
+
next_offset: int | None
|
|
222
|
+
indexing: bool # backfill still running -> results may be partial
|
|
223
|
+
took_ms: float
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class SearchScope(BaseModel):
|
|
227
|
+
"""The caller's searchable scope. Derived server-side from the JWT alone —
|
|
228
|
+
there is no request parameter that widens or narrows it."""
|
|
229
|
+
model_config = {"frozen": True}
|
|
230
|
+
|
|
231
|
+
user_id: str
|
|
232
|
+
username: str
|
|
233
|
+
viewer_project_ids: frozenset[str] = frozenset()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class _SnippetResult(BaseModel):
|
|
237
|
+
"""Internal: a snippet window plus its UTF-16 highlight offsets."""
|
|
238
|
+
text: str
|
|
239
|
+
truncated_start: bool
|
|
240
|
+
truncated_end: bool
|
|
241
|
+
offsets: list[tuple[int, int]]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Text helpers — tokenization, diacritic folding, UTF-16 offsets
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
@lru_cache(maxsize=8192)
|
|
249
|
+
def _fold_char(ch: str) -> str:
|
|
250
|
+
"""Fold one character to exactly one character.
|
|
251
|
+
|
|
252
|
+
NFD-decompose, drop combining marks, lowercase, then collapse back to a
|
|
253
|
+
single character — a combining mark that folds away becomes ``\\x00``,
|
|
254
|
+
which never matches a query token. Cached because real text reuses a very
|
|
255
|
+
small alphabet.
|
|
256
|
+
"""
|
|
257
|
+
stripped = "".join(
|
|
258
|
+
c for c in unicodedata.normalize("NFD", ch) if not unicodedata.combining(c)
|
|
259
|
+
)
|
|
260
|
+
lowered = stripped.lower()
|
|
261
|
+
return lowered[0] if lowered else "\x00"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _fold(text: str) -> str:
|
|
265
|
+
"""Case- and diacritic-fold ``text`` **preserving a 1:1 code-point mapping**
|
|
266
|
+
to the original.
|
|
267
|
+
|
|
268
|
+
The FTS5 tokenizer is ``unicode61 remove_diacritics 2``, so ``café`` and
|
|
269
|
+
``cafe`` are the same token to SQLite. Highlight offsets, though, have to
|
|
270
|
+
point back into the ORIGINAL string — so folding must not change the
|
|
271
|
+
length, which rules out a plain ``NFKD`` normalize.
|
|
272
|
+
|
|
273
|
+
This runs over the FULL body of every returned hit to locate matches, so it
|
|
274
|
+
is the hottest Python on the query path: a naive per-character loop cost
|
|
275
|
+
~22 ms of a ~50 ms query against the real corpus. ASCII text — effectively
|
|
276
|
+
all of it — has no diacritics to strip and folds to exactly ``str.lower()``,
|
|
277
|
+
which is one C-level call and length-preserving, so it takes that path.
|
|
278
|
+
"""
|
|
279
|
+
if text.isascii():
|
|
280
|
+
return text.lower()
|
|
281
|
+
return "".join(map(_fold_char, text))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def tokenize_query(query: str) -> list[str]:
|
|
285
|
+
"""Split raw user input into FTS5 tokens (contract §2.2).
|
|
286
|
+
|
|
287
|
+
Input is split on every non-alphanumeric character, so quotes, ``*``,
|
|
288
|
+
``(``, ``)``, ``:``, ``^``, ``-`` and the words AND / OR / NOT / NEAR carry
|
|
289
|
+
no special meaning — there is no query DSL in v1 and no way to inject one.
|
|
290
|
+
Tokens come back diacritic- and case-folded to match the tokenizer.
|
|
291
|
+
"""
|
|
292
|
+
tokens: list[str] = []
|
|
293
|
+
current: list[str] = []
|
|
294
|
+
for ch in query:
|
|
295
|
+
if ch.isalnum():
|
|
296
|
+
current.append(ch)
|
|
297
|
+
elif current:
|
|
298
|
+
tokens.append("".join(current))
|
|
299
|
+
current = []
|
|
300
|
+
if current:
|
|
301
|
+
tokens.append("".join(current))
|
|
302
|
+
return [_fold(t) for t in tokens if t]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def build_match_expr(tokens: Iterable[str]) -> str:
|
|
306
|
+
"""Build the FTS5 MATCH expression: every token quoted (so no FTS5 syntax
|
|
307
|
+
character can escape), joined by implicit AND, with ``*`` appended to the
|
|
308
|
+
LAST token only. ``deep lin`` becomes ``"deep" "lin"*`` — that trailing
|
|
309
|
+
star is what makes this a typeahead rather than a whole-word search."""
|
|
310
|
+
parts = list(tokens)
|
|
311
|
+
quoted = ['"' + t.replace('"', '""') + '"' for t in parts]
|
|
312
|
+
if quoted:
|
|
313
|
+
quoted[-1] += "*"
|
|
314
|
+
return " ".join(quoted)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _utf16_prefix(text: str) -> list[int]:
|
|
318
|
+
"""Cumulative UTF-16 code-unit length at each code-point index.
|
|
319
|
+
|
|
320
|
+
Python string indices are code points; JavaScript's are UTF-16 code units.
|
|
321
|
+
A single emoji before a match shifts every downstream highlight by one, so
|
|
322
|
+
the server converts rather than leaving the client to guess (contract §3.5).
|
|
323
|
+
"""
|
|
324
|
+
prefix = [0] * (len(text) + 1)
|
|
325
|
+
for i, ch in enumerate(text):
|
|
326
|
+
prefix[i + 1] = prefix[i] + (2 if ord(ch) > 0xFFFF else 1)
|
|
327
|
+
return prefix
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _is_token_char(ch: str) -> bool:
|
|
331
|
+
return ch.isalnum()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _find_token_spans(
|
|
335
|
+
folded_content: str, tokens: list[str]
|
|
336
|
+
) -> list[tuple[int, int]]:
|
|
337
|
+
"""Locate every literal occurrence of each token in ``folded_content``.
|
|
338
|
+
|
|
339
|
+
Matches are anchored at a token boundary the way the ``unicode61``
|
|
340
|
+
tokenizer sees one: a match must start at the beginning of a word, and
|
|
341
|
+
every token except the last (which matched as a prefix) must also end at a
|
|
342
|
+
word boundary. Returns merged, ascending, non-overlapping ``[start, end)``
|
|
343
|
+
code-point spans.
|
|
344
|
+
"""
|
|
345
|
+
spans: list[tuple[int, int]] = []
|
|
346
|
+
last_index = len(tokens) - 1
|
|
347
|
+
for idx, token in enumerate(tokens):
|
|
348
|
+
if not token:
|
|
349
|
+
continue
|
|
350
|
+
is_prefix = idx == last_index
|
|
351
|
+
start = 0
|
|
352
|
+
while True:
|
|
353
|
+
pos = folded_content.find(token, start)
|
|
354
|
+
if pos < 0:
|
|
355
|
+
break
|
|
356
|
+
start = pos + 1
|
|
357
|
+
if pos > 0 and _is_token_char(folded_content[pos - 1]):
|
|
358
|
+
continue # mid-word: the tokenizer would not have matched here
|
|
359
|
+
end = pos + len(token)
|
|
360
|
+
if not is_prefix and end < len(folded_content) and _is_token_char(
|
|
361
|
+
folded_content[end]
|
|
362
|
+
):
|
|
363
|
+
continue # a longer word: only the final token matches as a prefix
|
|
364
|
+
spans.append((pos, end))
|
|
365
|
+
if not spans:
|
|
366
|
+
return []
|
|
367
|
+
spans.sort()
|
|
368
|
+
merged = [spans[0]]
|
|
369
|
+
for span in spans[1:]:
|
|
370
|
+
prev_start, prev_end = merged[-1]
|
|
371
|
+
if span[0] <= prev_end:
|
|
372
|
+
merged[-1] = (prev_start, max(prev_end, span[1]))
|
|
373
|
+
else:
|
|
374
|
+
merged.append(span)
|
|
375
|
+
return merged
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _snap_outward(text: str, start: int, end: int, budget: int) -> tuple[int, int]:
|
|
379
|
+
"""Widen ``[start, end)`` to the enclosing word boundaries, spending at most
|
|
380
|
+
``budget`` extra characters in total. Keeps the window from slicing a word
|
|
381
|
+
in half without ever breaching ``SNIPPET_CHARS``."""
|
|
382
|
+
left = start
|
|
383
|
+
while left > 0 and budget > 0 and _is_token_char(text[left - 1]):
|
|
384
|
+
left -= 1
|
|
385
|
+
budget -= 1
|
|
386
|
+
right = end
|
|
387
|
+
while right < len(text) and budget > 0 and _is_token_char(text[right]):
|
|
388
|
+
right += 1
|
|
389
|
+
budget -= 1
|
|
390
|
+
return left, right
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def build_snippet(content: str, tokens: list[str]) -> _SnippetResult:
|
|
394
|
+
"""Build the returned snippet and the offsets the frontend highlights with.
|
|
395
|
+
|
|
396
|
+
A window of at most ``SNIPPET_CHARS`` characters centred on the first
|
|
397
|
+
match, snapped outward to word boundaries. No ellipsis is embedded — the
|
|
398
|
+
truncation booleans say which side was cut so the client renders the
|
|
399
|
+
ellipsis itself and it never lands inside a highlight range. Offsets are
|
|
400
|
+
UTF-16 code units into the returned snippet, half-open, ascending and
|
|
401
|
+
non-overlapping; an empty list is legal (a tokenizer-level match with no
|
|
402
|
+
literal substring) and is not an error.
|
|
403
|
+
"""
|
|
404
|
+
folded = _fold(content)
|
|
405
|
+
spans = _find_token_spans(folded, tokens)
|
|
406
|
+
|
|
407
|
+
if spans:
|
|
408
|
+
first_start, first_end = spans[0]
|
|
409
|
+
centre = (first_start + first_end) // 2
|
|
410
|
+
else:
|
|
411
|
+
centre = 0
|
|
412
|
+
half = SNIPPET_CORE_CHARS // 2
|
|
413
|
+
if len(content) <= SNIPPET_CORE_CHARS:
|
|
414
|
+
start, end = 0, len(content)
|
|
415
|
+
else:
|
|
416
|
+
# Centre the window on the first match, then slide it back inside the
|
|
417
|
+
# string if centring pushed it past either edge.
|
|
418
|
+
start = min(max(0, centre - half), len(content) - SNIPPET_CORE_CHARS)
|
|
419
|
+
end = start + SNIPPET_CORE_CHARS
|
|
420
|
+
start, end = _snap_outward(content, start, end, SNIPPET_CHARS - (end - start))
|
|
421
|
+
|
|
422
|
+
snippet = content[start:end]
|
|
423
|
+
prefix = _utf16_prefix(snippet)
|
|
424
|
+
offsets: list[tuple[int, int]] = []
|
|
425
|
+
for span_start, span_end in spans:
|
|
426
|
+
if span_start >= end or span_end <= start:
|
|
427
|
+
continue
|
|
428
|
+
clipped_start = max(span_start, start) - start
|
|
429
|
+
clipped_end = min(span_end, end) - start
|
|
430
|
+
if clipped_end > clipped_start:
|
|
431
|
+
offsets.append((prefix[clipped_start], prefix[clipped_end]))
|
|
432
|
+
|
|
433
|
+
return _SnippetResult(
|
|
434
|
+
text=snippet,
|
|
435
|
+
truncated_start=start > 0,
|
|
436
|
+
truncated_end=end < len(content),
|
|
437
|
+
offsets=offsets,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def recency_weight(age_days: float) -> float:
|
|
442
|
+
"""``0.25 + 0.75 * 0.5^(age_days / 30)`` — multiplicative with a floor.
|
|
443
|
+
|
|
444
|
+
A message from today can outrank an otherwise-identical one from long ago
|
|
445
|
+
by up to 4x, but the floor means recency can never fully bury a much
|
|
446
|
+
stronger older match (contract §3.6).
|
|
447
|
+
"""
|
|
448
|
+
decay = 0.5 ** (max(0.0, age_days) / RECENCY_HALF_LIFE_D)
|
|
449
|
+
return RECENCY_FLOOR + (1.0 - RECENCY_FLOOR) * decay
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
# ChatSearchIndex
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
class ChatSearchIndex:
|
|
457
|
+
"""Derived FTS5 index over chat message bodies. Rebuildable at any time.
|
|
458
|
+
|
|
459
|
+
Writes arrive on the event-loop thread from
|
|
460
|
+
:class:`ChatSearchIndexSubscriber` (single-row upserts inside WAL —
|
|
461
|
+
microseconds). Reads arrive on threadpool workers from the route, which is
|
|
462
|
+
why connections are thread-local: ``sqlite3`` connections are not
|
|
463
|
+
thread-safe and ``run_in_threadpool`` hands work to an arbitrary worker.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
def __init__(self, model_ctx: "ModelContext") -> None:
|
|
467
|
+
self._model_ctx = model_ctx
|
|
468
|
+
self._db_path = Path(model_ctx.base_dir) / "search" / "chat_index.db"
|
|
469
|
+
self._local = threading.local()
|
|
470
|
+
self._available = False
|
|
471
|
+
self._backfilling = False
|
|
472
|
+
|
|
473
|
+
# -- lifecycle ---------------------------------------------------------
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def available(self) -> bool:
|
|
477
|
+
"""False when FTS5 is missing from the SQLite build or the kill switch
|
|
478
|
+
is set; the route turns this into a 503 rather than an empty result."""
|
|
479
|
+
return self._available
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def backfilling(self) -> bool:
|
|
483
|
+
"""True while :meth:`reconcile_all` runs. Surfaces as the response's
|
|
484
|
+
``indexing`` flag so the UI can say "still indexing your history" rather
|
|
485
|
+
than rendering a bare "no results"."""
|
|
486
|
+
return self._backfilling
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def db_path(self) -> Path:
|
|
490
|
+
return self._db_path
|
|
491
|
+
|
|
492
|
+
def open(self) -> None:
|
|
493
|
+
"""Create the DB and schema, probe FTS5, and honor the kill switch.
|
|
494
|
+
|
|
495
|
+
A missing search feature must never take the chat server down: any
|
|
496
|
+
failure here logs once, leaves :attr:`available` False and makes every
|
|
497
|
+
write method a silent no-op. Idempotent. Recreates the file from
|
|
498
|
+
scratch when the on-disk schema version differs — the index holds no
|
|
499
|
+
authoritative data, so a full rebuild beats a migration ladder.
|
|
500
|
+
"""
|
|
501
|
+
if os.environ.get("MANIGENT_SEARCH_DISABLED", "").strip():
|
|
502
|
+
logger.warning("Chat search disabled by MANIGENT_SEARCH_DISABLED")
|
|
503
|
+
self._available = False
|
|
504
|
+
return
|
|
505
|
+
try:
|
|
506
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
507
|
+
conn = self._connect()
|
|
508
|
+
version = self._read_schema_version(conn)
|
|
509
|
+
if version is not None and version != SCHEMA_VERSION:
|
|
510
|
+
logger.info(
|
|
511
|
+
f"Chat search index schema {version} != {SCHEMA_VERSION}; rebuilding"
|
|
512
|
+
)
|
|
513
|
+
conn.close()
|
|
514
|
+
self._reset_thread_state()
|
|
515
|
+
for suffix in ("", "-wal", "-shm"):
|
|
516
|
+
Path(str(self._db_path) + suffix).unlink(missing_ok=True)
|
|
517
|
+
conn = self._connect()
|
|
518
|
+
version = None
|
|
519
|
+
conn.executescript(_SCHEMA_SQL)
|
|
520
|
+
if version is None:
|
|
521
|
+
conn.execute(
|
|
522
|
+
"INSERT OR REPLACE INTO index_meta(key, value) VALUES ('schema_version', ?)",
|
|
523
|
+
(str(SCHEMA_VERSION),),
|
|
524
|
+
)
|
|
525
|
+
conn.commit()
|
|
526
|
+
self._available = True
|
|
527
|
+
except sqlite3.Error as e:
|
|
528
|
+
logger.error(
|
|
529
|
+
f"Chat search index unavailable ({e}); /me/search/messages will 503",
|
|
530
|
+
exc_info=True,
|
|
531
|
+
)
|
|
532
|
+
self._available = False
|
|
533
|
+
|
|
534
|
+
def close(self) -> None:
|
|
535
|
+
conn = getattr(self._local, "conn", None)
|
|
536
|
+
if conn is not None:
|
|
537
|
+
conn.close()
|
|
538
|
+
self._reset_thread_state()
|
|
539
|
+
self._available = False
|
|
540
|
+
|
|
541
|
+
def _reset_thread_state(self) -> None:
|
|
542
|
+
self._local = threading.local()
|
|
543
|
+
|
|
544
|
+
def _connect(self) -> sqlite3.Connection:
|
|
545
|
+
conn = sqlite3.connect(self._db_path)
|
|
546
|
+
conn.row_factory = sqlite3.Row
|
|
547
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
548
|
+
conn.execute("PRAGMA synchronous = NORMAL")
|
|
549
|
+
conn.execute("PRAGMA busy_timeout = 5000")
|
|
550
|
+
self._local.conn = conn
|
|
551
|
+
return conn
|
|
552
|
+
|
|
553
|
+
def _conn(self) -> sqlite3.Connection:
|
|
554
|
+
"""Per-thread connection (see the class docstring)."""
|
|
555
|
+
conn = getattr(self._local, "conn", None)
|
|
556
|
+
if conn is None:
|
|
557
|
+
conn = self._connect()
|
|
558
|
+
return conn
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _read_schema_version(conn: sqlite3.Connection) -> int | None:
|
|
562
|
+
try:
|
|
563
|
+
row = conn.execute(
|
|
564
|
+
"SELECT value FROM index_meta WHERE key = 'schema_version'"
|
|
565
|
+
).fetchone()
|
|
566
|
+
except sqlite3.OperationalError:
|
|
567
|
+
return None # fresh file: index_meta does not exist yet
|
|
568
|
+
if row is None:
|
|
569
|
+
return None
|
|
570
|
+
try:
|
|
571
|
+
return int(row["value"])
|
|
572
|
+
except (TypeError, ValueError):
|
|
573
|
+
return None
|
|
574
|
+
|
|
575
|
+
# -- write path --------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
def upsert_project(self, project: Project) -> None:
|
|
578
|
+
"""Mirror the ACL-relevant columns of a project. These columns are the
|
|
579
|
+
ONLY authorization inputs on the query path — see :meth:`search`."""
|
|
580
|
+
if not self._available:
|
|
581
|
+
return
|
|
582
|
+
try:
|
|
583
|
+
conn = self._conn()
|
|
584
|
+
conn.execute(
|
|
585
|
+
"""
|
|
586
|
+
INSERT INTO projects
|
|
587
|
+
(project_id, project_name, display_name, surface,
|
|
588
|
+
created_by, fd_requester_name, coordinator_name)
|
|
589
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
590
|
+
ON CONFLICT(project_id) DO UPDATE SET
|
|
591
|
+
project_name = excluded.project_name,
|
|
592
|
+
display_name = excluded.display_name,
|
|
593
|
+
surface = excluded.surface,
|
|
594
|
+
created_by = excluded.created_by,
|
|
595
|
+
fd_requester_name = excluded.fd_requester_name,
|
|
596
|
+
coordinator_name = excluded.coordinator_name
|
|
597
|
+
""",
|
|
598
|
+
(
|
|
599
|
+
project.id,
|
|
600
|
+
project.name,
|
|
601
|
+
project.display_name,
|
|
602
|
+
str(project.surface),
|
|
603
|
+
project.created_by or "",
|
|
604
|
+
project.fd_requester_name,
|
|
605
|
+
project.coordinator_name,
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
conn.commit()
|
|
609
|
+
except sqlite3.Error:
|
|
610
|
+
logger.warning(f"Search index: upsert_project({project.id}) failed", exc_info=True)
|
|
611
|
+
|
|
612
|
+
def upsert_room(
|
|
613
|
+
self, project: Project, chatroom_name: str, fingerprint: str | None = None
|
|
614
|
+
) -> None:
|
|
615
|
+
"""Mirror a room, deriving the two pre-computed display fields.
|
|
616
|
+
|
|
617
|
+
``room_type`` is ``"dm"`` iff the project is DM-shaped AND the room is
|
|
618
|
+
``user-communication`` — mirroring ``Project.is_dm_shaped`` exactly, so
|
|
619
|
+
the client never has to re-derive DM-ness from a project name.
|
|
620
|
+
``room_label`` is the coordinator (agent) name for a DM row and the
|
|
621
|
+
chatroom name otherwise (contract §3.4).
|
|
622
|
+
|
|
623
|
+
This is also where clause 2 of the ACL is resolved: the room-name
|
|
624
|
+
predicate is evaluated ONCE here, by the model that owns it, and stored
|
|
625
|
+
as ``is_user_communication`` for :meth:`_candidate_rows` to read as a
|
|
626
|
+
plain column.
|
|
627
|
+
"""
|
|
628
|
+
if not self._available:
|
|
629
|
+
return
|
|
630
|
+
is_uc = Chatroom.is_user_communication_name(chatroom_name)
|
|
631
|
+
is_dm = project.is_dm_shaped and is_uc
|
|
632
|
+
room_type = "dm" if is_dm else "project"
|
|
633
|
+
room_label = project.coordinator_name if is_dm else chatroom_name
|
|
634
|
+
try:
|
|
635
|
+
conn = self._conn()
|
|
636
|
+
conn.execute(
|
|
637
|
+
"""
|
|
638
|
+
INSERT INTO rooms
|
|
639
|
+
(project_id, chatroom_name, room_type, room_label,
|
|
640
|
+
is_user_communication, fingerprint)
|
|
641
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
642
|
+
ON CONFLICT(project_id, chatroom_name) DO UPDATE SET
|
|
643
|
+
room_type = excluded.room_type,
|
|
644
|
+
room_label = excluded.room_label,
|
|
645
|
+
is_user_communication = excluded.is_user_communication,
|
|
646
|
+
fingerprint = COALESCE(excluded.fingerprint, rooms.fingerprint)
|
|
647
|
+
""",
|
|
648
|
+
(
|
|
649
|
+
project.id, chatroom_name, room_type, room_label,
|
|
650
|
+
int(is_uc), fingerprint,
|
|
651
|
+
),
|
|
652
|
+
)
|
|
653
|
+
conn.commit()
|
|
654
|
+
except sqlite3.Error:
|
|
655
|
+
logger.warning(
|
|
656
|
+
f"Search index: upsert_room({project.id}/{chatroom_name}) failed",
|
|
657
|
+
exc_info=True,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def index_message(
|
|
661
|
+
self, project_id: str, chatroom_name: str, message: ChatMessage
|
|
662
|
+
) -> None:
|
|
663
|
+
"""Upsert one message.
|
|
664
|
+
|
|
665
|
+
No-op for ``is_ack`` rows (protocol acknowledgments, never user-authored
|
|
666
|
+
text — the same exclusion ``collect_recent_activity`` applies) and for
|
|
667
|
+
blank content. The upsert is keyed on
|
|
668
|
+
``(project_id, chatroom_name, message_id)``, so changelog replay and
|
|
669
|
+
backfill converge on the same row.
|
|
670
|
+
"""
|
|
671
|
+
if not self._available:
|
|
672
|
+
return
|
|
673
|
+
if message.is_ack or not message.content.strip():
|
|
674
|
+
return
|
|
675
|
+
try:
|
|
676
|
+
conn = self._conn()
|
|
677
|
+
self._insert_message(conn, project_id, chatroom_name, message)
|
|
678
|
+
conn.commit()
|
|
679
|
+
except sqlite3.Error:
|
|
680
|
+
logger.warning(
|
|
681
|
+
f"Search index: index_message({project_id}/{chatroom_name}) failed",
|
|
682
|
+
exc_info=True,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
@staticmethod
|
|
686
|
+
def _insert_message(
|
|
687
|
+
conn: sqlite3.Connection,
|
|
688
|
+
project_id: str,
|
|
689
|
+
chatroom_name: str,
|
|
690
|
+
message: ChatMessage,
|
|
691
|
+
) -> None:
|
|
692
|
+
ts = message.ts
|
|
693
|
+
if ts.tzinfo is None:
|
|
694
|
+
ts = ts.replace(tzinfo=UTC)
|
|
695
|
+
conn.execute(
|
|
696
|
+
"""
|
|
697
|
+
INSERT INTO messages
|
|
698
|
+
(message_id, project_id, chatroom_name, ts, author_id, author_name, content)
|
|
699
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
700
|
+
ON CONFLICT(project_id, chatroom_name, message_id) DO UPDATE SET
|
|
701
|
+
ts = excluded.ts,
|
|
702
|
+
author_id = excluded.author_id,
|
|
703
|
+
author_name = excluded.author_name,
|
|
704
|
+
content = excluded.content
|
|
705
|
+
""",
|
|
706
|
+
(
|
|
707
|
+
message.id,
|
|
708
|
+
project_id,
|
|
709
|
+
chatroom_name,
|
|
710
|
+
ts.timestamp(),
|
|
711
|
+
message.from_participant_id,
|
|
712
|
+
message.from_participant_name,
|
|
713
|
+
message.content,
|
|
714
|
+
),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def drop_room(self, project_id: str, chatroom_name: str) -> None:
|
|
718
|
+
"""Delete every message row for a room.
|
|
719
|
+
|
|
720
|
+
Called on CHATROOM_CLEARED — critically, since a user who wipes a room's
|
|
721
|
+
history must not keep finding it in search — and on ROOM_DELETED for
|
|
722
|
+
every room the model layer will actually delete (the subscriber applies
|
|
723
|
+
``Chatroom.is_system_room_name`` first; see ``_dispatch``).
|
|
724
|
+
"""
|
|
725
|
+
if not self._available:
|
|
726
|
+
return
|
|
727
|
+
try:
|
|
728
|
+
conn = self._conn()
|
|
729
|
+
conn.execute(
|
|
730
|
+
"DELETE FROM messages WHERE project_id = ? AND chatroom_name = ?",
|
|
731
|
+
(project_id, chatroom_name),
|
|
732
|
+
)
|
|
733
|
+
conn.execute(
|
|
734
|
+
"UPDATE rooms SET fingerprint = NULL WHERE project_id = ? AND chatroom_name = ?",
|
|
735
|
+
(project_id, chatroom_name),
|
|
736
|
+
)
|
|
737
|
+
conn.commit()
|
|
738
|
+
except sqlite3.Error:
|
|
739
|
+
logger.warning(
|
|
740
|
+
f"Search index: drop_room({project_id}/{chatroom_name}) failed",
|
|
741
|
+
exc_info=True,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def drop_project(self, project_id: str) -> None:
|
|
745
|
+
"""Delete a project and all of its rooms and messages.
|
|
746
|
+
|
|
747
|
+
Called from ``_delete_project_in_place``, which tears down the
|
|
748
|
+
changelog itself and so is never observed by the subscriber. Without
|
|
749
|
+
this hook a deleted project's messages stay searchable forever — a
|
|
750
|
+
permission leak, not merely staleness.
|
|
751
|
+
"""
|
|
752
|
+
if not self._available:
|
|
753
|
+
return
|
|
754
|
+
try:
|
|
755
|
+
conn = self._conn()
|
|
756
|
+
conn.execute("DELETE FROM messages WHERE project_id = ?", (project_id,))
|
|
757
|
+
conn.execute("DELETE FROM rooms WHERE project_id = ?", (project_id,))
|
|
758
|
+
conn.execute("DELETE FROM projects WHERE project_id = ?", (project_id,))
|
|
759
|
+
conn.commit()
|
|
760
|
+
except sqlite3.Error:
|
|
761
|
+
logger.warning(f"Search index: drop_project({project_id}) failed", exc_info=True)
|
|
762
|
+
|
|
763
|
+
# -- backfill / reconcile ---------------------------------------------
|
|
764
|
+
|
|
765
|
+
@staticmethod
|
|
766
|
+
def _fingerprint(path: Path) -> str | None:
|
|
767
|
+
try:
|
|
768
|
+
st = path.stat()
|
|
769
|
+
except OSError:
|
|
770
|
+
return None
|
|
771
|
+
return f"{st.st_size}:{st.st_mtime_ns}"
|
|
772
|
+
|
|
773
|
+
async def reconcile_all(self) -> None:
|
|
774
|
+
"""Walk every project and room, re-indexing only what changed.
|
|
775
|
+
|
|
776
|
+
Each room's ``CHATS.ndjson`` is compared against the stored
|
|
777
|
+
``(size, mtime_ns)`` fingerprint; a room that matches is skipped after a
|
|
778
|
+
single ``stat()`` without ever being opened. This is BOTH the first-run
|
|
779
|
+
backfill of existing history AND the ongoing drift repair — it catches
|
|
780
|
+
anything that reached ``CHATS.ndjson`` without passing the subscriber.
|
|
781
|
+
Yields to the event loop between rooms so an in-progress backfill never
|
|
782
|
+
starves live requests.
|
|
783
|
+
"""
|
|
784
|
+
if not self._available:
|
|
785
|
+
return
|
|
786
|
+
import asyncio
|
|
787
|
+
|
|
788
|
+
self._backfilling = True
|
|
789
|
+
indexed_rooms = 0
|
|
790
|
+
indexed_messages = 0
|
|
791
|
+
try:
|
|
792
|
+
try:
|
|
793
|
+
projects = Project.list_all(self._model_ctx)
|
|
794
|
+
except (OSError, ValueError):
|
|
795
|
+
logger.warning("Search index: could not list projects", exc_info=True)
|
|
796
|
+
return
|
|
797
|
+
live_ids = set()
|
|
798
|
+
for project in projects:
|
|
799
|
+
live_ids.add(project.id)
|
|
800
|
+
self.upsert_project(project)
|
|
801
|
+
try:
|
|
802
|
+
room_names = project.chatrooms
|
|
803
|
+
except (OSError, ValueError):
|
|
804
|
+
continue
|
|
805
|
+
for chatroom_name in room_names:
|
|
806
|
+
changed, message_count = self._reconcile_room(project, chatroom_name)
|
|
807
|
+
if changed:
|
|
808
|
+
indexed_rooms += 1
|
|
809
|
+
indexed_messages += message_count
|
|
810
|
+
await asyncio.sleep(0)
|
|
811
|
+
self._prune_missing_projects(live_ids)
|
|
812
|
+
finally:
|
|
813
|
+
self._backfilling = False
|
|
814
|
+
if indexed_rooms:
|
|
815
|
+
logger.info(
|
|
816
|
+
f"Chat search index: reindexed {indexed_messages} message(s) "
|
|
817
|
+
f"across {indexed_rooms} room(s)"
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def _reconcile_room(self, project: Project, chatroom_name: str) -> tuple[bool, int]:
|
|
821
|
+
"""Re-index one room if its CHATS.ndjson fingerprint moved. Returns
|
|
822
|
+
``(changed, messages_indexed)``."""
|
|
823
|
+
room = Chatroom.get(project.id, chatroom_name, self._model_ctx)
|
|
824
|
+
if room is None:
|
|
825
|
+
return (False, 0)
|
|
826
|
+
fingerprint = self._fingerprint(room.chats_path)
|
|
827
|
+
try:
|
|
828
|
+
conn = self._conn()
|
|
829
|
+
row = conn.execute(
|
|
830
|
+
"SELECT fingerprint FROM rooms WHERE project_id = ? AND chatroom_name = ?",
|
|
831
|
+
(project.id, chatroom_name),
|
|
832
|
+
).fetchone()
|
|
833
|
+
except sqlite3.Error:
|
|
834
|
+
logger.warning("Search index: fingerprint read failed", exc_info=True)
|
|
835
|
+
return (False, 0)
|
|
836
|
+
if row is not None and row["fingerprint"] == fingerprint and fingerprint is not None:
|
|
837
|
+
return (False, 0)
|
|
838
|
+
|
|
839
|
+
self.upsert_room(project, chatroom_name)
|
|
840
|
+
count = 0
|
|
841
|
+
try:
|
|
842
|
+
entries = room.get_log_entries()
|
|
843
|
+
except (OSError, ValueError):
|
|
844
|
+
logger.warning(
|
|
845
|
+
f"Search index: could not read {project.id}/{chatroom_name}", exc_info=True
|
|
846
|
+
)
|
|
847
|
+
return (False, 0)
|
|
848
|
+
try:
|
|
849
|
+
conn = self._conn()
|
|
850
|
+
with conn: # one transaction per room
|
|
851
|
+
conn.execute(
|
|
852
|
+
"DELETE FROM messages WHERE project_id = ? AND chatroom_name = ?",
|
|
853
|
+
(project.id, chatroom_name),
|
|
854
|
+
)
|
|
855
|
+
for entry in entries:
|
|
856
|
+
if not isinstance(entry, ChatMessage):
|
|
857
|
+
continue
|
|
858
|
+
if entry.is_ack or not entry.content.strip():
|
|
859
|
+
continue
|
|
860
|
+
self._insert_message(conn, project.id, chatroom_name, entry)
|
|
861
|
+
count += 1
|
|
862
|
+
# Normally an ON CONFLICT update of the fingerprint alone: the
|
|
863
|
+
# upsert_room() above already wrote the row. The INSERT branch
|
|
864
|
+
# is only reachable if that call was swallowed by an error.
|
|
865
|
+
conn.execute(
|
|
866
|
+
"""
|
|
867
|
+
INSERT INTO rooms (project_id, chatroom_name, room_type, room_label,
|
|
868
|
+
is_user_communication, fingerprint)
|
|
869
|
+
VALUES (?, ?, 'project', ?, ?, ?)
|
|
870
|
+
ON CONFLICT(project_id, chatroom_name)
|
|
871
|
+
DO UPDATE SET fingerprint = excluded.fingerprint
|
|
872
|
+
""",
|
|
873
|
+
(
|
|
874
|
+
project.id, chatroom_name, chatroom_name,
|
|
875
|
+
int(Chatroom.is_user_communication_name(chatroom_name)),
|
|
876
|
+
fingerprint,
|
|
877
|
+
),
|
|
878
|
+
)
|
|
879
|
+
except sqlite3.Error:
|
|
880
|
+
logger.warning(
|
|
881
|
+
f"Search index: reindex of {project.id}/{chatroom_name} failed", exc_info=True
|
|
882
|
+
)
|
|
883
|
+
return (False, 0)
|
|
884
|
+
return (True, count)
|
|
885
|
+
|
|
886
|
+
def _prune_missing_projects(self, live_ids: set[str]) -> None:
|
|
887
|
+
"""Drop index rows for projects that no longer exist on disk — the
|
|
888
|
+
second half of the deletion story, covering any path that removed a
|
|
889
|
+
project without calling :meth:`drop_project`."""
|
|
890
|
+
try:
|
|
891
|
+
conn = self._conn()
|
|
892
|
+
stored = {r["project_id"] for r in conn.execute("SELECT project_id FROM projects")}
|
|
893
|
+
except sqlite3.Error:
|
|
894
|
+
return
|
|
895
|
+
for project_id in stored - live_ids:
|
|
896
|
+
logger.info(f"Chat search index: pruning deleted project {project_id}")
|
|
897
|
+
self.drop_project(project_id)
|
|
898
|
+
|
|
899
|
+
# -- query path --------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
def search(
|
|
902
|
+
self,
|
|
903
|
+
query: str,
|
|
904
|
+
scope: SearchScope,
|
|
905
|
+
*,
|
|
906
|
+
limit: int = 20,
|
|
907
|
+
offset: int = 0,
|
|
908
|
+
) -> SearchResponse:
|
|
909
|
+
"""Ranked message hits across every room ``scope`` can read.
|
|
910
|
+
|
|
911
|
+
Blocking — the route wraps this in ``run_in_threadpool`` so a slow
|
|
912
|
+
query never stalls the single-process event loop (including WebSocket
|
|
913
|
+
traffic).
|
|
914
|
+
|
|
915
|
+
Two-stage ranking: FTS5/BM25 selects a bounded candidate pool in C,
|
|
916
|
+
then Python re-weights it by recency. A single SQL expression is not an
|
|
917
|
+
option because ``exp()`` is not available in a stock CPython SQLite
|
|
918
|
+
build, and a per-row Python UDF is slower than re-ranking 200 rows.
|
|
919
|
+
|
|
920
|
+
The ACL predicate is inlined into the matching statement, so a row the
|
|
921
|
+
caller may not read is never materialized. Every caller gets their own
|
|
922
|
+
plate; there is no bypass parameter.
|
|
923
|
+
|
|
924
|
+
Raises:
|
|
925
|
+
SearchIndexError: the index could not be queried. The route turns
|
|
926
|
+
this into a 503; it is never reported as zero hits.
|
|
927
|
+
"""
|
|
928
|
+
started = _now_monotonic_ms()
|
|
929
|
+
limit = max(1, min(limit, MAX_LIMIT))
|
|
930
|
+
offset = max(0, min(offset, MAX_OFFSET))
|
|
931
|
+
tokens = tokenize_query(query)
|
|
932
|
+
|
|
933
|
+
def _empty() -> SearchResponse:
|
|
934
|
+
return SearchResponse(
|
|
935
|
+
query=query, hits=[], limit=limit, offset=offset,
|
|
936
|
+
has_more=False, next_offset=None,
|
|
937
|
+
indexing=self._backfilling,
|
|
938
|
+
took_ms=round(_now_monotonic_ms() - started, 3),
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
# Below the floor after tokenization is a normal typeahead state, not an
|
|
942
|
+
# error: typing one character returns 200 with no hits (contract §2.2).
|
|
943
|
+
if sum(len(t) for t in tokens) < MIN_QUERY_CHARS:
|
|
944
|
+
return _empty()
|
|
945
|
+
|
|
946
|
+
rows = self._candidate_rows(build_match_expr(tokens), scope)
|
|
947
|
+
if not rows:
|
|
948
|
+
return _empty()
|
|
949
|
+
|
|
950
|
+
now = datetime.now(UTC).timestamp()
|
|
951
|
+
ranked = []
|
|
952
|
+
for row in rows:
|
|
953
|
+
# bm25() is negative-is-better; flip it so a bigger score is better.
|
|
954
|
+
relevance = max(0.0, -float(row["rel"]))
|
|
955
|
+
age_days = max(0.0, (now - float(row["ts"])) / 86400.0)
|
|
956
|
+
ranked.append((relevance * recency_weight(age_days), row))
|
|
957
|
+
ranked.sort(key=lambda pair: pair[0], reverse=True)
|
|
958
|
+
|
|
959
|
+
page = ranked[offset:offset + limit]
|
|
960
|
+
has_more = (offset + limit) < len(ranked)
|
|
961
|
+
hits = [self._build_hit(row, score, tokens) for score, row in page]
|
|
962
|
+
return SearchResponse(
|
|
963
|
+
query=query,
|
|
964
|
+
hits=hits,
|
|
965
|
+
limit=limit,
|
|
966
|
+
offset=offset,
|
|
967
|
+
has_more=has_more,
|
|
968
|
+
next_offset=(offset + len(hits)) if has_more else None,
|
|
969
|
+
indexing=self._backfilling,
|
|
970
|
+
took_ms=round(_now_monotonic_ms() - started, 3),
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def _candidate_rows(self, match_expr: str, scope: SearchScope) -> list[sqlite3.Row]:
|
|
974
|
+
"""One statement: MATCH + the ACL predicate + BM25 ordering, bounded to
|
|
975
|
+
``CANDIDATE_POOL`` rows. Raises :class:`SearchIndexError` on a SQLite
|
|
976
|
+
failure rather than degrading to an empty result."""
|
|
977
|
+
viewer_ids = sorted(scope.viewer_project_ids)
|
|
978
|
+
# The contract §5 three-clause rule. The invariant is that search returns
|
|
979
|
+
# a hit iff the same user could open that room via the room-read endpoint:
|
|
980
|
+
# not narrower, not wider.
|
|
981
|
+
#
|
|
982
|
+
# Clause 2's room test is a mirrored boolean, NOT a room-name pattern:
|
|
983
|
+
# `Chatroom.is_user_communication_name` was already called at index-write
|
|
984
|
+
# time (see upsert_room) and stored in `rooms.is_user_communication`, so
|
|
985
|
+
# the model owns the rule and SQL only reads the answer. A `LIKE` here
|
|
986
|
+
# leaked across a user boundary once — see the module docstring.
|
|
987
|
+
acl = [
|
|
988
|
+
"p.created_by = :user_id",
|
|
989
|
+
"(p.fd_requester_name = :username AND r.is_user_communication = 1)",
|
|
990
|
+
]
|
|
991
|
+
params: dict[str, object] = {
|
|
992
|
+
"match": match_expr,
|
|
993
|
+
"user_id": scope.user_id,
|
|
994
|
+
"username": scope.username,
|
|
995
|
+
"pool": CANDIDATE_POOL,
|
|
996
|
+
}
|
|
997
|
+
if viewer_ids:
|
|
998
|
+
placeholders = []
|
|
999
|
+
for i, project_id in enumerate(viewer_ids):
|
|
1000
|
+
key = f"viewer{i}"
|
|
1001
|
+
params[key] = project_id
|
|
1002
|
+
placeholders.append(f":{key}")
|
|
1003
|
+
acl.append(f"p.project_id IN ({', '.join(placeholders)})")
|
|
1004
|
+
sql = f"""
|
|
1005
|
+
SELECT m.message_id, m.project_id, m.chatroom_name, m.ts,
|
|
1006
|
+
m.author_id, m.author_name, m.content,
|
|
1007
|
+
r.room_type, r.room_label,
|
|
1008
|
+
p.project_name, p.coordinator_name,
|
|
1009
|
+
bm25(messages_fts) AS rel
|
|
1010
|
+
FROM messages_fts
|
|
1011
|
+
JOIN messages m ON m.rowid = messages_fts.rowid
|
|
1012
|
+
JOIN rooms r ON r.project_id = m.project_id
|
|
1013
|
+
AND r.chatroom_name = m.chatroom_name
|
|
1014
|
+
JOIN projects p ON p.project_id = m.project_id
|
|
1015
|
+
WHERE messages_fts MATCH :match
|
|
1016
|
+
AND ({' OR '.join(acl)})
|
|
1017
|
+
ORDER BY bm25(messages_fts)
|
|
1018
|
+
LIMIT :pool
|
|
1019
|
+
"""
|
|
1020
|
+
try:
|
|
1021
|
+
return self._conn().execute(sql, params).fetchall()
|
|
1022
|
+
except sqlite3.Error as e:
|
|
1023
|
+
# NOT swallowed into an empty result: a locked or corrupted index
|
|
1024
|
+
# must be distinguishable from "nothing matched" (contract §2.3).
|
|
1025
|
+
logger.warning("Search index: query failed", exc_info=True)
|
|
1026
|
+
raise SearchIndexError(str(e)) from e
|
|
1027
|
+
|
|
1028
|
+
def _build_hit(self, row: sqlite3.Row, score: float, tokens: list[str]) -> SearchHit:
|
|
1029
|
+
snippet = build_snippet(row["content"], tokens)
|
|
1030
|
+
return SearchHit(
|
|
1031
|
+
message_id=row["message_id"],
|
|
1032
|
+
project_id=row["project_id"],
|
|
1033
|
+
project_name=row["project_name"],
|
|
1034
|
+
room_id=row["chatroom_name"],
|
|
1035
|
+
room_type=row["room_type"],
|
|
1036
|
+
room_label=row["room_label"],
|
|
1037
|
+
ts=datetime.fromtimestamp(float(row["ts"]), tz=UTC),
|
|
1038
|
+
author_id=row["author_id"],
|
|
1039
|
+
author_name=row["author_name"],
|
|
1040
|
+
snippet=snippet.text,
|
|
1041
|
+
snippet_truncated_start=snippet.truncated_start,
|
|
1042
|
+
snippet_truncated_end=snippet.truncated_end,
|
|
1043
|
+
match_offsets=snippet.offsets,
|
|
1044
|
+
score=round(score, 6),
|
|
1045
|
+
deep_link=_build_deep_link(
|
|
1046
|
+
room_type=row["room_type"],
|
|
1047
|
+
project_id=row["project_id"],
|
|
1048
|
+
chatroom_name=row["chatroom_name"],
|
|
1049
|
+
coordinator_name=row["coordinator_name"],
|
|
1050
|
+
message_id=row["message_id"],
|
|
1051
|
+
),
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _build_deep_link(
|
|
1056
|
+
*,
|
|
1057
|
+
room_type: str,
|
|
1058
|
+
project_id: str,
|
|
1059
|
+
chatroom_name: str,
|
|
1060
|
+
coordinator_name: str,
|
|
1061
|
+
message_id: str,
|
|
1062
|
+
) -> DeepLink:
|
|
1063
|
+
"""Build the click-through target (contract §4.2).
|
|
1064
|
+
|
|
1065
|
+
Both shapes are routes that already exist in ``App.tsx``:
|
|
1066
|
+
``dm/:agentName/:threadId`` and ``projects/:projectId/chatrooms/:chatroomName``.
|
|
1067
|
+
A DM thread's ``:threadId`` **is** its ``project_id`` — each thread is its
|
|
1068
|
+
own project — which is how ``ProjectItem.tsx`` and ``DmsPanel.tsx`` already
|
|
1069
|
+
build DM links.
|
|
1070
|
+
"""
|
|
1071
|
+
if room_type == "dm":
|
|
1072
|
+
return DeepLink(
|
|
1073
|
+
path=(
|
|
1074
|
+
f"/dm/{quote(coordinator_name, safe='')}/{quote(project_id, safe='')}"
|
|
1075
|
+
f"?msg={quote(message_id, safe='')}"
|
|
1076
|
+
),
|
|
1077
|
+
kind="dm",
|
|
1078
|
+
project_id=project_id,
|
|
1079
|
+
chatroom_name=chatroom_name,
|
|
1080
|
+
agent_name=coordinator_name,
|
|
1081
|
+
message_id=message_id,
|
|
1082
|
+
)
|
|
1083
|
+
return DeepLink(
|
|
1084
|
+
path=(
|
|
1085
|
+
f"/projects/{quote(project_id, safe='')}"
|
|
1086
|
+
f"/chatrooms/{quote(chatroom_name, safe='')}"
|
|
1087
|
+
f"?msg={quote(message_id, safe='')}"
|
|
1088
|
+
),
|
|
1089
|
+
kind="project_room",
|
|
1090
|
+
project_id=project_id,
|
|
1091
|
+
chatroom_name=chatroom_name,
|
|
1092
|
+
agent_name=None,
|
|
1093
|
+
message_id=message_id,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _now_monotonic_ms() -> float:
|
|
1098
|
+
import time
|
|
1099
|
+
|
|
1100
|
+
return time.perf_counter() * 1000.0
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
# ---------------------------------------------------------------------------
|
|
1104
|
+
# Changelog subscriber
|
|
1105
|
+
# ---------------------------------------------------------------------------
|
|
1106
|
+
|
|
1107
|
+
class ChatSearchIndexSubscriber(ChangelogSubscriber):
|
|
1108
|
+
"""Indexes each entry AFTER ``ModelContextChangelogSubscriber`` has written it.
|
|
1109
|
+
|
|
1110
|
+
Registered directly after ``model_ctx.changelog_subscriber`` in the
|
|
1111
|
+
``runloop_factory`` list. ``ChangelogRunloop`` calls subscribers in
|
|
1112
|
+
insertion order per entry, so by the time this runs the row is already
|
|
1113
|
+
durable in ``CHATS.ndjson`` — the index mirrors what was just written
|
|
1114
|
+
rather than racing it.
|
|
1115
|
+
|
|
1116
|
+
Must be idempotent (entries are replayed) and must never raise: an
|
|
1117
|
+
exception here would wedge the runloop cursor for a room's chat delivery,
|
|
1118
|
+
and search is strictly less important than chat.
|
|
1119
|
+
"""
|
|
1120
|
+
|
|
1121
|
+
def __init__(self, index: ChatSearchIndex, model_ctx: "ModelContext") -> None:
|
|
1122
|
+
self._index = index
|
|
1123
|
+
self._model_ctx = model_ctx
|
|
1124
|
+
|
|
1125
|
+
async def on_entry(
|
|
1126
|
+
self, entry: "ChangelogEntry", project_id: str, project_name: str
|
|
1127
|
+
) -> None:
|
|
1128
|
+
if not self._index.available:
|
|
1129
|
+
return
|
|
1130
|
+
try:
|
|
1131
|
+
await self._dispatch(entry, project_id)
|
|
1132
|
+
except Exception: # never propagate — see the class docstring
|
|
1133
|
+
logger.warning(
|
|
1134
|
+
f"Search index: subscriber failed on {entry.entry_type} "
|
|
1135
|
+
f"v{entry.version} in {project_id}",
|
|
1136
|
+
exc_info=True,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
async def _dispatch(self, entry: "ChangelogEntry", project_id: str) -> None:
|
|
1140
|
+
entry_type = entry.entry_type
|
|
1141
|
+
payload = entry.payload
|
|
1142
|
+
|
|
1143
|
+
if entry_type == ChangelogEntryType.MESSAGE:
|
|
1144
|
+
chatroom_name = getattr(payload, "chatroom_name", None)
|
|
1145
|
+
if not chatroom_name:
|
|
1146
|
+
return
|
|
1147
|
+
if getattr(payload, "is_ack", False):
|
|
1148
|
+
return # protocol noise, never user-authored text
|
|
1149
|
+
message = ChatMessage.from_message_payload(payload, version=entry.version)
|
|
1150
|
+
# A room the index has not seen yet (first message after a restart
|
|
1151
|
+
# that missed ROOM_CREATED) would otherwise produce a message row
|
|
1152
|
+
# with no rooms row to join, i.e. an invisible hit.
|
|
1153
|
+
self._ensure_room(project_id, chatroom_name)
|
|
1154
|
+
self._index.index_message(project_id, chatroom_name, message)
|
|
1155
|
+
|
|
1156
|
+
elif entry_type == ChangelogEntryType.PROJECT_CREATED:
|
|
1157
|
+
self._upsert_project(project_id)
|
|
1158
|
+
|
|
1159
|
+
elif entry_type == ChangelogEntryType.ROOM_CREATED:
|
|
1160
|
+
chatroom_name = getattr(payload, "chatroom_name", None)
|
|
1161
|
+
if chatroom_name:
|
|
1162
|
+
self._ensure_room(project_id, chatroom_name)
|
|
1163
|
+
|
|
1164
|
+
elif entry_type == ChangelogEntryType.DISPLAY_NAME_CHANGED:
|
|
1165
|
+
self._upsert_project(project_id)
|
|
1166
|
+
|
|
1167
|
+
elif entry_type == ChangelogEntryType.ROOM_DELETED:
|
|
1168
|
+
chatroom_name = getattr(payload, "chatroom_name", None)
|
|
1169
|
+
# Mirror ``ModelContext._handle_room_deleted``: it REFUSES to delete
|
|
1170
|
+
# the two system rooms, and ``_delete_project_in_place`` relies on
|
|
1171
|
+
# that refusal — it emits ROOM_DELETED for a room in a *different,
|
|
1172
|
+
# surviving* project. Dropping those rows here would silently empty a
|
|
1173
|
+
# live, still-readable room until the next reconcile_all at boot.
|
|
1174
|
+
if chatroom_name and not Chatroom.is_system_room_name(chatroom_name):
|
|
1175
|
+
self._index.drop_room(project_id, chatroom_name)
|
|
1176
|
+
|
|
1177
|
+
elif entry_type == ChangelogEntryType.CHATROOM_CLEARED:
|
|
1178
|
+
# No skip here: a clear is honored for every room, system ones
|
|
1179
|
+
# included. Wiped history that stayed searchable would be the leak.
|
|
1180
|
+
chatroom_name = getattr(payload, "chatroom_name", None)
|
|
1181
|
+
if chatroom_name:
|
|
1182
|
+
self._index.drop_room(project_id, chatroom_name)
|
|
1183
|
+
|
|
1184
|
+
# Everything else — file events, batch events, participant adds,
|
|
1185
|
+
# allowlist updates — carries no indexable text.
|
|
1186
|
+
|
|
1187
|
+
def _project(self, project_id: str) -> Project | None:
|
|
1188
|
+
try:
|
|
1189
|
+
return Project.get(project_id, self._model_ctx)
|
|
1190
|
+
except ValueError:
|
|
1191
|
+
return None # Project.get raises rather than returning None
|
|
1192
|
+
|
|
1193
|
+
def _upsert_project(self, project_id: str) -> None:
|
|
1194
|
+
project = self._project(project_id)
|
|
1195
|
+
if project is not None:
|
|
1196
|
+
self._index.upsert_project(project)
|
|
1197
|
+
|
|
1198
|
+
def _ensure_room(self, project_id: str, chatroom_name: str) -> None:
|
|
1199
|
+
project = self._project(project_id)
|
|
1200
|
+
if project is None:
|
|
1201
|
+
return
|
|
1202
|
+
self._index.upsert_project(project)
|
|
1203
|
+
self._index.upsert_room(project, chatroom_name)
|