@sentry/junior-dashboard 0.276.0 → 0.277.0
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/dist/app.js +168 -33
- package/dist/client.js +88 -83
- package/dist/index.d.ts +1 -1
- package/dist/index.js +163 -31
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +80 -12
- package/src/auth.ts +5 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/conversations/AutoArchiveToggle.tsx +35 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationDetails.tsx +6 -3
- package/src/client/conversations/ConversationHeader.tsx +9 -0
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationMemories.tsx +64 -67
- package/src/client/conversations/ConversationPage.tsx +21 -8
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationWatches.tsx +15 -21
- package/src/client/conversations/ConversationWorkspace.tsx +4 -4
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- package/src/client/conversations/TranscriptMemoryPopover.tsx +20 -25
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +2 -1
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/queries.ts +48 -238
- package/src/client/conversations/usePinConversation.ts +88 -0
- package/src/client/display-text/DisplayText.tsx +7 -2
- package/src/client/display-text/codeBlocks.ts +92 -0
- package/src/client/format.ts +3 -67
- package/src/client/pages/code/RepositoryTabs.tsx +2 -2
- package/src/client/pages/settings/AutoArchiveSettings.tsx +145 -0
- package/src/client/pages/settings/SettingsPage.tsx +2 -0
- package/src/client/review-desk/ConversationReviewItems.tsx +7 -9
- package/src/client/review-desk/queries.ts +2 -1
- package/src/client/types.ts +1 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AUTO_ARCHIVE_MAX_GRACE_HOURS,
|
|
5
|
+
dashboardIdentitySchema,
|
|
6
|
+
type DashboardIdentity,
|
|
7
|
+
} from "../../../api/schema";
|
|
8
|
+
import { Button } from "../../components/ui/button";
|
|
9
|
+
import { Checkbox } from "../../components/ui/checkbox";
|
|
10
|
+
import { Field, FieldDescription, FieldLabel } from "../../components/ui/field";
|
|
11
|
+
import { NativeSelect } from "../../components/ui/native-select";
|
|
12
|
+
import { InlineError } from "../../components/InlineError";
|
|
13
|
+
import { Card } from "../../components/layout/Card";
|
|
14
|
+
import { useArchiveDoneConversations } from "../../conversations/autoArchiveQueries";
|
|
15
|
+
import { patch } from "../../http";
|
|
16
|
+
import type { DashboardCoreData } from "../../types";
|
|
17
|
+
|
|
18
|
+
const dashboardCoreQueryKey = ["dashboard", "core"] as const;
|
|
19
|
+
|
|
20
|
+
/** Grace periods offered in Settings, in hours. */
|
|
21
|
+
const AUTO_ARCHIVE_GRACE_OPTIONS = [1, 6, 24, 72, AUTO_ARCHIVE_MAX_GRACE_HOURS];
|
|
22
|
+
|
|
23
|
+
function formatArchivedCount(count: number): string {
|
|
24
|
+
if (count === 0) return "No done conversations to archive.";
|
|
25
|
+
return count === 1
|
|
26
|
+
? "Archived 1 conversation."
|
|
27
|
+
: `Archived ${count} conversations.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatGraceHours(hours: number): string {
|
|
31
|
+
if (hours % 24 !== 0) return hours === 1 ? "1 hour" : `${hours} hours`;
|
|
32
|
+
const days = hours / 24;
|
|
33
|
+
if (days === 7) return "1 week";
|
|
34
|
+
return days === 1 ? "1 day" : `${days} days`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Let the user turn auto-archive on for their conversations and pick how long
|
|
39
|
+
* finished work must stay quiet before Junior archives it.
|
|
40
|
+
*/
|
|
41
|
+
export function AutoArchiveSettings({
|
|
42
|
+
identity,
|
|
43
|
+
}: {
|
|
44
|
+
identity: DashboardIdentity;
|
|
45
|
+
}) {
|
|
46
|
+
const queryClient = useQueryClient();
|
|
47
|
+
const update = useMutation({
|
|
48
|
+
mutationFn: (
|
|
49
|
+
settings: Pick<
|
|
50
|
+
DashboardIdentity,
|
|
51
|
+
"autoArchiveDefault" | "autoArchiveGraceHours"
|
|
52
|
+
>,
|
|
53
|
+
) => patch(dashboardIdentitySchema, "/api/me", settings),
|
|
54
|
+
onSuccess: (updated) => {
|
|
55
|
+
queryClient.setQueryData<DashboardCoreData>(
|
|
56
|
+
dashboardCoreQueryKey,
|
|
57
|
+
(current) => (current ? { ...current, me: updated } : current),
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const archiveDone = useArchiveDoneConversations();
|
|
62
|
+
const enabled = identity.autoArchiveDefault ?? false;
|
|
63
|
+
const graceHours = identity.autoArchiveGraceHours ?? 24;
|
|
64
|
+
// Keep a saved value that is not a preset, so the select shows the truth.
|
|
65
|
+
const graceOptions = AUTO_ARCHIVE_GRACE_OPTIONS.includes(graceHours)
|
|
66
|
+
? AUTO_ARCHIVE_GRACE_OPTIONS
|
|
67
|
+
: [...AUTO_ARCHIVE_GRACE_OPTIONS, graceHours].sort((a, b) => a - b);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Card className="mt-6 mb-0" padding="md" variant="raised">
|
|
71
|
+
<h2 className="m-0 text-lg font-bold">Conversations</h2>
|
|
72
|
+
<label className="mt-4 flex cursor-pointer items-start gap-3">
|
|
73
|
+
<Checkbox
|
|
74
|
+
checked={enabled}
|
|
75
|
+
className="mt-0.5"
|
|
76
|
+
disabled={update.isPending}
|
|
77
|
+
onCheckedChange={(checked) =>
|
|
78
|
+
update.mutate({ autoArchiveDefault: checked })
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
<span className="grid gap-0.5">
|
|
82
|
+
<span className="text-sm font-medium">
|
|
83
|
+
Auto-archive conversations when done
|
|
84
|
+
</span>
|
|
85
|
+
<span className="text-xs text-dashboard-text-muted">
|
|
86
|
+
Archive a conversation for you when all of its linked work, such as
|
|
87
|
+
pull requests, is merged or closed and nobody replies during the
|
|
88
|
+
wait time. You can change this for each conversation in its details.
|
|
89
|
+
</span>
|
|
90
|
+
</span>
|
|
91
|
+
</label>
|
|
92
|
+
<Field className="mt-5 max-w-xs">
|
|
93
|
+
<FieldLabel htmlFor="auto-archive-grace">Wait time</FieldLabel>
|
|
94
|
+
<NativeSelect
|
|
95
|
+
aria-describedby="auto-archive-grace-help"
|
|
96
|
+
disabled={update.isPending}
|
|
97
|
+
id="auto-archive-grace"
|
|
98
|
+
onChange={(event) =>
|
|
99
|
+
update.mutate({ autoArchiveGraceHours: Number(event.target.value) })
|
|
100
|
+
}
|
|
101
|
+
value={graceHours}
|
|
102
|
+
>
|
|
103
|
+
{graceOptions.map((hours) => (
|
|
104
|
+
<option key={hours} value={hours}>
|
|
105
|
+
{formatGraceHours(hours)}
|
|
106
|
+
</option>
|
|
107
|
+
))}
|
|
108
|
+
</NativeSelect>
|
|
109
|
+
<FieldDescription id="auto-archive-grace-help">
|
|
110
|
+
How long finished work and the conversation stay quiet before archive.
|
|
111
|
+
</FieldDescription>
|
|
112
|
+
</Field>
|
|
113
|
+
{update.isError ? (
|
|
114
|
+
<InlineError className="mt-3">
|
|
115
|
+
Could not save this setting. Try again.
|
|
116
|
+
</InlineError>
|
|
117
|
+
) : null}
|
|
118
|
+
<div className="mt-5 grid gap-1.5">
|
|
119
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
120
|
+
<Button
|
|
121
|
+
disabled={archiveDone.isPending}
|
|
122
|
+
onClick={() => archiveDone.mutate()}
|
|
123
|
+
type="button"
|
|
124
|
+
>
|
|
125
|
+
{archiveDone.isPending ? "Archiving…" : "Archive done now"}
|
|
126
|
+
</Button>
|
|
127
|
+
{archiveDone.isSuccess ? (
|
|
128
|
+
<p className="m-0 text-sm text-dashboard-text-muted" role="status">
|
|
129
|
+
{formatArchivedCount(archiveDone.data.archived)}
|
|
130
|
+
</p>
|
|
131
|
+
) : null}
|
|
132
|
+
{archiveDone.isError ? (
|
|
133
|
+
<InlineError>
|
|
134
|
+
Could not archive conversations. Try again.
|
|
135
|
+
</InlineError>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
139
|
+
Archive every done conversation once, without the wait time. Skips
|
|
140
|
+
conversations where you turned auto-archive off.
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</Card>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -24,6 +24,7 @@ import { InlineError } from "../../components/InlineError";
|
|
|
24
24
|
import { Card } from "../../components/layout/Card";
|
|
25
25
|
import { DashboardApiError, fetchDashboardJson, patch, put } from "../../http";
|
|
26
26
|
import type { DashboardCoreData } from "../../types";
|
|
27
|
+
import { AutoArchiveSettings } from "./AutoArchiveSettings";
|
|
27
28
|
import { SettingsPageLayout } from "./SettingsPageLayout";
|
|
28
29
|
|
|
29
30
|
const dashboardCoreQueryKey = ["dashboard", "core"] as const;
|
|
@@ -108,6 +109,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
|
|
|
108
109
|
</form>
|
|
109
110
|
<ConversationStartSetting />
|
|
110
111
|
<ComposerAutocorrectSetting />
|
|
112
|
+
<AutoArchiveSettings identity={identity} />
|
|
111
113
|
<CustomizationForm />
|
|
112
114
|
</SettingsPageLayout>
|
|
113
115
|
);
|
|
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
4
4
|
|
|
5
5
|
import { LoadMorePagination } from "../components/Pagination";
|
|
6
|
+
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
6
7
|
import {
|
|
7
8
|
DetailsCard,
|
|
8
9
|
DetailsList,
|
|
@@ -74,9 +75,9 @@ function ReviewItemRow(props: {
|
|
|
74
75
|
if (props.defaultExpanded) setExpanded(true);
|
|
75
76
|
}, [props.defaultExpanded]);
|
|
76
77
|
return (
|
|
77
|
-
|
|
78
|
+
<Collapsible className="min-w-0" onOpenChange={setExpanded} open={expanded}>
|
|
78
79
|
<DetailsRow
|
|
79
|
-
|
|
80
|
+
disclosure
|
|
80
81
|
icon={<Inbox aria-hidden="true" className="size-3.5" />}
|
|
81
82
|
meta={
|
|
82
83
|
<span className="grid size-5 place-items-center">
|
|
@@ -86,14 +87,11 @@ function ReviewItemRow(props: {
|
|
|
86
87
|
/>
|
|
87
88
|
</span>
|
|
88
89
|
}
|
|
89
|
-
onToggle={() => setExpanded((value) => !value)}
|
|
90
90
|
title={props.item.title}
|
|
91
91
|
/>
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
) : null}
|
|
97
|
-
</>
|
|
92
|
+
<CollapsibleContent className="px-1.5 pb-1">
|
|
93
|
+
<ReviewItemCard item={props.item} />
|
|
94
|
+
</CollapsibleContent>
|
|
95
|
+
</Collapsible>
|
|
98
96
|
);
|
|
99
97
|
}
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
import { conversationDetailTitle } from "../format";
|
|
21
21
|
import { fetchDashboardJson, post } from "../http";
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/** Prefix of every Review item read, for refreshes after a change. */
|
|
24
|
+
export const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
|
|
24
25
|
const COUNT_REFRESH_MS = 60_000;
|
|
25
26
|
|
|
26
27
|
/**
|
package/src/client/types.ts
CHANGED
|
@@ -176,6 +176,7 @@ export type Conversation = {
|
|
|
176
176
|
annotations?: ConversationSummaryReport["annotations"];
|
|
177
177
|
sidebarAnnotations?: ConversationSummaryReport["sidebarAnnotations"];
|
|
178
178
|
archivedAt?: string | null;
|
|
179
|
+
pinnedAt?: string | null;
|
|
179
180
|
auxiliaryCosts?: ConversationSummaryReport["auxiliaryCosts"];
|
|
180
181
|
channel?: string;
|
|
181
182
|
channelName?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
3
|
|
|
4
|
-
/** Revalidate
|
|
4
|
+
/** Revalidate mock detail reads on content, after the route checks viewer access. */
|
|
5
5
|
export const revalidateConversation: MiddlewareHandler = async (
|
|
6
6
|
context,
|
|
7
7
|
next,
|
|
@@ -135,12 +135,36 @@ const DASHBOARD_QA_PLAN_CHECK_ID =
|
|
|
135
135
|
"junior:internal:dashboard-qa:advisor-plan:claim-check";
|
|
136
136
|
/** Process-local archive state so mock restore can be exercised in visual QA. */
|
|
137
137
|
const mockArchivedConversationIds = new Set<string>([ARCHIVED_CONVERSATION_ID]);
|
|
138
|
+
/** Process-local pins so the Pinned section can be exercised in visual QA. */
|
|
139
|
+
const mockPinnedConversationIds = new Set<string>();
|
|
138
140
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
+
/** Process-local auto-archive choices for mock visual QA. */
|
|
142
|
+
const mockAutoArchiveByConversationId = new Map<string, boolean>();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Restore process-local archive, pin, and auto-archive state so tests sharing
|
|
146
|
+
* a worker do not leak state.
|
|
147
|
+
*/
|
|
148
|
+
export function resetMockConversationViewerState(): void {
|
|
141
149
|
mockArchivedConversationIds.clear();
|
|
142
150
|
mockArchivedConversationIds.add(ARCHIVED_CONVERSATION_ID);
|
|
151
|
+
mockPinnedConversationIds.clear();
|
|
152
|
+
mockAutoArchiveByConversationId.clear();
|
|
143
153
|
}
|
|
154
|
+
|
|
155
|
+
/** Add the viewer's mock archive and pin times to one conversation. */
|
|
156
|
+
function withMockViewerState<T extends { conversationId: string }>(
|
|
157
|
+
report: T & { archivedAt?: string | null },
|
|
158
|
+
): T & { archivedAt: string | undefined; pinnedAt: string | null } {
|
|
159
|
+
return {
|
|
160
|
+
...report,
|
|
161
|
+
archivedAt: mockArchivedConversationIds.has(report.conversationId)
|
|
162
|
+
? (report.archivedAt ?? iso(NOW_MS, -2 * 24 * 60 * 60_000))
|
|
163
|
+
: undefined,
|
|
164
|
+
pinnedAt: mockPinnedConversationIds.has(report.conversationId) ? NOW : null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
const PEOPLE_ACTIVITY_DAYS = 90;
|
|
145
169
|
const PEOPLE_PROFILE_ACTIVITY_DAYS = 365;
|
|
146
170
|
const PUBLIC_MOCK_CHANNEL_IDS = new Set([
|
|
@@ -1657,19 +1681,11 @@ function summaryFromConversation(
|
|
|
1657
1681
|
...summary,
|
|
1658
1682
|
...(activityPreview ? { activityPreview } : undefined),
|
|
1659
1683
|
};
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
? {
|
|
1664
|
-
|
|
1665
|
-
archivedAt:
|
|
1666
|
-
summaryWithActivity.archivedAt ?? iso(NOW_MS, -2 * 24 * 60 * 60_000),
|
|
1667
|
-
}
|
|
1668
|
-
: { ...summaryWithActivity, archivedAt: undefined };
|
|
1669
|
-
return withArchiveState.channel &&
|
|
1670
|
-
PUBLIC_MOCK_CHANNEL_IDS.has(withArchiveState.channel)
|
|
1671
|
-
? { ...withArchiveState, locationId: `mock:${withArchiveState.channel}` }
|
|
1672
|
-
: withArchiveState;
|
|
1684
|
+
const withViewerState = withMockViewerState(summaryWithActivity);
|
|
1685
|
+
return withViewerState.channel &&
|
|
1686
|
+
PUBLIC_MOCK_CHANNEL_IDS.has(withViewerState.channel)
|
|
1687
|
+
? { ...withViewerState, locationId: `mock:${withViewerState.channel}` }
|
|
1688
|
+
: withViewerState;
|
|
1673
1689
|
}
|
|
1674
1690
|
|
|
1675
1691
|
function mockConversationFeed(nowMs: number): ConversationFeed {
|
|
@@ -1924,11 +1940,70 @@ export function setMockConversationArchived(
|
|
|
1924
1940
|
(conversation) => conversation.conversationId === conversationId,
|
|
1925
1941
|
);
|
|
1926
1942
|
if (!exists) return undefined;
|
|
1927
|
-
if (archived)
|
|
1928
|
-
|
|
1943
|
+
if (archived) {
|
|
1944
|
+
mockArchivedConversationIds.add(conversationId);
|
|
1945
|
+
// Match the API: archive also unpins.
|
|
1946
|
+
mockPinnedConversationIds.delete(conversationId);
|
|
1947
|
+
} else {
|
|
1948
|
+
mockArchivedConversationIds.delete(conversationId);
|
|
1949
|
+
// Match the API: a manual restore turns auto-archive off.
|
|
1950
|
+
mockAutoArchiveByConversationId.set(conversationId, false);
|
|
1951
|
+
}
|
|
1929
1952
|
return { archivedAt: archived ? NOW : null };
|
|
1930
1953
|
}
|
|
1931
1954
|
|
|
1955
|
+
/** Pin or unpin one mock conversation for local dashboard visual QA. */
|
|
1956
|
+
export function setMockConversationPinned(
|
|
1957
|
+
conversationId: string,
|
|
1958
|
+
pinned: boolean,
|
|
1959
|
+
): { pinnedAt: string | null } | undefined {
|
|
1960
|
+
const exists = mockConversations(NOW_MS).some(
|
|
1961
|
+
(conversation) => conversation.conversationId === conversationId,
|
|
1962
|
+
);
|
|
1963
|
+
if (!exists) return undefined;
|
|
1964
|
+
if (pinned) {
|
|
1965
|
+
mockPinnedConversationIds.add(conversationId);
|
|
1966
|
+
// Match the API: a pin also restores an archived conversation.
|
|
1967
|
+
mockArchivedConversationIds.delete(conversationId);
|
|
1968
|
+
} else {
|
|
1969
|
+
mockPinnedConversationIds.delete(conversationId);
|
|
1970
|
+
}
|
|
1971
|
+
return { pinnedAt: pinned ? NOW : null };
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/** Archive every done mock conversation once, like the archive-done API. */
|
|
1975
|
+
export function archiveMockDoneConversations(): { archived: number } {
|
|
1976
|
+
let archived = 0;
|
|
1977
|
+
for (const summary of mockConversationFeed(NOW_MS).conversations) {
|
|
1978
|
+
if (
|
|
1979
|
+
summary.archivedAt ||
|
|
1980
|
+
summary.pinnedAt ||
|
|
1981
|
+
!summary.assignedWork ||
|
|
1982
|
+
summary.unfinishedWork ||
|
|
1983
|
+
!summary.finishedWorkAt ||
|
|
1984
|
+
mockAutoArchiveByConversationId.get(summary.conversationId) === false
|
|
1985
|
+
) {
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
mockArchivedConversationIds.add(summary.conversationId);
|
|
1989
|
+
archived += 1;
|
|
1990
|
+
}
|
|
1991
|
+
return { archived };
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/** Save a mock auto-archive choice for local dashboard visual QA. */
|
|
1995
|
+
export function setMockConversationAutoArchive(
|
|
1996
|
+
conversationId: string,
|
|
1997
|
+
autoArchive: boolean,
|
|
1998
|
+
): { autoArchive: boolean } | undefined {
|
|
1999
|
+
const exists = mockConversations(NOW_MS).some(
|
|
2000
|
+
(conversation) => conversation.conversationId === conversationId,
|
|
2001
|
+
);
|
|
2002
|
+
if (!exists) return undefined;
|
|
2003
|
+
mockAutoArchiveByConversationId.set(conversationId, autoArchive);
|
|
2004
|
+
return { autoArchive };
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1932
2007
|
/** Return accepted mailbox rows for local dashboard visual QA. */
|
|
1933
2008
|
export function readMockConversationPendingMessages(
|
|
1934
2009
|
conversationId: string,
|
|
@@ -2151,22 +2226,23 @@ export function readMockConversationDetail(
|
|
|
2151
2226
|
canFork:
|
|
2152
2227
|
!parentConversationId &&
|
|
2153
2228
|
conversationDetail.eventHistory.status !== "redacted",
|
|
2229
|
+
...(conversationDetail.isParticipant
|
|
2230
|
+
? {
|
|
2231
|
+
autoArchive:
|
|
2232
|
+
mockAutoArchiveByConversationId.get(conversationId) ?? false,
|
|
2233
|
+
}
|
|
2234
|
+
: undefined),
|
|
2154
2235
|
};
|
|
2155
|
-
const
|
|
2156
|
-
|
|
2157
|
-
...detail,
|
|
2158
|
-
archivedAt: detail.archivedAt ?? iso(NOW_MS, -2 * 24 * 60 * 60_000),
|
|
2159
|
-
}
|
|
2160
|
-
: { ...detail, archivedAt: undefined };
|
|
2161
|
-
const events = withArchiveState.events.slice(-limit);
|
|
2236
|
+
const withViewerState = withMockViewerState(detail);
|
|
2237
|
+
const events = withViewerState.events.slice(-limit);
|
|
2162
2238
|
const bounded =
|
|
2163
|
-
events.length <
|
|
2239
|
+
events.length < withViewerState.events.length && events[0]
|
|
2164
2240
|
? {
|
|
2165
|
-
...
|
|
2241
|
+
...withViewerState,
|
|
2166
2242
|
events,
|
|
2167
2243
|
previousCursor: mockBeforeCursor(conversationId, events[0].seq),
|
|
2168
2244
|
}
|
|
2169
|
-
:
|
|
2245
|
+
: withViewerState;
|
|
2170
2246
|
return bounded.channel && PUBLIC_MOCK_CHANNEL_IDS.has(bounded.channel)
|
|
2171
2247
|
? { ...bounded, locationId: `mock:${bounded.channel}` }
|
|
2172
2248
|
: bounded;
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
actorProfileReportSchema,
|
|
8
8
|
archiveConversationBodySchema,
|
|
9
9
|
archiveConversationResponseSchema,
|
|
10
|
+
pinConversationBodySchema,
|
|
11
|
+
pinConversationResponseSchema,
|
|
12
|
+
archiveDoneConversationsResponseSchema,
|
|
13
|
+
conversationAutoArchiveSchema,
|
|
10
14
|
conversationChildAgentsReportSchema,
|
|
11
15
|
conversationDetailQuerySchema,
|
|
12
16
|
conversationDetailReportSchema,
|
|
@@ -66,6 +70,9 @@ import {
|
|
|
66
70
|
readMockAutomationExecutions,
|
|
67
71
|
readMockAutomationList,
|
|
68
72
|
setMockConversationArchived,
|
|
73
|
+
setMockConversationPinned,
|
|
74
|
+
setMockConversationAutoArchive,
|
|
75
|
+
archiveMockDoneConversations,
|
|
69
76
|
} from "./fixtures";
|
|
70
77
|
import { readMockConversationWatches } from "./watches";
|
|
71
78
|
|
|
@@ -289,6 +296,46 @@ export function createMockReportingApi(): Hono<{
|
|
|
289
296
|
? jsonResponse(archiveConversationResponseSchema, result)
|
|
290
297
|
: errorResponse("Conversation not found.", 404);
|
|
291
298
|
});
|
|
299
|
+
app.patch("/conversations/:conversationId/pin", async (c) => {
|
|
300
|
+
const params = conversationParamsSchema.safeParse(c.req.param());
|
|
301
|
+
if (!params.success) {
|
|
302
|
+
return errorResponse("Invalid route parameters.", 400);
|
|
303
|
+
}
|
|
304
|
+
const body = pinConversationBodySchema.safeParse(await c.req.json());
|
|
305
|
+
if (!body.success) {
|
|
306
|
+
return errorResponse("Invalid request body.", 400);
|
|
307
|
+
}
|
|
308
|
+
const result = setMockConversationPinned(
|
|
309
|
+
params.data.conversationId,
|
|
310
|
+
body.data.pinned,
|
|
311
|
+
);
|
|
312
|
+
return result
|
|
313
|
+
? jsonResponse(pinConversationResponseSchema, result)
|
|
314
|
+
: errorResponse("Conversation not found.", 404);
|
|
315
|
+
});
|
|
316
|
+
app.post("/conversations/archive-done", () =>
|
|
317
|
+
jsonResponse(
|
|
318
|
+
archiveDoneConversationsResponseSchema,
|
|
319
|
+
archiveMockDoneConversations(),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
app.patch("/conversations/:conversationId/auto-archive", async (c) => {
|
|
323
|
+
const params = conversationParamsSchema.safeParse(c.req.param());
|
|
324
|
+
if (!params.success) {
|
|
325
|
+
return errorResponse("Invalid route parameters.", 400);
|
|
326
|
+
}
|
|
327
|
+
const body = conversationAutoArchiveSchema.safeParse(await c.req.json());
|
|
328
|
+
if (!body.success) {
|
|
329
|
+
return errorResponse("Invalid request body.", 400);
|
|
330
|
+
}
|
|
331
|
+
const result = setMockConversationAutoArchive(
|
|
332
|
+
params.data.conversationId,
|
|
333
|
+
body.data.autoArchive,
|
|
334
|
+
);
|
|
335
|
+
return result
|
|
336
|
+
? jsonResponse(conversationAutoArchiveSchema, result)
|
|
337
|
+
: errorResponse("Conversation not found.", 404);
|
|
338
|
+
});
|
|
292
339
|
// Fixed bodies so dashboard mock can exercise image/file attachment cards.
|
|
293
340
|
app.get("/conversations/:conversationId/attachments/:attachmentId", (c) => {
|
|
294
341
|
const conversationId = c.req.param("conversationId");
|