@agent-native/core 0.161.20 → 0.161.23
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/clips/actions/finalize-meeting.ts +57 -21
- package/corpus/templates/clips/actions/get-meeting.ts +11 -3
- package/corpus/templates/clips/actions/request-transcript.ts +183 -8
- package/corpus/templates/clips/actions/update-meeting.ts +66 -14
- package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +80 -135
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +470 -35
- package/corpus/templates/clips/changelog/2026-08-18-meeting-notes-support-rich-editing-for-personal-notes-summar.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +13 -3
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +13 -7
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
- package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
- package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
- package/corpus/templates/design/playwright.config.ts +19 -2
- package/corpus/templates/mail/app/components/email/EmailList.tsx +74 -22
- package/corpus/templates/mail/app/components/email/infinite-pagination.ts +82 -0
- package/corpus/templates/mail/app/hooks/use-emails.ts +1 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +15 -10
- package/corpus/templates/mail/app/pages/inbox-zero.ts +35 -0
- package/corpus/templates/mail/changelog/2026-08-18-pinned-labels-load-matches.md +6 -0
- package/corpus/templates/slides/actions/get-deck.ts +5 -1
- package/corpus/templates/slides/actions/list-decks.ts +29 -8
- package/corpus/templates/slides/actions/view-screen.ts +7 -3
- package/corpus/templates/slides/changelog/2026-08-18-decks-appear-under-mine.md +6 -0
- package/corpus/templates/slides/shared/ownership.ts +6 -0
- package/dist/agent/engine/builder-engine.js +15 -8
- package/dist/agent/production-agent.d.ts +15 -0
- package/dist/agent/production-agent.js +32 -9
- package/dist/app-config/schema.d.ts +2 -0
- package/dist/app-config/workspace.d.ts +2 -0
- package/dist/app-config/workspace.js +18 -0
- package/dist/catalog.json +23 -23
- package/dist/client/analytics.d.ts +1 -0
- package/dist/client/api-path.js +7 -3
- package/dist/client/frame.js +20 -6
- package/dist/client/org/workspace-app-links.js +10 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +26 -2
- package/dist/observability/routes.d.ts +2 -2
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/server/app-origin-config.d.ts +1 -0
- package/dist/server/app-origin-config.js +3 -0
- package/dist/server/auth.js +2 -2
- package/dist/server/cookie-namespace.js +17 -2
- package/dist/server/credential-provider.js +5 -2
- package/dist/server/derived-secret.js +3 -2
- package/dist/server/onboarding-html.js +34 -2
- package/dist/server/workspace-oauth.js +11 -6
- package/dist/shared/runtime-config.d.ts +2 -0
- package/dist/shared/runtime-config.js +12 -3
- package/dist/vite/client.js +5 -2
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UseDesignHotkeysProps } from "@/hooks/useDesignHotkeys";
|
|
2
|
+
import { SHOW_DESIGN_SECONDARY_LEFT_PANELS } from "@/pages/design-editor/types";
|
|
2
3
|
|
|
3
4
|
export const DESIGN_SHORTCUT_CATEGORIES = [
|
|
4
5
|
"essential",
|
|
@@ -136,13 +137,18 @@ export const DESIGN_SHORTCUTS: readonly DesignShortcutDefinition[] = [
|
|
|
136
137
|
labelKey: "designEditor.keyboardShortcuts.commands.showLayers",
|
|
137
138
|
handler: "onShowLayersPanel",
|
|
138
139
|
}),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
...(SHOW_DESIGN_SECONDARY_LEFT_PANELS
|
|
141
|
+
? [
|
|
142
|
+
shortcut({
|
|
143
|
+
id: "show-assets",
|
|
144
|
+
category: "view" as const,
|
|
145
|
+
bindings: ["alt+2"],
|
|
146
|
+
labelKey:
|
|
147
|
+
"designEditor.keyboardShortcuts.commands.showAssets" as const,
|
|
148
|
+
handler: "onShowAssetsPanel" as const,
|
|
149
|
+
}),
|
|
150
|
+
]
|
|
151
|
+
: []),
|
|
146
152
|
shortcut({
|
|
147
153
|
id: "toggle-ui",
|
|
148
154
|
category: "view",
|
|
@@ -6,6 +6,8 @@ import { useAgentRouteState } from "@agent-native/core/client/navigation";
|
|
|
6
6
|
import { useEffect } from "react";
|
|
7
7
|
import { useLocation, useParams } from "react-router";
|
|
8
8
|
|
|
9
|
+
import { normalizeDesignLeftPanel } from "@/pages/design-editor/tool-state";
|
|
10
|
+
|
|
9
11
|
export interface NavigationState {
|
|
10
12
|
view: string;
|
|
11
13
|
designId?: string;
|
|
@@ -120,12 +122,11 @@ function normalizeEditorView(
|
|
|
120
122
|
|
|
121
123
|
function normalizeInspectorTab(
|
|
122
124
|
value: unknown,
|
|
123
|
-
): "design" | "comments" | "tweaks" | "code" |
|
|
125
|
+
): "design" | "comments" | "tweaks" | "code" | undefined {
|
|
124
126
|
return value === "design" ||
|
|
125
127
|
value === "comments" ||
|
|
126
128
|
value === "tweaks" ||
|
|
127
|
-
value === "code"
|
|
128
|
-
value === "extensions"
|
|
129
|
+
value === "code"
|
|
129
130
|
? value
|
|
130
131
|
: undefined;
|
|
131
132
|
}
|
|
@@ -141,16 +142,7 @@ function normalizeLeftPanel(
|
|
|
141
142
|
| "import"
|
|
142
143
|
| "code"
|
|
143
144
|
| undefined {
|
|
144
|
-
|
|
145
|
-
return value === "file" ||
|
|
146
|
-
value === "agent" ||
|
|
147
|
-
value === "assets" ||
|
|
148
|
-
value === "tools" ||
|
|
149
|
-
value === "tokens" ||
|
|
150
|
-
value === "import" ||
|
|
151
|
-
value === "code"
|
|
152
|
-
? value
|
|
153
|
-
: undefined;
|
|
145
|
+
return normalizeDesignLeftPanel(value);
|
|
154
146
|
}
|
|
155
147
|
|
|
156
148
|
function normalizeEditorMode(
|
|
@@ -176,9 +168,12 @@ export function editorPathFromCommand(cmd: NavigationState): string | null {
|
|
|
176
168
|
const editorView = normalizeEditorView(cmd.editorView);
|
|
177
169
|
if (editorView) params.set("view", editorView);
|
|
178
170
|
if (editorView === "single") params.set("mode", cmd.mode ?? "interact");
|
|
179
|
-
const
|
|
171
|
+
const rawInspectorTab = cmd.inspectorTab ?? cmd.inspector;
|
|
172
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
180
173
|
if (inspectorTab) params.set("inspector", inspectorTab);
|
|
181
|
-
const leftPanel = normalizeLeftPanel(
|
|
174
|
+
const leftPanel = normalizeLeftPanel(
|
|
175
|
+
cmd.leftPanel ?? cmd.panel ?? rawInspectorTab,
|
|
176
|
+
);
|
|
182
177
|
if (leftPanel) params.set("panel", leftPanel);
|
|
183
178
|
const screen = cmd.fileId ?? cmd.screenId ?? cmd.filename ?? cmd.screen;
|
|
184
179
|
if (screen) params.set("screen", screen);
|
|
@@ -201,10 +196,11 @@ export function editorCommandFromNavigate(
|
|
|
201
196
|
): DesignEditorCommand | null {
|
|
202
197
|
if (cmd.view !== "editor" || !cmd.designId) return null;
|
|
203
198
|
const editorView = normalizeEditorView(cmd.editorView);
|
|
204
|
-
const
|
|
199
|
+
const rawInspectorTab = cmd.inspectorTab ?? cmd.inspector;
|
|
200
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
205
201
|
const leftPanel =
|
|
206
202
|
normalizeLeftPanel(cmd.leftPanel ?? cmd.panel) ??
|
|
207
|
-
normalizeLeftPanel(
|
|
203
|
+
normalizeLeftPanel(rawInspectorTab);
|
|
208
204
|
const command: DesignEditorCommand = {
|
|
209
205
|
designId: cmd.designId,
|
|
210
206
|
issuedAt: Date.now(),
|
|
@@ -255,11 +251,12 @@ export function useNavigationState(enabled = true) {
|
|
|
255
251
|
if (editorView) state.editorView = editorView;
|
|
256
252
|
const mode = normalizeEditorMode(searchParams.get("mode"));
|
|
257
253
|
if (mode) state.mode = mode;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
);
|
|
254
|
+
const rawInspectorTab = searchParams.get("inspector");
|
|
255
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
261
256
|
if (inspectorTab) state.inspectorTab = inspectorTab;
|
|
262
|
-
const leftPanel = normalizeLeftPanel(
|
|
257
|
+
const leftPanel = normalizeLeftPanel(
|
|
258
|
+
searchParams.get("panel") ?? rawInspectorTab,
|
|
259
|
+
);
|
|
263
260
|
if (leftPanel) state.leftPanel = leftPanel;
|
|
264
261
|
const screen = searchParams.get("screen");
|
|
265
262
|
if (screen) state.screen = screen;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isStandaloneHttpUrl } from "@shared/html-content";
|
|
2
2
|
|
|
3
|
+
import { normalizeDesignLeftPanel } from "./tool-state";
|
|
3
4
|
import {
|
|
4
5
|
type DesignFile,
|
|
5
6
|
type DesignLeftPanel,
|
|
6
7
|
type DesignTool,
|
|
7
8
|
type EditorMode,
|
|
8
|
-
SHOW_DESIGN_CODE_LEFT_PANEL,
|
|
9
9
|
} from "./types";
|
|
10
10
|
|
|
11
11
|
// Definition moved to shared/ so the server write path and code-layer's
|
|
@@ -127,10 +127,7 @@ export function getDesignEditorStateUrlSearch(args: {
|
|
|
127
127
|
mode?: EditorMode | null;
|
|
128
128
|
}) {
|
|
129
129
|
const params = new URLSearchParams(args.currentSearch);
|
|
130
|
-
const leftPanel =
|
|
131
|
-
args.leftPanel === "code" && !SHOW_DESIGN_CODE_LEFT_PANEL
|
|
132
|
-
? null
|
|
133
|
-
: args.leftPanel;
|
|
130
|
+
const leftPanel = normalizeDesignLeftPanel(args.leftPanel) ?? null;
|
|
134
131
|
params.set("view", args.viewMode);
|
|
135
132
|
if (leftPanel && leftPanel !== "file") {
|
|
136
133
|
params.set("panel", leftPanel);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CreationTool } from "@/components/design/design-canvas/creation";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
isDesignLeftPanelEnabled,
|
|
5
5
|
type DesignLeftPanel,
|
|
6
6
|
type DesignTool,
|
|
7
7
|
type EditorMode,
|
|
@@ -10,18 +10,10 @@ import {
|
|
|
10
10
|
export function normalizeDesignLeftPanel(
|
|
11
11
|
value: unknown,
|
|
12
12
|
): DesignLeftPanel | undefined {
|
|
13
|
-
if (value === "extensions")
|
|
14
|
-
|
|
15
|
-
return SHOW_DESIGN_CODE_LEFT_PANEL ? "code" : undefined;
|
|
13
|
+
if (value === "extensions") {
|
|
14
|
+
return isDesignLeftPanelEnabled("tools") ? "tools" : undefined;
|
|
16
15
|
}
|
|
17
|
-
return value
|
|
18
|
-
value === "agent" ||
|
|
19
|
-
value === "assets" ||
|
|
20
|
-
value === "tools" ||
|
|
21
|
-
value === "tokens" ||
|
|
22
|
-
value === "import"
|
|
23
|
-
? value
|
|
24
|
-
: undefined;
|
|
16
|
+
return isDesignLeftPanelEnabled(value) ? value : undefined;
|
|
25
17
|
}
|
|
26
18
|
|
|
27
19
|
export const MOVE_GROUP_TOOL_PRESENTATIONS = {
|
|
@@ -33,7 +33,44 @@ export type DesignLeftPanel =
|
|
|
33
33
|
| "import"
|
|
34
34
|
| "code";
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Keep the still-evolving Design editor side panels out of the default
|
|
38
|
+
* surface until their Figma parity work is ready. Flip this switch to expose
|
|
39
|
+
* Assets, Tools, Tokens, and Code together while iterating on them. The E2E
|
|
40
|
+
* harness sets the Vite flag so those existing advanced-surface tests keep
|
|
41
|
+
* exercising the gated panels.
|
|
42
|
+
*/
|
|
43
|
+
const designSecondaryLeftPanelsSetting =
|
|
44
|
+
typeof import.meta !== "undefined"
|
|
45
|
+
? (import.meta as { env?: Record<string, string> }).env
|
|
46
|
+
?.VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS
|
|
47
|
+
: undefined;
|
|
48
|
+
export const SHOW_DESIGN_SECONDARY_LEFT_PANELS =
|
|
49
|
+
designSecondaryLeftPanelsSetting === "1" ||
|
|
50
|
+
designSecondaryLeftPanelsSetting === "true";
|
|
51
|
+
export const SHOW_DESIGN_CODE_LEFT_PANEL = SHOW_DESIGN_SECONDARY_LEFT_PANELS;
|
|
52
|
+
|
|
53
|
+
export function isDesignLeftPanelEnabled(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): value is DesignLeftPanel {
|
|
56
|
+
if (
|
|
57
|
+
value !== "file" &&
|
|
58
|
+
value !== "agent" &&
|
|
59
|
+
value !== "assets" &&
|
|
60
|
+
value !== "tools" &&
|
|
61
|
+
value !== "tokens" &&
|
|
62
|
+
value !== "import" &&
|
|
63
|
+
value !== "code"
|
|
64
|
+
) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (value === "code") return SHOW_DESIGN_CODE_LEFT_PANEL;
|
|
69
|
+
if (value === "assets" || value === "tools" || value === "tokens") {
|
|
70
|
+
return SHOW_DESIGN_SECONDARY_LEFT_PANELS;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
37
74
|
|
|
38
75
|
/** Zoom percentage applied when entering single-screen (focused) editor mode. */
|
|
39
76
|
export const FOCUSED_SCREEN_ZOOM = 100;
|
|
@@ -22,9 +22,24 @@ const E2E_DATABASE_URL =
|
|
|
22
22
|
process.env.E2E_DATABASE_URL ??
|
|
23
23
|
`file:${path.join(import.meta.dirname, "data", "e2e.db")}`;
|
|
24
24
|
const BROWSER_CHANNEL = process.env.E2E_BROWSER_CHANNEL;
|
|
25
|
+
const SHOW_SECONDARY_PANELS_IN_E2E =
|
|
26
|
+
process.env.E2E_SHOW_DESIGN_SECONDARY_LEFT_PANELS !== "0";
|
|
27
|
+
const SECONDARY_PANELS_ENV = SHOW_SECONDARY_PANELS_IN_E2E
|
|
28
|
+
? "VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS=1 "
|
|
29
|
+
: "VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS=0 ";
|
|
30
|
+
const ADVANCED_PANEL_SPEC_FILES = [
|
|
31
|
+
/canvas-tools\.spec\.ts$/,
|
|
32
|
+
/code-native-deep-surfaces\.spec\.ts$/,
|
|
33
|
+
/code-native-pr-surfaces\.spec\.ts$/,
|
|
34
|
+
/code-workbench-local-files\.spec\.ts$/,
|
|
35
|
+
];
|
|
25
36
|
|
|
26
37
|
export default defineConfig({
|
|
27
38
|
testDir: "./e2e",
|
|
39
|
+
// These suites intentionally exercise panels that are not mounted in the
|
|
40
|
+
// disabled profile. Ignoring them keeps that profile focused on verifying
|
|
41
|
+
// the hidden-panel contract without failing on missing advanced controls.
|
|
42
|
+
testIgnore: SHOW_SECONDARY_PANELS_IN_E2E ? [] : ADVANCED_PANEL_SPEC_FILES,
|
|
28
43
|
// The editor is heavy (iframe bridge + polling); give generous budgets.
|
|
29
44
|
timeout: 90_000,
|
|
30
45
|
expect: { timeout: 15_000 },
|
|
@@ -56,9 +71,11 @@ export default defineConfig({
|
|
|
56
71
|
// generic DATABASE_URL, but set both to an absolute SQLite URL so a
|
|
57
72
|
// `.env` Postgres URL or a changed command cwd can never override this
|
|
58
73
|
// throwaway local db.
|
|
59
|
-
command: `APP_NAME=design DESIGN_DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} PORT=${PORT} corepack pnpm dev`,
|
|
74
|
+
command: `APP_NAME=design ${SECONDARY_PANELS_ENV}DESIGN_DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} DATABASE_URL=${JSON.stringify(E2E_DATABASE_URL)} PORT=${PORT} corepack pnpm dev`,
|
|
60
75
|
url: BASE_URL,
|
|
61
|
-
|
|
76
|
+
// The panel flag is compiled into the Vite bundle. Reusing a local
|
|
77
|
+
// server can therefore run this profile with the opposite setting.
|
|
78
|
+
reuseExistingServer: false,
|
|
62
79
|
// Cold Vite dep-optimization on first boot can exceed two minutes.
|
|
63
80
|
timeout: 300_000,
|
|
64
81
|
stdout: "ignore",
|
|
@@ -62,6 +62,11 @@ import { ensureThread, warmThreads } from "@/lib/thread-cache";
|
|
|
62
62
|
import { cn } from "@/lib/utils";
|
|
63
63
|
|
|
64
64
|
import { EmailListItem } from "./EmailListItem";
|
|
65
|
+
import {
|
|
66
|
+
observeNextPage,
|
|
67
|
+
retryNextPage as runPaginationRetry,
|
|
68
|
+
shouldShowPaginationRetry,
|
|
69
|
+
} from "./infinite-pagination";
|
|
65
70
|
|
|
66
71
|
type EmailsPage = { emails: EmailMessage[]; nextPageToken?: string };
|
|
67
72
|
type InfiniteEmails = InfiniteData<EmailsPage, string | undefined>;
|
|
@@ -83,6 +88,7 @@ interface EmailListProps {
|
|
|
83
88
|
hasNextPage?: boolean;
|
|
84
89
|
fetchNextPage?: () => Promise<unknown>;
|
|
85
90
|
isFetchingNextPage?: boolean;
|
|
91
|
+
isFetchNextPageError?: boolean;
|
|
86
92
|
focusedId: string | null;
|
|
87
93
|
setFocusedId: (id: string | null) => void;
|
|
88
94
|
selectedIds: Set<string>;
|
|
@@ -366,6 +372,7 @@ export function EmailList({
|
|
|
366
372
|
hasNextPage: hasNextPageProp,
|
|
367
373
|
fetchNextPage: fetchNextPageProp,
|
|
368
374
|
isFetchingNextPage: isFetchingNextPageProp,
|
|
375
|
+
isFetchNextPageError: isFetchNextPageErrorProp,
|
|
369
376
|
focusedId,
|
|
370
377
|
setFocusedId,
|
|
371
378
|
selectedIds,
|
|
@@ -397,6 +404,7 @@ export function EmailList({
|
|
|
397
404
|
hasNextPage: fetchedEmailsHasNextPage,
|
|
398
405
|
fetchNextPage: fetchFetchedNextPage,
|
|
399
406
|
isFetchingNextPage: fetchedEmailsFetchingNextPage,
|
|
407
|
+
isFetchNextPageError: fetchedEmailsFetchNextPageError,
|
|
400
408
|
} = useEmails(view, searchQuery, labelParam ?? undefined, {
|
|
401
409
|
enabled: emailsProp === undefined,
|
|
402
410
|
});
|
|
@@ -410,6 +418,13 @@ export function EmailList({
|
|
|
410
418
|
const fetchNextPage = fetchNextPageProp ?? fetchFetchedNextPage;
|
|
411
419
|
const isFetchingNextPage =
|
|
412
420
|
isFetchingNextPageProp ?? fetchedEmailsFetchingNextPage;
|
|
421
|
+
const isFetchNextPageError =
|
|
422
|
+
isFetchNextPageErrorProp ?? fetchedEmailsFetchNextPageError;
|
|
423
|
+
const paginationRetryInFlightRef = useRef(false);
|
|
424
|
+
const retryNextPage = useCallback(
|
|
425
|
+
() => runPaginationRetry(fetchNextPage, paginationRetryInFlightRef),
|
|
426
|
+
[fetchNextPage],
|
|
427
|
+
);
|
|
413
428
|
const markRead = useMarkRead();
|
|
414
429
|
const markThreadRead = useMarkThreadRead();
|
|
415
430
|
const toggleStar = useToggleStar();
|
|
@@ -1127,31 +1142,20 @@ export function EmailList({
|
|
|
1127
1142
|
}, [focusedIndex, rowVirtualizer]);
|
|
1128
1143
|
|
|
1129
1144
|
// Infinite scroll — fetch next page when the sentinel enters the viewport.
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
// sentinel is still visible after a fetch, a reconnecting observer would
|
|
1133
|
-
// immediately fire again and we'd loop (visible to the user as "Loading
|
|
1134
|
-
// more..." flashing every ~second while tab is idle).
|
|
1145
|
+
// Re-arm the observer when a fetch completes so a still-visible sentinel can
|
|
1146
|
+
// walk through consecutive pages with no client-side filtered matches.
|
|
1135
1147
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
1136
|
-
const isFetchingNextPageRef = useRef(isFetchingNextPage);
|
|
1137
|
-
isFetchingNextPageRef.current = isFetchingNextPage;
|
|
1138
1148
|
useEffect(() => {
|
|
1139
1149
|
const el = sentinelRef.current;
|
|
1140
|
-
if (!el
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
},
|
|
1150
|
-
{ rootMargin: "200px" },
|
|
1151
|
-
);
|
|
1152
|
-
observer.observe(el);
|
|
1153
|
-
return () => observer.disconnect();
|
|
1154
|
-
}, [hasNextPage, fetchNextPage]);
|
|
1150
|
+
if (!el) return;
|
|
1151
|
+
return observeNextPage({
|
|
1152
|
+
element: el,
|
|
1153
|
+
hasNextPage: Boolean(hasNextPage),
|
|
1154
|
+
isFetchingNextPage: Boolean(isFetchingNextPage),
|
|
1155
|
+
isFetchNextPageError: Boolean(isFetchNextPageError),
|
|
1156
|
+
fetchNextPage,
|
|
1157
|
+
});
|
|
1158
|
+
}, [hasNextPage, isFetchingNextPage, isFetchNextPageError, fetchNextPage]);
|
|
1155
1159
|
|
|
1156
1160
|
// Advance selection when an email is snoozed (same logic as archiveFocused)
|
|
1157
1161
|
useEffect(() => {
|
|
@@ -1597,6 +1601,41 @@ export function EmailList({
|
|
|
1597
1601
|
return <MailLoadingState containerRef={containerRef} />;
|
|
1598
1602
|
}
|
|
1599
1603
|
|
|
1604
|
+
// Client-sliced inbox tabs can have no matches on the first page even when
|
|
1605
|
+
// later inbox pages contain matching threads. Keep the sentinel mounted so
|
|
1606
|
+
// the infinite query can continue before showing an empty state.
|
|
1607
|
+
if (threads.length === 0 && hasNextPage) {
|
|
1608
|
+
return (
|
|
1609
|
+
<div className="flex h-full flex-col" ref={containerRef}>
|
|
1610
|
+
<div className="flex flex-1 items-center justify-center" />
|
|
1611
|
+
<div
|
|
1612
|
+
ref={sentinelRef}
|
|
1613
|
+
className="flex items-center justify-center py-3"
|
|
1614
|
+
>
|
|
1615
|
+
{isFetchingNextPage && (
|
|
1616
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
1617
|
+
<Spinner className="size-3 text-muted-foreground" />
|
|
1618
|
+
{t("mail.empty.loadingMore")}
|
|
1619
|
+
</div>
|
|
1620
|
+
)}
|
|
1621
|
+
{shouldShowPaginationRetry({
|
|
1622
|
+
hasNextPage: Boolean(hasNextPage),
|
|
1623
|
+
isFetchingNextPage: Boolean(isFetchingNextPage),
|
|
1624
|
+
isFetchNextPageError: Boolean(isFetchNextPageError),
|
|
1625
|
+
}) && (
|
|
1626
|
+
<button
|
|
1627
|
+
type="button"
|
|
1628
|
+
onClick={() => void retryNextPage()}
|
|
1629
|
+
className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm hover:bg-primary/90"
|
|
1630
|
+
>
|
|
1631
|
+
{t("mail.error.tryAgain")}
|
|
1632
|
+
</button>
|
|
1633
|
+
)}
|
|
1634
|
+
</div>
|
|
1635
|
+
</div>
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1600
1639
|
// Empty state
|
|
1601
1640
|
if (threads.length === 0) {
|
|
1602
1641
|
if (searchQuery) {
|
|
@@ -1737,6 +1776,19 @@ export function EmailList({
|
|
|
1737
1776
|
{t("mail.empty.loadingMore")}
|
|
1738
1777
|
</div>
|
|
1739
1778
|
)}
|
|
1779
|
+
{shouldShowPaginationRetry({
|
|
1780
|
+
hasNextPage: Boolean(hasNextPage),
|
|
1781
|
+
isFetchingNextPage: Boolean(isFetchingNextPage),
|
|
1782
|
+
isFetchNextPageError: Boolean(isFetchNextPageError),
|
|
1783
|
+
}) && (
|
|
1784
|
+
<button
|
|
1785
|
+
type="button"
|
|
1786
|
+
onClick={() => void retryNextPage()}
|
|
1787
|
+
className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm hover:bg-primary/90"
|
|
1788
|
+
>
|
|
1789
|
+
{t("mail.error.tryAgain")}
|
|
1790
|
+
</button>
|
|
1791
|
+
)}
|
|
1740
1792
|
</div>
|
|
1741
1793
|
)}
|
|
1742
1794
|
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
interface ObserveNextPageOptions {
|
|
2
|
+
element: Element;
|
|
3
|
+
hasNextPage: boolean;
|
|
4
|
+
isFetchingNextPage: boolean;
|
|
5
|
+
isFetchNextPageError: boolean;
|
|
6
|
+
fetchNextPage: () => Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PaginationRetryState {
|
|
10
|
+
hasNextPage: boolean;
|
|
11
|
+
isFetchingNextPage: boolean;
|
|
12
|
+
isFetchNextPageError: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function shouldShowPaginationRetry({
|
|
16
|
+
hasNextPage,
|
|
17
|
+
isFetchingNextPage,
|
|
18
|
+
isFetchNextPageError,
|
|
19
|
+
}: PaginationRetryState): boolean {
|
|
20
|
+
return hasNextPage && isFetchNextPageError && !isFetchingNextPage;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run an explicit pagination retry without leaking a rejected provider promise
|
|
25
|
+
* to the browser. The shared state closes the synchronous double-click window
|
|
26
|
+
* before React Query has reflected isFetchingNextPage.
|
|
27
|
+
*/
|
|
28
|
+
export async function retryNextPage(
|
|
29
|
+
fetchNextPage: () => Promise<unknown>,
|
|
30
|
+
inFlightState: { current: boolean },
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (inFlightState.current) return;
|
|
33
|
+
inFlightState.current = true;
|
|
34
|
+
try {
|
|
35
|
+
await fetchNextPage();
|
|
36
|
+
} catch {
|
|
37
|
+
// coercion-ok: React Query owns the error; catch prevents an unhandled retry rejection.
|
|
38
|
+
// React Query owns the visible error state for the failed page.
|
|
39
|
+
} finally {
|
|
40
|
+
inFlightState.current = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Observe a visible infinite-scroll sentinel for one page at a time.
|
|
46
|
+
*
|
|
47
|
+
* The owning effect re-runs when `isFetchingNextPage` changes. That lets the
|
|
48
|
+
* next observer receive the sentinel's already-visible state after a fetch,
|
|
49
|
+
* including when several consecutive pages contain no filtered matches. A
|
|
50
|
+
* failed page is deliberately left unobserved until the user explicitly
|
|
51
|
+
* retries it, so a persistent provider error cannot create a request loop.
|
|
52
|
+
*/
|
|
53
|
+
export function observeNextPage({
|
|
54
|
+
element,
|
|
55
|
+
hasNextPage,
|
|
56
|
+
isFetchingNextPage,
|
|
57
|
+
isFetchNextPageError,
|
|
58
|
+
fetchNextPage,
|
|
59
|
+
}: ObserveNextPageOptions): () => void {
|
|
60
|
+
if (!hasNextPage || isFetchingNextPage || isFetchNextPageError)
|
|
61
|
+
return () => {};
|
|
62
|
+
|
|
63
|
+
let fetchInFlight = false;
|
|
64
|
+
const observer = new IntersectionObserver(
|
|
65
|
+
(entries) => {
|
|
66
|
+
if (!entries[0]?.isIntersecting || fetchInFlight) return;
|
|
67
|
+
|
|
68
|
+
fetchInFlight = true;
|
|
69
|
+
void fetchNextPage()
|
|
70
|
+
.catch(() => {
|
|
71
|
+
// React Query owns the visible error state; avoid a global
|
|
72
|
+
// unhandledrejection for transient list-page fetch failures.
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
75
|
+
fetchInFlight = false;
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
{ rootMargin: "200px" },
|
|
79
|
+
);
|
|
80
|
+
observer.observe(element);
|
|
81
|
+
return () => observer.disconnect();
|
|
82
|
+
}
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
import { groupIntoThreads, type ThreadSummary } from "@/lib/threads";
|
|
32
32
|
import { cn } from "@/lib/utils";
|
|
33
33
|
|
|
34
|
+
import { shouldShowInboxZero } from "./inbox-zero";
|
|
35
|
+
|
|
34
36
|
function ContactPanel({
|
|
35
37
|
emailId,
|
|
36
38
|
contactEmail,
|
|
@@ -315,6 +317,7 @@ export function InboxPage() {
|
|
|
315
317
|
hasNextPage,
|
|
316
318
|
fetchNextPage,
|
|
317
319
|
isFetchingNextPage,
|
|
320
|
+
isFetchNextPageError,
|
|
318
321
|
} = useEmails(view, searchQuery, effectiveLabel);
|
|
319
322
|
const hasEmailData = rawEmails !== undefined;
|
|
320
323
|
const emailListLoading =
|
|
@@ -590,16 +593,17 @@ export function InboxPage() {
|
|
|
590
593
|
|
|
591
594
|
const isMobile = useIsMobile();
|
|
592
595
|
const hasThread = !!threadId;
|
|
593
|
-
const
|
|
594
|
-
view
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
596
|
+
const isInboxZero = shouldShowInboxZero({
|
|
597
|
+
view,
|
|
598
|
+
activeLabel,
|
|
599
|
+
hasEmailData,
|
|
600
|
+
isLoading: emailListLoading,
|
|
601
|
+
isError,
|
|
602
|
+
hasThread,
|
|
603
|
+
searchQuery,
|
|
604
|
+
threadCount: threads.length,
|
|
605
|
+
hasNextPage: Boolean(hasNextPage),
|
|
606
|
+
});
|
|
603
607
|
const [sidebarContactEmail, setSidebarContactEmail] = useState<
|
|
604
608
|
string | undefined
|
|
605
609
|
>();
|
|
@@ -680,6 +684,7 @@ export function InboxPage() {
|
|
|
680
684
|
hasNextPage={hasNextPage}
|
|
681
685
|
fetchNextPage={fetchNextPage}
|
|
682
686
|
isFetchingNextPage={isFetchingNextPage}
|
|
687
|
+
isFetchNextPageError={isFetchNextPageError}
|
|
683
688
|
/>
|
|
684
689
|
)}
|
|
685
690
|
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface InboxZeroState {
|
|
2
|
+
view: string;
|
|
3
|
+
activeLabel: string | null | undefined;
|
|
4
|
+
hasEmailData: boolean;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
isError: boolean;
|
|
7
|
+
hasThread: boolean;
|
|
8
|
+
searchQuery: string | undefined;
|
|
9
|
+
threadCount: number;
|
|
10
|
+
hasNextPage: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function shouldShowInboxZero({
|
|
14
|
+
view,
|
|
15
|
+
activeLabel,
|
|
16
|
+
hasEmailData,
|
|
17
|
+
isLoading,
|
|
18
|
+
isError,
|
|
19
|
+
hasThread,
|
|
20
|
+
searchQuery,
|
|
21
|
+
threadCount,
|
|
22
|
+
hasNextPage,
|
|
23
|
+
}: InboxZeroState): boolean {
|
|
24
|
+
return (
|
|
25
|
+
view === "inbox" &&
|
|
26
|
+
(!activeLabel || activeLabel === "important") &&
|
|
27
|
+
hasEmailData &&
|
|
28
|
+
!isLoading &&
|
|
29
|
+
!isError &&
|
|
30
|
+
!hasThread &&
|
|
31
|
+
!searchQuery &&
|
|
32
|
+
threadCount === 0 &&
|
|
33
|
+
!hasNextPage
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import { formatSlideHtml } from "../server/lib/slide-content-patch.js";
|
|
8
8
|
import { summarizeSlideAnimationTargets } from "../server/lib/validate-slide-animations.js";
|
|
9
|
+
import { normalizeOwnerEmail } from "../shared/ownership.js";
|
|
9
10
|
import { hashSlideContent } from "../shared/slide-fit.js";
|
|
10
11
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
11
12
|
|
|
@@ -112,6 +113,7 @@ export default defineAction({
|
|
|
112
113
|
const data = JSON.parse(row.data);
|
|
113
114
|
const slides = data?.slides || [];
|
|
114
115
|
const ownerEmail = getRequestUserEmail();
|
|
116
|
+
const normalizedOwnerEmail = normalizeOwnerEmail(ownerEmail);
|
|
115
117
|
const selectedSlideIndex =
|
|
116
118
|
args.slideId === undefined
|
|
117
119
|
? -1
|
|
@@ -198,7 +200,9 @@ export default defineAction({
|
|
|
198
200
|
id: row.id,
|
|
199
201
|
title: row.title || data?.title,
|
|
200
202
|
visibility: row.visibility,
|
|
201
|
-
createdByMe:
|
|
203
|
+
createdByMe:
|
|
204
|
+
normalizedOwnerEmail !== null &&
|
|
205
|
+
normalizeOwnerEmail(row.ownerEmail) === normalizedOwnerEmail,
|
|
202
206
|
designSystemId: row.designSystemId ?? null,
|
|
203
207
|
slideCount: slides.length,
|
|
204
208
|
slideNumbering:
|