.github/9
ISSUE_TEMPLATE/3bug_report.md config.yml feature_request.md
workflows/3ci.yml clean-clone.yml release.yml
CODEOWNERS dependabot.yml pull_request_template.md
apps/630
api/250
src/243
common/30
__tests__/3authorize.test.ts bigint-serialization.interceptor.test.ts files-limits.test.ts
filters/1all-exceptions.filter.ts
guards/3auth.guard.ts internal-service.guard.ts rate-limit.guard.ts
interceptors/2actor-context.interceptor.ts bigint-serialization.interceptor.ts
middleware/2idempotency.test.ts idempotency.ts
pipes/1zod-validation.pipe.ts
actor-context.ts audit.service.ts cache.test.ts cache.ts cookies.ts core.module.ts deprecation.ts email.service.ts errors.ts event-bus.ts log-scrubber.test.ts log-scrubber.ts logger.ts permission-cache-bridge.ts prisma.service.ts redis.service.ts trace-metadata.ts types.ts
config/1env.ts
lib/11
__tests__/5crypto.test.ts filter-builder.test.ts jwt.test.ts pagination.test.ts positions.test.ts
authorize.ts crypto.ts filter-builder.ts jwt.ts pagination.ts positions.ts
modules/197
ai/28
__tests__/4ai-writing.test.ts database-ai.test.ts generate-executor.test.ts prompts.test.ts
generator/4executor.ts generate.controller.ts generate.dto.ts generate.service.ts
ai-chat.service.ts ai-rate-limit.ts ai-writing.controller.ts ai-writing.dto.ts ai-writing.service.ts ai.controller.ts ai.module.ts ai.service.ts budget-guard.service.ts database-ai.controller.ts database-ai.dto.ts database-ai.service.ts db-prompts.ts dto.ts gateway-provider.ts ledger.service.ts prompts.ts rag.controller.ts rag.dto.ts rag.service.ts
api-keys/3api-keys.controller.ts api-keys.module.ts api-keys.service.ts
audit/2audit-log.controller.ts audit.module.ts
auth/4auth.controller.ts auth.module.ts auth.service.ts dto.ts
automations/5automations.controller.ts automations.module.ts automations.service.ts dto.ts webhook-incoming.controller.ts
blocks/5
__tests__/1batch-version-cas.test.ts
block.service.ts blocks.controller.ts blocks.module.ts dto.ts
comments/4comments.controller.ts comments.module.ts comments.service.ts dto.ts
databases/9
__tests__/3normalize-select-options.test.ts option-id-canonicalization.test.ts record-open-query-shape.test.ts
database.service.ts databases.controller.ts databases.module.ts dto.ts property-cache-schema.ts query.service.ts
docs/2docs.controller.ts docs.module.ts
files/10
__tests__/1quota.test.ts
dto.ts files.controller.ts files.module.ts files.service.ts quota.ts record.ts s3.ts uploads.controller.ts uploads.service.ts
forms/18debug.controller.ts form.dto.ts forms.controller.ts forms.module.ts forms.service.ts markdown.test.ts markdown.ts optional-auth.guard.ts public-forms.controller.ts public-forms.service.ts question.dto.ts questions.service.ts rate-limit.ts recaptcha.ts slug.ts submissions.controller.ts submissions.service.ts submit-schema.ts
health/3health.controller.ts health.module.ts health.service.ts
integrations/3integrations.controller.ts integrations.module.ts integrations.service.ts
internal/12
__tests__/2flush-query-shape.test.ts internal.endpoints.test.ts
ai-test-seam.controller.ts ai-test-seam.dto.ts ai-test-seam.service.ts internal.controller.ts internal.dto.ts internal.module.ts internal.service.ts test-seam.controller.ts test-seam.dto.ts test-seam.service.ts
notifications/3notifications.controller.ts notifications.module.ts notifications.service.ts
oauth/6
__tests__/1oauth.scopes.test.ts
oauth.controller.ts oauth.module.ts oauth.service.ts scopes.ts token-keys.ts
org/15audit.service.ts embed-preview.controller.ts guards.ts members.service.ts org-admin.controller.ts org-policy.service.ts org.module.ts overview.service.ts policy-effective.controller.ts policy.schema.ts policy.test.ts retention.service.ts totp.test.ts totp.ts two-factor.service.ts
orgs/5dto.ts orgs.controller.ts orgs.module.ts orgs.service.ts util.ts
pages/4dto.ts page.service.ts pages.controller.ts pages.module.ts
scim/15
__tests__/3email-mapping.test.ts etag.test.ts filters.test.ts
etag.ts filters.ts groups.service.ts schemas.ts scim-admin.controller.ts scim-admin.service.ts scim-auth.guard.ts scim-auth.service.ts scim-errors.ts scim.controller.ts scim.module.ts users.service.ts
search/4
__tests__/1search.service.test.ts
search.controller.ts search.module.ts search.service.ts
sessions/3sessions.controller.ts sessions.module.ts sessions.service.ts
share/3share.controller.ts share.module.ts share.service.ts
sso/16
__tests__/4enforcement.test.ts oidc.verify.test.ts provisioning.test.ts saml.validate.test.ts
enforcement.ts oidc.controller.ts oidc.service.ts oidc.ts provisioning.service.ts saml.controller.ts saml.ts schemas.ts sso-admin.controller.ts sso-admin.service.ts sso-secrets.ts sso.module.ts
teams/4dto.ts teams.controller.ts teams.module.ts teams.service.ts
users/4dto.ts users.controller.ts users.module.ts users.service.ts
webhooks/3webhooks.controller.ts webhooks.module.ts webhooks.service.ts
workspaces/4dto.ts workspaces.controller.ts workspaces.module.ts workspaces.service.ts
types/1xmldom.d.ts
app.module.ts constants.ts main.ts
_probe_rawbody.cjs .env.example Dockerfile package.json README.md tsconfig.json vitest.config.ts
realtime/33
src/17auth.ts comments.ts config.ts event-source.ts http-store.ts index.ts metrics.ts notifications.ts page-updated-source.ts presence.ts prisma-store.ts protocol.ts rate-limit.ts room-key.ts rooms.ts server.ts update-log.ts
test/11auth.test.ts config.test.ts event-source.test.ts http-store.test.ts metrics-merge.test.ts page-updated-source.test.ts presence.test.ts protocol.test.ts rate-limit.test.ts rooms.test.ts task-0094-builders.test.ts
.env.example Dockerfile package.json README.md tsconfig.json
web/255
src/244
app/45
__tests__/1theme-tokens.test.ts
(app)/26
w/25
[workspace]/25
ask/1page.tsx
databases/1
[id]/1page.tsx
favorites/1page.tsx
files/1page.tsx
notifications/1page.tsx
page/4
[id]/4
[block]/1page.tsx
share/1page.tsx
layout.tsx page.tsx
search/1page.tsx
settings/10
appearance/1page.tsx
automations/3
[id]/1page.tsx
new/1page.tsx
page.tsx
billing/1page.tsx
developers/1page.tsx
members/1page.tsx
permissions/1page.tsx
teams/1page.tsx
page.tsx
shared/1page.tsx
templates/1page.tsx
trash/1page.tsx
layout.tsx page.tsx
layout.tsx
(auth)/4
login/1page.tsx
register/1page.tsx
reset-password/1page.tsx
layout.tsx
api/2
metrics/1route.ts
v1/1
health/1route.ts
f/1
[slug]/1page.tsx
health/1
ready/1route.ts
org/2
[orgId]/2
admin/2
integrations/1page.tsx
page.tsx
share/1
[id]/1page.tsx
error.tsx global-error.tsx globals.css layout.tsx not-found.tsx page.tsx providers.tsx
components/118
admin/10
org-admin/8ApiKeysTab.tsx AuditTab.tsx MembersTab.tsx OverviewTab.tsx PoliciesTab.tsx RetentionTab.tsx WebhooksTab.tsx WorkspacePicker.tsx
ScimCard.tsx SsoTabs.tsx
ai/6ai-panel.tsx ask-ai-submenu.tsx db-ai-dialog.tsx generate-page-dialog.tsx writing-actions-menu.tsx writing-diff.tsx
automations/5action-editor.tsx condition-editor.tsx lib.ts run-history.tsx trigger-editor.tsx
collaboration/6CursorLayer.tsx OfflineBanner.tsx PresenceBar.tsx README.md ReviewFailedDialog.tsx StatusChip.tsx
comments/3comment-panel.tsx comment-thread.tsx mention-input.tsx
database/27
__tests__/2cell-editor.test.tsx cell-renderer.test.tsx
AiExtractDialog.tsx AiFormulaDialog.tsx AiGenerateDialog.tsx board-view.tsx buttons-panel.tsx calendar-view.tsx cell-editor.tsx cell-renderer.tsx chart-view.tsx database-page-view.tsx database-toolbar.tsx database-view-renderer.tsx db-embedded.tsx filter-builder.tsx form-builder-view.tsx formula-editor.tsx gallery-view.tsx group-builder.tsx list-view.tsx property-editor.tsx record-header.tsx record-panel.tsx sort-builder.tsx table-view.tsx timeline-view.tsx
dialogs/7
__tests__/1move-to-dialog.test.tsx
create-dialog.tsx dialog.tsx file-permissions-dialog.tsx invite-dialog.tsx move-to-dialog.tsx share-dialog.tsx
editor/19
__tests__/3code-block-text.test.ts code-block.test.tsx expandable.test.tsx
block-card.tsx block-renderer.tsx block-save-queue.ts block-toolbar.tsx context-menu.tsx expandable.tsx history-popover.tsx inline-editor.tsx insert-dialog.tsx page-canvas.tsx page-spinner.tsx page-view.tsx selection-toolbar.tsx slash-menu.tsx template-dialog.tsx type-menu.tsx
files/4FileBlock.tsx FileBrowser.tsx FilePicker.tsx PreviewModal.tsx
form/4form-field.tsx form-renderer.tsx form-responses-view.tsx form-view.tsx
layout/8
__tests__/1page-tree-icon.test.tsx
command-palette.tsx factory-logo.tsx page-tree-node.tsx sidebar.tsx theme-provider.tsx topbar.tsx workspace-shell.tsx
mentions/3
__tests__/1mention-model.test.ts
mention-model.ts MentionPopup.tsx
search/3ask-ai-search.tsx results-list.tsx search-dialog.tsx
settings/10
api-keys/4ApiKeyCreateDialog.tsx ApiKeyRevealDialog.tsx ApiKeyTable.tsx ScopesBadge.tsx
webhooks/5CurlExamples.tsx DeliveryInspector.tsx EventExplorer.tsx OverviewTab.tsx WebhooksTab.tsx
settings-shell.tsx
ui/3confirm-dialog.tsx form.tsx toaster.tsx
hooks/21
__tests__/4realtime-frame-handler.test.ts use-blocks.test.ts use-database-options.test.ts use-workspace.test.tsx
realtime-frame-handler.ts use-ai-stream.ts use-auth.ts use-blocks.ts use-database.ts use-db-ai-stream.ts use-forms.ts use-members.ts use-mention-sources.ts use-notifications.ts use-offline-status.ts use-pages.ts use-presence-announcer.ts use-presence-toasts.ts use-presence.ts use-realtime.ts use-workspace.ts
lib/50
__tests__/21ai-database.test.ts ai-generate.test.ts ai-sse.test.ts ai-writing.test.ts api-keys.test.ts api-members.test.ts auth-refresh.test.ts auth-store.test.ts batch-queue-drop.test.ts block-content.test.ts clipboard-import.test.ts code-highlight.test.ts offline-status.test.ts presence-announcer.test.ts presence-color.test.ts presence-cursor.test.ts presence-emitter.test.ts realtime-url.test.ts search.test.ts utils.test.ts writing-diff.test.ts
files/7
__tests__/2picker.test.ts upload.test.ts
api.ts picker.ts upload-store.ts upload.ts use-file-uploads.ts
ai-database.ts ai.ts api-keys.ts api.ts auth-store.ts batch-queue.ts block-content.ts clipboard-import.ts code-highlight.ts constants.ts offline-status.ts org-admin.ts presence-announcer.ts presence-color.ts presence-cursor.ts presence-editor-views.ts presence-emitter.ts query-client.ts realtime.ts search.ts types.ts utils.ts
stores/8
__tests__/1block-patch-store.test.ts
block-patch-store.ts comment-pill-store.ts database-store.ts editor-store.ts offlineQueue.ts presence-store.ts ui-store.ts
instrumentation.ts middleware.ts
.env.example Dockerfile next-env.d.ts next.config.mjs package.json postcss.config.mjs README.md tailwind.config.ts tsconfig.json tsconfig.tsbuildinfo vitest.config.ts
worker/92
src/87
__tests__/13ai-embedding.test.ts ai-request.test.ts ai-usage-budget.test.ts audit-export.test.ts email-decision.test.ts encrypt.test.ts health-search-index-lag.test.ts mime.test.ts retention.test.ts s3-sign.test.ts search-index.test.ts smtp.test.ts webhook-sign.test.ts
health/1server.ts
lib/12
__tests__/1clamav.test.ts
clamav.ts encrypt.ts job-otel.ts mime.ts notify.ts prisma.ts redis-stream.ts redis.ts s3.ts sharp.ts smtp.ts
processors/56
automations/28
__tests__/3condition-evaluator.spec.ts executors.spec.ts run-pipeline.spec.ts
executors/17assign-user.ts call-integration.ts change-status.ts classify.ts create-notification.ts create-page.ts email-cap.ts helpers.ts integration-registry.ts permission.ts record-write.ts registry.ts run-ai-action.ts send-email.ts send-webhook.ts types.ts update-record.ts
condition-evaluator.ts dead-letter.ts log.ts matcher.ts pipeline.ts run-state.ts schedule.ts watchdog.ts
files/15
__tests__/4errors.test.ts scan-gate.test.ts status.test.ts temp.test.ts
steps/4finalize.ts metadata.ts scan.ts thumbnails.ts
cleanup.ts errors.ts generateThumbs.ts processor.ts status.ts temp.ts types.ts
search-index/4builders.ts grants.ts index.ts processor.ts
ai-embedding.ts ai.processor.ts audit-export.ts automations.processor.ts email-decision.processor.ts email.processor.ts maintenance.processor.ts retention.ts webhooks.processor.ts
config.ts events.ts index.ts logger.ts queues.ts
.env.example Dockerfile package.json README.md tsconfig.json
docs/73
api/17ai.md authentication.md automations.md blocks.md comments.md databases.md files.md forms.md openapi.yaml org-admin.md pages.md scim.md search.md sso.md versioning.md webhooks.md workspaces.md
architecture/10ai-architecture.md automation.md backend-architecture.md database-architecture.md frontend-architecture.md realtime-architecture.md search-architecture.md security-architecture.md storage.md system-architecture.md
database/2database-architecture.md redis-keys.md
development/8admin-guide.md coding-standards.md deployment.md development-guide.md files-pipeline.md perf-report.md performance.md testing.md
product/4functional-requirements.md non-functional-requirements.md product-requirements.md ux-requirements.md
requirements/1requirements-matrix.md
security/5audit-report.md files.md forms.md license-allowlist.md security-architecture.md
tasks/16phase-00-foundation.md phase-01-authentication.md phase-02-workspace.md phase-03-pages.md phase-04-editor.md phase-05-databases.md phase-06-relations.md phase-07-collaboration.md phase-08-search.md phase-09-files.md phase-10-forms.md phase-11-automation.md phase-12-ai.md phase-13-api.md phase-14-enterprise.md phase-15-production.md
ui/9ai.md automations.md design-system.md developers.md files.md forms.md org-admin.md scim.md sso.md
support-matrix.md
infrastructure/71
caddy/1Caddyfile
docker/6
init/101-extensions.sql
opensearch/1init-indices.json
.env.docker docker-compose.yml opensearch-init.sh README.md
kubernetes/40
observability/20
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1sync-sources.mjs
alertmanager.yaml grafana.yaml kustomization.yaml namespace.yaml node-exporter.yaml otel-collector.yaml prometheus.yaml
postgres/7configmap.yaml cronjob-backup.yaml cronjob-opensearch-snapshot.yaml pgbouncer.yaml replica.yaml statefulset.yaml walg-iam-policy.yaml
api.yaml configmap.yaml ingress.yaml kustomization.yaml namespace.yaml networkpolicy.yaml pdb.yaml README.md realtime.yaml redis.yaml secret.example.yaml web.yaml worker.yaml
nginx/2nginx.conf README.md
observability/14
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1dashboard-completeness.mjs
README.md
terraform/8compute.tf data.tf main.tf network.tf outputs.tf README.md security.tf variables.tf
packages/289
ai/24
src/20
providers/7anthropic.ts google.ts http.ts index.ts openai-compatible.ts rag.test.ts writing.test.ts
schemas/2database.ts generate.ts
adapters.test.ts config.test.ts config.ts gateway.test.ts gateway.ts index.ts tools.test.ts tools.ts types.ts usage.test.ts usage.ts
package.json README.md tsconfig.json vitest.config.ts
auth/11
src/7index.ts passwords.test.ts passwords.ts sessions.test.ts sessions.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
collaboration/17
src/13awareness.ts codec.test.ts codec.ts index.ts lww.test.ts lww.ts mapping.ts offline.ts outbox.test.ts outbox.ts provider.test.ts provider.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
config/7
src/4eslint.base.js index.ts prettier.base.json tsconfig.base.json
package.json README.md tsconfig.json
database-client/5
src/2index.test.ts index.ts
package.json README.md tsconfig.json
database-engine/33
src/29
filter/2filter.test.ts index.ts
formula/8evaluator.ts formula.test.ts functions.ts index.ts lexer.ts parser.ts types.ts validate.ts
query/6group.ts index.ts paginate.ts pivot.ts query.test.ts sort.ts
relations/2index.ts relations.test.ts
rollups/2index.ts rollups.test.ts
values/5dates.ts index.ts registry.ts types.ts values.test.ts
views/3buttons.ts index.ts views.test.ts
index.ts
package.json README.md tsconfig.json vitest.config.ts
editor/25
src/21
marks/1highlight.ts
nodes/2equation.ts mention.ts
autosave.test.ts autosave.ts clipboard.test.ts clipboard.ts codec.test.ts codec.ts context-menu.test.ts context-menu.ts diff.test.ts diff.ts factory.ts history.test.ts history.ts index.ts positions.ts slash.test.ts slash.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
observability/20
src/16
__tests__/8db-redis-metrics.test.ts metrics.test.ts otel.test.ts pino-otel.test.ts prometheus.test.ts resource.test.ts scrubber.test.ts traceparent.test.ts
index.ts metrics.ts otel.ts pino-otel.ts prometheus.ts resource.ts scrubber.ts traceparent.ts
package.json README.md tsconfig.json vitest.config.ts
permissions/11
src/7index.ts levels.test.ts levels.ts resolve.test.ts resolve.ts rules.test.ts rules.ts
package.json README.md tsconfig.json vitest.config.ts
search/22
src/18chunking.test.ts chunking.ts client.test.ts client.ts embeddings.test.ts embeddings.ts hybrid.test.ts hybrid.ts index.ts indexer.test.ts indexer.ts mapping.test.ts mapping.ts queries.test.ts queries.ts rrf.test.ts rrf.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
shared/61
src/57
dto/17ai.dto.ts auth.dto.test.ts auth.dto.ts block.dto.ts browser-safety.test.ts comment.dto.ts database.dto.test.ts database.dto.ts file.dto.ts formula-ast.ts index.ts notification.dto.ts page.dto.ts search.dto.ts share.dto.ts webhook.dto.ts workspace.dto.ts
events/8audit-catalog.test.ts audit-catalog.ts audit-csv.test.ts audit-csv.ts audit-scope.test.ts audit-scope.ts catalog.test.ts catalog.ts
realtime/6comment-events.ts frames.test.ts frames.ts notification-policy.ts notification-prefs.ts task-0094.test.ts
automation.test.ts automation.ts constants.ts crypto.test.ts crypto.ts env.ts errors.ts events.test.ts events.ts forms.test.ts forms.ts index.ts markdown.test.ts markdown.ts mentions.test.ts mentions.ts net.test.ts net.ts positions.test.ts positions.ts presence.test.ts presence.ts security-headers.ts webhook-delivery.test.ts webhook-delivery.ts webhook-events.ts
package.json README.md tsconfig.json vitest.config.ts
storage/16
src/12client.test.ts client.ts index.ts keys.test.ts keys.ts mime.test.ts mime.ts presign.test.ts presign.ts thumbnails.test.ts thumbnails.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
ui/37
src/33
components/19avatar.tsx badge.tsx button.tsx checkbox.tsx command.tsx context-menu.tsx dialog.tsx dropdown-menu.tsx empty-state.tsx index.ts popover.tsx separator.tsx skeleton.tsx slider.tsx spinner.tsx switch.tsx tabs.tsx toast.tsx tooltip.tsx
color.ts command-filter.test.ts command-filter.ts focus.test.ts focus.ts formatters.test.ts formatters.ts index.ts theme.test.ts theme.ts toast-store.test.ts toast-store.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
prisma/16
migrations/13
20260927000000_init/1migration.sql
20260928000000_search_documents_content_hash/1migration.sql
20260929000000_workspace_usage_storage_bytes/1migration.sql
20260930000000_form_submitted_notification_type/1migration.sql
20261001000000_ai_phase12/1migration.sql
20261002000000_webhooks_secret_encrypted_dlq/1migration.sql
20261002111436_webhook_delivery_latency/1migration.sql
20261002120000_org_admin_2fa_retention_unique/1migration.sql
20261002215124_audit_export/1migration.sql
20261003210000_perf_indexes_hot_paths/1migration.sql
20261004004500_perf_indexes_hot_paths_2/1migration.sql
migration_lock.toml README.md
schema.prisma seed.ts verify.ts
scripts/7backup-health.ts docs-completeness.mjs generate-openapi.mjs minio-init.ts opensearch-reindex.ts storage-orphans.ts verify-clean-clone.sh
tests/169
e2e/63
.auth/1user.json
fixtures/7
files/6clip.mp4 clip.wav clip.webm hero-1400.png note.docx note.txt
realtime.ts
mocks/2oidc-issuer.ts saml-idp.ts
playwright-report/3
data/2a6557bae9c2714a658968d4eb8534195c08ebda8.png e3092d07d00a16063e46385c02a559a9054fa847.webm
index.html
reports/1files-security.json
specs/34ai-database.spec.ts ai-generate.spec.ts ai-rag.spec.ts ai-security.spec.ts ai-writing.spec.ts ai.spec.ts api-keys.spec.ts appearance-theme.spec.ts auth.spec.ts automation.spec.ts collab-two-tab.spec.ts comments.spec.ts databases.spec.ts default-workspace-alias.spec.ts developers.spec.ts editor.spec.ts files.spec.ts final-flow.spec.ts forms.spec.ts oauth.spec.ts org-admin.spec.ts pages.spec.ts performance.spec.ts presence-cursors.spec.ts qa-paste-toggle.spec.ts realtime.spec.ts scim.spec.ts search-index-lag.spec.ts search.spec.ts sharing.spec.ts sso.spec.ts task-0094-comments-notifications.spec.ts webhooks.spec.ts workspace.spec.ts
test-results-final-flow/3
final-flow-Release-v1-0-fi-6c06b-w-state-carries-sequential--chromium-desktop/2test-failed-1.png trace.zip
.last-run.json
utils/4api.ts dnd.ts search-lag.ts wait.ts
.gitignore fixtures.ts global-setup.ts package.json playwright.config.ts playwright.final-flow.config.ts README.md tsconfig.json
fixtures/1
files/1make-sparse.ts
integration/60
ai-capability/2ai-app.capability.test.ts gateway.capability.test.ts
drill/2seed-drill.mjs verify-restore.mjs
fixtures/5
files/5clip.mp4 clip.wav doc.pdf hero-1400.png note.txt
helpers/3api.ts containers.ts seed.ts
realtime/11control-channel.test.ts convergence.test.ts flush-rebase-http.test.ts handshake.test.ts heartbeat-limits.test.ts lifecycle.test.ts presence.test.ts rebase.test.ts resume.test.ts rt-helpers.ts task-0094-fanout.test.ts
ai-chat.integration.test.ts ai-database.integration.test.ts ai-generate.integration.test.ts ai-rag.integration.test.ts ai-writing.integration.test.ts api-keys.integration.test.ts audit-completeness.integration.test.ts audit-export-s3.integration.test.ts auth.integration.test.ts automation-pipeline.integration.test.ts automations.integration.test.ts blocks.integration.test.ts cache-invalidation.integration.test.ts comments.integration.test.ts databases.integration.test.ts files-lifecycle.integration.test.ts files-pipeline.integration.test.ts files-security.integration.test.ts files.integration.test.ts forms-responses.integration.test.ts forms.integration.test.ts oauth.integration.test.ts openapi.contract.test.ts org-admin.integration.test.ts package.json pages.integration.test.ts permissions.integration.test.ts README.md replica-routing.integration.test.ts retention-engine.integration.test.ts scim.integration.test.ts search.integration.test.ts sso.integration.test.ts tsconfig.json vitest.config.ts webhooks.integration.test.ts workspace.integration.test.ts
performance/28
fixtures/1search-100k.ts
helpers/2seed-100k.mjs seed-1k-load.mjs
reports/7api-load-smoke.json database-load-smoke.json editor-local.json realtime-load.json search-full.json search-last.json search-reduced.json
api-load.js api.js archive.tar database-load.js database.js editor-local.mjs extract-100k-env.mjs package.json README.md realtime-1k.js realtime-load.js realtime.js run-realtime-load.sh search-search.k6.js search-summary.json seed-100k.mjs threshold-breach-demo.js verify-zero-op-loss.mjs
security/16
helpers/1app.ts
audit-report.test.ts auth-paths.test.ts deps.test.ts formula-fuzz.test.ts gitleaks-config.test.ts headers.test.ts images.test.ts oracle.ts package.json permission-fuzz.test.ts rate-limits.test.ts README.md tsconfig.json upload-abuse.test.ts vitest.config.ts
README.md
.dockerignore .env .env.bak-task0125 .env.example .eslintrc.json .gitignore .npmrc .prettierignore .prettierrc.json AGENTS.md ARCHITECTURE.md CHANGELOG.md CONTRIBUTING.md gitleaks.toml LICENSE package.json pnpm-lock.yaml pnpm-workspace.yaml PRODUCT-SPEC.md README.md ROADMAP.md SECURITY.md tsconfig.json turbo.json
use-ai-stream.tsapps/web/src/hooks/use-ai-stream.tsraw← hooks15,347 B← Back to the app
1"use client";
2 
3/**
4 * TASK-0122 — Ask AI panel state (the streaming conversation hook).
5 *
6 * Owns, for ONE workspace: the conversation list, the active conversation's
7 * messages, the in-flight SSE stream, and the error banner state. The panel
8 * component is a thin view over this hook.
9 *
10 * Streaming model (mirrors docs/api/ai.md):
11 *   - `send(content)` → ensure a conversation (seeded with the entry point's
12 *     context) → optimistic user bubble → `sendAiMessage` frames append to a
13 *     live assistant bubble (`delta`), collect tool calls, then `done`
14 *     finalizes the rows (the server persisted them — the ids come back).
15 *   - `stop()` aborts the fetch; the server persists the partial
16 *     (`metadata.truncated`) and we refresh the rows on the next open.
17 *   - `retry()` re-sends the LAST user message with `retryOf` (no duplicate
18 *     user row) after a mid-stream / pre-stream error.
19 */
20import { useCallback, useEffect, useRef, useState } from "react";
21import {
22  aiApi,
23  sendAiMessage,
24  AiPreStreamError,
25  type AiContextRef,
26  type AiConversationSummary,
27  type AiMessage,
28  type AiSseFrame
29} from "@/lib/ai";
30 
31export interface AiBannerError {
32  code: string;
33  message: string;
34  retryable: boolean;
35  retryAfterSeconds?: number;
36}
37 
38interface AiTurn {
39  userMessageId?: string;
40  content: string;
41  toolCalls: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
42  usage?: { tokensIn: number; tokensOut: number };
43  done?: boolean;
44  assistantMessageId?: string;
45  truncated?: boolean;
46  error?: string;
47}
48 
49/** Optimistic row ids (never collide with server cuids). */
50let tmpSeq = 0;
51const tmpId = (prefix: string) => `${prefix}-tmp-${Date.now().toString(36)}-${(tmpSeq += 1)}`;
52 
53export function useAiStream(workspaceId: string | null, context: AiContextRef | null) {
54  const [conversations, setConversations] = useState<AiConversationSummary[]>([]);
55  const [convError, setConvError] = useState<string | null>(null);
56  const [activeId, setActiveId] = useState<string | null>(null);
57  const [messages, setMessages] = useState<AiMessage[]>([]);
58  const [messagesLoading, setMessagesLoading] = useState(false);
59  const [streaming, setStreaming] = useState(false);
60  const [banner, setBanner] = useState<AiBannerError | null>(null);
61  /** The in-flight assistant bubble (empty = none). */
62  const [live, setLive] = useState<AiTurn | null>(null);
63  /** The optimistic user bubble for the in-flight turn. */
64  const [pendingUser, setPendingUser] = useState<{ id: string; content: string } | null>(null);
65 
66  /**
67   * Synchronous source of truth for the in-flight turn. `setLive` is only the
68   * render trigger; every transition is computed from `liveRef` and the ref
69   * is updated in the SAME synchronous block (before the React state commit).
70   *
71   * Why a ref instead of reading the `live` state: the finalize runs right
72   * after `sendAiMessage` resolves — before React has re-rendered, so the
73   * `live` closure is stale. And the ref must NOT be updated inside a
74   * `setLive` *updater*: React runs updaters during render (asynchronously,
75   * twice under StrictMode), so a finalize that reads a ref written in an
76   * updater would see the pre-`done` value. That bug made Retry send the
77   * optimistic `tmpId("u")` as `retryOf` (server 404: "Message not found")
78   * because the server's real `userMessageId` (from the `done` frame) had
79   * never landed in the ref.
80   */
81  const liveRef = useRef<AiTurn | null>(null);
82 
83  const abortRef = useRef<AbortController | null>(null);
84  /** The last turn's text (+ its server row id once known) — the Retry
85   *  button re-sends THIS, whether or not the user row was persisted
86   *  (a pre-stream 429 on a fresh conversation has no row to retryOf). */
87  const lastSentRef = useRef<{ content: string; userMessageId?: string } | null>(null);
88  // Refs so `send`/`retry` (stable callbacks) see fresh context without
89  // re-creating on every keystroke of an unrelated input.
90  const contextRef = useRef(context);
91  contextRef.current = context;
92 
93  /* --------------------------------------------------------- conv list */
94 
95  const refreshConversations = useCallback(async () => {
96    if (!workspaceId) return;
97    try {
98      const list = await aiApi.listConversations(workspaceId);
99      setConversations(list);
100      setConvError(null);
101    } catch {
102      setConvError("Could not load conversations.");
103    }
104  }, [workspaceId]);
105 
106  useEffect(() => {
107    setConversations([]);
108    setActiveId(null);
109    setMessages([]);
110    setBanner(null);
111    if (workspaceId) void refreshConversations();
112  }, [workspaceId, refreshConversations]);
113 
114  /* -------------------------------------------------------- active conv */
115 
116  const openConversation = useCallback(
117    async (id: string) => {
118      if (streaming) stop();
119      setActiveId(id);
120      setMessages([]);
121      setBanner(null);
122      setMessagesLoading(true);
123      try {
124        const rows = await aiApi.listMessages(id);
125        setMessages(rows);
126      } catch {
127        setBanner({ code: "ERROR", message: "Could not load this conversation.", retryable: false });
128      } finally {
129        setMessagesLoading(false);
130      }
131    },
132    // eslint-disable-next-line react-hooks/exhaustive-deps
133    [streaming]
134  );
135 
136  const startNew = useCallback(() => {
137    if (streaming) stop();
138    setActiveId(null);
139    setMessages([]);
140    setBanner(null);
141    // eslint-disable-next-line react-hooks/exhaustive-deps
142  }, [streaming]);
143 
144  const removeConversation = useCallback(
145    async (id: string) => {
146      try {
147        await aiApi.deleteConversation(id);
148      } catch {
149        /* the row simply stays if the delete failed — non-fatal */
150      }
151      setConversations((list) => list.filter((c) => c.id !== id));
152      if (activeId === id) startNew();
153    },
154    [activeId, startNew]
155  );
156 
157  /** Inline rename (Enter/blur) — the server PATCHes the title. */
158  const renameConversation = useCallback(
159    async (id: string, title: string) => {
160      const t = title.trim();
161      if (!t) return;
162      try {
163        const updated = await aiApi.renameConversation(id, t);
164        setConversations((list) => list.map((c) => (c.id === id ? { ...c, title: updated.title ?? t } : c)));
165      } catch {
166        /* keep the old title; the row is untouched */
167      }
168    },
169    []
170  );
171 
172  /* -------------------------------------------------------------- send */
173 
174  const applyFrame = useCallback((frame: AiSseFrame) => {
175    // Transition from the ref (the synchronous truth) and commit BOTH the ref
176    // and the render state in the same synchronous block. `setLive(next)` is
177    // a plain value (not an updater), so StrictMode's double-invocation does
178    // not apply and there is no async-timing hazard.
179    const t = liveRef.current;
180    if (t) {
181      let next = t;
182      if (frame.type === "delta") next = { ...t, content: t.content + frame.text };
183      else if (frame.type === "tool_call")
184        next = { ...t, toolCalls: [...t.toolCalls, { id: frame.id, name: frame.name, arguments: frame.arguments }] };
185      else if (frame.type === "usage") next = { ...t, usage: { tokensIn: frame.tokensIn, tokensOut: frame.tokensOut } };
186      else if (frame.type === "done")
187        next = {
188          ...t,
189          done: true,
190          assistantMessageId: frame.assistantMessageId,
191          userMessageId: frame.userMessageId ?? t.userMessageId,
192          truncated: frame.reason === "aborted",
193          ...(frame.reason === "aborted" ? { error: "aborted" } : {})
194        };
195      if (next !== t) {
196        liveRef.current = next;
197        setLive(next);
198      }
199    }
200    if (frame.type === "error") {
201      setBanner({
202        code: frame.code,
203        message: frame.message,
204        retryable: frame.retryable === true,
205        ...(typeof frame.retryAfterSeconds === "number" ? { retryAfterSeconds: frame.retryAfterSeconds } : {})
206      });
207    }
208  }, []);
209 
210  /** Ensure the turn has a conversation (create on the fly, seeded context). */
211  const ensureConversation = useCallback(async (): Promise<string | null> => {
212    if (activeId) return activeId;
213    if (!workspaceId) return null;
214    const created = await aiApi.createConversation(workspaceId, {
215      ...(contextRef.current && Object.keys(contextRef.current).length > 0 ? { context: contextRef.current } : {})
216    });
217    setActiveId(created.id);
218    setConversations((list) => [created, ...list]);
219    return created.id;
220  }, [activeId, workspaceId]);
221 
222  const send = useCallback(
223    async (content: string) => {
224      const text = content.trim();
225      if (!text || streaming || !workspaceId) return;
226      setBanner(null);
227      const convId = await ensureConversation();
228      if (!convId) return;
229      const userRow = { id: tmpId("u"), content: text };
230      lastSentRef.current = { content: text };
231      setPendingUser(userRow);
232      liveRef.current = { userMessageId: userRow.id, content: "", toolCalls: [] };
233      setLive(liveRef.current);
234      setStreaming(true);
235      const ac = new AbortController();
236      abortRef.current = ac;
237      try {
238        const reason = await sendAiMessage(
239          { workspaceId, conversationId: convId, content: text, context: contextRef.current ?? undefined, signal: ac.signal },
240          applyFrame
241        );
242        // Finalize: `liveRef` is the synchronous truth (every transition is
243        // computed from the ref in `applyFrame`, so it already carries the
244        // server's real `userMessageId` from the `done` frame). Reading it
245        // here — not the stale `live` closure, and not a ref written inside a
246        // `setLive` updater (React runs updaters asynchronously during
247        // render) — is what lets Retry send the REAL row id as `retryOf`
248        // instead of the optimistic tmpId (which 404s server-side).
249        const t = liveRef.current;
250        if (t) {
251          if (t.userMessageId) lastSentRef.current = { content: text, userMessageId: t.userMessageId };
252          setMessages((rows) => [
253            ...rows,
254            {
255              id: t.userMessageId ?? userRow.id,
256              role: "user",
257              content: text,
258              createdAt: new Date().toISOString()
259            },
260            ...(t.content
261              ? [
262                  {
263                    id: t.assistantMessageId ?? tmpId("a"),
264                    role: "assistant" as const,
265                    content: t.content,
266                    model: null,
267                    tokensIn: t.usage?.tokensIn ?? null,
268                    tokensOut: t.usage?.tokensOut ?? null,
269                    createdAt: new Date().toISOString(),
270                    metadata: { feature: "chat", ...(t.truncated ? { truncated: true } : {}), ...(t.error ? { error: t.error } : {}) }
271                  }
272                ]
273              : [])
274          ]);
275        }
276        liveRef.current = null;
277        setLive(null);
278        void reason;
279      } catch (err) {
280        if (err instanceof AiPreStreamError) {
281          setBanner({
282            code: err.code,
283            message: err.message,
284            retryable: err.code === "RATE_LIMITED" || err.status >= 500,
285            ...(err.retryAfterSeconds !== undefined ? { retryAfterSeconds: err.retryAfterSeconds } : {})
286          });
287          // pre-stream: the user row was NOT persisted — re-queue the text
288          // on Retry (no retryOf — the server has no row to re-send).
289        } else if (!(err instanceof Error && err.name === "AbortError")) {
290          setBanner({ code: "ERROR", message: "Network error — the AI reply could not be completed.", retryable: true });
291        }
292      } finally {
293        setPendingUser(null);
294        setStreaming(false);
295        abortRef.current = null;
296        void refreshConversations(); // auto-title / message counts changed
297      }
298    },
299    [workspaceId, streaming, ensureConversation, applyFrame, refreshConversations]
300  );
301 
302  const stop = useCallback(() => {
303    abortRef.current?.abort();
304  }, []);
305 
306  /**
307   * Re-send the last turn after an error (the Retry button).
308   *
309   * `lastSentRef` carries the turn's text + the persisted user-row id (set
310   * when the `done` frame arrived). When the row exists (a mid-stream error)
311   * we send `retryOf` so the server re-runs WITHOUT duplicating the user
312   * message; when it does not (a pre-stream 402/429 on a fresh conversation)
313   * we send a plain new message — same text, new row.
314   */
315  const retry = useCallback(async () => {
316    if (streaming || !workspaceId || !activeId) return;
317    const last = lastSentRef.current;
318    if (!last) return;
319    setBanner(null);
320    liveRef.current = { userMessageId: last.userMessageId, content: "", toolCalls: [] };
321    setLive(liveRef.current);
322    setStreaming(true);
323    const ac = new AbortController();
324    abortRef.current = ac;
325    try {
326      await sendAiMessage(
327        {
328          workspaceId,
329          conversationId: activeId,
330          content: last.content,
331          context: contextRef.current ?? undefined,
332          ...(last.userMessageId ? { retryOf: last.userMessageId } : {}),
333          signal: ac.signal
334        },
335        applyFrame
336      );
337      // Finalize outside the updater (same StrictMode reason as send). The
338      // user row already lives in `messages` (the original send added it, or
339      // it came from the server) — retry appends ONLY the new assistant row;
340      // with `retryOf` the server did not create a second user row either.
341      const t = liveRef.current;
342      if (t && t.content && !ac.signal.aborted) {
343        if (t.userMessageId) lastSentRef.current = { content: last.content, userMessageId: t.userMessageId };
344        setMessages((rows) => [
345          ...rows,
346          {
347            id: t.assistantMessageId ?? tmpId("a"),
348            role: "assistant",
349            content: t.content,
350            model: null,
351            tokensIn: t.usage?.tokensIn ?? null,
352            tokensOut: t.usage?.tokensOut ?? null,
353            createdAt: new Date().toISOString(),
354            metadata: { feature: "chat", ...(t.truncated ? { truncated: true } : {}) }
355          }
356        ]);
357      }
358      liveRef.current = null;
359      setLive(null);
360    } catch (err) {
361      if (err instanceof AiPreStreamError) {
362        setBanner({
363          code: err.code,
364          message: err.message,
365          retryable: err.code === "RATE_LIMITED" || err.status >= 500,
366          ...(err.retryAfterSeconds !== undefined ? { retryAfterSeconds: err.retryAfterSeconds } : {})
367        });
368      } else if (!(err instanceof Error && err.name === "AbortError")) {
369        setBanner({ code: "ERROR", message: "Network error — the AI reply could not be completed.", retryable: true });
370      }
371    } finally {
372      setStreaming(false);
373      abortRef.current = null;
374      void refreshConversations();
375    }
376  }, [streaming, workspaceId, activeId, applyFrame, refreshConversations]);
377 
378  // Aborting on unmount (panel close) → the server persists the partial.
379  useEffect(() => () => abortRef.current?.abort(), []);
380 
381  return {
382    conversations,
383    convError,
384    activeId,
385    messages,
386    messagesLoading,
387    streaming,
388    banner,
389    live,
390    pendingUser,
391    send,
392    stop,
393    retry,
394    openConversation,
395    startNew,
396    removeConversation,
397    renameConversation,
398    refreshConversations
399  };
400}
401