@agent-native/core 0.161.10 → 0.161.14
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/analytics/actions/clear-selected-dashboard-object.ts +71 -0
- package/corpus/templates/analytics/actions/view-screen.ts +68 -4
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -66
- package/corpus/templates/analytics/app/lib/selected-object.ts +53 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +21 -0
- package/corpus/templates/analytics/changelog/2026-08-18-ask-no-stale-dashboard-context.md +6 -0
- package/corpus/templates/calendar/app/root.tsx +12 -4
- package/corpus/templates/calendar/changelog/2026-08-18-calendar-stays-responsive-in-the-desktop-app-without-focus-t.md +6 -0
- package/corpus/templates/clips/app/components/library/recording-card.tsx +19 -3
- package/corpus/templates/clips/changelog/2026-08-18-deleting-a-recording-no-longer-freezes-clips.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +9 -10
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +4 -3
- package/corpus/templates/clips/desktop/src/lib/recording-start-guard.ts +26 -0
- package/corpus/templates/content/actions/export-document.ts +68 -3
- package/corpus/templates/content/changelog/2026-08-06-database-exports-now-include-their-pages.md +6 -0
- package/corpus/templates/content/shared/document-export.ts +25 -0
- package/corpus/templates/design/.agents/skills/creative-context/SKILL.md +3 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +66 -0
- package/corpus/templates/design/app/pages/Index.tsx +45 -1
- package/corpus/templates/design/app/pages/design-editor/creative-context-precedent.ts +233 -0
- package/corpus/templates/design/changelog/2026-08-11-new-designs-now-skip-the-intake-questions-and-follow-your-ex.md +6 -0
- package/corpus/templates/dispatch/app/chat-first-pages.ts +40 -40
- package/corpus/templates/dispatch/app/i18n-data.ts +5 -5
- package/corpus/templates/factory/AGENTS.md +30 -29
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +56 -9
- package/dist/action.js +68 -2
- package/dist/agent/chat-health-alert.js +10 -1
- package/dist/client/AssistantChat.js +1 -0
- package/dist/client/chat-first/app-open-actions.js +1 -1
- package/dist/client/chat-first/copy.js +2 -2
- package/dist/client/conversation/use-near-bottom-autoscroll.js +34 -4
- package/dist/client/embed-auth.js +40 -3
- package/dist/client/mcp-apps/McpAppRenderer.js +7 -2
- package/dist/client/route-state.js +42 -5
- package/dist/client/use-change-version.js +71 -25
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +223 -45
- package/dist/collab/awareness.js +37 -4
- package/dist/collab/client.js +17 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/embed-app.js +71 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/poll.d.ts +22 -14
- package/dist/server/poll.js +243 -67
- package/dist/styles/agent-conversation.css +25 -4
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
appStateKeyForBrowserTab,
|
|
4
|
+
compareAndSetAppState,
|
|
5
|
+
getCurrentRequestBrowserTabId,
|
|
6
|
+
readAppState,
|
|
7
|
+
} from "@agent-native/core/application-state";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
const SELECTED_OBJECT_STATE_KEY = "selected-object";
|
|
11
|
+
const SELECTED_OBJECT_SOURCE_FIELD = "__agentNativeSelectedObjectSource";
|
|
12
|
+
|
|
13
|
+
function selectedDashboardId(value: Record<string, unknown>): string | null {
|
|
14
|
+
const id =
|
|
15
|
+
value.type === "dashboard"
|
|
16
|
+
? value.id
|
|
17
|
+
: value.type === "dashboard-panel"
|
|
18
|
+
? value.dashboardId
|
|
19
|
+
: null;
|
|
20
|
+
return typeof id === "string" ? id : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default defineAction({
|
|
24
|
+
agentTool: false,
|
|
25
|
+
description:
|
|
26
|
+
"Clear the current browser tab's Analytics dashboard selection when it still belongs to that tab.",
|
|
27
|
+
schema: z.object({
|
|
28
|
+
browserTabId: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[A-Za-z0-9_-]{1,96}$/)
|
|
31
|
+
.optional(),
|
|
32
|
+
dashboardId: z.string().min(1).optional(),
|
|
33
|
+
expectedSelection: z.record(z.string(), z.unknown()).optional(),
|
|
34
|
+
source: z.string().min(1).max(96),
|
|
35
|
+
}),
|
|
36
|
+
run: async ({ browserTabId, dashboardId, expectedSelection, source }) => {
|
|
37
|
+
const effectiveBrowserTabId =
|
|
38
|
+
browserTabId ?? getCurrentRequestBrowserTabId();
|
|
39
|
+
const scopedStateKey = appStateKeyForBrowserTab(
|
|
40
|
+
SELECTED_OBJECT_STATE_KEY,
|
|
41
|
+
effectiveBrowserTabId,
|
|
42
|
+
);
|
|
43
|
+
const tabCurrent =
|
|
44
|
+
scopedStateKey === SELECTED_OBJECT_STATE_KEY
|
|
45
|
+
? null
|
|
46
|
+
: await readAppState(scopedStateKey);
|
|
47
|
+
const globalCurrent =
|
|
48
|
+
!effectiveBrowserTabId || !tabCurrent
|
|
49
|
+
? await readAppState(SELECTED_OBJECT_STATE_KEY)
|
|
50
|
+
: null;
|
|
51
|
+
const current = tabCurrent ?? globalCurrent;
|
|
52
|
+
const stateKey = tabCurrent ? scopedStateKey : SELECTED_OBJECT_STATE_KEY;
|
|
53
|
+
if (!current || current[SELECTED_OBJECT_SOURCE_FIELD] !== source) {
|
|
54
|
+
return { cleared: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const currentDashboardId = selectedDashboardId(current);
|
|
58
|
+
if (!currentDashboardId) return { cleared: false };
|
|
59
|
+
if (dashboardId && currentDashboardId !== dashboardId) {
|
|
60
|
+
return { cleared: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
cleared: await compareAndSetAppState(
|
|
65
|
+
stateKey,
|
|
66
|
+
expectedSelection ?? current,
|
|
67
|
+
null,
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -24,6 +24,35 @@ import { getMonitor, listMonitors } from "../server/lib/uptime-monitors.js";
|
|
|
24
24
|
|
|
25
25
|
const SESSION_FILTER_KEYS = new Set(["range", "app", "q"]);
|
|
26
26
|
const REPLAY_RANGES = new Set(["24h", "7d", "30d", "90d", "all"]);
|
|
27
|
+
const DASHBOARD_PATH_RE = /^\/(?:adhoc|dashboards)\/([^/]+)\/?$/;
|
|
28
|
+
|
|
29
|
+
function dashboardIdFromPathname(pathname: string): string | null {
|
|
30
|
+
const match = pathname.match(DASHBOARD_PATH_RE);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
try {
|
|
33
|
+
return decodeURIComponent(match[1]);
|
|
34
|
+
} catch {
|
|
35
|
+
return match[1];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizedDashboardId(value: unknown): string | null {
|
|
40
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
41
|
+
try {
|
|
42
|
+
return decodeURIComponent(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAskPathname(pathname: string): boolean {
|
|
49
|
+
return (
|
|
50
|
+
pathname === "" ||
|
|
51
|
+
pathname === "/" ||
|
|
52
|
+
pathname === "/ask" ||
|
|
53
|
+
pathname === "/overview"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
27
56
|
|
|
28
57
|
export default defineAction({
|
|
29
58
|
description:
|
|
@@ -38,10 +67,47 @@ export default defineAction({
|
|
|
38
67
|
search?: string;
|
|
39
68
|
searchParams?: Record<string, string>;
|
|
40
69
|
} | null;
|
|
41
|
-
const
|
|
70
|
+
const rawNav = navigation as any;
|
|
71
|
+
const pathname = typeof url?.pathname === "string" ? url.pathname : "";
|
|
72
|
+
const hasAuthoritativePathname = pathname.length > 0;
|
|
73
|
+
const pathnameDashboardId = hasAuthoritativePathname
|
|
74
|
+
? dashboardIdFromPathname(pathname)
|
|
75
|
+
: null;
|
|
76
|
+
const navigationDashboardId = normalizedDashboardId(rawNav?.dashboardId);
|
|
77
|
+
// The URL is authoritative during route transitions in both directions.
|
|
78
|
+
// A stale Ask navigation object must not hide the dashboard selection or
|
|
79
|
+
// trigger Ask-only behavior after the URL has moved to a dashboard.
|
|
80
|
+
const isAskView = hasAuthoritativePathname
|
|
81
|
+
? isAskPathname(pathname)
|
|
82
|
+
: rawNav?.view === "ask";
|
|
83
|
+
const hasStaleAskNavigation =
|
|
84
|
+
hasAuthoritativePathname && !isAskView && rawNav?.view === "ask";
|
|
85
|
+
const hasStaleDashboardNavigation =
|
|
86
|
+
hasAuthoritativePathname &&
|
|
87
|
+
rawNav?.view === "adhoc" &&
|
|
88
|
+
(pathnameDashboardId === null ||
|
|
89
|
+
navigationDashboardId !== pathnameDashboardId);
|
|
90
|
+
const effectiveNavigation = isAskView
|
|
91
|
+
? { view: "ask" }
|
|
92
|
+
: pathnameDashboardId !== null
|
|
93
|
+
? {
|
|
94
|
+
...(rawNav?.view === "adhoc" &&
|
|
95
|
+
navigationDashboardId === pathnameDashboardId
|
|
96
|
+
? rawNav
|
|
97
|
+
: {}),
|
|
98
|
+
view: "adhoc",
|
|
99
|
+
dashboardId: pathnameDashboardId,
|
|
100
|
+
}
|
|
101
|
+
: hasStaleAskNavigation || hasStaleDashboardNavigation
|
|
102
|
+
? null
|
|
103
|
+
: navigation;
|
|
104
|
+
const nav = effectiveNavigation as any;
|
|
105
|
+
const selectedObject = isAskView
|
|
106
|
+
? null
|
|
107
|
+
: await readAppStateForCurrentTab("selected-object");
|
|
42
108
|
|
|
43
109
|
const screen: Record<string, unknown> = {};
|
|
44
|
-
if (
|
|
110
|
+
if (effectiveNavigation) screen.navigation = effectiveNavigation;
|
|
45
111
|
if (url?.pathname) screen.pathname = url.pathname;
|
|
46
112
|
if (selectedObject) screen.selectedObject = selectedObject;
|
|
47
113
|
|
|
@@ -65,8 +131,6 @@ export default defineAction({
|
|
|
65
131
|
}
|
|
66
132
|
}
|
|
67
133
|
|
|
68
|
-
const nav = navigation as any;
|
|
69
|
-
|
|
70
134
|
if (nav?.view === "adhoc" && nav?.dashboardId) {
|
|
71
135
|
try {
|
|
72
136
|
const orgId = getRequestOrgId() || null;
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
type AgentSidebarStateChangeDetail,
|
|
6
6
|
useAgentChatContext,
|
|
7
7
|
} from "@agent-native/core/client/agent-chat";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
readClientAppState,
|
|
11
|
-
setClientAppState,
|
|
12
|
-
} from "@agent-native/core/client/hooks";
|
|
13
|
-
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { setClientAppState } from "@agent-native/core/client/hooks";
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
14
10
|
|
|
11
|
+
import {
|
|
12
|
+
clearSelectedDashboardObjectIfOwned,
|
|
13
|
+
type SelectedDashboardObject,
|
|
14
|
+
} from "@/lib/selected-object";
|
|
15
15
|
import { TAB_ID } from "@/lib/tab-id";
|
|
16
16
|
|
|
17
17
|
const DASHBOARD_CONTEXT_KEY = "analytics-selected-dashboard";
|
|
@@ -80,28 +80,6 @@ function dashboardContext(
|
|
|
80
80
|
return lines.join("\n");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
async function deleteSelectedObjectIfOwned(dashboardId: string) {
|
|
84
|
-
try {
|
|
85
|
-
const current = await readClientAppState<Record<string, unknown>>(
|
|
86
|
-
SELECTED_OBJECT_STATE_KEY,
|
|
87
|
-
);
|
|
88
|
-
if (current?.[SELECTED_OBJECT_SOURCE_FIELD] !== TAB_ID) return;
|
|
89
|
-
const selectedDashboardId =
|
|
90
|
-
current.type === "dashboard"
|
|
91
|
-
? current.id
|
|
92
|
-
: current.type === "dashboard-panel"
|
|
93
|
-
? current.dashboardId
|
|
94
|
-
: null;
|
|
95
|
-
if (selectedDashboardId !== dashboardId) return;
|
|
96
|
-
await deleteClientAppState(SELECTED_OBJECT_STATE_KEY, {
|
|
97
|
-
keepalive: true,
|
|
98
|
-
requestSource: TAB_ID,
|
|
99
|
-
});
|
|
100
|
-
} catch {
|
|
101
|
-
// Best effort only; avoid clearing another tab's selected object on errors.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
83
|
function panelSelectionMarker(dashboardId: string, panelId: string): string {
|
|
106
84
|
return `Analytics panel selection: dashboard=${encodeURIComponent(
|
|
107
85
|
dashboardId,
|
|
@@ -153,6 +131,7 @@ export function useDashboardChatContext(
|
|
|
153
131
|
? panelSelection.panelId
|
|
154
132
|
: null;
|
|
155
133
|
const hasSelectedPanel = selectedPanelId !== null;
|
|
134
|
+
const publishedSelectionRef = useRef<SelectedDashboardObject | null>(null);
|
|
156
135
|
|
|
157
136
|
const selectPanelForChat = useCallback(
|
|
158
137
|
(
|
|
@@ -179,6 +158,20 @@ export function useDashboardChatContext(
|
|
|
179
158
|
? "Inspect this panel with get-sql-dashboard (includeConfig: true) before changing it, then use mutate-dashboard for edits."
|
|
180
159
|
: "Inspect the linked Explorer config before changing this chart.",
|
|
181
160
|
].filter(Boolean);
|
|
161
|
+
const selection: SelectedDashboardObject = {
|
|
162
|
+
type: "dashboard-panel",
|
|
163
|
+
dashboardId: id,
|
|
164
|
+
dashboardKind: kind,
|
|
165
|
+
dashboardTitle: displayDashboardTitle,
|
|
166
|
+
panelId: panel.panelId,
|
|
167
|
+
panelTitle: displayPanelTitle,
|
|
168
|
+
panelKind: panel.panelKind,
|
|
169
|
+
chartType: panel.chartType || undefined,
|
|
170
|
+
source: panel.source || undefined,
|
|
171
|
+
configId: panel.configId || undefined,
|
|
172
|
+
extensionId: panel.extensionId || undefined,
|
|
173
|
+
[SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
|
|
174
|
+
};
|
|
182
175
|
|
|
183
176
|
// The panel context occupies one stable composer slot, so selecting a
|
|
184
177
|
// different panel replaces the prior chip instead of accumulating chips.
|
|
@@ -189,27 +182,11 @@ export function useDashboardChatContext(
|
|
|
189
182
|
openSidebar: options.openSidebar === true,
|
|
190
183
|
focus: options.focus ?? false,
|
|
191
184
|
});
|
|
192
|
-
setClientAppState(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
dashboardKind: kind,
|
|
198
|
-
dashboardTitle: displayDashboardTitle,
|
|
199
|
-
panelId: panel.panelId,
|
|
200
|
-
panelTitle: displayPanelTitle,
|
|
201
|
-
panelKind: panel.panelKind,
|
|
202
|
-
chartType: panel.chartType || undefined,
|
|
203
|
-
source: panel.source || undefined,
|
|
204
|
-
configId: panel.configId || undefined,
|
|
205
|
-
extensionId: panel.extensionId || undefined,
|
|
206
|
-
[SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
keepalive: true,
|
|
210
|
-
requestSource: TAB_ID,
|
|
211
|
-
},
|
|
212
|
-
).catch(() => {});
|
|
185
|
+
setClientAppState(SELECTED_OBJECT_STATE_KEY, selection, {
|
|
186
|
+
keepalive: true,
|
|
187
|
+
requestSource: TAB_ID,
|
|
188
|
+
}).catch(() => {});
|
|
189
|
+
publishedSelectionRef.current = selection;
|
|
213
190
|
},
|
|
214
191
|
[id, isAgentSidebarOpen, kind, title],
|
|
215
192
|
);
|
|
@@ -261,23 +238,21 @@ export function useDashboardChatContext(
|
|
|
261
238
|
useEffect(() => {
|
|
262
239
|
if (!id || hasSelectedPanel) return;
|
|
263
240
|
const displayTitle = title?.trim() || id;
|
|
241
|
+
const selection: SelectedDashboardObject = {
|
|
242
|
+
type: "dashboard",
|
|
243
|
+
id,
|
|
244
|
+
kind,
|
|
245
|
+
title: displayTitle,
|
|
246
|
+
panelCount,
|
|
247
|
+
canEdit,
|
|
248
|
+
[SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
|
|
249
|
+
};
|
|
264
250
|
const timer = setTimeout(() => {
|
|
265
|
-
setClientAppState(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
kind,
|
|
271
|
-
title: displayTitle,
|
|
272
|
-
panelCount,
|
|
273
|
-
canEdit,
|
|
274
|
-
[SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
keepalive: true,
|
|
278
|
-
requestSource: TAB_ID,
|
|
279
|
-
},
|
|
280
|
-
).catch(() => {});
|
|
251
|
+
setClientAppState(SELECTED_OBJECT_STATE_KEY, selection, {
|
|
252
|
+
keepalive: true,
|
|
253
|
+
requestSource: TAB_ID,
|
|
254
|
+
}).catch(() => {});
|
|
255
|
+
publishedSelectionRef.current = selection;
|
|
281
256
|
}, CONTEXT_PUBLISH_DELAY_MS);
|
|
282
257
|
return () => clearTimeout(timer);
|
|
283
258
|
}, [canEdit, hasSelectedPanel, id, kind, panelCount, title]);
|
|
@@ -293,7 +268,7 @@ export function useDashboardChatContext(
|
|
|
293
268
|
key: DASHBOARD_PANEL_CONTEXT_KEY,
|
|
294
269
|
openSidebar: false,
|
|
295
270
|
});
|
|
296
|
-
|
|
271
|
+
clearSelectedDashboardObjectIfOwned(publishedSelectionRef.current);
|
|
297
272
|
};
|
|
298
273
|
}, [id]);
|
|
299
274
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readClientAppState } from "@agent-native/core/client/application-state";
|
|
2
|
+
import { callAction } from "@agent-native/core/client/hooks";
|
|
3
|
+
|
|
4
|
+
import { TAB_ID } from "@/lib/tab-id";
|
|
5
|
+
|
|
6
|
+
export type SelectedDashboardObject = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export async function readSelectedDashboardObject(): Promise<SelectedDashboardObject | null> {
|
|
9
|
+
try {
|
|
10
|
+
const value =
|
|
11
|
+
await readClientAppState<SelectedDashboardObject>("selected-object");
|
|
12
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: null;
|
|
15
|
+
// A browser-side selection probe: "could not read" and "nothing selected"
|
|
16
|
+
// drive the same UI, and throwing breaks route transitions.
|
|
17
|
+
// coercion-ok: unreadable selection and no selection render identically
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clear this tab's dashboard selection without touching another tab's state.
|
|
25
|
+
* The action validates the tab marker and deletes with a compare-and-set so a
|
|
26
|
+
* route transition cannot erase a newer selection after the read.
|
|
27
|
+
*/
|
|
28
|
+
export async function clearSelectedDashboardObjectIfOwned(
|
|
29
|
+
dashboardIdOrSelection?: string | SelectedDashboardObject | null,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const dashboardId =
|
|
32
|
+
typeof dashboardIdOrSelection === "string"
|
|
33
|
+
? dashboardIdOrSelection
|
|
34
|
+
: undefined;
|
|
35
|
+
const expectedSelection =
|
|
36
|
+
dashboardIdOrSelection && typeof dashboardIdOrSelection === "object"
|
|
37
|
+
? dashboardIdOrSelection
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
if (!dashboardId && !expectedSelection) return;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await callAction("clear-selected-dashboard-object", {
|
|
44
|
+
...(dashboardId ? { dashboardId } : {}),
|
|
45
|
+
...(expectedSelection ? { expectedSelection } : {}),
|
|
46
|
+
browserTabId: TAB_ID,
|
|
47
|
+
source: TAB_ID,
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
// coercion-ok: best-effort cleanup must not break Ask navigation when state APIs fail.
|
|
51
|
+
// Best effort only; stale context is safer than clearing another tab's state.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -7,6 +7,10 @@ import { CreativeContextComposerChip } from "@agent-native/creative-context/clie
|
|
|
7
7
|
import { useEffect, useMemo } from "react";
|
|
8
8
|
|
|
9
9
|
import { ANALYTICS_CHAT_STORAGE_KEY } from "@/lib/chat-handoff";
|
|
10
|
+
import {
|
|
11
|
+
clearSelectedDashboardObjectIfOwned,
|
|
12
|
+
readSelectedDashboardObject,
|
|
13
|
+
} from "@/lib/selected-object";
|
|
10
14
|
import { TAB_ID } from "@/lib/tab-id";
|
|
11
15
|
|
|
12
16
|
const DASHBOARD_CONTEXT_KEYS = new Set([
|
|
@@ -31,6 +35,23 @@ export default function AskPage() {
|
|
|
31
35
|
}
|
|
32
36
|
}, [removeChatContextItem, staleDashboardContextKey]);
|
|
33
37
|
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let mounted = true;
|
|
40
|
+
const pathnameAtMount = window.location.pathname;
|
|
41
|
+
|
|
42
|
+
void readSelectedDashboardObject().then((selection) => {
|
|
43
|
+
// If the user already navigated away, this Ask instance no longer owns
|
|
44
|
+
// cleanup. The action also CASes the captured selection, covering a
|
|
45
|
+
// selection change that happens after this read but before the write.
|
|
46
|
+
if (!mounted || window.location.pathname !== pathnameAtMount) return;
|
|
47
|
+
if (selection) void clearSelectedDashboardObjectIfOwned(selection);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
mounted = false;
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
34
55
|
return (
|
|
35
56
|
<div className="analytics-ask-page flex h-full min-h-0 flex-col bg-background">
|
|
36
57
|
<AgentChatSurface
|
|
@@ -202,6 +202,13 @@ function isPublicBookingPath(pathname: string): boolean {
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function isAgentNativeDesktop(): boolean {
|
|
206
|
+
return (
|
|
207
|
+
typeof navigator !== "undefined" &&
|
|
208
|
+
/AgentNativeDesktop/i.test(navigator.userAgent)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
205
212
|
function AppContent() {
|
|
206
213
|
const [cmdkOpen, setCmdkOpen] = useState(false);
|
|
207
214
|
const navigate = useNavigate();
|
|
@@ -249,11 +256,12 @@ export default function Root() {
|
|
|
249
256
|
createAgentNativeQueryClient({
|
|
250
257
|
defaultOptions: {
|
|
251
258
|
queries: {
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
259
|
+
// Chrome gets one focus refresh because external calendar events can
|
|
260
|
+
// change without a DB sync event (e.g. delayed Google webhooks).
|
|
261
|
+
// Desktop already has the shell's focus-aware DB sync, and repeated
|
|
262
|
+
// webview focus events otherwise duplicate the events request.
|
|
255
263
|
// request-storm-allow: one user-driven focus refresh for provider data.
|
|
256
|
-
refetchOnWindowFocus:
|
|
264
|
+
refetchOnWindowFocus: !isAgentNativeDesktop(),
|
|
257
265
|
// Flat retry: calendar data fetches don't need the auth-aware
|
|
258
266
|
// retry function — auth errors surface through the booking flow.
|
|
259
267
|
retry: 1,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
IconAlertTriangle,
|
|
15
15
|
IconExternalLink,
|
|
16
16
|
} from "@tabler/icons-react";
|
|
17
|
-
import { useCallback, useMemo, useState } from "react";
|
|
17
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
18
18
|
import { Link } from "react-router";
|
|
19
19
|
|
|
20
20
|
import { AgentViewCount } from "@/components/player/recording-views-badge";
|
|
@@ -94,6 +94,8 @@ export function RecordingCard({
|
|
|
94
94
|
const t = useT();
|
|
95
95
|
const { formatDate, formatRelativeTime } = useFormatters();
|
|
96
96
|
const [hovered, setHovered] = useState(false);
|
|
97
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
98
|
+
const pendingTrashRef = useRef(false);
|
|
97
99
|
|
|
98
100
|
const duration = useMemo(
|
|
99
101
|
() => formatDuration(recording.durationMs),
|
|
@@ -185,6 +187,11 @@ export function RecordingCard({
|
|
|
185
187
|
attemptOpenDesktopApp();
|
|
186
188
|
}, []);
|
|
187
189
|
|
|
190
|
+
const requestTrash = useCallback(() => {
|
|
191
|
+
pendingTrashRef.current = true;
|
|
192
|
+
setMenuOpen(false);
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
188
195
|
return (
|
|
189
196
|
<div
|
|
190
197
|
role="article"
|
|
@@ -378,7 +385,7 @@ export function RecordingCard({
|
|
|
378
385
|
</div>
|
|
379
386
|
|
|
380
387
|
{showActions && (
|
|
381
|
-
<DropdownMenu>
|
|
388
|
+
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
382
389
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
383
390
|
<button
|
|
384
391
|
className="pointer-events-auto rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
@@ -390,6 +397,12 @@ export function RecordingCard({
|
|
|
390
397
|
<DropdownMenuContent
|
|
391
398
|
align="end"
|
|
392
399
|
onClick={(e) => e.stopPropagation()}
|
|
400
|
+
onCloseAutoFocus={(event) => {
|
|
401
|
+
if (!pendingTrashRef.current) return;
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
pendingTrashRef.current = false;
|
|
404
|
+
setTimeout(() => onTrash?.(recording), 0);
|
|
405
|
+
}}
|
|
393
406
|
>
|
|
394
407
|
{onShare && (
|
|
395
408
|
<DropdownMenuItem onSelect={() => onShare(recording)}>
|
|
@@ -453,7 +466,10 @@ export function RecordingCard({
|
|
|
453
466
|
))}
|
|
454
467
|
{onTrash && (
|
|
455
468
|
<DropdownMenuItem
|
|
456
|
-
onSelect={() =>
|
|
469
|
+
onSelect={(event) => {
|
|
470
|
+
event.preventDefault();
|
|
471
|
+
requestTrash();
|
|
472
|
+
}}
|
|
457
473
|
className="text-destructive focus:text-destructive"
|
|
458
474
|
>
|
|
459
475
|
<IconTrash className="h-4 w-4 me-2" />{" "}
|
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
copyRecordingShareLink,
|
|
113
113
|
recordingShareUrl,
|
|
114
114
|
} from "./lib/recording-link";
|
|
115
|
+
import { boundedCleanup } from "./lib/recording-start-guard";
|
|
115
116
|
import { REWIND_AGENT_PROMPT } from "./lib/rewind-agent-prompt";
|
|
116
117
|
import { getRewindStatusPresentation } from "./lib/rewind-status";
|
|
117
118
|
import {
|
|
@@ -3218,16 +3219,14 @@ export function App() {
|
|
|
3218
3219
|
bubbleStreamTransferredToRecorder.current = false;
|
|
3219
3220
|
recordingFlowGateRef.current = false;
|
|
3220
3221
|
setRecordingFlowActive(false);
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
// ignore — best-effort
|
|
3230
|
-
}
|
|
3222
|
+
// Bounded, not just best-effort: a plain unbounded await here would
|
|
3223
|
+
// let a stuck native command (e.g. a ScreenCaptureKit handshake that
|
|
3224
|
+
// never returns) turn this "always recovers" block into another
|
|
3225
|
+
// permanent hang on top of the one that just failed — exactly the
|
|
3226
|
+
// "stuck on Preparing…, have to restart" symptom this exists to
|
|
3227
|
+
// prevent.
|
|
3228
|
+
await boundedCleanup(invoke("set_recording_state", { active: false }));
|
|
3229
|
+
await boundedCleanup(invoke("show_popover"));
|
|
3231
3230
|
}
|
|
3232
3231
|
}
|
|
3233
3232
|
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
type NativeRecordingRequestOptions,
|
|
85
85
|
} from "./recording-request";
|
|
86
86
|
import {
|
|
87
|
+
boundedCleanup,
|
|
87
88
|
guardRecordingStart,
|
|
88
89
|
RECORDING_START_TIMEOUT_MS,
|
|
89
90
|
RecordingStartCancelledError,
|
|
@@ -2748,7 +2749,7 @@ async function tryStartRewindFullscreenRecording(
|
|
|
2748
2749
|
},
|
|
2749
2750
|
async countdown() {
|
|
2750
2751
|
console.log("[rewind-latency] countdown shown after preparation");
|
|
2751
|
-
await invoke("hide_preparing")
|
|
2752
|
+
await boundedCleanup(invoke("hide_preparing"));
|
|
2752
2753
|
// Overlap whisper startup with the countdown so the capture boundary
|
|
2753
2754
|
// never waits on it, but have both settled before activation.
|
|
2754
2755
|
await Promise.all([
|
|
@@ -2792,7 +2793,7 @@ async function tryStartRewindFullscreenRecording(
|
|
|
2792
2793
|
});
|
|
2793
2794
|
}
|
|
2794
2795
|
} catch (err) {
|
|
2795
|
-
await invoke("hide_preparing")
|
|
2796
|
+
await boundedCleanup(invoke("hide_preparing"));
|
|
2796
2797
|
transcriptionAborted = true;
|
|
2797
2798
|
// Cast: `transcriptionCapture` is only assigned inside the
|
|
2798
2799
|
// `startRewindTranscription` closure, which TS's control-flow analysis
|
|
@@ -4090,7 +4091,7 @@ export async function startRecording(
|
|
|
4090
4091
|
},
|
|
4091
4092
|
});
|
|
4092
4093
|
} catch (err) {
|
|
4093
|
-
await invoke("hide_recording_chrome")
|
|
4094
|
+
await boundedCleanup(invoke("hide_recording_chrome"));
|
|
4094
4095
|
const e = err as { name?: string; message?: string } | null;
|
|
4095
4096
|
console.error(
|
|
4096
4097
|
"[clips-recorder] startRecording threw:",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export const RECORDING_START_TIMEOUT_MS = 90_000;
|
|
2
2
|
|
|
3
|
+
// Cleanup invokes (closing overlay windows, clearing recording state) are
|
|
4
|
+
// normally instant. This bounds them well above any real duration but far
|
|
5
|
+
// below forever, so a stuck native call (e.g. a ScreenCaptureKit handshake
|
|
6
|
+
// that never returns its completion) can't turn "recovery" into another
|
|
7
|
+
// permanent hang on top of the one the start guard already gave up on.
|
|
8
|
+
export const RECOVERY_INVOKE_TIMEOUT_MS = 5_000;
|
|
9
|
+
|
|
3
10
|
export class RecordingStartTimeoutError extends Error {
|
|
4
11
|
constructor(timeoutMs: number) {
|
|
5
12
|
super(
|
|
@@ -82,3 +89,22 @@ export function guardRecordingStart<T>(
|
|
|
82
89
|
}, timeoutMs);
|
|
83
90
|
});
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Best-effort recovery step that must never block the caller. Recording-start
|
|
95
|
+
* failure paths (and their "always restore the UI" `finally` blocks) await
|
|
96
|
+
* cleanup invokes like `hide_recording_chrome` — if the underlying native
|
|
97
|
+
* command hangs, an unbounded `await ...catch(() => {})` hangs the whole
|
|
98
|
+
* recovery with it, leaving the toolbar/"Preparing…" state stuck until the
|
|
99
|
+
* user restarts the app. Swallows both success and failure; only existence
|
|
100
|
+
* to cap how long a single cleanup step can stall recovery.
|
|
101
|
+
*/
|
|
102
|
+
export function boundedCleanup(
|
|
103
|
+
operation: Promise<unknown>,
|
|
104
|
+
timeoutMs = RECOVERY_INVOKE_TIMEOUT_MS,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
return guardRecordingStart(operation, { timeoutMs }).then(
|
|
107
|
+
() => undefined,
|
|
108
|
+
() => undefined,
|
|
109
|
+
);
|
|
110
|
+
}
|