@agent-native/core 0.164.4 → 0.164.12
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/.agents/skills/event-management/SKILL.md +84 -0
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/delete-event.ts +1 -1
- package/corpus/templates/calendar/actions/delete-events.ts +508 -0
- package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
- package/corpus/templates/calendar/actions/list-events.ts +53 -0
- 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/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
- package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
- 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 +3 -2
- package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
- package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
- package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
- package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
- 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/components/factory/FactoryHistoryView.tsx +709 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
- package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +47 -0
- package/corpus/templates/factory/app/routes/factory.tsx +324 -61
- package/corpus/templates/factory/server/connectors/slack.ts +44 -2
- package/corpus/templates/factory/server/plugins/agent-chat.ts +12 -8
- 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/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts +1 -0
- package/dist/collab/storage.js +1 -1
- package/dist/collab/struct-routes.d.ts +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/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 +5 -5
- package/dist/progress/routes.d.ts +1 -1
- 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/routes.d.ts +9 -9
- 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-engine-api-key-route.d.ts +1 -1
- 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/realtime-token.d.ts +1 -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/docs/content/actions-access-control.mdx +136 -2
- package/docs/content/actions-agent-tools.mdx +101 -0
- package/docs/content/actions-defining.mdx +2 -2
- package/docs/content/audit-log.mdx +1 -1
- package/docs/content/authentication.mdx +1 -1
- package/docs/content/cloudflare.mdx +43 -0
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/deploy-an-app.mdx +109 -0
- package/docs/content/deployment-environment-variables.mdx +101 -0
- package/docs/content/deployment.mdx +125 -792
- package/docs/content/docs-components.mdx +2 -2
- package/docs/content/environment-variables.mdx +41 -41
- package/docs/content/file-uploads.mdx +74 -40
- package/docs/content/harness-agents.mdx +2 -2
- package/docs/content/internationalization.mdx +61 -65
- package/docs/content/key-concepts.mdx +3 -3
- package/docs/content/multi-app-workspace.mdx +2 -2
- package/docs/content/netlify.mdx +47 -0
- package/docs/content/node-docker.mdx +138 -0
- package/docs/content/organizations-teams-permissions.mdx +5 -5
- package/docs/content/other-platforms.mdx +27 -0
- package/docs/content/ssr-caching.mdx +45 -0
- package/docs/content/template-assets-developers.mdx +2 -2
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +2 -2
- package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
- package/docs/content/template-content-sync.mdx +2 -2
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +2 -2
- package/docs/content/updating-ui-in-production.mdx +35 -0
- package/docs/content/vercel.mdx +38 -0
- package/docs/content/what-is-agent-native.mdx +1 -1
- package/docs/content/workspace-deployment.mdx +350 -0
- package/package.json +2 -2
- package/docs/content/agents.mdx +0 -9
- package/docs/content/database.mdx +0 -317
- package/docs/content/human-approval.mdx +0 -156
|
@@ -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";
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
type FactoryCanvasGraph,
|
|
30
32
|
type FactoryCanvasNode,
|
|
31
33
|
} from "@/components/factory/FactoryCanvas";
|
|
34
|
+
import { FactoryHistoryView } from "@/components/factory/FactoryHistoryView";
|
|
32
35
|
import { FactoryInspector } from "@/components/factory/FactoryInspector";
|
|
33
36
|
import { TriageStatusPill } from "@/components/triage/triage-status-pill";
|
|
34
37
|
import { Button } from "@/components/ui/button";
|
|
@@ -78,6 +81,7 @@ type TriageDecision = {
|
|
|
78
81
|
type TriageItem = {
|
|
79
82
|
itemId?: string;
|
|
80
83
|
id?: string;
|
|
84
|
+
title?: string | null;
|
|
81
85
|
source?: string | null;
|
|
82
86
|
sourceName?: string | null;
|
|
83
87
|
sourceUrl?: string | null;
|
|
@@ -87,6 +91,8 @@ type TriageItem = {
|
|
|
87
91
|
reason?: string | null;
|
|
88
92
|
decisionSummary?: string | null;
|
|
89
93
|
decisions?: TriageDecision[] | null;
|
|
94
|
+
createdAt?: string | null;
|
|
95
|
+
updatedAt?: string | null;
|
|
90
96
|
};
|
|
91
97
|
|
|
92
98
|
type TriageRule = {
|
|
@@ -126,6 +132,7 @@ type WorkspaceTab =
|
|
|
126
132
|
| "automations"
|
|
127
133
|
| "agents"
|
|
128
134
|
| "audit"
|
|
135
|
+
| "history"
|
|
129
136
|
| "settings";
|
|
130
137
|
|
|
131
138
|
type FactoryAutomationRun = {
|
|
@@ -159,6 +166,7 @@ type FactoryAutomation = {
|
|
|
159
166
|
timezone?: string | null;
|
|
160
167
|
condition?: string | null;
|
|
161
168
|
canUpdate?: boolean;
|
|
169
|
+
updatedAt?: string | number | null;
|
|
162
170
|
runs?: FactoryAutomationRun[] | null;
|
|
163
171
|
pastRuns?: FactoryAutomationRun[] | null;
|
|
164
172
|
};
|
|
@@ -178,9 +186,14 @@ export default function FactoryRoute() {
|
|
|
178
186
|
const [creating, setCreating] = useState(false);
|
|
179
187
|
const [draftGraph, setDraftGraph] = useState<FactoryCanvasGraph | null>(null);
|
|
180
188
|
const [dirty, setDirty] = useState(false);
|
|
189
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
190
|
+
const [saveConflictRemoteGraph, setSaveConflictRemoteGraph] =
|
|
191
|
+
useState<FactoryCanvasGraph | null>(null);
|
|
192
|
+
const [refreshingFactory, setRefreshingFactory] = useState(false);
|
|
181
193
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
182
194
|
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
|
183
195
|
const [auditRefreshToken, setAuditRefreshToken] = useState(0);
|
|
196
|
+
const draftRevisionRef = useRef(0);
|
|
184
197
|
|
|
185
198
|
function setActiveTab(tab: WorkspaceTab) {
|
|
186
199
|
setSearchParams(
|
|
@@ -188,6 +201,10 @@ export default function FactoryRoute() {
|
|
|
188
201
|
const next = new URLSearchParams(current);
|
|
189
202
|
if (tab === "overview") next.delete("tab");
|
|
190
203
|
else next.set("tab", tab);
|
|
204
|
+
if (tab === "history") {
|
|
205
|
+
next.delete("node");
|
|
206
|
+
next.delete("edge");
|
|
207
|
+
}
|
|
191
208
|
return next;
|
|
192
209
|
},
|
|
193
210
|
{ replace: true },
|
|
@@ -249,17 +266,17 @@ export default function FactoryRoute() {
|
|
|
249
266
|
const graphData =
|
|
250
267
|
rawGraphData?.factory.id === factoryId ? rawGraphData : undefined;
|
|
251
268
|
const graph = draftGraph ?? graphData?.graph ?? null;
|
|
252
|
-
const graphVersion = graphData?.factory.graphVersion ??
|
|
269
|
+
const graphVersion = graph?.version ?? graphData?.factory.graphVersion ?? 1;
|
|
253
270
|
const saveGraphMutation = useActionMutation("save-factory-graph");
|
|
254
271
|
const factoryList = (factoryListQuery.data ?? []) as FactorySummary[];
|
|
255
272
|
|
|
256
273
|
useEffect(() => {
|
|
257
|
-
if (!graphData || creating) return;
|
|
274
|
+
if (!graphData || creating || dirty) return;
|
|
258
275
|
setDraftGraph(graphData.graph);
|
|
259
276
|
setDirty(false);
|
|
260
277
|
setSelectedNodeId(null);
|
|
261
278
|
setSelectedEdgeId(null);
|
|
262
|
-
}, [creating, graphData]);
|
|
279
|
+
}, [creating, dirty, graphData]);
|
|
263
280
|
|
|
264
281
|
useEffect(() => {
|
|
265
282
|
setSelectedNodeId(searchParams.get("node"));
|
|
@@ -307,6 +324,9 @@ export default function FactoryRoute() {
|
|
|
307
324
|
}
|
|
308
325
|
|
|
309
326
|
function updateGraph(next: FactoryCanvasGraph) {
|
|
327
|
+
draftRevisionRef.current += 1;
|
|
328
|
+
setSaveError(null);
|
|
329
|
+
setSaveConflictRemoteGraph(null);
|
|
310
330
|
setDraftGraph(next);
|
|
311
331
|
setDirty(
|
|
312
332
|
!graphData?.graph ||
|
|
@@ -390,20 +410,87 @@ export default function FactoryRoute() {
|
|
|
390
410
|
|
|
391
411
|
async function saveGraph() {
|
|
392
412
|
if (!graph || !selectedFactoryId) return;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
? "
|
|
401
|
-
: "
|
|
402
|
-
|
|
403
|
-
|
|
413
|
+
const submittedDraftRevision = draftRevisionRef.current;
|
|
414
|
+
setSaveError(null);
|
|
415
|
+
try {
|
|
416
|
+
await saveGraphMutation.mutateAsync({
|
|
417
|
+
factoryId: selectedFactoryId,
|
|
418
|
+
name: graph.name,
|
|
419
|
+
description: graph.description,
|
|
420
|
+
prompt: creating ? "" : (graphData?.factory.prompt ?? ""),
|
|
421
|
+
source: "manual",
|
|
422
|
+
changeSummary: creating
|
|
423
|
+
? "Created from the Factory visual editor."
|
|
424
|
+
: "Updated in the Factory visual editor.",
|
|
425
|
+
expectedGraphVersion: creating ? 0 : graph.version,
|
|
426
|
+
graph,
|
|
427
|
+
});
|
|
428
|
+
setCreating(false);
|
|
429
|
+
setDirty(draftRevisionRef.current !== submittedDraftRevision);
|
|
430
|
+
setSaveConflictRemoteGraph(null);
|
|
431
|
+
await Promise.all([graphQuery.refetch(), factoryListQuery.refetch()]);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
setSaveError(
|
|
434
|
+
error instanceof Error
|
|
435
|
+
? error.message
|
|
436
|
+
: t("factoryRoute.saveConflictFallback"),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function refreshFactoryAfterSaveConflict() {
|
|
442
|
+
setRefreshingFactory(true);
|
|
443
|
+
try {
|
|
444
|
+
const results = await Promise.all([
|
|
445
|
+
graphQuery.refetch(),
|
|
446
|
+
factoryListQuery.refetch(),
|
|
447
|
+
]);
|
|
448
|
+
if (results.some((result) => result.isError)) {
|
|
449
|
+
throw new Error("Factory refresh failed.");
|
|
450
|
+
}
|
|
451
|
+
const remoteGraph = (results[0].data as FactoryGraphResponse | undefined)
|
|
452
|
+
?.graph;
|
|
453
|
+
if (dirty) {
|
|
454
|
+
if (!remoteGraph) throw new Error("Factory graph refresh failed.");
|
|
455
|
+
setSaveConflictRemoteGraph(remoteGraph);
|
|
456
|
+
setSaveError(t("factoryRoute.saveConflictFallback"));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
setSaveError(null);
|
|
460
|
+
} catch {
|
|
461
|
+
setSaveError(t("factoryRoute.saveConflictFallback"));
|
|
462
|
+
} finally {
|
|
463
|
+
setRefreshingFactory(false);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function discardLocalFactoryChanges() {
|
|
468
|
+
if (!saveConflictRemoteGraph) return;
|
|
469
|
+
setDraftGraph(saveConflictRemoteGraph);
|
|
470
|
+
setSaveConflictRemoteGraph(null);
|
|
471
|
+
setSaveError(null);
|
|
472
|
+
setDirty(false);
|
|
404
473
|
setCreating(false);
|
|
474
|
+
setSelectedNodeId(null);
|
|
475
|
+
setSelectedEdgeId(null);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function handleFactoryRestored(result?: { graph: FactoryCanvasGraph }) {
|
|
479
|
+
setCreating(false);
|
|
480
|
+
if (result?.graph) setDraftGraph(result.graph);
|
|
481
|
+
else setDraftGraph(null);
|
|
405
482
|
setDirty(false);
|
|
406
|
-
|
|
483
|
+
setSaveError(null);
|
|
484
|
+
setSaveConflictRemoteGraph(null);
|
|
485
|
+
setSelectedNodeId(null);
|
|
486
|
+
setSelectedEdgeId(null);
|
|
487
|
+
const results = await Promise.all([
|
|
488
|
+
graphQuery.refetch(),
|
|
489
|
+
factoryListQuery.refetch(),
|
|
490
|
+
]);
|
|
491
|
+
if (results.some((result) => result.isError)) {
|
|
492
|
+
throw new Error("Factory view refresh failed.");
|
|
493
|
+
}
|
|
407
494
|
}
|
|
408
495
|
|
|
409
496
|
if (!selectedFactoryId && activeTab === "agents") {
|
|
@@ -653,6 +740,12 @@ export default function FactoryRoute() {
|
|
|
653
740
|
>
|
|
654
741
|
Activity
|
|
655
742
|
</TabButton>
|
|
743
|
+
<TabButton
|
|
744
|
+
active={activeTab === "history"}
|
|
745
|
+
onClick={() => setActiveTab("history")}
|
|
746
|
+
>
|
|
747
|
+
{t("factoryRoute.historyTab")}
|
|
748
|
+
</TabButton>
|
|
656
749
|
<TabButton
|
|
657
750
|
active={activeTab === "settings"}
|
|
658
751
|
onClick={() => setActiveTab("settings")}
|
|
@@ -721,8 +814,13 @@ export default function FactoryRoute() {
|
|
|
721
814
|
factoryId={factoryId}
|
|
722
815
|
dirty={dirty}
|
|
723
816
|
saving={saveGraphMutation.isPending}
|
|
817
|
+
saveError={saveError}
|
|
818
|
+
saveConflictNeedsResolution={Boolean(saveConflictRemoteGraph)}
|
|
819
|
+
refreshing={refreshingFactory}
|
|
724
820
|
onGraphChange={updateGraph}
|
|
725
821
|
onSave={() => void saveGraph()}
|
|
822
|
+
onRefresh={refreshFactoryAfterSaveConflict}
|
|
823
|
+
onDiscardLocalChanges={discardLocalFactoryChanges}
|
|
726
824
|
onAddNode={addNode}
|
|
727
825
|
onDeleteNode={deleteNode}
|
|
728
826
|
onConnect={connectNodes}
|
|
@@ -741,6 +839,13 @@ export default function FactoryRoute() {
|
|
|
741
839
|
factoryId={factoryId}
|
|
742
840
|
refreshToken={auditRefreshToken}
|
|
743
841
|
/>
|
|
842
|
+
) : activeTab === "history" ? (
|
|
843
|
+
<FactoryHistoryView
|
|
844
|
+
key={factoryId}
|
|
845
|
+
factoryId={factoryId}
|
|
846
|
+
hasUnsavedChanges={dirty}
|
|
847
|
+
onRestored={handleFactoryRestored}
|
|
848
|
+
/>
|
|
744
849
|
) : (
|
|
745
850
|
<SettingsView t={t} />
|
|
746
851
|
)}
|
|
@@ -777,6 +882,7 @@ function parseWorkspaceTab(value: string | null): WorkspaceTab {
|
|
|
777
882
|
value === "automations" ||
|
|
778
883
|
value === "agents" ||
|
|
779
884
|
value === "audit" ||
|
|
885
|
+
value === "history" ||
|
|
780
886
|
value === "settings"
|
|
781
887
|
? value
|
|
782
888
|
: "overview";
|
|
@@ -976,7 +1082,8 @@ function AutomationsView({
|
|
|
976
1082
|
current.body === nextDraft.body &&
|
|
977
1083
|
current.model === nextDraft.model &&
|
|
978
1084
|
current.schedule === nextDraft.schedule &&
|
|
979
|
-
current.enabled === nextDraft.enabled
|
|
1085
|
+
current.enabled === nextDraft.enabled &&
|
|
1086
|
+
current.updatedAt === nextDraft.updatedAt
|
|
980
1087
|
) {
|
|
981
1088
|
return current;
|
|
982
1089
|
}
|
|
@@ -986,16 +1093,25 @@ function AutomationsView({
|
|
|
986
1093
|
|
|
987
1094
|
async function saveAutomation() {
|
|
988
1095
|
if (!draft) return;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1096
|
+
try {
|
|
1097
|
+
await saveMutation.mutateAsync({
|
|
1098
|
+
factoryId,
|
|
1099
|
+
automationId: draft.id,
|
|
1100
|
+
name: draft.name,
|
|
1101
|
+
prompt: draft.prompt ?? draft.body ?? "",
|
|
1102
|
+
model: draft.model ?? "",
|
|
1103
|
+
schedule: draft.schedule ?? "",
|
|
1104
|
+
enabled: draft.enabled,
|
|
1105
|
+
});
|
|
1106
|
+
await automationsQuery.refetch();
|
|
1107
|
+
toast.success(t("factoryRoute.automationSaved"));
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
toast.error(
|
|
1110
|
+
error instanceof Error
|
|
1111
|
+
? error.message
|
|
1112
|
+
: t("factoryRoute.automationSaveFailed"),
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
999
1115
|
}
|
|
1000
1116
|
|
|
1001
1117
|
async function runAutomation() {
|
|
@@ -1031,27 +1147,34 @@ function AutomationsView({
|
|
|
1031
1147
|
role="tablist"
|
|
1032
1148
|
aria-label={t("factoryRoute.automationsTitle")}
|
|
1033
1149
|
>
|
|
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
|
-
|
|
1150
|
+
{automations.map((automation) => {
|
|
1151
|
+
const selected = activeAutomationId === automation.id;
|
|
1152
|
+
return (
|
|
1153
|
+
<button
|
|
1154
|
+
key={automation.id}
|
|
1155
|
+
type="button"
|
|
1156
|
+
id={`factory-automation-tab-${automation.id}`}
|
|
1157
|
+
role="tab"
|
|
1158
|
+
aria-selected={selected}
|
|
1159
|
+
aria-controls="factory-automation-panel"
|
|
1160
|
+
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 ${
|
|
1161
|
+
selected
|
|
1162
|
+
? "bg-primary/10 ring-1 ring-inset ring-primary/40"
|
|
1163
|
+
: "bg-muted/20 hover:bg-muted/50"
|
|
1164
|
+
}`}
|
|
1165
|
+
onClick={() => selectAutomation(automation.id)}
|
|
1166
|
+
>
|
|
1167
|
+
<span className="block truncate text-sm font-medium">
|
|
1168
|
+
{automation.name}
|
|
1169
|
+
</span>
|
|
1170
|
+
<span className="mt-1 block truncate text-xs text-muted-foreground">
|
|
1171
|
+
{automation.enabled
|
|
1172
|
+
? t("factoryRoute.automationEnabled")
|
|
1173
|
+
: t("factoryRoute.automationDisabled")}
|
|
1174
|
+
</span>
|
|
1175
|
+
</button>
|
|
1176
|
+
);
|
|
1177
|
+
})}
|
|
1055
1178
|
</div>
|
|
1056
1179
|
)}
|
|
1057
1180
|
</CardContent>
|
|
@@ -1189,8 +1312,24 @@ function AutomationsView({
|
|
|
1189
1312
|
</div>
|
|
1190
1313
|
</div>
|
|
1191
1314
|
<div className="grid gap-1.5">
|
|
1192
|
-
<
|
|
1315
|
+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
1316
|
+
<Label htmlFor="factory-automation-prompt">
|
|
1317
|
+
{t("factoryRoute.automationPrompt")}
|
|
1318
|
+
</Label>
|
|
1319
|
+
{draft.updatedAt ? (
|
|
1320
|
+
<span className="capitalize text-xs text-muted-foreground">
|
|
1321
|
+
{t("factoryRoute.automationLastUpdated")}{" "}
|
|
1322
|
+
<time
|
|
1323
|
+
dateTime={new Date(draft.updatedAt).toISOString()}
|
|
1324
|
+
title={formatAutomationDate(draft.updatedAt)}
|
|
1325
|
+
>
|
|
1326
|
+
{formatInboxDateTime(draft.updatedAt)}
|
|
1327
|
+
</time>
|
|
1328
|
+
</span>
|
|
1329
|
+
) : null}
|
|
1330
|
+
</div>
|
|
1193
1331
|
<Textarea
|
|
1332
|
+
id="factory-automation-prompt"
|
|
1194
1333
|
value={draft.prompt ?? draft.body ?? ""}
|
|
1195
1334
|
onChange={(event) =>
|
|
1196
1335
|
setDraft({ ...draft, prompt: event.target.value })
|
|
@@ -1226,11 +1365,25 @@ function AutomationsView({
|
|
|
1226
1365
|
</span>
|
|
1227
1366
|
{run.threadId && (
|
|
1228
1367
|
<a
|
|
1229
|
-
className="inline-flex items-center
|
|
1368
|
+
className="inline-flex items-center text-xs text-muted-foreground underline-offset-4 hover:underline"
|
|
1230
1369
|
href={`/chat/${encodeURIComponent(run.threadId)}`}
|
|
1370
|
+
onClick={(event) => {
|
|
1371
|
+
if (
|
|
1372
|
+
event.metaKey ||
|
|
1373
|
+
event.ctrlKey ||
|
|
1374
|
+
event.shiftKey ||
|
|
1375
|
+
event.altKey ||
|
|
1376
|
+
event.button !== 0
|
|
1377
|
+
) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
event.preventDefault();
|
|
1381
|
+
requestAgentChatThreadOpen({
|
|
1382
|
+
threadId: run.threadId!,
|
|
1383
|
+
});
|
|
1384
|
+
}}
|
|
1231
1385
|
>
|
|
1232
1386
|
{t("factoryRoute.automationOpenThread")}
|
|
1233
|
-
<IconExternalLink className="size-3" />
|
|
1234
1387
|
</a>
|
|
1235
1388
|
)}
|
|
1236
1389
|
{run.error && (
|
|
@@ -1259,6 +1412,52 @@ function formatAutomationDate(value: string | number | null | undefined) {
|
|
|
1259
1412
|
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
1260
1413
|
}
|
|
1261
1414
|
|
|
1415
|
+
function formatInboxDateTime(value: string | number | null | undefined) {
|
|
1416
|
+
if (!value) return null;
|
|
1417
|
+
const date = new Date(value);
|
|
1418
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
1419
|
+
return date.toLocaleString(undefined, {
|
|
1420
|
+
month: "short",
|
|
1421
|
+
day: "numeric",
|
|
1422
|
+
year: "numeric",
|
|
1423
|
+
hour: "numeric",
|
|
1424
|
+
minute: "2-digit",
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function formatInboxAge(value: string | number | null | undefined) {
|
|
1429
|
+
if (!value) return null;
|
|
1430
|
+
const date = new Date(value);
|
|
1431
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
1432
|
+
|
|
1433
|
+
const elapsedSeconds = Math.max(
|
|
1434
|
+
0,
|
|
1435
|
+
Math.floor((Date.now() - date.getTime()) / 1_000),
|
|
1436
|
+
);
|
|
1437
|
+
if (elapsedSeconds < 60) return "now";
|
|
1438
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
1439
|
+
if (elapsedMinutes < 60) return `${elapsedMinutes}m`;
|
|
1440
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
1441
|
+
if (elapsedHours < 24) return `${elapsedHours}h`;
|
|
1442
|
+
const elapsedDays = Math.floor(elapsedHours / 24);
|
|
1443
|
+
if (elapsedDays < 30) return `${elapsedDays}d`;
|
|
1444
|
+
const elapsedMonths = Math.floor(elapsedDays / 30);
|
|
1445
|
+
if (elapsedMonths < 12) return `${elapsedMonths}mo`;
|
|
1446
|
+
return `${Math.floor(elapsedMonths / 12)}y`;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const reviewListColumns =
|
|
1450
|
+
"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";
|
|
1451
|
+
|
|
1452
|
+
function formatInboxSource(source: string | null | undefined) {
|
|
1453
|
+
const normalized = source?.toLowerCase() ?? "";
|
|
1454
|
+
if (normalized.includes("slack")) return "Slack";
|
|
1455
|
+
if (normalized.includes("github")) return "GitHub";
|
|
1456
|
+
if (normalized.includes("sentry")) return "Sentry";
|
|
1457
|
+
const trimmed = source?.trim();
|
|
1458
|
+
return trimmed ? trimmed : "Source";
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1262
1461
|
function formatModelName(model: string | null | undefined) {
|
|
1263
1462
|
const value = model?.trim();
|
|
1264
1463
|
if (!value) return "the app default";
|
|
@@ -1279,6 +1478,7 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1279
1478
|
);
|
|
1280
1479
|
const [feedbackNote, setFeedbackNote] = useState("");
|
|
1281
1480
|
const [verdict, setVerdict] = useState<Verdict | null>(null);
|
|
1481
|
+
const selectedRowRef = useRef<HTMLButtonElement | null>(null);
|
|
1282
1482
|
const listQuery = useActionQuery("list-triage-items", {
|
|
1283
1483
|
limit: 50,
|
|
1284
1484
|
...(status.trim()
|
|
@@ -1312,6 +1512,14 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1312
1512
|
setSelectedId(searchParams.get("itemId"));
|
|
1313
1513
|
}, [searchParams]);
|
|
1314
1514
|
|
|
1515
|
+
useEffect(() => {
|
|
1516
|
+
if (!selectedId) return;
|
|
1517
|
+
selectedRowRef.current?.scrollIntoView({
|
|
1518
|
+
block: "nearest",
|
|
1519
|
+
inline: "nearest",
|
|
1520
|
+
});
|
|
1521
|
+
}, [selectedId, normalizedItems.length]);
|
|
1522
|
+
|
|
1315
1523
|
function selectItem(itemId: string) {
|
|
1316
1524
|
setSelectedId(itemId);
|
|
1317
1525
|
setVerdict(null);
|
|
@@ -1371,19 +1579,51 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1371
1579
|
</p>
|
|
1372
1580
|
) : (
|
|
1373
1581
|
<div className="grid gap-1.5 p-2">
|
|
1582
|
+
<div
|
|
1583
|
+
className={`hidden px-4 py-2 text-[11px] uppercase tracking-wide text-muted-foreground sm:grid ${reviewListColumns}`}
|
|
1584
|
+
>
|
|
1585
|
+
<span />
|
|
1586
|
+
<span>{t("triage.risk")}</span>
|
|
1587
|
+
<span>{t("triage.status")}</span>
|
|
1588
|
+
<span>{t("triage.updatedAt")}</span>
|
|
1589
|
+
<span>{t("triage.reason")}</span>
|
|
1590
|
+
</div>
|
|
1374
1591
|
{normalizedItems.map((item) => {
|
|
1375
1592
|
const id = item.itemId ?? item.id ?? "";
|
|
1593
|
+
const selected = selectedId === id;
|
|
1594
|
+
const createdAtLabel = formatInboxDateTime(item.createdAt);
|
|
1595
|
+
const updatedAge = formatInboxAge(item.updatedAt);
|
|
1376
1596
|
return (
|
|
1377
1597
|
<button
|
|
1378
1598
|
key={id}
|
|
1599
|
+
ref={selected ? selectedRowRef : undefined}
|
|
1379
1600
|
type="button"
|
|
1380
|
-
|
|
1601
|
+
aria-current={selected ? "true" : undefined}
|
|
1602
|
+
className={`grid rounded-lg px-4 py-3 text-left transition-colors ${reviewListColumns} ${
|
|
1603
|
+
selected
|
|
1604
|
+
? "bg-primary/10 ring-1 ring-inset ring-primary/40"
|
|
1605
|
+
: "bg-muted/20 hover:bg-muted/50"
|
|
1606
|
+
}`}
|
|
1381
1607
|
onClick={() => selectItem(id)}
|
|
1382
1608
|
>
|
|
1383
1609
|
<span className="min-w-0">
|
|
1610
|
+
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1611
|
+
{formatInboxSource(item.source ?? item.sourceName)}
|
|
1612
|
+
</span>
|
|
1384
1613
|
<span className="block truncate text-sm font-medium">
|
|
1385
|
-
{item.
|
|
1614
|
+
{item.title?.trim() ||
|
|
1615
|
+
item.sourceName ||
|
|
1616
|
+
item.source ||
|
|
1617
|
+
"Untitled"}
|
|
1386
1618
|
</span>
|
|
1619
|
+
{createdAtLabel && item.createdAt && (
|
|
1620
|
+
<time
|
|
1621
|
+
className="mt-0.5 block text-xs text-muted-foreground"
|
|
1622
|
+
dateTime={item.createdAt}
|
|
1623
|
+
>
|
|
1624
|
+
{createdAtLabel}
|
|
1625
|
+
</time>
|
|
1626
|
+
)}
|
|
1387
1627
|
{item.sourceUrl && (
|
|
1388
1628
|
<span className="mt-1 flex max-w-full items-center gap-1 truncate text-xs text-primary">
|
|
1389
1629
|
{item.sourceUrl}
|
|
@@ -1392,20 +1632,36 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1392
1632
|
)}
|
|
1393
1633
|
</span>
|
|
1394
1634
|
<span>
|
|
1395
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1396
|
-
|
|
1635
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1636
|
+
{t("triage.risk")}
|
|
1397
1637
|
</span>
|
|
1398
1638
|
<TriageStatusPill status={item.risk} />
|
|
1399
1639
|
</span>
|
|
1400
1640
|
<span>
|
|
1401
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1402
|
-
|
|
1641
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1642
|
+
{t("triage.status")}
|
|
1403
1643
|
</span>
|
|
1404
1644
|
<TriageStatusPill status={item.status} />
|
|
1405
1645
|
</span>
|
|
1406
|
-
<span
|
|
1407
|
-
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1408
|
-
|
|
1646
|
+
<span>
|
|
1647
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1648
|
+
{t("triage.updatedAt")}
|
|
1649
|
+
</span>
|
|
1650
|
+
{updatedAge && item.updatedAt ? (
|
|
1651
|
+
<time
|
|
1652
|
+
className="text-sm tabular-nums text-muted-foreground"
|
|
1653
|
+
dateTime={item.updatedAt}
|
|
1654
|
+
title={formatAutomationDate(item.updatedAt)}
|
|
1655
|
+
>
|
|
1656
|
+
{updatedAge}
|
|
1657
|
+
</time>
|
|
1658
|
+
) : (
|
|
1659
|
+
<span className="text-sm text-muted-foreground">-</span>
|
|
1660
|
+
)}
|
|
1661
|
+
</span>
|
|
1662
|
+
<span className="min-w-0 truncate">
|
|
1663
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-muted-foreground sm:hidden">
|
|
1664
|
+
{t("triage.reason")}
|
|
1409
1665
|
</span>
|
|
1410
1666
|
<span className="text-sm">
|
|
1411
1667
|
{item.reason ?? item.decisionSummary ?? "-"}
|
|
@@ -1431,8 +1687,15 @@ function InboxView({ t }: { t: ReturnType<typeof useT> }) {
|
|
|
1431
1687
|
<>
|
|
1432
1688
|
<div className="flex items-start justify-between gap-3">
|
|
1433
1689
|
<div>
|
|
1690
|
+
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
1691
|
+
{formatInboxSource(
|
|
1692
|
+
selectedItem.source ?? selectedItem.sourceName,
|
|
1693
|
+
)}
|
|
1694
|
+
</p>
|
|
1434
1695
|
<p className="text-sm font-medium">
|
|
1435
|
-
{selectedItem.
|
|
1696
|
+
{selectedItem.title?.trim() ||
|
|
1697
|
+
selectedItem.sourceName ||
|
|
1698
|
+
selectedItem.source}
|
|
1436
1699
|
</p>
|
|
1437
1700
|
{selectedItem.sourceUrl && (
|
|
1438
1701
|
<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
|
+
}
|