@agent-native/core 0.168.6 → 0.168.9
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/mutate-dashboard.ts +8 -1
- package/corpus/templates/calendar/actions/delete-event.ts +34 -18
- package/corpus/templates/calendar/actions/delete-events.ts +13 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +22 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +19 -0
- package/corpus/templates/calendar/server/lib/google-api.ts +18 -1
- package/corpus/templates/clips/actions/finalize-meeting.ts +16 -0
- package/corpus/templates/clips/actions/list-meetings.ts +1 -0
- package/corpus/templates/clips/actions/list-recordings.ts +14 -0
- package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
- package/corpus/templates/clips/app/components/library/recording-card.tsx +2 -2
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +20 -3
- package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +12 -5
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
- package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
- package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
- package/corpus/templates/factory/AGENTS.md +12 -22
- package/corpus/templates/factory/README.md +11 -8
- package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
- package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
- package/corpus/templates/factory/actions/create-factory.ts +211 -0
- package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
- package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
- package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
- package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
- package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
- package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
- package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
- package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
- package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
- package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
- package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
- package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
- package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
- package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
- package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
- package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
- package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
- package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
- package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
- package/corpus/templates/factory/actions/view-screen.ts +22 -30
- package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
- package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
- package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
- package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
- package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
- package/corpus/templates/factory/app/routes/factory.tsx +78 -62
- package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
- package/corpus/templates/factory/server/db/schema.ts +18 -3
- package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
- package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
- package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
- package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
- package/corpus/templates/factory/server/triage/audit.ts +4 -0
- package/corpus/templates/factory/server/triage/ids.ts +4 -0
- package/dist/cli/native-dependencies.js +4 -1
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +9 -4
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat/index.js +1 -1
- package/dist/client/chat-first/apps-rail.js +6 -6
- package/dist/client/chat-first/primary-nav.js +14 -6
- package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
- package/dist/client/integrations/IntegrationsPanel.js +29 -21
- package/dist/client/integrations/index.d.ts +1 -1
- package/dist/client/integrations/index.js +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/ssr-handler.js +19 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -49,6 +49,13 @@ const mutationTargetSchema = {
|
|
|
49
49
|
.describe("Where in rowNumber to place the panel. Defaults to end."),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
const insertPanelSchema = z
|
|
53
|
+
.record(z.string(), z.unknown())
|
|
54
|
+
.refine(
|
|
55
|
+
(panel) => typeof panel.id === "string" && panel.id.trim().length > 0,
|
|
56
|
+
{ message: "panel.id must be a non-empty string" },
|
|
57
|
+
);
|
|
58
|
+
|
|
52
59
|
const mutationOperationSchema = z.discriminatedUnion("op", [
|
|
53
60
|
z.object({
|
|
54
61
|
op: z.literal("movePanels"),
|
|
@@ -72,7 +79,7 @@ const mutationOperationSchema = z.discriminatedUnion("op", [
|
|
|
72
79
|
}),
|
|
73
80
|
z.object({
|
|
74
81
|
op: z.literal("insertPanel"),
|
|
75
|
-
panel:
|
|
82
|
+
panel: insertPanelSchema,
|
|
76
83
|
...mutationTargetSchema,
|
|
77
84
|
}),
|
|
78
85
|
z.object({
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
normalizeGuestNotificationMessage,
|
|
6
6
|
sendEventGuestNotificationNote,
|
|
7
7
|
} from "../server/lib/event-guest-notifications.js";
|
|
8
|
+
import { isGoogleNotFoundError } from "../server/lib/google-api.js";
|
|
8
9
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
9
10
|
import {
|
|
10
11
|
cliBoolean,
|
|
@@ -71,25 +72,39 @@ export default defineAction({
|
|
|
71
72
|
? "none"
|
|
72
73
|
: (args.sendUpdates ?? (shouldNotifyGuests ? "all" : "none")),
|
|
73
74
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
let eventForNotification;
|
|
76
|
+
try {
|
|
77
|
+
eventForNotification = shouldNotifyGuests
|
|
78
|
+
? await googleCalendar.getEvent(googleEventId, {
|
|
79
|
+
ownerEmail,
|
|
80
|
+
accountEmail,
|
|
81
|
+
})
|
|
82
|
+
: undefined;
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
if (args.removeOnly) {
|
|
85
|
+
await googleCalendar.removeEventFromCalendar(
|
|
86
|
+
googleEventId,
|
|
87
|
+
{ ownerEmail, accountEmail },
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
await googleCalendar.deleteEvent(
|
|
92
|
+
googleEventId,
|
|
93
|
+
{ ownerEmail, accountEmail },
|
|
94
|
+
options,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!isGoogleNotFoundError(error)) throw error;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
alreadyAbsent: true,
|
|
103
|
+
id: `google-${googleEventId}`,
|
|
104
|
+
accountEmail,
|
|
105
|
+
scope: args.scope,
|
|
106
|
+
removedOnly: args.removeOnly ?? false,
|
|
107
|
+
};
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
const guestNotification =
|
|
@@ -105,6 +120,7 @@ export default defineAction({
|
|
|
105
120
|
|
|
106
121
|
return {
|
|
107
122
|
success: true,
|
|
123
|
+
alreadyAbsent: false,
|
|
108
124
|
id: `google-${googleEventId}`,
|
|
109
125
|
accountEmail,
|
|
110
126
|
scope: args.scope,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type WeekdayName,
|
|
11
11
|
} from "../server/lib/event-weekday.js";
|
|
12
12
|
import { zonedDateTimeToUtcIso } from "../server/lib/find-time.js";
|
|
13
|
+
import { isGoogleNotFoundError } from "../server/lib/google-api.js";
|
|
13
14
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
14
15
|
import type { CalendarEvent } from "../shared/api.js";
|
|
15
16
|
import {
|
|
@@ -36,7 +37,7 @@ const MAX_MATCHED_EVENTS = 200;
|
|
|
36
37
|
* firing every delete at once and turning a clean batch into retries. */
|
|
37
38
|
const DELETE_CONCURRENCY = 4;
|
|
38
39
|
|
|
39
|
-
type Outcome = "deleted" | "matched" | "skipped" | "failed";
|
|
40
|
+
type Outcome = "deleted" | "already_absent" | "matched" | "skipped" | "failed";
|
|
40
41
|
|
|
41
42
|
interface EventResult {
|
|
42
43
|
id: string;
|
|
@@ -449,6 +450,7 @@ export default defineAction({
|
|
|
449
450
|
...summaryBase,
|
|
450
451
|
dryRun: true,
|
|
451
452
|
deleted: 0,
|
|
453
|
+
alreadyAbsent: 0,
|
|
452
454
|
failed: 0,
|
|
453
455
|
skipped: results.length,
|
|
454
456
|
events: [...targets.map((target) => target.display), ...results],
|
|
@@ -483,6 +485,13 @@ export default defineAction({
|
|
|
483
485
|
}
|
|
484
486
|
return { ...target.display, outcome: "deleted" };
|
|
485
487
|
} catch (error) {
|
|
488
|
+
if (isGoogleNotFoundError(error)) {
|
|
489
|
+
return {
|
|
490
|
+
...target.display,
|
|
491
|
+
outcome: "already_absent",
|
|
492
|
+
reason: "Already absent from Google Calendar",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
486
495
|
return {
|
|
487
496
|
...target.display,
|
|
488
497
|
outcome: "failed",
|
|
@@ -498,6 +507,9 @@ export default defineAction({
|
|
|
498
507
|
dryRun: false,
|
|
499
508
|
deleted: deleteResults.filter((entry) => entry.outcome === "deleted")
|
|
500
509
|
.length,
|
|
510
|
+
alreadyAbsent: deleteResults.filter(
|
|
511
|
+
(entry) => entry.outcome === "already_absent",
|
|
512
|
+
).length,
|
|
501
513
|
failed: deleteResults.filter((entry) => entry.outcome === "failed")
|
|
502
514
|
.length,
|
|
503
515
|
skipped: results.length,
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "@/components/ui/tooltip";
|
|
36
36
|
import { useUpdateEvent } from "@/hooks/use-events";
|
|
37
37
|
import { useViewPreferences } from "@/hooks/use-view-preferences";
|
|
38
|
+
import { getDisplayDateInTimezone } from "@/lib/calendar-timezone";
|
|
38
39
|
import { getEditableEventTitle } from "@/lib/event-form-utils";
|
|
39
40
|
import { isOutOfOfficeEvent } from "@/lib/out-of-office";
|
|
40
41
|
import { cn } from "@/lib/utils";
|
|
@@ -447,15 +448,33 @@ export function EventDetailPanel({
|
|
|
447
448
|
) : (
|
|
448
449
|
<>
|
|
449
450
|
<span className="text-foreground">
|
|
450
|
-
{format(
|
|
451
|
+
{format(
|
|
452
|
+
getDisplayDateInTimezone(
|
|
453
|
+
event.start,
|
|
454
|
+
event.startTimeZone ?? event.endTimeZone,
|
|
455
|
+
),
|
|
456
|
+
"h:mm a",
|
|
457
|
+
)}
|
|
451
458
|
{" → "}
|
|
452
|
-
{format(
|
|
459
|
+
{format(
|
|
460
|
+
getDisplayDateInTimezone(
|
|
461
|
+
event.end,
|
|
462
|
+
event.endTimeZone ?? event.startTimeZone,
|
|
463
|
+
),
|
|
464
|
+
"h:mm a",
|
|
465
|
+
)}
|
|
453
466
|
</span>
|
|
454
467
|
<span className="ml-2 text-muted-foreground/70">
|
|
455
468
|
{formatDuration(event.start, event.end)}
|
|
456
469
|
</span>
|
|
457
470
|
<div className="mt-0.5 text-muted-foreground">
|
|
458
|
-
{format(
|
|
471
|
+
{format(
|
|
472
|
+
getDisplayDateInTimezone(
|
|
473
|
+
event.start,
|
|
474
|
+
event.startTimeZone ?? event.endTimeZone,
|
|
475
|
+
),
|
|
476
|
+
"EEE MMM d",
|
|
477
|
+
)}
|
|
459
478
|
</div>
|
|
460
479
|
</>
|
|
461
480
|
)}
|
|
@@ -118,6 +118,25 @@ export function getDateTimePartsInTimezone(
|
|
|
118
118
|
return dateTimeParts(value, normalizeTimezone(timezone));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Build a local Date carrying an event's wall-clock fields so date-fns can
|
|
123
|
+
* format those fields without converting them through the browser timezone.
|
|
124
|
+
* The result is for display only; it must not be used for date arithmetic.
|
|
125
|
+
*/
|
|
126
|
+
export function getDisplayDateInTimezone(
|
|
127
|
+
value: Date | string,
|
|
128
|
+
timezone?: string | null,
|
|
129
|
+
): Date {
|
|
130
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
131
|
+
if (!timezone) return parsed;
|
|
132
|
+
|
|
133
|
+
const parts = getDateTimePartsInTimezone(value, timezone);
|
|
134
|
+
if (!parts) return parsed;
|
|
135
|
+
|
|
136
|
+
const [year, month, day] = parts.date.split("-").map(Number);
|
|
137
|
+
return new Date(year, month - 1, day, parts.hour, parts.minute, parts.second);
|
|
138
|
+
}
|
|
139
|
+
|
|
121
140
|
export function getDateKeyInTimezone(
|
|
122
141
|
value: Date | string,
|
|
123
142
|
timezone: string,
|
|
@@ -113,6 +113,23 @@ export function createOAuth2Client(
|
|
|
113
113
|
// Authenticated fetch helper
|
|
114
114
|
// ---------------------------------------------------------------------------
|
|
115
115
|
|
|
116
|
+
export class GoogleApiError extends Error {
|
|
117
|
+
readonly status: number;
|
|
118
|
+
|
|
119
|
+
constructor(status: number, message: string) {
|
|
120
|
+
super(`Google API error (${status}): ${message}`);
|
|
121
|
+
this.name = "GoogleApiError";
|
|
122
|
+
this.status = status;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isGoogleNotFoundError(error: unknown): boolean {
|
|
127
|
+
return (
|
|
128
|
+
(error instanceof GoogleApiError && error.status === 404) ||
|
|
129
|
+
(error instanceof Error && /^Google API error \(404\):/.test(error.message))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
export async function googleFetch(
|
|
117
134
|
url: string,
|
|
118
135
|
accessToken: string,
|
|
@@ -133,7 +150,7 @@ export async function googleFetch(
|
|
|
133
150
|
(data as any)?.error?.message ||
|
|
134
151
|
(data as any)?.error_description ||
|
|
135
152
|
res.statusText;
|
|
136
|
-
throw new
|
|
153
|
+
throw new GoogleApiError(res.status, msg);
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
return data;
|
|
@@ -27,6 +27,17 @@ import { loadAgentsMdContext } from "./lib/agents-md-context.js";
|
|
|
27
27
|
// handles crashed processes without stealing an active text-model run.
|
|
28
28
|
const PENDING_STALE_MS = 2 * 60 * 1000;
|
|
29
29
|
|
|
30
|
+
export function skippedFinalizeResult(meetingId: string) {
|
|
31
|
+
return {
|
|
32
|
+
meetingId,
|
|
33
|
+
summaryMd: "",
|
|
34
|
+
bullets: [],
|
|
35
|
+
actionItems: [],
|
|
36
|
+
provider: null,
|
|
37
|
+
skipped: "no-transcript" as const,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
function trimContextValue(value: string, maxChars: number): string {
|
|
31
42
|
const trimmed = value.replace(/\0/g, "").trim();
|
|
32
43
|
if (trimmed.length <= maxChars) return trimmed;
|
|
@@ -80,6 +91,11 @@ export default defineAction({
|
|
|
80
91
|
updatedAt: nowIso,
|
|
81
92
|
})
|
|
82
93
|
.where(eq(schema.meetings.id, args.meetingId));
|
|
94
|
+
// The desktop stop path can finish before its final transcript flush
|
|
95
|
+
// lands. That is a completed recording without notes, not an action
|
|
96
|
+
// failure worth returning as HTTP 500; the explicit regenerate flow
|
|
97
|
+
// still receives the actionable error below.
|
|
98
|
+
if (!args.force) return skippedFinalizeResult(args.meetingId);
|
|
83
99
|
throw new Error(
|
|
84
100
|
`Cannot finalize meeting ${args.meetingId} — no transcript text available yet.`,
|
|
85
101
|
);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "drizzle-orm";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
|
+
import { effectiveDuration, parseEdits } from "../app/lib/timestamp-mapping.js";
|
|
17
18
|
import { getDb, schema } from "../server/db/index.js";
|
|
18
19
|
import {
|
|
19
20
|
agentRecordingAccessFilter,
|
|
@@ -303,6 +304,9 @@ export default defineAction({
|
|
|
303
304
|
thumbnailUrl: schema.recordings.thumbnailUrl,
|
|
304
305
|
animatedThumbnailUrl: schema.recordings.animatedThumbnailUrl,
|
|
305
306
|
durationMs: schema.recordings.durationMs,
|
|
307
|
+
// Needed to derive the edited-length badge below; dropped before the
|
|
308
|
+
// response is built so the raw edits blob never reaches the client.
|
|
309
|
+
editsJson: schema.recordings.editsJson,
|
|
306
310
|
status: schema.recordings.status,
|
|
307
311
|
uploadProgress: schema.recordings.uploadProgress,
|
|
308
312
|
failureReason: schema.recordings.failureReason,
|
|
@@ -417,7 +421,17 @@ export default defineAction({
|
|
|
417
421
|
description: r.description,
|
|
418
422
|
thumbnailUrl: r.thumbnailUrl,
|
|
419
423
|
animatedThumbnailUrl: r.animatedThumbnailUrl,
|
|
424
|
+
// Raw source length. StitchManager sums this across queued
|
|
425
|
+
// recordings to size the concatenated export, which always
|
|
426
|
+
// includes each source's full untrimmed media — trims are applied
|
|
427
|
+
// at export/playback time, not by dropping bytes from the source.
|
|
420
428
|
durationMs: r.durationMs,
|
|
429
|
+
// Edited length, not the original recorded length — matches what
|
|
430
|
+
// the clip page itself shows once trims/cuts are applied.
|
|
431
|
+
effectiveDurationMs: effectiveDuration(
|
|
432
|
+
r.durationMs,
|
|
433
|
+
parseEdits(r.editsJson),
|
|
434
|
+
),
|
|
421
435
|
status: r.status,
|
|
422
436
|
uploadProgress: r.uploadProgress,
|
|
423
437
|
failureReason: r.failureReason,
|
|
@@ -23,6 +23,8 @@ import { z } from "zod";
|
|
|
23
23
|
|
|
24
24
|
import { getDb, schema } from "../server/db/index.js";
|
|
25
25
|
import {
|
|
26
|
+
recordCalendarFetchError,
|
|
27
|
+
recordCalendarFetchSuccess,
|
|
26
28
|
resolveCalendarAccessToken,
|
|
27
29
|
shouldMarkNeedsReauth,
|
|
28
30
|
} from "../server/lib/calendar-event-meetings.js";
|
|
@@ -98,24 +100,11 @@ export default defineAction({
|
|
|
98
100
|
// sync error via shouldMarkNeedsReauth without flipping status.
|
|
99
101
|
const accessToken = await resolveCalendarAccessToken(account);
|
|
100
102
|
if (!accessToken) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.update(schema.calendarAccounts)
|
|
107
|
-
.set({
|
|
108
|
-
status: "needs-reauth",
|
|
109
|
-
lastSyncError: "Token refresh failed — reconnect required.",
|
|
110
|
-
updatedAt: new Date().toISOString(),
|
|
111
|
-
})
|
|
112
|
-
.where(eq(schema.calendarAccounts.id, account.id));
|
|
113
|
-
} catch (writeErr: any) {
|
|
114
|
-
console.warn(
|
|
115
|
-
`[sync-calendars] failed to flag account ${account.id} as needs-reauth:`,
|
|
116
|
-
writeErr?.message ?? writeErr,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
103
|
+
await recordCalendarFetchError(
|
|
104
|
+
account,
|
|
105
|
+
new Error("Token refresh failed"),
|
|
106
|
+
{ needsReauth: true },
|
|
107
|
+
);
|
|
119
108
|
errors.push({
|
|
120
109
|
accountId: account.id,
|
|
121
110
|
error: "needs-reauth",
|
|
@@ -206,18 +195,10 @@ export default defineAction({
|
|
|
206
195
|
perAccountEvents += 1;
|
|
207
196
|
}
|
|
208
197
|
|
|
209
|
-
//
|
|
210
|
-
//
|
|
198
|
+
// Keep the success write isolated and guarded so an in-flight sweep
|
|
199
|
+
// cannot overwrite a concurrent needs-reauth decision.
|
|
211
200
|
try {
|
|
212
|
-
await
|
|
213
|
-
.update(schema.calendarAccounts)
|
|
214
|
-
.set({
|
|
215
|
-
lastSyncedAt: new Date().toISOString(),
|
|
216
|
-
lastSyncError: null,
|
|
217
|
-
status: "connected",
|
|
218
|
-
updatedAt: new Date().toISOString(),
|
|
219
|
-
})
|
|
220
|
-
.where(eq(schema.calendarAccounts.id, account.id));
|
|
201
|
+
await recordCalendarFetchSuccess(account);
|
|
221
202
|
} catch (writeErr: any) {
|
|
222
203
|
console.warn(
|
|
223
204
|
`[sync-calendars] failed to update account ${account.id} after success:`,
|
|
@@ -98,8 +98,8 @@ export function RecordingCard({
|
|
|
98
98
|
const pendingTrashRef = useRef(false);
|
|
99
99
|
|
|
100
100
|
const duration = useMemo(
|
|
101
|
-
() => formatDuration(recording.
|
|
102
|
-
[recording.
|
|
101
|
+
() => formatDuration(recording.effectiveDurationMs),
|
|
102
|
+
[recording.effectiveDurationMs],
|
|
103
103
|
);
|
|
104
104
|
const relative = useMemo(() => {
|
|
105
105
|
const date = new Date(recording.createdAt);
|
|
@@ -51,6 +51,7 @@ export function RecordingOptionsMenu({
|
|
|
51
51
|
const [open, setOpen] = useState(false);
|
|
52
52
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
53
53
|
const deletedWhileOpenRef = useRef(false);
|
|
54
|
+
const pendingDeleteConfirmRef = useRef(false);
|
|
54
55
|
const showDownload = canDownload && Boolean(onDownload);
|
|
55
56
|
const showDelete = canDelete;
|
|
56
57
|
const trashRecording = useActionMutation<any, { id: string }>(
|
|
@@ -99,7 +100,22 @@ export function RecordingOptionsMenu({
|
|
|
99
100
|
<IconDotsVertical className="h-4 w-4" />
|
|
100
101
|
</Button>
|
|
101
102
|
</DropdownMenuTrigger>
|
|
102
|
-
<DropdownMenuContent
|
|
103
|
+
<DropdownMenuContent
|
|
104
|
+
align="end"
|
|
105
|
+
className="w-44"
|
|
106
|
+
onCloseAutoFocus={(event) => {
|
|
107
|
+
// Opening the AlertDialog while this menu is still tearing down
|
|
108
|
+
// leaves `pointer-events: none` stuck on <body>: two dismissable
|
|
109
|
+
// layers overlap and the survivor never restores the style. Wait
|
|
110
|
+
// for the menu to finish closing, and keep focus off the trigger
|
|
111
|
+
// so the dialog owns it.
|
|
112
|
+
if (pendingDeleteConfirmRef.current) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
pendingDeleteConfirmRef.current = false;
|
|
115
|
+
setOpen(true);
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
103
119
|
{showDownload ? (
|
|
104
120
|
<DropdownMenuItem
|
|
105
121
|
onSelect={handleDownload}
|
|
@@ -116,7 +132,8 @@ export function RecordingOptionsMenu({
|
|
|
116
132
|
<DropdownMenuItem
|
|
117
133
|
onSelect={(event) => {
|
|
118
134
|
event.preventDefault();
|
|
119
|
-
|
|
135
|
+
pendingDeleteConfirmRef.current = true;
|
|
136
|
+
setMenuOpen(false);
|
|
120
137
|
}}
|
|
121
138
|
className="text-destructive focus:text-destructive"
|
|
122
139
|
>
|
|
@@ -132,7 +149,7 @@ export function RecordingOptionsMenu({
|
|
|
132
149
|
if (!deletedWhileOpenRef.current) return;
|
|
133
150
|
deletedWhileOpenRef.current = false;
|
|
134
151
|
event.preventDefault();
|
|
135
|
-
|
|
152
|
+
onDeleted?.();
|
|
136
153
|
}}
|
|
137
154
|
>
|
|
138
155
|
<AlertDialogHeader>
|
|
@@ -15,6 +15,7 @@ export interface RecordingSummary {
|
|
|
15
15
|
thumbnailUrl: string | null;
|
|
16
16
|
animatedThumbnailUrl: string | null;
|
|
17
17
|
durationMs: number;
|
|
18
|
+
effectiveDurationMs: number;
|
|
18
19
|
status: "uploading" | "processing" | "ready" | "failed";
|
|
19
20
|
uploadProgress?: number;
|
|
20
21
|
failureReason?: string | null;
|
|
@@ -247,9 +247,8 @@ function nativeSaveFailureMessage(reason: string | null | undefined): string {
|
|
|
247
247
|
return "The desktop recorder finished and saved a local copy, but Clips could not upload it. You can retry from the Clips menu without recording again.";
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
export function
|
|
250
|
+
export function BackButton({ onBack }: { onBack: () => void }) {
|
|
251
251
|
const t = useT();
|
|
252
|
-
const navigate = useNavigate();
|
|
253
252
|
|
|
254
253
|
return (
|
|
255
254
|
<Tooltip>
|
|
@@ -258,7 +257,7 @@ export function BackToLibraryButton() {
|
|
|
258
257
|
variant="ghost"
|
|
259
258
|
size="icon"
|
|
260
259
|
className="shrink-0"
|
|
261
|
-
onClick={
|
|
260
|
+
onClick={onBack}
|
|
262
261
|
aria-label={t("recordingPage.backToLibrary")}
|
|
263
262
|
>
|
|
264
263
|
<IconArrowLeft className="h-4 w-4 rtl:-scale-x-100" />
|
|
@@ -1053,7 +1052,9 @@ export default function RecordingPage() {
|
|
|
1053
1052
|
return (
|
|
1054
1053
|
<div className="flex min-h-screen w-full flex-col bg-background">
|
|
1055
1054
|
<header className="flex min-w-0 shrink-0 items-center gap-2 border-b border-border px-3 py-2 sm:px-4 sm:py-3">
|
|
1056
|
-
<
|
|
1055
|
+
<BackButton
|
|
1056
|
+
onBack={() => navigate("/library", { replace: true })}
|
|
1057
|
+
/>
|
|
1057
1058
|
<div className="min-w-0 flex-1">
|
|
1058
1059
|
<p className="truncate text-sm font-medium">{visibleTitle}</p>
|
|
1059
1060
|
<p className="truncate text-xs text-muted-foreground">
|
|
@@ -1369,7 +1370,13 @@ export default function RecordingPage() {
|
|
|
1369
1370
|
{/* Main video column */}
|
|
1370
1371
|
<div className="flex w-full min-w-0 flex-col xl:flex-1">
|
|
1371
1372
|
<header className="flex min-w-0 shrink-0 items-center gap-2 border-b border-border px-3 py-2 sm:px-4 sm:py-3">
|
|
1372
|
-
<
|
|
1373
|
+
<BackButton
|
|
1374
|
+
onBack={
|
|
1375
|
+
editing
|
|
1376
|
+
? () => setEditing(false)
|
|
1377
|
+
: () => navigate("/library", { replace: true })
|
|
1378
|
+
}
|
|
1379
|
+
/>
|
|
1373
1380
|
<div className="flex-1 min-w-0">
|
|
1374
1381
|
<EditableRecordingTitle
|
|
1375
1382
|
recordingId={recording.id}
|
|
@@ -4,6 +4,7 @@ import { open as openExternal } from "@tauri-apps/plugin-shell";
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
5
5
|
|
|
6
6
|
import { callAppBundleIdsForJoinUrl } from "../lib/meeting-call-app";
|
|
7
|
+
import { stopMeetingBeforeTranscriptFlush } from "../lib/meeting-stop";
|
|
7
8
|
import {
|
|
8
9
|
appendFinalTranscript,
|
|
9
10
|
onFinalTranscript,
|
|
@@ -195,18 +196,33 @@ export function useMeetingTranscription({
|
|
|
195
196
|
}
|
|
196
197
|
});
|
|
197
198
|
await invoke("silence_detector_stop").catch(() => {});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
199
|
+
await stopMeetingBeforeTranscriptFlush({
|
|
200
|
+
// Stamp actualEnd as soon as capture is torn down. Transcript
|
|
201
|
+
// history and network flushes may be slow or unavailable, but they
|
|
202
|
+
// must not leave the meeting looking live in the meantime.
|
|
203
|
+
stopRecording: async () => {
|
|
204
|
+
await callClipsAction("stop-meeting-recording", {
|
|
205
|
+
meetingId: session.meetingId,
|
|
206
|
+
}).catch((err) => {
|
|
207
|
+
console.warn("[clips-popover] stop meeting action failed:", err);
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
waitForHistory: async () => {
|
|
211
|
+
if (reason !== "app-quit") {
|
|
212
|
+
await session.historyInFlight?.catch(() => {});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
// Final flush waits for any in-flight flush first
|
|
216
|
+
// (flushTranscript's single-flight coalescing) and sends the
|
|
217
|
+
// definitive snapshot.
|
|
218
|
+
flushTranscript: async () => {
|
|
219
|
+
await flushTranscript().catch((err) => {
|
|
220
|
+
console.warn(
|
|
221
|
+
"[clips-popover] meeting transcript save failed:",
|
|
222
|
+
err,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
},
|
|
210
226
|
});
|
|
211
227
|
if (session.lines.length) {
|
|
212
228
|
const finalizePromise = callClipsAction("finalize-meeting", {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface StopMeetingBeforeTranscriptFlushOptions {
|
|
2
|
+
stopRecording: () => Promise<void>;
|
|
3
|
+
waitForHistory: () => Promise<void>;
|
|
4
|
+
flushTranscript: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Persist the terminal meeting state before waiting on transcript work. */
|
|
8
|
+
export async function stopMeetingBeforeTranscriptFlush({
|
|
9
|
+
stopRecording,
|
|
10
|
+
waitForHistory,
|
|
11
|
+
flushTranscript,
|
|
12
|
+
}: StopMeetingBeforeTranscriptFlushOptions): Promise<void> {
|
|
13
|
+
await stopRecording();
|
|
14
|
+
await waitForHistory();
|
|
15
|
+
await flushTranscript();
|
|
16
|
+
}
|