@sentry/junior-dashboard 0.275.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 +194 -40
- package/dist/client.js +120 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +189 -38
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +88 -15
- 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/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +66 -35
- package/src/client/conversations/ConversationHeader.tsx +19 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +114 -77
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +183 -60
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +42 -26
- 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/PendingMailboxStack.tsx +11 -1
- package/src/client/conversations/QuickQuestion.tsx +476 -0
- package/src/client/conversations/TelemetryMetrics.tsx +2 -1
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptEventView.tsx +25 -2
- package/src/client/conversations/TranscriptMemoryPopover.tsx +174 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +139 -0
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/eventTranscript.ts +8 -0
- 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/ReviewItemActions.tsx +1 -1
- package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
- package/src/client/review-desk/ReviewItemContext.tsx +41 -23
- package/src/client/review-desk/queries.ts +2 -1
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +7 -0
- package/src/client.tsx +3 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -0
- package/src/mock-reporting/watches.ts +2 -0
- package/src/shell.ts +27 -2
- package/src/tailwind.css +8 -19
- package/src/client/components/ActiveIndicator.tsx +0 -14
|
@@ -91,6 +91,22 @@ export function latestReviewItemId(
|
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Return a message for another item's excerpt, without the Review item
|
|
96
|
+
* changes that it records: those show on their own items. Returns undefined
|
|
97
|
+
* when nothing else remains.
|
|
98
|
+
*/
|
|
99
|
+
export function excerptMessage(
|
|
100
|
+
event: ConversationReportEvent,
|
|
101
|
+
): ConversationReportEvent | undefined {
|
|
102
|
+
const { data } = event;
|
|
103
|
+
if (data.type !== "message" || !data.reviewItemChanges) return event;
|
|
104
|
+
// A message with changes lists its own Events. With none, it only
|
|
105
|
+
// records changes.
|
|
106
|
+
if (!data.events) return undefined;
|
|
107
|
+
return { ...event, data: { ...data, reviewItemChanges: undefined } };
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
/** True when an open item asks a question that the viewer can answer. */
|
|
95
111
|
export function reviewItemWaitsForAnswer(item: ReviewItemPayload): boolean {
|
|
96
112
|
return (
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
browserTracingIntegration,
|
|
3
|
+
init,
|
|
4
|
+
type BrowserOptions,
|
|
5
|
+
} from "@sentry/browser";
|
|
6
|
+
import type { BrowserSentryConfig } from "@sentry/junior/instrumentation";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
__JUNIOR_DASHBOARD_SENTRY__?: Omit<
|
|
11
|
+
BrowserSentryConfig,
|
|
12
|
+
"baggage" | "sentryTrace"
|
|
13
|
+
>;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type StreamedSpanJSON = Parameters<
|
|
18
|
+
NonNullable<BrowserOptions["beforeSendSpan"]>
|
|
19
|
+
>[0];
|
|
20
|
+
|
|
21
|
+
const QUERY_ATTRIBUTE_KEYS = [
|
|
22
|
+
"http.fragment",
|
|
23
|
+
"http.query",
|
|
24
|
+
"url.fragment",
|
|
25
|
+
"url.query",
|
|
26
|
+
];
|
|
27
|
+
// Element selectors in interaction and web vital attributes include these
|
|
28
|
+
// attribute values, such as a conversation title in an `aria-label`. The SDK
|
|
29
|
+
// does not escape quotes in values, so a value can contain `"]` and fake
|
|
30
|
+
// selector parts. Cutting from the first marker to the last `"]` removes every
|
|
31
|
+
// value, at the cost of the selector parts between them.
|
|
32
|
+
const ELEMENT_ATTRIBUTE_START = /\[(?:aria-label|type|name|title|alt)="/;
|
|
33
|
+
|
|
34
|
+
function scrubSelector(value: string): string {
|
|
35
|
+
const start = value.search(ELEMENT_ATTRIBUTE_START);
|
|
36
|
+
if (start === -1) return value;
|
|
37
|
+
const end = value.lastIndexOf('"]');
|
|
38
|
+
return end < start
|
|
39
|
+
? value.slice(0, start)
|
|
40
|
+
: value.slice(0, start) + value.slice(end + 2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function scrubAttribute(value: string): string {
|
|
44
|
+
// Page and API URLs carry search text in the query string.
|
|
45
|
+
if (/^https?:\/\//.test(value)) return value.split(/[?#]/, 1)[0] ?? value;
|
|
46
|
+
return scrubSelector(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Browser span names are fixed labels such as "Click" or "GET localhost". The
|
|
50
|
+
// page content they describe is in their attributes.
|
|
51
|
+
function scrubSpan(span: StreamedSpanJSON): StreamedSpanJSON {
|
|
52
|
+
for (const key of QUERY_ATTRIBUTE_KEYS) {
|
|
53
|
+
delete span.attributes[key];
|
|
54
|
+
}
|
|
55
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
span.attributes[key] = scrubAttribute(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return span;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Send dashboard performance spans to the server's Sentry project when the
|
|
65
|
+
* server provides a DSN. Only tracing is enabled: error events and
|
|
66
|
+
* breadcrumbs could carry conversation content shown on the page.
|
|
67
|
+
*/
|
|
68
|
+
export function initDashboardSentry(): void {
|
|
69
|
+
const config = window.__JUNIOR_DASHBOARD_SENTRY__;
|
|
70
|
+
if (!config) return;
|
|
71
|
+
init({
|
|
72
|
+
...config,
|
|
73
|
+
beforeSendSpan: scrubSpan,
|
|
74
|
+
defaultIntegrations: false,
|
|
75
|
+
integrations: [browserTracingIntegration()],
|
|
76
|
+
});
|
|
77
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ConversationSummaryReport,
|
|
16
16
|
ConversationTurnFailureCode,
|
|
17
17
|
ConversationTurnFailureReason,
|
|
18
|
+
ReviewItemChangeRecord,
|
|
18
19
|
} from "@sentry/junior/api/schema";
|
|
19
20
|
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
20
21
|
import type { ConversationEventPresentation } from "@sentry/junior-plugin-api";
|
|
@@ -90,6 +91,8 @@ export type TranscriptViewStructuredEventPart = {
|
|
|
90
91
|
name: string;
|
|
91
92
|
namespace: string;
|
|
92
93
|
presentation: ConversationEventPresentation;
|
|
94
|
+
/** Turn that emitted the event. */
|
|
95
|
+
turnId?: string;
|
|
93
96
|
type: "structured_event";
|
|
94
97
|
version: number;
|
|
95
98
|
};
|
|
@@ -113,6 +116,7 @@ export type TranscriptViewTurnContext = {
|
|
|
113
116
|
kind: string;
|
|
114
117
|
loadedAt: string;
|
|
115
118
|
pluginName: string;
|
|
119
|
+
turnId: string;
|
|
116
120
|
version: number;
|
|
117
121
|
};
|
|
118
122
|
|
|
@@ -134,6 +138,8 @@ export type TranscriptViewMessage = {
|
|
|
134
138
|
events?: ConversationReportMessageEvent[];
|
|
135
139
|
/** Whether the source message addressed Junior directly. */
|
|
136
140
|
explicitMention?: boolean;
|
|
141
|
+
/** Person changes to Review items that this message records. */
|
|
142
|
+
reviewItemChanges?: ReviewItemChangeRecord[];
|
|
137
143
|
/** People mentioned in the event text, resolved by the API. */
|
|
138
144
|
mentions?: ActorIdentity[];
|
|
139
145
|
/** Whether a non-mention message was used as input to a turn. */
|
|
@@ -170,6 +176,7 @@ export type Conversation = {
|
|
|
170
176
|
annotations?: ConversationSummaryReport["annotations"];
|
|
171
177
|
sidebarAnnotations?: ConversationSummaryReport["sidebarAnnotations"];
|
|
172
178
|
archivedAt?: string | null;
|
|
179
|
+
pinnedAt?: string | null;
|
|
173
180
|
auxiliaryCosts?: ConversationSummaryReport["auxiliaryCosts"];
|
|
174
181
|
channel?: string;
|
|
175
182
|
channelName?: string;
|
package/src/client.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { Toaster } from "./client/components/ui/toast";
|
|
|
7
7
|
import { TooltipProvider } from "./client/components/ui/tooltip";
|
|
8
8
|
import { DashboardShell } from "./client/App";
|
|
9
9
|
import { getDashboardAgentName } from "./client/agentName";
|
|
10
|
+
import { initDashboardSentry } from "./client/sentry";
|
|
10
11
|
|
|
11
12
|
declare global {
|
|
12
13
|
interface Window {
|
|
@@ -66,6 +67,8 @@ function DashboardErrorPanel(props: { error: Error }) {
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
initDashboardSentry();
|
|
71
|
+
|
|
69
72
|
const root = document.getElementById("dashboard-root");
|
|
70
73
|
if (!root) {
|
|
71
74
|
throw new Error("Junior dashboard root element was not found");
|
|
@@ -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");
|
|
@@ -43,6 +43,7 @@ export function readMockConversationWatches(
|
|
|
43
43
|
label: "getsentry/junior#2383",
|
|
44
44
|
namespace: "github",
|
|
45
45
|
resourceType: "pull_request",
|
|
46
|
+
url: "https://github.com/getsentry/junior/pull/2383",
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
49
|
createdAt: iso(-2 * HOUR_MS),
|
|
@@ -79,6 +80,7 @@ export function readMockConversationWatches(
|
|
|
79
80
|
label: "getsentry/junior#2351",
|
|
80
81
|
namespace: "github",
|
|
81
82
|
resourceType: "pull_request",
|
|
83
|
+
url: "https://github.com/getsentry/junior/pull/2351",
|
|
82
84
|
},
|
|
83
85
|
{
|
|
84
86
|
createdAt: iso(-10 * 60_000),
|
package/src/shell.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { BrowserSentryConfig } from "@sentry/junior/instrumentation";
|
|
3
4
|
import {
|
|
4
5
|
dashboardAvatarHeaderAsset,
|
|
5
6
|
dashboardFaviconAsset,
|
|
@@ -210,9 +211,32 @@ export function renderInstallIcon(): Response {
|
|
|
210
211
|
});
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
function scriptJson(value: unknown): string {
|
|
215
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The browser SDK reads these meta tags to continue the trace of the request
|
|
219
|
+
// that served this page, so its pageload span joins the server spans.
|
|
220
|
+
function renderSentryHead(sentry: BrowserSentryConfig | undefined): string {
|
|
221
|
+
if (!sentry) return "";
|
|
222
|
+
const { baggage, sentryTrace, ...options } = sentry;
|
|
223
|
+
return [
|
|
224
|
+
sentryTrace &&
|
|
225
|
+
`<meta name="sentry-trace" content="${escapeHtml(sentryTrace)}" />`,
|
|
226
|
+
baggage && `<meta name="baggage" content="${escapeHtml(baggage)}" />`,
|
|
227
|
+
`<script>window.__JUNIOR_DASHBOARD_SENTRY__ = ${scriptJson(options)};</script>`,
|
|
228
|
+
]
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.join("\n ");
|
|
231
|
+
}
|
|
232
|
+
|
|
213
233
|
/** Render the authenticated dashboard HTML shell. */
|
|
214
|
-
export function renderDashboard(
|
|
215
|
-
|
|
234
|
+
export function renderDashboard(
|
|
235
|
+
basePath: string,
|
|
236
|
+
agentName: string,
|
|
237
|
+
sentry?: BrowserSentryConfig,
|
|
238
|
+
): Response {
|
|
239
|
+
const encodedAgentName = scriptJson(agentName);
|
|
216
240
|
return new Response(
|
|
217
241
|
`<!DOCTYPE html>
|
|
218
242
|
<html lang="en">
|
|
@@ -228,6 +252,7 @@ export function renderDashboard(basePath: string, agentName: string): Response {
|
|
|
228
252
|
<link rel="manifest" href="${DASHBOARD_MANIFEST_PATH}" />
|
|
229
253
|
<link rel="apple-touch-icon" href="${DASHBOARD_INSTALL_ICON_PATH}" />
|
|
230
254
|
<title>${escapeHtml(agentName)}</title>
|
|
255
|
+
${renderSentryHead(sentry)}
|
|
231
256
|
<style>
|
|
232
257
|
${readDashboardTailwind()}
|
|
233
258
|
</style>
|
package/src/tailwind.css
CHANGED
|
@@ -342,30 +342,19 @@
|
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
@keyframes junior-active-indicator {
|
|
346
|
-
0%,
|
|
347
|
-
100% {
|
|
348
|
-
box-shadow:
|
|
349
|
-
0 0 0 0 rgba(110, 231, 183, 0),
|
|
350
|
-
0 0 8px rgba(110, 231, 183, 0.35);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
50% {
|
|
354
|
-
box-shadow:
|
|
355
|
-
0 0 0 4px rgba(110, 231, 183, 0.18),
|
|
356
|
-
0 0 14px rgba(110, 231, 183, 0.65);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
345
|
@media (max-width: 767px) {
|
|
361
346
|
/* Keep long mobile feeds cheap while preserving their full scroll range. */
|
|
362
347
|
.mobile-conversation-row {
|
|
363
348
|
content-visibility: auto;
|
|
364
349
|
contain-intrinsic-size: auto 3.5rem;
|
|
365
350
|
}
|
|
351
|
+
}
|
|
366
352
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
353
|
+
/* The Quick question selection. Utilities cannot style a highlight. */
|
|
354
|
+
::highlight(quick-question) {
|
|
355
|
+
background-color: color-mix(
|
|
356
|
+
in oklab,
|
|
357
|
+
var(--color-dashboard-accent) 30%,
|
|
358
|
+
transparent
|
|
359
|
+
);
|
|
371
360
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { cn } from "../styles";
|
|
2
|
-
|
|
3
|
-
/** Render the animated dot used to show active work. */
|
|
4
|
-
export function ActiveIndicator(props: { className?: string }) {
|
|
5
|
-
return (
|
|
6
|
-
<span
|
|
7
|
-
aria-hidden="true"
|
|
8
|
-
className={cn(
|
|
9
|
-
"shrink-0 rounded-full bg-emerald-300 shadow-[0_0_10px_rgba(110,231,183,0.55)] animate-[junior-active-indicator_1.8s_ease-in-out_infinite] motion-reduce:animate-none",
|
|
10
|
-
props.className,
|
|
11
|
-
)}
|
|
12
|
-
/>
|
|
13
|
-
);
|
|
14
|
-
}
|