@agent-native/core 0.168.7 → 0.168.10
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/sync-calendars.ts +10 -29
- 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/content/app/components/editor/SlashCommandMenu.tsx +1 -0
- package/corpus/templates/content/app/global.css +5 -0
- package/corpus/templates/content/docs/solutions/2026-08-20-code-block-slash-production-regression-shape.md +492 -0
- package/corpus/templates/content/docs/solutions/2026-08-20-code-block-surface-background-shape.md +273 -0
- 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/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +10 -3
- 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
|
);
|
|
@@ -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:`,
|
|
@@ -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
|
+
}
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
|
|
24
24
|
export const CALENDAR_MEETING_ID_PREFIX = "gcal";
|
|
25
25
|
|
|
26
|
+
const CALENDAR_REQUEST_SAFETY_MARGIN_MS = 60 * 1000;
|
|
27
|
+
|
|
26
28
|
export interface CalendarAccountForEvents {
|
|
27
29
|
id: string;
|
|
28
30
|
provider: string;
|
|
@@ -74,8 +76,7 @@ export function shouldMarkNeedsReauth(message: string): boolean {
|
|
|
74
76
|
lower.includes("google calendar event failed (401)") ||
|
|
75
77
|
lower.includes("invalid_grant") ||
|
|
76
78
|
lower.includes("invalid_token") ||
|
|
77
|
-
lower.includes("insufficient_scope")
|
|
78
|
-
lower.includes("token refresh failed")
|
|
79
|
+
lower.includes("insufficient_scope")
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -122,10 +123,16 @@ export async function resolveCalendarAccessToken(
|
|
|
122
123
|
});
|
|
123
124
|
} catch (err) {
|
|
124
125
|
// Only a permanent failure (dead refresh token / bad OAuth client) means
|
|
125
|
-
// "needs-reauth" — collapse those to `null` as before.
|
|
126
|
-
//
|
|
127
|
-
// record it as a soft sync error without flipping account status.
|
|
126
|
+
// "needs-reauth" — collapse those to `null` as before. During the refresh
|
|
127
|
+
// buffer, a transient failure can safely reuse the still-valid token.
|
|
128
128
|
if (isPermanentRefreshFailure(err)) return null;
|
|
129
|
+
if (
|
|
130
|
+
bundle?.accessToken &&
|
|
131
|
+
bundle.expiresAt &&
|
|
132
|
+
bundle.expiresAt > Date.now() + CALENDAR_REQUEST_SAFETY_MARGIN_MS
|
|
133
|
+
) {
|
|
134
|
+
return bundle.accessToken;
|
|
135
|
+
}
|
|
129
136
|
throw err;
|
|
130
137
|
}
|
|
131
138
|
if (!refreshed.access_token) return null;
|
|
@@ -158,13 +165,13 @@ export async function recordCalendarFetchSuccess(
|
|
|
158
165
|
.set({
|
|
159
166
|
lastSyncedAt: now,
|
|
160
167
|
lastSyncError: null,
|
|
161
|
-
status: "connected",
|
|
162
168
|
updatedAt: now,
|
|
163
169
|
})
|
|
164
170
|
.where(
|
|
165
171
|
and(
|
|
166
172
|
eq(schema.calendarAccounts.id, account.id),
|
|
167
173
|
eq(schema.calendarAccounts.ownerEmail, account.ownerEmail),
|
|
174
|
+
eq(schema.calendarAccounts.status, "connected"),
|
|
168
175
|
),
|
|
169
176
|
);
|
|
170
177
|
}
|
|
@@ -172,10 +179,11 @@ export async function recordCalendarFetchSuccess(
|
|
|
172
179
|
export async function recordCalendarFetchError(
|
|
173
180
|
account: CalendarAccountForEvents,
|
|
174
181
|
error: unknown,
|
|
182
|
+
options: { needsReauth?: boolean } = {},
|
|
175
183
|
): Promise<CalendarFetchError> {
|
|
176
184
|
const message =
|
|
177
185
|
error instanceof Error ? error.message : String(error || "Calendar failed");
|
|
178
|
-
const needsReauth = shouldMarkNeedsReauth(message);
|
|
186
|
+
const needsReauth = options.needsReauth ?? shouldMarkNeedsReauth(message);
|
|
179
187
|
if (!account.ownerEmail) {
|
|
180
188
|
return { accountId: account.id, error: message, needsReauth };
|
|
181
189
|
}
|
|
@@ -183,7 +191,7 @@ export async function recordCalendarFetchError(
|
|
|
183
191
|
await db
|
|
184
192
|
.update(schema.calendarAccounts)
|
|
185
193
|
.set({
|
|
186
|
-
|
|
194
|
+
...(needsReauth ? { status: "needs-reauth" as const } : {}),
|
|
187
195
|
lastSyncError: needsReauth
|
|
188
196
|
? "Google Calendar needs to be reconnected."
|
|
189
197
|
: message,
|
|
@@ -320,7 +328,9 @@ export async function fetchLiveCalendarEventFromId(virtualId: string) {
|
|
|
320
328
|
return null;
|
|
321
329
|
}
|
|
322
330
|
if (!accessToken) {
|
|
323
|
-
await recordCalendarFetchError(account, new Error("Token refresh failed")
|
|
331
|
+
await recordCalendarFetchError(account, new Error("Token refresh failed"), {
|
|
332
|
+
needsReauth: true,
|
|
333
|
+
});
|
|
324
334
|
return null;
|
|
325
335
|
}
|
|
326
336
|
|
|
@@ -729,6 +729,11 @@
|
|
|
729
729
|
background: hsl(var(--muted));
|
|
730
730
|
border-radius: 0 0 6px 6px;
|
|
731
731
|
}
|
|
732
|
+
.notion-editor .notion-code-block {
|
|
733
|
+
background: hsl(var(--muted));
|
|
734
|
+
border-radius: 6px;
|
|
735
|
+
margin: 0.6em 0;
|
|
736
|
+
}
|
|
732
737
|
.notion-editor pre code {
|
|
733
738
|
font-family: inherit;
|
|
734
739
|
background: none;
|