@vellumai/assistant 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -35,10 +35,17 @@ const slackFileMetadataSchema = z.object({
|
|
|
35
35
|
export const slackMessageMetadataSchema = z.object({
|
|
36
36
|
source: z.literal("slack"),
|
|
37
37
|
channelId: z.string(),
|
|
38
|
+
channelName: z.string().optional(),
|
|
38
39
|
channelTs: z.string(),
|
|
39
40
|
threadTs: z.string().optional(),
|
|
40
41
|
displayName: z.string().optional(),
|
|
41
42
|
actorExternalUserId: z.string().optional(),
|
|
43
|
+
actorTimezone: z.string().optional(),
|
|
44
|
+
actorTimezoneLabel: z.string().optional(),
|
|
45
|
+
actorTimezoneOffsetSeconds: z.number().optional(),
|
|
46
|
+
timestampTimezone: z.string().optional(),
|
|
47
|
+
timestampTimezoneLabel: z.string().optional(),
|
|
48
|
+
speakerTimezoneLabel: z.string().optional(),
|
|
42
49
|
eventKind: z.enum(["message", "reaction"]),
|
|
43
50
|
reaction: slackReactionMetadataSchema.optional(),
|
|
44
51
|
editedAt: z.number().optional(),
|
|
@@ -50,6 +57,155 @@ export type SlackReactionMetadata = z.infer<typeof slackReactionMetadataSchema>;
|
|
|
50
57
|
export type SlackFileMetadata = z.infer<typeof slackFileMetadataSchema>;
|
|
51
58
|
export type SlackMessageMetadata = z.infer<typeof slackMessageMetadataSchema>;
|
|
52
59
|
|
|
60
|
+
type SlackTimezoneMetadata = Pick<
|
|
61
|
+
SlackMessageMetadata,
|
|
62
|
+
| "actorTimezone"
|
|
63
|
+
| "actorTimezoneLabel"
|
|
64
|
+
| "actorTimezoneOffsetSeconds"
|
|
65
|
+
| "timestampTimezone"
|
|
66
|
+
| "timestampTimezoneLabel"
|
|
67
|
+
| "speakerTimezoneLabel"
|
|
68
|
+
>;
|
|
69
|
+
type SlackTimezoneMetadataInput = Omit<
|
|
70
|
+
Partial<SlackTimezoneMetadata>,
|
|
71
|
+
"actorTimezoneOffsetSeconds"
|
|
72
|
+
> & {
|
|
73
|
+
actorTimezoneOffsetSeconds?: unknown;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const COMMON_SLACK_TIMEZONE_LABEL_BY_IANA = new Map<string, string>([
|
|
77
|
+
["America/New_York", "ET"],
|
|
78
|
+
["America/Detroit", "ET"],
|
|
79
|
+
["America/Indiana/Indianapolis", "ET"],
|
|
80
|
+
["America/Kentucky/Louisville", "ET"],
|
|
81
|
+
["America/Toronto", "ET"],
|
|
82
|
+
["America/Montreal", "ET"],
|
|
83
|
+
["America/Chicago", "CT"],
|
|
84
|
+
["America/Winnipeg", "CT"],
|
|
85
|
+
["America/Mexico_City", "CT"],
|
|
86
|
+
["America/Denver", "MT"],
|
|
87
|
+
["America/Boise", "MT"],
|
|
88
|
+
["America/Phoenix", "MT"],
|
|
89
|
+
["America/Edmonton", "MT"],
|
|
90
|
+
["America/Los_Angeles", "PT"],
|
|
91
|
+
["America/Vancouver", "PT"],
|
|
92
|
+
["America/Tijuana", "PT"],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const COMPACT_SLACK_TIMEZONE_LABEL_BY_NAME = new Map<string, string>([
|
|
96
|
+
["EASTERN TIME", "ET"],
|
|
97
|
+
["EASTERN STANDARD TIME", "ET"],
|
|
98
|
+
["EASTERN DAYLIGHT TIME", "ET"],
|
|
99
|
+
["EST", "ET"],
|
|
100
|
+
["EDT", "ET"],
|
|
101
|
+
["CENTRAL TIME", "CT"],
|
|
102
|
+
["CENTRAL STANDARD TIME", "CT"],
|
|
103
|
+
["CENTRAL DAYLIGHT TIME", "CT"],
|
|
104
|
+
["CST", "CT"],
|
|
105
|
+
["CDT", "CT"],
|
|
106
|
+
["MOUNTAIN TIME", "MT"],
|
|
107
|
+
["MOUNTAIN STANDARD TIME", "MT"],
|
|
108
|
+
["MOUNTAIN DAYLIGHT TIME", "MT"],
|
|
109
|
+
["MST", "MT"],
|
|
110
|
+
["MDT", "MT"],
|
|
111
|
+
["PACIFIC TIME", "PT"],
|
|
112
|
+
["PACIFIC STANDARD TIME", "PT"],
|
|
113
|
+
["PACIFIC DAYLIGHT TIME", "PT"],
|
|
114
|
+
["PST", "PT"],
|
|
115
|
+
["PDT", "PT"],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const slackShortTimeZoneFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
119
|
+
|
|
120
|
+
function compactStoredSlackTimezoneLabel(
|
|
121
|
+
label: string | null | undefined,
|
|
122
|
+
): string | null {
|
|
123
|
+
const trimmed = label?.trim();
|
|
124
|
+
if (!trimmed) return null;
|
|
125
|
+
return (
|
|
126
|
+
COMPACT_SLACK_TIMEZONE_LABEL_BY_NAME.get(trimmed.toUpperCase()) ?? trimmed
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getSlackShortTimeZoneFormatter(timeZone: string): Intl.DateTimeFormat {
|
|
131
|
+
let formatter = slackShortTimeZoneFormatters.get(timeZone);
|
|
132
|
+
if (!formatter) {
|
|
133
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
134
|
+
timeZone,
|
|
135
|
+
timeZoneName: "short",
|
|
136
|
+
});
|
|
137
|
+
slackShortTimeZoneFormatters.set(timeZone, formatter);
|
|
138
|
+
}
|
|
139
|
+
return formatter;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractSlackShortTimeZoneName(
|
|
143
|
+
timeZone: string,
|
|
144
|
+
nowMs: number,
|
|
145
|
+
): string | null {
|
|
146
|
+
try {
|
|
147
|
+
const part = getSlackShortTimeZoneFormatter(timeZone)
|
|
148
|
+
.formatToParts(new Date(nowMs))
|
|
149
|
+
.find((p) => p.type === "timeZoneName");
|
|
150
|
+
return part?.value ?? null;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatSlackTimezoneLabel(
|
|
157
|
+
timeZone: string | null | undefined,
|
|
158
|
+
opts: { persistedLabel?: string | null; nowMs?: number } = {},
|
|
159
|
+
): string | null {
|
|
160
|
+
const persisted = compactStoredSlackTimezoneLabel(opts.persistedLabel);
|
|
161
|
+
if (persisted) return persisted;
|
|
162
|
+
|
|
163
|
+
const trimmedTimezone = timeZone?.trim();
|
|
164
|
+
if (!trimmedTimezone) return null;
|
|
165
|
+
const mapped = COMMON_SLACK_TIMEZONE_LABEL_BY_IANA.get(trimmedTimezone);
|
|
166
|
+
if (mapped) return mapped;
|
|
167
|
+
return (
|
|
168
|
+
extractSlackShortTimeZoneName(trimmedTimezone, opts.nowMs ?? Date.now()) ??
|
|
169
|
+
trimmedTimezone
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function trimmedOptionalString(
|
|
174
|
+
value: string | null | undefined,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
const trimmed = value?.trim();
|
|
177
|
+
return trimmed ? trimmed : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildSlackTimezoneMetadata(
|
|
181
|
+
input: SlackTimezoneMetadataInput,
|
|
182
|
+
): Partial<SlackTimezoneMetadata> {
|
|
183
|
+
const actorTimezone = trimmedOptionalString(input.actorTimezone);
|
|
184
|
+
const actorTimezoneLabel = trimmedOptionalString(input.actorTimezoneLabel);
|
|
185
|
+
const timestampTimezone = trimmedOptionalString(input.timestampTimezone);
|
|
186
|
+
const timestampTimezoneLabel = trimmedOptionalString(
|
|
187
|
+
input.timestampTimezoneLabel,
|
|
188
|
+
);
|
|
189
|
+
const speakerTimezoneLabel = trimmedOptionalString(
|
|
190
|
+
input.speakerTimezoneLabel,
|
|
191
|
+
);
|
|
192
|
+
const actorTimezoneOffsetSeconds =
|
|
193
|
+
typeof input.actorTimezoneOffsetSeconds === "number" &&
|
|
194
|
+
Number.isFinite(input.actorTimezoneOffsetSeconds)
|
|
195
|
+
? input.actorTimezoneOffsetSeconds
|
|
196
|
+
: undefined;
|
|
197
|
+
return {
|
|
198
|
+
...(actorTimezone ? { actorTimezone } : {}),
|
|
199
|
+
...(actorTimezoneLabel ? { actorTimezoneLabel } : {}),
|
|
200
|
+
...(actorTimezoneOffsetSeconds !== undefined
|
|
201
|
+
? { actorTimezoneOffsetSeconds }
|
|
202
|
+
: {}),
|
|
203
|
+
...(timestampTimezone ? { timestampTimezone } : {}),
|
|
204
|
+
...(timestampTimezoneLabel ? { timestampTimezoneLabel } : {}),
|
|
205
|
+
...(speakerTimezoneLabel ? { speakerTimezoneLabel } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
53
209
|
/**
|
|
54
210
|
* Parse a JSON string into `SlackMessageMetadata`. Returns `null` on parse
|
|
55
211
|
* error, on non-object payloads, when `source !== "slack"`, or when any
|
|
@@ -12,7 +12,6 @@ import { describe, expect, test } from "bun:test";
|
|
|
12
12
|
import type { Message } from "../../../providers/types.js";
|
|
13
13
|
import {
|
|
14
14
|
extractTagLineTexts,
|
|
15
|
-
isReactionTagLine,
|
|
16
15
|
parentAlias,
|
|
17
16
|
type RenderableSlackMessage,
|
|
18
17
|
renderSlackTranscript,
|
|
@@ -114,19 +113,108 @@ describe("renderSlackTranscript — basics", () => {
|
|
|
114
113
|
expect(renderSlackTranscript([])).toEqual([]);
|
|
115
114
|
});
|
|
116
115
|
|
|
116
|
+
test("renders timezone-aware rows with compact local timestamps and speaker suffixes", () => {
|
|
117
|
+
const out = renderSlackTranscript([
|
|
118
|
+
{
|
|
119
|
+
...userMsg("1772681640.000001", "aaron", "hey there"),
|
|
120
|
+
metadata: {
|
|
121
|
+
source: "slack",
|
|
122
|
+
channelId: CHANNEL,
|
|
123
|
+
channelTs: "1772681640.000001",
|
|
124
|
+
eventKind: "message",
|
|
125
|
+
displayName: "aaron",
|
|
126
|
+
actorTimezone: "America/Denver",
|
|
127
|
+
actorTimezoneLabel: "MT",
|
|
128
|
+
timestampTimezone: "America/Denver",
|
|
129
|
+
timestampTimezoneLabel: "MT",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
...userMsg("1772681760.000002", "jordan", "whatsup"),
|
|
134
|
+
metadata: {
|
|
135
|
+
source: "slack",
|
|
136
|
+
channelId: CHANNEL,
|
|
137
|
+
channelTs: "1772681760.000002",
|
|
138
|
+
eventKind: "message",
|
|
139
|
+
displayName: "jordan",
|
|
140
|
+
actorTimezone: "America/New_York",
|
|
141
|
+
actorTimezoneLabel: "Eastern Time",
|
|
142
|
+
timestampTimezone: "America/Denver",
|
|
143
|
+
timestampTimezoneLabel: "MT",
|
|
144
|
+
speakerTimezoneLabel: "Eastern Time",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
...userMsg(
|
|
149
|
+
"1772681820.000003",
|
|
150
|
+
"aaron",
|
|
151
|
+
"nm, i wonder how my assistant is doing",
|
|
152
|
+
),
|
|
153
|
+
metadata: {
|
|
154
|
+
source: "slack",
|
|
155
|
+
channelId: CHANNEL,
|
|
156
|
+
channelTs: "1772681820.000003",
|
|
157
|
+
eventKind: "message",
|
|
158
|
+
displayName: "aaron",
|
|
159
|
+
actorTimezone: "America/Denver",
|
|
160
|
+
actorTimezoneLabel: "MT",
|
|
161
|
+
timestampTimezone: "America/Denver",
|
|
162
|
+
timestampTimezoneLabel: "MT",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
...userMsg("1772681880.000004", null, "i'm good", {
|
|
167
|
+
role: "assistant",
|
|
168
|
+
}),
|
|
169
|
+
metadata: {
|
|
170
|
+
source: "slack",
|
|
171
|
+
channelId: CHANNEL,
|
|
172
|
+
channelTs: "1772681880.000004",
|
|
173
|
+
eventKind: "message",
|
|
174
|
+
timestampTimezone: "America/Denver",
|
|
175
|
+
timestampTimezoneLabel: "Mountain Time",
|
|
176
|
+
speakerTimezoneLabel: "ET",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
...userMsg("1772681940.000005", "jordan", "ayeeeee"),
|
|
181
|
+
metadata: {
|
|
182
|
+
source: "slack",
|
|
183
|
+
channelId: CHANNEL,
|
|
184
|
+
channelTs: "1772681940.000005",
|
|
185
|
+
eventKind: "message",
|
|
186
|
+
displayName: "jordan",
|
|
187
|
+
actorTimezone: "America/New_York",
|
|
188
|
+
actorTimezoneLabel: "ET",
|
|
189
|
+
timestampTimezone: "America/Denver",
|
|
190
|
+
timestampTimezoneLabel: "MT",
|
|
191
|
+
speakerTimezoneLabel: "ET",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect(out).toEqual([
|
|
197
|
+
textMsg("user", "[mar 4 2026 8:34 PM MT aaron] hey there"),
|
|
198
|
+
textMsg("user", "[mar 4 2026 8:36 PM MT jordan (ET)] whatsup"),
|
|
199
|
+
textMsg(
|
|
200
|
+
"user",
|
|
201
|
+
"[mar 4 2026 8:37 PM MT aaron] nm, i wonder how my assistant is doing",
|
|
202
|
+
),
|
|
203
|
+
textMsg("assistant", "[mar 4 2026 8:38 PM MT assistant] i'm good"),
|
|
204
|
+
textMsg("user", "[mar 4 2026 8:39 PM MT jordan (ET)] ayeeeee"),
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
117
208
|
test("renders top-level message with MM/DD/YY HH:MM tag", () => {
|
|
118
209
|
const out = renderSlackTranscript([userMsg(TS_14_25, "@alice", "hi")]);
|
|
119
210
|
expect(out).toEqual([textMsg("user", "[11/14/23 14:25 @alice]: hi")]);
|
|
120
211
|
});
|
|
121
212
|
|
|
122
|
-
test("renders thread reply
|
|
213
|
+
test("renders thread reply without parent alias arrow", () => {
|
|
123
214
|
const out = renderSlackTranscript([
|
|
124
215
|
userMsg(TS_14_28, "@bob", "got it", { threadTs: TS_14_25 }),
|
|
125
216
|
]);
|
|
126
|
-
|
|
127
|
-
expect(out).toEqual([
|
|
128
|
-
textMsg("user", `[11/14/23 14:28 @bob → ${alias}]: got it`),
|
|
129
|
-
]);
|
|
217
|
+
expect(out).toEqual([textMsg("user", "[11/14/23 14:28 @bob]: got it")]);
|
|
130
218
|
});
|
|
131
219
|
|
|
132
220
|
test("renders edited message with editedAt suffix", () => {
|
|
@@ -153,18 +241,17 @@ describe("renderSlackTranscript — basics", () => {
|
|
|
153
241
|
]);
|
|
154
242
|
});
|
|
155
243
|
|
|
156
|
-
test("edited message in a thread renders
|
|
244
|
+
test("edited message in a thread renders edit suffix without thread arrow", () => {
|
|
157
245
|
const out = renderSlackTranscript([
|
|
158
246
|
userMsg(TS_14_28, "@bob", "got it (edit)", {
|
|
159
247
|
threadTs: TS_14_25,
|
|
160
248
|
editedAt: MS_14_30,
|
|
161
249
|
}),
|
|
162
250
|
]);
|
|
163
|
-
const alias = parentAlias(TS_14_25);
|
|
164
251
|
expect(out).toEqual([
|
|
165
252
|
textMsg(
|
|
166
253
|
"user",
|
|
167
|
-
|
|
254
|
+
"[11/14/23 14:28 @bob, edited 11/14/23 14:30]: got it (edit)",
|
|
168
255
|
),
|
|
169
256
|
]);
|
|
170
257
|
});
|
|
@@ -292,10 +379,9 @@ describe("renderSlackTranscript — basics", () => {
|
|
|
292
379
|
TS_14_25,
|
|
293
380
|
TS_14_28,
|
|
294
381
|
]);
|
|
295
|
-
expect(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
]);
|
|
382
|
+
expect(
|
|
383
|
+
out.renderedMessages.map((entry) => entry.tagLineProvenance),
|
|
384
|
+
).toEqual(["none", "none"]);
|
|
299
385
|
});
|
|
300
386
|
|
|
301
387
|
test("omits sender label for user-role message with null senderLabel (no displayName)", () => {
|
|
@@ -575,50 +661,6 @@ describe("parentAlias", () => {
|
|
|
575
661
|
});
|
|
576
662
|
});
|
|
577
663
|
|
|
578
|
-
// ── isReactionTagLine ────────────────────────────────────────────────────────
|
|
579
|
-
|
|
580
|
-
describe("isReactionTagLine", () => {
|
|
581
|
-
// Pinned to the exact shapes `renderReaction` and the overflow trailer
|
|
582
|
-
// produce. The helper is the public contract that lets consumers
|
|
583
|
-
// re-label the transcript without double-attributing reaction lines,
|
|
584
|
-
// so drift here silently breaks `buildActiveThreadBlockFromRenderable`.
|
|
585
|
-
const alias = parentAlias("1700000000.000100");
|
|
586
|
-
|
|
587
|
-
test("matches reaction-add line", () => {
|
|
588
|
-
expect(
|
|
589
|
-
isReactionTagLine(`[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
|
|
590
|
-
).toBe(true);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test("matches reaction-remove line", () => {
|
|
594
|
-
expect(
|
|
595
|
-
isReactionTagLine(`[11/14/23 14:28 @bob removed 👍 from ${alias}]`),
|
|
596
|
-
).toBe(true);
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
test("matches overflow trailer line", () => {
|
|
600
|
-
expect(isReactionTagLine(`[…and 2 more reactions to ${alias}]`)).toBe(true);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
test("does not match a regular message tag line", () => {
|
|
604
|
-
expect(isReactionTagLine("[11/14/23 14:25 @alice]: hi")).toBe(false);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("does not match content-only assistant output", () => {
|
|
608
|
-
expect(isReactionTagLine("on it. here's the answer")).toBe(false);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
test("does not match the `[deleted]` sentinel", () => {
|
|
612
|
-
expect(isReactionTagLine("[deleted]")).toBe(false);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
test("does not match a user-deleted marker", () => {
|
|
616
|
-
expect(
|
|
617
|
-
isReactionTagLine("[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
|
|
618
|
-
).toBe(false);
|
|
619
|
-
});
|
|
620
|
-
});
|
|
621
|
-
|
|
622
664
|
// ── reaction cap ─────────────────────────────────────────────────────────────
|
|
623
665
|
|
|
624
666
|
describe("renderSlackTranscript — reaction cap", () => {
|
|
@@ -865,13 +907,12 @@ describe("renderSlackTranscript — four design-brief scenarios", () => {
|
|
|
865
907
|
userMsg(replyTs, "@dan", "I'll join", { threadTs: aliceTopTs }),
|
|
866
908
|
];
|
|
867
909
|
const out = renderSlackTranscript(messages);
|
|
868
|
-
const aliceAlias = parentAlias(aliceTopTs);
|
|
869
910
|
expect(extractTagLineTexts(out)).toEqual([
|
|
870
911
|
"[11/14/23 14:25 @alice]: lunch?",
|
|
871
|
-
|
|
872
|
-
|
|
912
|
+
"[11/14/23 14:26 @bob]: yes!",
|
|
913
|
+
"[11/14/23 14:27 @alice]: 12:30 ok?",
|
|
873
914
|
"[11/14/23 14:28 @carol]: standup soon",
|
|
874
|
-
|
|
915
|
+
"[11/14/23 14:28 @dan]: I'll join",
|
|
875
916
|
]);
|
|
876
917
|
});
|
|
877
918
|
|
|
@@ -883,12 +924,8 @@ describe("renderSlackTranscript — four design-brief scenarios", () => {
|
|
|
883
924
|
userMsg(replyTs, "@ed", "joining now", { threadTs: carolTopTs }),
|
|
884
925
|
];
|
|
885
926
|
const out = renderSlackTranscript(messages);
|
|
886
|
-
const carolAlias = parentAlias(carolTopTs);
|
|
887
927
|
const texts = extractTagLineTexts(out);
|
|
888
|
-
|
|
889
|
-
expect(texts[texts.length - 1]).toBe(
|
|
890
|
-
`[11/14/23 14:28 @ed → ${carolAlias}]: joining now`,
|
|
891
|
-
);
|
|
928
|
+
expect(texts[texts.length - 1]).toBe("[11/14/23 14:28 @ed]: joining now");
|
|
892
929
|
expect(texts[3]).toBe("[11/14/23 14:28 @carol]: standup soon");
|
|
893
930
|
});
|
|
894
931
|
|
|
@@ -900,11 +937,8 @@ describe("renderSlackTranscript — four design-brief scenarios", () => {
|
|
|
900
937
|
userMsg(replyTs, "@frank", "+1", { threadTs: carolTopTs }),
|
|
901
938
|
];
|
|
902
939
|
const out = renderSlackTranscript(messages);
|
|
903
|
-
const carolAlias = parentAlias(carolTopTs);
|
|
904
940
|
const texts = extractTagLineTexts(out);
|
|
905
|
-
expect(texts[texts.length - 1]).toBe(
|
|
906
|
-
`[11/14/23 14:28 @frank → ${carolAlias}]: +1`,
|
|
907
|
-
);
|
|
941
|
+
expect(texts[texts.length - 1]).toBe("[11/14/23 14:28 @frank]: +1");
|
|
908
942
|
});
|
|
909
943
|
|
|
910
944
|
test("scenario: new top-level message (no threadTs)", () => {
|
|
@@ -924,7 +958,7 @@ describe("renderSlackTranscript — four design-brief scenarios", () => {
|
|
|
924
958
|
// ── mixed legacy + post-upgrade fixture ──────────────────────────────────────
|
|
925
959
|
|
|
926
960
|
describe("renderSlackTranscript — mixed legacy + post-upgrade", () => {
|
|
927
|
-
test("legacy rows
|
|
961
|
+
test("legacy rows intermix chronologically with post-upgrade rows", () => {
|
|
928
962
|
const messages: RenderableSlackMessage[] = [
|
|
929
963
|
// Post-upgrade: 14:28 reply in alice's thread
|
|
930
964
|
userMsg("1699972080.000900", "@bob", "yes!", { threadTs: TS_14_25 }),
|
|
@@ -935,16 +969,14 @@ describe("renderSlackTranscript — mixed legacy + post-upgrade", () => {
|
|
|
935
969
|
userMsg(TS_14_25, "@alice", "lunch?"),
|
|
936
970
|
];
|
|
937
971
|
const out = renderSlackTranscript(messages);
|
|
938
|
-
const alias = parentAlias(TS_14_25);
|
|
939
972
|
|
|
940
973
|
const texts = extractTagLineTexts(out);
|
|
941
974
|
expect(texts).toEqual([
|
|
942
975
|
"[11/14/23 14:25 @alice]: lunch?",
|
|
943
976
|
"[11/14/23 14:26 @dana]: drive-by note",
|
|
944
|
-
|
|
977
|
+
"[11/14/23 14:28 @bob]: yes!",
|
|
945
978
|
]);
|
|
946
|
-
|
|
947
|
-
expect(texts[1].includes("→")).toBe(false);
|
|
979
|
+
expect(texts.every((text) => !text.includes("→"))).toBe(true);
|
|
948
980
|
});
|
|
949
981
|
|
|
950
982
|
test("legacy assistant row carries assistant role and emits content verbatim", () => {
|