@agent-native/core 0.164.4 → 0.164.11
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
- package/corpus/templates/calendar/app/i18n-data.ts +120 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +12 -0
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +18 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +114 -20
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +83 -78
- package/corpus/templates/clips/changelog/2026-08-19-clips-nightly-builds-are-available-for-trying-the-latest-upd.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +1 -0
- package/corpus/templates/clips/desktop/README.md +19 -4
- package/corpus/templates/clips/desktop/src/app.tsx +78 -16
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -2
- package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -0
- package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
- package/corpus/templates/clips/server/routes/api/clips-updater.json.get.ts +41 -22
- package/corpus/templates/factory/AGENTS.md +1 -1
- package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
- package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
- package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
- package/corpus/templates/factory/app/routes/factory.tsx +209 -46
- package/corpus/templates/factory/server/connectors/slack.ts +44 -2
- package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
- package/corpus/templates/mail/actions/view-screen.ts +1 -1
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
- package/corpus/templates/mail/server/handlers/google-auth.ts +125 -24
- package/corpus/templates/mail/server/lib/gmail-query.ts +5 -2
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +0 -12
- package/corpus/templates/plan/changelog/2026-08-19-plan-prompts-now-guide-you-to-connect-ai-before-sending-and-.md +6 -0
- package/corpus/templates/slides/actions/approve-deck-access-request.ts +234 -0
- package/corpus/templates/slides/actions/get-deck-access-status.ts +35 -2
- package/corpus/templates/slides/actions/request-deck-access.ts +263 -64
- package/corpus/templates/slides/app/components/editor/MissingDeckAccessPane.tsx +196 -0
- package/corpus/templates/slides/app/components/editor/RequestDeckAccessDialog.tsx +101 -0
- package/corpus/templates/slides/app/hooks/use-deck-access.ts +7 -4
- package/corpus/templates/slides/app/i18n/en-US.ts +29 -6
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +147 -169
- package/corpus/templates/slides/app/routes/access-request.approve.tsx +250 -0
- package/corpus/templates/slides/server/db/schema.ts +8 -0
- package/corpus/templates/slides/server/lib/access-request-email.ts +36 -0
- package/corpus/templates/slides/server/lib/emails.ts +26 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +7 -0
- package/corpus/templates/slides/server/plugins/db.ts +50 -0
- package/corpus/templates/slides/shared/deck-access.ts +31 -0
- package/dist/a2a/task-store.d.ts +1 -0
- package/dist/a2a/task-store.js +1 -1
- package/dist/agent/observational-memory/store.d.ts +1 -0
- package/dist/agent/observational-memory/store.js +1 -1
- package/dist/agent/run-store.d.ts +1 -0
- package/dist/agent/run-store.js +1 -1
- package/dist/agent/tool-approval-store.d.ts +1 -0
- package/dist/agent/tool-approval-store.js +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.js +1 -1
- package/dist/browser-sessions/store.d.ts +1 -0
- package/dist/browser-sessions/store.js +1 -1
- package/dist/checkpoints/store.d.ts +1 -0
- package/dist/checkpoints/store.js +1 -1
- package/dist/cli/code-agent-schedules.d.ts +11 -0
- package/dist/cli/code-agent-schedules.js +44 -5
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AssistantChat.d.ts +5 -1
- package/dist/client/AssistantChat.js +40 -6
- package/dist/client/EnvironmentBadge.d.ts +7 -13
- package/dist/client/EnvironmentBadge.js +43 -92
- package/dist/client/agent-chat/index.d.ts +1 -0
- package/dist/client/agent-chat/index.js +1 -0
- package/dist/client/agent-chat-adapter.js +76 -50
- package/dist/client/app-providers.js +4 -4
- package/dist/client/chat/message-components.d.ts +4 -0
- package/dist/client/chat/message-components.js +28 -1
- package/dist/client/chat/run-recovery.js +26 -0
- package/dist/client/chat-first/apps-rail.d.ts +1 -0
- package/dist/client/chat-first/apps-rail.js +28 -2
- package/dist/client/sse-event-processor.js +13 -8
- package/dist/client/use-active-agent-chat-run.d.ts +2 -0
- package/dist/client/use-active-agent-chat-run.js +22 -0
- package/dist/coding-tools/sandbox/executions-store.d.ts +1 -0
- package/dist/coding-tools/sandbox/executions-store.js +1 -1
- package/dist/collab/awareness-store.d.ts +1 -0
- package/dist/collab/awareness-store.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts +1 -0
- package/dist/collab/storage.js +1 -1
- package/dist/db/client.js +3 -3
- package/dist/db/ddl-guard.js +8 -0
- package/dist/db/migration-runtime.d.ts +23 -0
- package/dist/db/migration-runtime.js +41 -0
- package/dist/db/migrations.d.ts +1 -2
- package/dist/db/migrations.js +2 -24
- package/dist/email-catalog/log.d.ts +1 -0
- package/dist/email-catalog/log.js +1 -1
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.js +1 -0
- package/dist/guards/release-schema-complete.d.ts +22 -0
- package/dist/guards/release-schema-complete.js +144 -0
- package/dist/integrations/a2a-continuations-store.d.ts +1 -0
- package/dist/integrations/a2a-continuations-store.js +1 -1
- package/dist/integrations/awaiting-input-store.d.ts +1 -0
- package/dist/integrations/awaiting-input-store.js +1 -1
- package/dist/integrations/config-store.d.ts +1 -0
- package/dist/integrations/config-store.js +1 -1
- package/dist/integrations/controls-store.d.ts +1 -0
- package/dist/integrations/controls-store.js +1 -1
- package/dist/integrations/identity-links-store.d.ts +1 -0
- package/dist/integrations/identity-links-store.js +1 -1
- package/dist/integrations/installations-store.d.ts +1 -0
- package/dist/integrations/installations-store.js +1 -1
- package/dist/integrations/remote-commands-store.d.ts +1 -0
- package/dist/integrations/remote-commands-store.js +1 -1
- package/dist/integrations/remote-devices-store.d.ts +1 -0
- package/dist/integrations/remote-devices-store.js +1 -1
- package/dist/integrations/remote-push-store.d.ts +1 -0
- package/dist/integrations/remote-push-store.js +1 -1
- package/dist/integrations/remote-run-events-store.d.ts +1 -0
- package/dist/integrations/remote-run-events-store.js +1 -1
- package/dist/integrations/scope-store.d.ts +1 -0
- package/dist/integrations/scope-store.js +1 -1
- package/dist/integrations/thread-mapping-store.d.ts +1 -0
- package/dist/integrations/thread-mapping-store.js +1 -1
- package/dist/integrations/usage-budget-store.d.ts +1 -0
- package/dist/integrations/usage-budget-store.js +1 -1
- package/dist/jobs/background-automation-runner.js +111 -9
- package/dist/jobs/run-history.d.ts +1 -0
- package/dist/jobs/run-history.js +1 -1
- package/dist/jobs/scheduler-health.d.ts +1 -0
- package/dist/jobs/scheduler-health.js +1 -1
- package/dist/mcp/approval-store.d.ts +1 -0
- package/dist/mcp/approval-store.js +1 -1
- package/dist/mcp/connect-store.d.ts +1 -0
- package/dist/mcp/connect-store.js +1 -1
- package/dist/mcp/oauth-store.d.ts +1 -0
- package/dist/mcp/oauth-store.js +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/notifications/store.d.ts +1 -0
- package/dist/notifications/store.js +1 -1
- package/dist/oauth-tokens/store.d.ts +1 -0
- package/dist/oauth-tokens/store.js +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/store.d.ts +1 -0
- package/dist/progress/store.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/provider-api/corpus-jobs-store.d.ts +1 -0
- package/dist/provider-api/corpus-jobs-store.js +1 -1
- package/dist/provider-api/custom-registry.d.ts +1 -0
- package/dist/provider-api/custom-registry.js +1 -1
- package/dist/provider-api/quota-governor.d.ts +1 -0
- package/dist/provider-api/quota-governor.js +1 -1
- package/dist/provider-api/staged-datasets-store.d.ts +1 -0
- package/dist/provider-api/staged-datasets-store.js +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/resources/store.d.ts +1 -0
- package/dist/resources/store.js +1 -1
- package/dist/scripts/dev/index.js +15 -1
- package/dist/search/index.js +3 -0
- package/dist/secrets/storage.d.ts +1 -0
- package/dist/secrets/storage.js +1 -1
- package/dist/server/agent-chat-plugin.js +38 -9
- package/dist/server/agent-teams-run-queue.d.ts +1 -0
- package/dist/server/agent-teams-run-queue.js +1 -1
- package/dist/server/auth-marketing.js +3 -1
- package/dist/server/auth.d.ts +18 -1
- package/dist/server/auth.js +370 -154
- package/dist/server/embed-session.d.ts +1 -0
- package/dist/server/embed-session.js +1 -1
- package/dist/server/google-auth-plugin.js +3 -1
- package/dist/server/google-oauth.d.ts +10 -0
- package/dist/server/google-oauth.js +25 -1
- package/dist/server/identity-sso-store.d.ts +1 -0
- package/dist/server/identity-sso-store.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +200 -31
- package/dist/server/poll.d.ts +2 -0
- package/dist/server/poll.js +19 -1
- package/dist/server/release-migrations.js +5 -0
- package/dist/server/release-schema.d.ts +46 -0
- package/dist/server/release-schema.js +307 -0
- package/dist/settings/store.d.ts +1 -0
- package/dist/settings/store.js +1 -1
- package/dist/shared/environment-lanes.d.ts +28 -0
- package/dist/shared/environment-lanes.js +61 -0
- package/dist/sharing/actions/share-resource.js +22 -2
- package/dist/styles/agent-native.css +17 -15
- package/dist/triggers/routes.d.ts +11 -0
- package/dist/triggers/routes.js +10 -0
- package/dist/usage/alerts-store.d.ts +1 -0
- package/dist/usage/alerts-store.js +1 -1
- package/dist/usage/store.d.ts +1 -0
- package/dist/usage/store.js +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AgentToggleButton,
|
|
3
|
+
requestAgentChatThreadOpen,
|
|
3
4
|
useChatModels,
|
|
4
5
|
} from "@agent-native/core/client/agent-chat";
|
|
5
6
|
import {
|
|
@@ -18,8 +19,9 @@ import {
|
|
|
18
19
|
IconPlus,
|
|
19
20
|
IconRefresh,
|
|
20
21
|
} from "@tabler/icons-react";
|
|
21
|
-
import { useEffect, useMemo, useState } from "react";
|
|
22
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
22
23
|
import { Link, useSearchParams } from "react-router";
|
|
24
|
+
import { toast } from "sonner";
|
|
23
25
|
|
|
24
26
|
import { FactoryAgentsView } from "@/components/factory/FactoryAgentsView";
|
|
25
27
|
import { FactoryAuditView } from "@/components/factory/FactoryAuditView";
|
|
@@ -78,6 +80,7 @@ type TriageDecision = {
|
|
|
78
80
|
type TriageItem = {
|
|
79
81
|
itemId?: string;
|
|
80
82
|
id?: string;
|
|
83
|
+
title?: string | null;
|
|
81
84
|
source?: string | null;
|
|
82
85
|
sourceName?: string | null;
|
|
83
86
|
sourceUrl?: string | null;
|
|
@@ -87,6 +90,8 @@ type TriageItem = {
|
|
|
87
90
|
reason?: string | null;
|
|
88
91
|
decisionSummary?: string | null;
|
|
89
92
|
decisions?: TriageDecision[] | null;
|
|
93
|
+
createdAt?: string | null;
|
|
94
|
+
updatedAt?: string | null;
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
type TriageRule = {
|
|
@@ -159,6 +164,7 @@ type FactoryAutomation = {
|
|
|
159
164
|
timezone?: string | null;
|
|
160
165
|
condition?: string | null;
|
|
161
166
|
canUpdate?: boolean;
|
|
167
|
+
updatedAt?: string | number | null;
|
|
162
168
|
runs?: FactoryAutomationRun[] | null;
|
|
163
169
|
pastRuns?: FactoryAutomationRun[] | null;
|
|
164
170
|
};
|
|
@@ -976,7 +982,8 @@ function AutomationsView({
|
|
|
976
982
|
current.body === nextDraft.body &&
|
|
977
983
|
current.model === nextDraft.model &&
|
|
978
984
|
current.schedule === nextDraft.schedule &&
|
|
979
|
-
current.enabled === nextDraft.enabled
|
|
985
|
+
current.enabled === nextDraft.enabled &&
|
|
986
|
+
current.updatedAt === nextDraft.updatedAt
|
|
980
987
|
) {
|
|
981
988
|
return current;
|
|
982
989
|
}
|
|
@@ -986,16 +993,25 @@ function AutomationsView({
|
|
|
986
993
|
|
|
987
994
|
async function saveAutomation() {
|
|
988
995
|
if (!draft) return;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
996
|
+
try {
|
|
997
|
+
await saveMutation.mutateAsync({
|
|
998
|
+
factoryId,
|
|
999
|
+
automationId: draft.id,
|
|
1000
|
+
name: draft.name,
|
|
1001
|
+
prompt: draft.prompt ?? draft.body ?? "",
|
|
1002
|
+
model: draft.model ?? "",
|
|
1003
|
+
schedule: draft.schedule ?? "",
|
|
1004
|
+
enabled: draft.enabled,
|
|
1005
|
+
});
|
|
1006
|
+
await automationsQuery.refetch();
|
|
1007
|
+
toast.success(t("factoryRoute.automationSaved"));
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
toast.error(
|
|
1010
|
+
error instanceof Error
|
|
1011
|
+
? error.message
|
|
1012
|
+
: t("factoryRoute.automationSaveFailed"),
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
999
1015
|
}
|
|
1000
1016
|
|
|
1001
1017
|
async function runAutomation() {
|
|
@@ -1031,27 +1047,34 @@ function AutomationsView({
|
|
|
1031
1047
|
role="tablist"
|
|
1032
1048
|
aria-label={t("factoryRoute.automationsTitle")}
|
|
1033
1049
|
>
|
|
1034
|
-
{automations.map((automation) =>
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
{automation.
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1050
|
+
{automations.map((automation) => {
|
|
1051
|
+
const selected = activeAutomationId === automation.id;
|
|
1052
|
+
return (
|
|
1053
|
+
<button
|
|
1054
|
+
key={automation.id}
|
|
1055
|
+
type="button"
|
|
1056
|
+
id={`factory-automation-tab-${automation.id}`}
|
|
1057
|
+
role="tab"
|
|
1058
|
+
aria-selected={selected}
|
|
1059
|
+
aria-controls="factory-automation-panel"
|
|
1060
|
+
className={`w-full cursor-pointer rounded-lg p-4 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
|
|
1061
|
+
selected
|
|
1062
|
+
? "bg-primary/10 ring-1 ring-inset ring-primary/40"
|
|
1063
|
+
: "bg-muted/20 hover:bg-muted/50"
|
|
1064
|
+
}`}
|
|
1065
|
+
onClick={() => selectAutomation(automation.id)}
|
|
1066
|
+
>
|
|
1067
|
+
<span className="block truncate text-sm font-medium">
|
|
1068
|
+
{automation.name}
|
|
1069
|
+
</span>
|
|
1070
|
+
<span className="mt-1 block truncate text-xs text-muted-foreground">
|
|
1071
|
+
{automation.enabled
|
|
1072
|
+
? t("factoryRoute.automationEnabled")
|
|
1073
|
+
: t("factoryRoute.automationDisabled")}
|
|
1074
|
+
</span>
|
|
1075
|
+
</button>
|
|
1076
|
+
);
|
|
1077
|
+
})}
|
|
1055
1078
|
</div>
|
|
1056
1079
|
)}
|
|
1057
1080
|
</CardContent>
|
|
@@ -1189,8 +1212,24 @@ function AutomationsView({
|
|
|
1189
1212
|
</div>
|
|
1190
1213
|
</div>
|
|
1191
1214
|
<div className="grid gap-1.5">
|
|
1192
|
-
<
|
|
1215
|
+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
1216
|
+
<Label htmlFor="factory-automation-prompt">
|
|
1217
|
+
{t("factoryRoute.automationPrompt")}
|
|
1218
|
+
</Label>
|
|
1219
|
+
{draft.updatedAt ? (
|
|
1220
|
+
<span className="capitalize text-xs text-muted-foreground">
|
|
1221
|
+
{t("factoryRoute.automationLastUpdated")}{" "}
|
|
1222
|
+
<time
|
|
1223
|
+
dateTime={new Date(draft.updatedAt).toISOString()}
|
|
1224
|
+
title={formatAutomationDate(draft.updatedAt)}
|
|
1225
|
+
>
|
|
1226
|
+
{formatInboxDateTime(draft.updatedAt)}
|
|
1227
|
+
</time>
|
|
1228
|
+
</span>
|
|
1229
|
+
) : null}
|
|
1230
|
+
</div>
|
|
1193
1231
|
<Textarea
|
|
1232
|
+
id="factory-automation-prompt"
|
|
1194
1233
|
value={draft.prompt ?? draft.body ?? ""}
|
|
1195
1234
|
onChange={(event) =>
|
|
1196
1235
|
setDraft({ ...draft, prompt: event.target.value })
|
|
@@ -1226,11 +1265,25 @@ function AutomationsView({
|
|
|
1226
1265
|
</span>
|
|
1227
1266
|
{run.threadId && (
|
|
1228
1267
|
<a
|
|
1229
|
-
className="inline-flex items-center
|
|
1268
|
+
className="inline-flex items-center text-xs text-muted-foreground underline-offset-4 hover:underline"
|
|
1230
1269
|
href={`/chat/${encodeURIComponent(run.threadId)}`}
|
|
1270
|
+
onClick={(event) => {
|
|
1271
|
+
if (
|
|
1272
|
+
event.metaKey ||
|
|
1273
|
+
event.ctrlKey ||
|
|
1274
|
+
event.shiftKey ||
|
|
1275
|
+
event.altKey ||
|
|
1276
|
+
event.button !== 0
|
|
1277
|
+
) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
event.preventDefault();
|
|
1281
|
+
requestAgentChatThreadOpen({
|
|
1282
|
+
threadId: run.threadId!,
|
|
1283
|
+
});
|
|
1284
|
+
}}
|
|
1231
1285
|
>
|
|
1232
1286
|
{t("factoryRoute.automationOpenThread")}
|
|
1233
|
-
<IconExternalLink className="size-3" />
|
|
1234
1287
|
</a>
|
|
1235
1288
|
)}
|
|
1236
1289
|
{run.error && (
|
|
@@ -1259,6 +1312,52 @@ function formatAutomationDate(value: string | number | null | undefined) {
|
|
|
1259
1312
|
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
1260
1313
|
}
|
|
1261
1314
|
|
|
1315
|
+
function formatInboxDateTime(value: string | number | null | undefined) {
|
|
1316
|
+
if (!value) return null;
|
|
1317
|
+
const date = new Date(value);
|
|
1318
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
1319
|
+
return date.toLocaleString(undefined, {
|
|
1320
|
+
month: "short",
|
|
1321
|
+
day: "numeric",
|
|
1322
|
+
year: "numeric",
|
|
1323
|
+
hour: "numeric",
|
|
1324
|
+
minute: "2-digit",
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function formatInboxAge(value: string | number | null | undefined) {
|
|
1329
|
+
if (!value) return null;
|
|
1330
|
+
const date = new Date(value);
|
|
1331
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
1332
|
+
|
|
1333
|
+
const elapsedSeconds = Math.max(
|
|
1334
|
+
0,
|
|
1335
|
+
Math.floor((Date.now() - date.getTime()) / 1_000),
|
|
1336
|
+
);
|
|
1337
|
+
if (elapsedSeconds < 60) return "now";
|
|
1338
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
1339
|
+
if (elapsedMinutes < 60) return `${elapsedMinutes}m`;
|
|
1340
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
1341
|
+
if (elapsedHours < 24) return `${elapsedHours}h`;
|
|
1342
|
+
const elapsedDays = Math.floor(elapsedHours / 24);
|
|
1343
|
+
if (elapsedDays < 30) return `${elapsedDays}d`;
|
|
1344
|
+
const elapsedMonths = Math.floor(elapsedDays / 30);
|
|
1345
|
+
if (elapsedMonths < 12) return `${elapsedMonths}mo`;
|
|
1346
|
+
return `${Math.floor(elapsedMonths / 12)}y`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const reviewListColumns =
|
|
1350
|
+
"w-full gap-3 sm:grid-cols-[minmax(0,1.3fr)_minmax(4.75rem,auto)_minmax(5.5rem,auto)_minmax(4.5rem,auto)_minmax(0,1.3fr)] sm:items-start";
|
|
1351
|
+
|
|
1352
|
+
function formatInboxSource(source: string | null | undefined) {
|
|
1353
|
+
const normalized = source?.toLowerCase() ?? "";
|
|
1354
|
+
if (normalized.includes("slack")) return "Slack";
|
|
1355
|
+
if (normalized.includes("github")) return "GitHub";
|
|
1356
|
+
if (normalized.includes("sentry")) return "Sentry";
|
|
1357
|
+
const trimmed = source?.trim();
|
|
1358
|
+
return trimmed ? trimmed : "Source";
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1262
1361
|
function formatModelName(model: string | null | undefined) {
|
|
1263
1362
|
const value = model?.trim();
|
|
1264
1363
|
if (!value) return "the app default";
|
|
@@ -1279,6 +1378,7 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1279
1378
|
);
|
|
1280
1379
|
const [feedbackNote, setFeedbackNote] = useState("");
|
|
1281
1380
|
const [verdict, setVerdict] = useState<Verdict | null>(null);
|
|
1381
|
+
const selectedRowRef = useRef<HTMLButtonElement | null>(null);
|
|
1282
1382
|
const listQuery = useActionQuery("list-triage-items", {
|
|
1283
1383
|
limit: 50,
|
|
1284
1384
|
...(status.trim()
|
|
@@ -1312,6 +1412,14 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1312
1412
|
setSelectedId(searchParams.get("itemId"));
|
|
1313
1413
|
}, [searchParams]);
|
|
1314
1414
|
|
|
1415
|
+
useEffect(() => {
|
|
1416
|
+
if (!selectedId) return;
|
|
1417
|
+
selectedRowRef.current?.scrollIntoView({
|
|
1418
|
+
block: "nearest",
|
|
1419
|
+
inline: "nearest",
|
|
1420
|
+
});
|
|
1421
|
+
}, [selectedId, normalizedItems.length]);
|
|
1422
|
+
|
|
1315
1423
|
function selectItem(itemId: string) {
|
|
1316
1424
|
setSelectedId(itemId);
|
|
1317
1425
|
setVerdict(null);
|
|
@@ -1371,19 +1479,51 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1371
1479
|
</p>
|
|
1372
1480
|
) : (
|
|
1373
1481
|
<div className="grid gap-1.5 p-2">
|
|
1482
|
+
<div
|
|
1483
|
+
className={`hidden px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground sm:grid ${reviewListColumns}`}
|
|
1484
|
+
>
|
|
1485
|
+
<span />
|
|
1486
|
+
<span>{t("triage.risk")}</span>
|
|
1487
|
+
<span>{t("triage.status")}</span>
|
|
1488
|
+
<span>{t("triage.updatedAt")}</span>
|
|
1489
|
+
<span>{t("triage.reason")}</span>
|
|
1490
|
+
</div>
|
|
1374
1491
|
{normalizedItems.map((item) => {
|
|
1375
1492
|
const id = item.itemId ?? item.id ?? "";
|
|
1493
|
+
const selected = selectedId === id;
|
|
1494
|
+
const createdAtLabel = formatInboxDateTime(item.createdAt);
|
|
1495
|
+
const updatedAge = formatInboxAge(item.updatedAt);
|
|
1376
1496
|
return (
|
|
1377
1497
|
<button
|
|
1378
1498
|
key={id}
|
|
1499
|
+
ref={selected ? selectedRowRef : undefined}
|
|
1379
1500
|
type="button"
|
|
1380
|
-
|
|
1501
|
+
aria-current={selected ? "true" : undefined}
|
|
1502
|
+
className={`grid rounded-lg px-4 py-3 text-left transition-colors ${reviewListColumns} ${
|
|
1503
|
+
selected
|
|
1504
|
+
? "bg-primary/10 ring-1 ring-inset ring-primary/40"
|
|
1505
|
+
: "bg-muted/20 hover:bg-muted/50"
|
|
1506
|
+
}`}
|
|
1381
1507
|
onClick={() => selectItem(id)}
|
|
1382
1508
|
>
|
|
1383
1509
|
<span className="min-w-0">
|
|
1510
|
+
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1511
|
+
{formatInboxSource(item.source ?? item.sourceName)}
|
|
1512
|
+
</span>
|
|
1384
1513
|
<span className="block truncate text-sm font-medium">
|
|
1385
|
-
{item.
|
|
1514
|
+
{item.title?.trim() ||
|
|
1515
|
+
item.sourceName ||
|
|
1516
|
+
item.source ||
|
|
1517
|
+
"Untitled"}
|
|
1386
1518
|
</span>
|
|
1519
|
+
{createdAtLabel && item.createdAt && (
|
|
1520
|
+
<time
|
|
1521
|
+
className="mt-0.5 block text-xs text-muted-foreground"
|
|
1522
|
+
dateTime={item.createdAt}
|
|
1523
|
+
>
|
|
1524
|
+
{createdAtLabel}
|
|
1525
|
+
</time>
|
|
1526
|
+
)}
|
|
1387
1527
|
{item.sourceUrl && (
|
|
1388
1528
|
<span className="mt-1 flex max-w-full items-center gap-1 truncate text-xs text-primary">
|
|
1389
1529
|
{item.sourceUrl}
|
|
@@ -1392,20 +1532,36 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1392
1532
|
)}
|
|
1393
1533
|
</span>
|
|
1394
1534
|
<span>
|
|
1395
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1396
|
-
|
|
1535
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1536
|
+
{t("triage.risk")}
|
|
1397
1537
|
</span>
|
|
1398
1538
|
<TriageStatusPill status={item.risk} />
|
|
1399
1539
|
</span>
|
|
1400
1540
|
<span>
|
|
1401
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1402
|
-
|
|
1541
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1542
|
+
{t("triage.status")}
|
|
1403
1543
|
</span>
|
|
1404
1544
|
<TriageStatusPill status={item.status} />
|
|
1405
1545
|
</span>
|
|
1406
|
-
<span
|
|
1407
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1408
|
-
|
|
1546
|
+
<span>
|
|
1547
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1548
|
+
{t("triage.updatedAt")}
|
|
1549
|
+
</span>
|
|
1550
|
+
{updatedAge && item.updatedAt ? (
|
|
1551
|
+
<time
|
|
1552
|
+
className="text-sm tabular-nums text-muted-foreground"
|
|
1553
|
+
dateTime={item.updatedAt}
|
|
1554
|
+
title={formatAutomationDate(item.updatedAt)}
|
|
1555
|
+
>
|
|
1556
|
+
{updatedAge}
|
|
1557
|
+
</time>
|
|
1558
|
+
) : (
|
|
1559
|
+
<span className="text-sm text-muted-foreground">-</span>
|
|
1560
|
+
)}
|
|
1561
|
+
</span>
|
|
1562
|
+
<span className="min-w-0 truncate">
|
|
1563
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1564
|
+
{t("triage.reason")}
|
|
1409
1565
|
</span>
|
|
1410
1566
|
<span className="text-sm">
|
|
1411
1567
|
{item.reason ?? item.decisionSummary ?? "-"}
|
|
@@ -1431,8 +1587,15 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1431
1587
|
<>
|
|
1432
1588
|
<div className="flex items-start justify-between gap-3">
|
|
1433
1589
|
<div>
|
|
1590
|
+
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1591
|
+
{formatInboxSource(
|
|
1592
|
+
selectedItem.source ?? selectedItem.sourceName,
|
|
1593
|
+
)}
|
|
1594
|
+
</p>
|
|
1434
1595
|
<p className="text-sm font-medium">
|
|
1435
|
-
{selectedItem.
|
|
1596
|
+
{selectedItem.title?.trim() ||
|
|
1597
|
+
selectedItem.sourceName ||
|
|
1598
|
+
selectedItem.source}
|
|
1436
1599
|
</p>
|
|
1437
1600
|
{selectedItem.sourceUrl && (
|
|
1438
1601
|
<a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
export type Workspace = "primary" | "secondary";
|
|
2
4
|
|
|
3
5
|
export type SlackTokenResolver = (workspace: Workspace) => Promise<string>;
|
|
@@ -20,6 +22,12 @@ export interface SlackTeamInfo {
|
|
|
20
22
|
domain: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export interface SlackUserInfo {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string | null;
|
|
28
|
+
displayName: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export interface ChannelHistoryResult {
|
|
24
32
|
messages: SlackMessage[];
|
|
25
33
|
has_more: boolean;
|
|
@@ -65,16 +73,20 @@ async function getToken(
|
|
|
65
73
|
);
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
function slackCacheScope(token: string): string {
|
|
77
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
async function slackApi<T>(
|
|
69
81
|
workspace: Workspace,
|
|
70
82
|
method: string,
|
|
71
83
|
params: Record<string, string> | undefined,
|
|
72
84
|
tokenResolver?: SlackTokenResolver,
|
|
73
85
|
): Promise<T> {
|
|
74
|
-
const
|
|
86
|
+
const token = await getToken(workspace, tokenResolver);
|
|
87
|
+
const cacheKey = `${workspace}:${slackCacheScope(token)}:${method}:${JSON.stringify(params ?? {})}`;
|
|
75
88
|
const cached = cache.get(cacheKey);
|
|
76
89
|
if (cached && cached.expiresAt > Date.now()) return cached.value as T;
|
|
77
|
-
const token = await getToken(workspace, tokenResolver);
|
|
78
90
|
const url = new URL(`https://slack.com/api/${method}`);
|
|
79
91
|
for (const [key, value] of Object.entries(params ?? {}))
|
|
80
92
|
url.searchParams.set(key, value);
|
|
@@ -255,3 +267,33 @@ export async function getTeamInfo(
|
|
|
255
267
|
);
|
|
256
268
|
return data.team ?? { id: data.team_id ?? "", name: workspace, domain: "" };
|
|
257
269
|
}
|
|
270
|
+
|
|
271
|
+
export async function getUserInfo(
|
|
272
|
+
workspace: Workspace,
|
|
273
|
+
userId: string,
|
|
274
|
+
tokenResolver?: SlackTokenResolver,
|
|
275
|
+
): Promise<SlackUserInfo> {
|
|
276
|
+
const id = userId.trim();
|
|
277
|
+
if (!id) throw new Error("A Slack user id is required.");
|
|
278
|
+
const data = await slackApi<{
|
|
279
|
+
user?: {
|
|
280
|
+
id?: string;
|
|
281
|
+
name?: string;
|
|
282
|
+
profile?: { display_name?: string };
|
|
283
|
+
};
|
|
284
|
+
}>(workspace, "users.info", { user: id }, tokenResolver);
|
|
285
|
+
const user = data.user;
|
|
286
|
+
if (!user || typeof user.id !== "string" || !user.id.trim()) {
|
|
287
|
+
throw new Error("Slack user response is missing a user id.");
|
|
288
|
+
}
|
|
289
|
+
const displayName =
|
|
290
|
+
typeof user.profile?.display_name === "string"
|
|
291
|
+
? user.profile.display_name.trim()
|
|
292
|
+
: "";
|
|
293
|
+
const name = typeof user.name === "string" ? user.name.trim() : "";
|
|
294
|
+
return {
|
|
295
|
+
id: user.id.trim(),
|
|
296
|
+
name: name || null,
|
|
297
|
+
displayName: displayName || null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -89,13 +89,15 @@ Builder, auto-approve, or auto-merge those items. Slack clear bugs use the
|
|
|
89
89
|
thread-preserving start-builder-for-item flow; GitHub and Sentry clear bugs use
|
|
90
90
|
the Builder agent-run flow. Slack repeat reports must be clustered by underlying
|
|
91
91
|
symptom, with one Builder thread for the cluster and 👀 on every grouped report.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
After classifying an item, call start-builder-for-item with clearBug true or
|
|
93
|
+
false and a short reason so a skip is recorded. Use /address-feedback for the
|
|
94
|
+
repository feedback workflow. For pull requests, follow review-prs: read the
|
|
95
|
+
complete diff and review evidence, verify current BuilderIO membership, preserve
|
|
96
|
+
the ultra-scary safety gate, and distinguish unknown or unresolved checks from
|
|
97
|
+
clean ones. Auto-merge also requires a verified Factory Builder run. When a user
|
|
98
|
+
says to do a review-gated item now, use the explicit approval action, which
|
|
99
|
+
records the approver and applies the rule's configured executor policy. Keep
|
|
100
|
+
Slack replies concise and link to the
|
|
99
101
|
Factory item when a review is needed. The scheduled builder-io-bot PR babysitter
|
|
100
102
|
posts its exact feedback-fix request through GitHub, persists a 20-minute quiet
|
|
101
103
|
window, and never approves or merges.`,
|
|
@@ -169,6 +169,8 @@ const FACTORY_DEFAULT_MODEL =
|
|
|
169
169
|
process.env.FACTORY_AUTOMATION_MODEL?.trim() || "gpt-5.6-luna";
|
|
170
170
|
const FACTORY_DEFAULT_MAX_ITERATIONS = 32;
|
|
171
171
|
const FACTORY_DEFAULT_MAX_RUN_INPUT_TOKENS = 1_000_000;
|
|
172
|
+
const SKIP_RECORD_GUARD =
|
|
173
|
+
"After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.";
|
|
172
174
|
|
|
173
175
|
const AUTOMATION_SEEDS: AutomationSeed[] = [
|
|
174
176
|
{
|
|
@@ -224,6 +226,7 @@ the representative source, every related source, and the need to fix the
|
|
|
224
226
|
underlying boundary across the whole cluster. Never add the reaction or tag
|
|
225
227
|
Builder for owner-managed Clips, Design, or Content work, or for a non-bug
|
|
226
228
|
report.
|
|
229
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
227
230
|
|
|
228
231
|
Keep each run bounded. Preserve action errors and do not claim a Builder reply,
|
|
229
232
|
PR, merge, or fix unless an action returned that state.
|
|
@@ -254,6 +257,7 @@ For each eligible clear bug, call start-builder-for-item with clearBug true,
|
|
|
254
257
|
an evidence-grounded reason, and clearErrorReport containing only the bounded
|
|
255
258
|
Sentry evidence. Builder should open a PR; do not claim it did so until the
|
|
256
259
|
run callback or PR observation confirms it.
|
|
260
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
257
261
|
`,
|
|
258
262
|
},
|
|
259
263
|
{
|
|
@@ -281,6 +285,7 @@ For each eligible item call start-builder-for-item with clearBug true,
|
|
|
281
285
|
evidence-grounded reason, and the bounded issue body as clearErrorReport.
|
|
282
286
|
Preserve failures and never report a successful Builder run without its action
|
|
283
287
|
confirmation.
|
|
288
|
+
After classifying each processed item, call start-builder-for-item with clearBug true or false and a short evidence-grounded reason so the skip or dispatch is recorded.
|
|
284
289
|
`,
|
|
285
290
|
},
|
|
286
291
|
{
|
|
@@ -522,6 +527,14 @@ async function ensureOrganizationAutomations(
|
|
|
522
527
|
if (promptGuard && !repaired.includes(promptGuard)) {
|
|
523
528
|
repaired = `${repaired.trimEnd()}\n\n${promptGuard}\n`;
|
|
524
529
|
}
|
|
530
|
+
if (
|
|
531
|
+
(seed.name === "factory-slack-feedback" ||
|
|
532
|
+
seed.name === "factory-sentry-errors" ||
|
|
533
|
+
seed.name === "factory-github-issues") &&
|
|
534
|
+
!repaired.includes(SKIP_RECORD_GUARD)
|
|
535
|
+
) {
|
|
536
|
+
repaired = `${repaired.trimEnd()}\n\n${SKIP_RECORD_GUARD}\n`;
|
|
537
|
+
}
|
|
525
538
|
if (repaired === existing.content) return;
|
|
526
539
|
|
|
527
540
|
const updated = await resourcePutIfCurrent({
|
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
addEyesReaction as writeEyesReaction,
|
|
5
5
|
getThread as readThread,
|
|
6
6
|
getTeamInfo as readTeamInfo,
|
|
7
|
+
getUserInfo as readUserInfo,
|
|
7
8
|
postThreadReply as writeThreadReply,
|
|
8
9
|
type ChannelHistoryResult,
|
|
9
10
|
type SlackPostMessageResult,
|
|
10
11
|
type SlackReactionResult,
|
|
11
12
|
type SlackTeamInfo,
|
|
13
|
+
type SlackUserInfo,
|
|
12
14
|
type SlackTokenResolver,
|
|
13
15
|
type ThreadRepliesResult,
|
|
14
16
|
type Workspace,
|
|
@@ -57,6 +59,9 @@ export function createSlackReader(identity: SlackReaderIdentity) {
|
|
|
57
59
|
getTeamInfo(workspace: Workspace): Promise<SlackTeamInfo> {
|
|
58
60
|
return readTeamInfo(workspace, tokenResolver);
|
|
59
61
|
},
|
|
62
|
+
getUserInfo(workspace: Workspace, userId: string): Promise<SlackUserInfo> {
|
|
63
|
+
return readUserInfo(workspace, userId, tokenResolver);
|
|
64
|
+
},
|
|
60
65
|
getThread(
|
|
61
66
|
workspace: Workspace,
|
|
62
67
|
channelId: string,
|
|
@@ -5,6 +5,7 @@ import { createSlackReader } from "./slack-client";
|
|
|
5
5
|
const SLACK_HISTORY_LIMIT = 100;
|
|
6
6
|
const MAX_HISTORY_PAGES = 5;
|
|
7
7
|
const MAX_SUMMARY_LENGTH = 500;
|
|
8
|
+
export const SLACK_USER_INFO_CONCURRENCY = 4;
|
|
8
9
|
|
|
9
10
|
export interface SlackPollInput {
|
|
10
11
|
workspace: Workspace;
|
|
@@ -37,17 +38,75 @@ function compactText(text: string): string {
|
|
|
37
38
|
: compact;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function messageLabel(message: SlackMessage): string {
|
|
41
|
+
function messageLabel(message: SlackMessage, resolvedLabel?: string): string {
|
|
41
42
|
if (message.bot_id || message.username) {
|
|
42
43
|
return `Slack bot ${message.username ?? message.bot_id ?? "unknown"}`;
|
|
43
44
|
}
|
|
44
|
-
return `Slack user ${message.user ?? "unknown"}`;
|
|
45
|
+
return `Slack user ${resolvedLabel ?? message.user ?? "unknown"}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slackUserLabel(
|
|
49
|
+
user: { name: string | null; displayName: string | null } | null,
|
|
50
|
+
userId: string,
|
|
51
|
+
): string {
|
|
52
|
+
const displayName = user?.displayName?.trim();
|
|
53
|
+
if (displayName) return displayName;
|
|
54
|
+
const name = user?.name?.trim();
|
|
55
|
+
if (name) return name.startsWith("@") ? name : `@${name}`;
|
|
56
|
+
return userId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function resolveUserLabels(
|
|
60
|
+
slack: ReturnType<typeof createSlackReader>,
|
|
61
|
+
workspace: Workspace,
|
|
62
|
+
messages: SlackMessage[],
|
|
63
|
+
): Promise<Map<string, string>> {
|
|
64
|
+
const userIds = [
|
|
65
|
+
...new Set(
|
|
66
|
+
messages
|
|
67
|
+
.filter((message) => !message.bot_id && !message.username)
|
|
68
|
+
.map((message) => message.user?.trim())
|
|
69
|
+
.filter((userId): userId is string => Boolean(userId)),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
const labels = new Map<string, string>();
|
|
73
|
+
await mapWithConcurrency(
|
|
74
|
+
userIds,
|
|
75
|
+
SLACK_USER_INFO_CONCURRENCY,
|
|
76
|
+
async (userId) => {
|
|
77
|
+
try {
|
|
78
|
+
const user = await slack.getUserInfo(workspace, userId);
|
|
79
|
+
labels.set(userId, slackUserLabel(user, userId));
|
|
80
|
+
} catch {
|
|
81
|
+
labels.set(userId, userId);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
return labels;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function mapWithConcurrency<T>(
|
|
89
|
+
values: T[],
|
|
90
|
+
concurrency: number,
|
|
91
|
+
mapper: (value: T) => Promise<void>,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
let nextIndex = 0;
|
|
94
|
+
const worker = async () => {
|
|
95
|
+
while (nextIndex < values.length) {
|
|
96
|
+
const index = nextIndex++;
|
|
97
|
+
await mapper(values[index]!);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
await Promise.all(
|
|
101
|
+
Array.from({ length: Math.min(concurrency, values.length) }, worker),
|
|
102
|
+
);
|
|
45
103
|
}
|
|
46
104
|
|
|
47
105
|
function toEnvelope(
|
|
48
106
|
channelId: string,
|
|
49
107
|
message: SlackMessage,
|
|
50
108
|
teamDomain?: string,
|
|
109
|
+
userLabels?: Map<string, string>,
|
|
51
110
|
): IngestionEnvelope {
|
|
52
111
|
const threadTs = message.thread_ts ?? message.ts;
|
|
53
112
|
const suppliedPermalink = (message as SlackMessage & { permalink?: string })
|
|
@@ -63,7 +122,10 @@ function toEnvelope(
|
|
|
63
122
|
externalId: `${channelId}:${threadTs}`,
|
|
64
123
|
receivedAt: new Date().toISOString(),
|
|
65
124
|
...(sourceUrl ? { sourceUrl } : {}),
|
|
66
|
-
title: messageLabel(
|
|
125
|
+
title: messageLabel(
|
|
126
|
+
message,
|
|
127
|
+
message.user ? userLabels?.get(message.user) : undefined,
|
|
128
|
+
),
|
|
67
129
|
summary: compactText(message.text),
|
|
68
130
|
channelId,
|
|
69
131
|
threadTs,
|
|
@@ -149,9 +211,15 @@ export async function pollSlackChannel({
|
|
|
149
211
|
{ value: priorTs, raw: priorLastSlackTs },
|
|
150
212
|
);
|
|
151
213
|
|
|
214
|
+
const userLabels = await resolveUserLabels(
|
|
215
|
+
slack,
|
|
216
|
+
workspace,
|
|
217
|
+
uniqueNewMessages.map(({ message }) => message),
|
|
218
|
+
);
|
|
219
|
+
|
|
152
220
|
return {
|
|
153
221
|
envelopes: uniqueNewMessages.map(({ message }) =>
|
|
154
|
-
toEnvelope(channelId, message, teamDomain),
|
|
222
|
+
toEnvelope(channelId, message, teamDomain, userLabels),
|
|
155
223
|
),
|
|
156
224
|
nextLastSlackTs: maxSeen.raw,
|
|
157
225
|
nextHistoryCursor,
|