@agent-native/core 0.157.5 → 0.157.7
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/agent-native.config.ts +5 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +23 -5
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/assets/netlify.toml +3 -0
- package/corpus/templates/brain/netlify.toml +3 -0
- package/corpus/templates/calendar/agent-native.config.ts +5 -0
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
- package/corpus/templates/calendar/netlify.toml +3 -0
- package/corpus/templates/chat/agent-native.config.ts +5 -0
- package/corpus/templates/chat/netlify.toml +3 -0
- package/corpus/templates/clips/actions/create-meeting.ts +19 -1
- package/corpus/templates/clips/actions/lib/meeting-content.ts +28 -0
- package/corpus/templates/clips/actions/list-meetings.ts +33 -11
- package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +48 -25
- package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +7 -6
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +388 -178
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -13
- package/corpus/templates/clips/app/components/transcript/transcript-segment-row.tsx +98 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
- package/corpus/templates/clips/app/lib/meeting-history-query.ts +11 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +4 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +49 -31
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +47 -21
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +15 -1
- package/corpus/templates/clips/netlify.toml +8 -4
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +10 -0
- package/corpus/templates/clips/shared/transcript-segments.ts +20 -2
- package/corpus/templates/content/netlify.toml +3 -0
- package/corpus/templates/design/app/i18n-data.ts +2 -0
- package/corpus/templates/design/netlify.toml +3 -0
- package/corpus/templates/dispatch/netlify.toml +3 -0
- package/corpus/templates/factory/netlify.toml +3 -0
- package/corpus/templates/forms/netlify.toml +3 -0
- package/corpus/templates/macros/netlify.toml +3 -0
- package/corpus/templates/mail/agent-native.config.ts +5 -0
- package/corpus/templates/mail/netlify.toml +3 -0
- package/corpus/templates/plan/netlify.toml +3 -0
- package/corpus/templates/slides/netlify.toml +3 -0
- package/dist/agent/harness/acp-adapter.js +17 -2
- package/dist/agent/harness/ai-sdk-adapter.js +14 -3
- package/dist/agent/harness/background.js +10 -5
- package/dist/agent/harness/hosted.d.ts +20 -0
- package/dist/agent/harness/hosted.js +100 -0
- package/dist/agent/harness/runner.js +100 -77
- package/dist/agent/observational-memory/observer.js +12 -0
- package/dist/agent/production-agent.d.ts +3 -0
- package/dist/agent/production-agent.js +74 -4
- package/dist/agent/thread-data-builder.d.ts +12 -0
- package/dist/agent/thread-data-builder.js +23 -0
- package/dist/agent/types.d.ts +6 -0
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.js +113 -17
- package/dist/client/AgentTaskCard.d.ts +2 -2
- package/dist/client/AgentTaskCard.js +8 -22
- package/dist/client/AssistantChat.d.ts +8 -0
- package/dist/client/AssistantChat.js +106 -59
- package/dist/client/MultiTabAssistantChat.js +50 -6
- package/dist/client/NewWorkspaceAppFlow.js +7 -1
- package/dist/client/agent-chat-adapter.d.ts +6 -0
- package/dist/client/agent-chat-adapter.js +36 -8
- package/dist/client/app-chat-sidebar.d.ts +5 -0
- package/dist/client/app-chat-sidebar.js +53 -9
- package/dist/client/chat/composer-draft.d.ts +4 -0
- package/dist/client/chat/composer-draft.js +63 -0
- package/dist/client/chat-first/apps-rail.js +10 -1
- package/dist/client/chat-first/primary-nav.js +10 -5
- package/dist/client/frame-protocol.d.ts +1 -0
- package/dist/client/hooks/index.d.ts +1 -1
- package/dist/client/hooks/index.js +1 -1
- package/dist/client/i18n.js +5 -2
- package/dist/client/progress/RunsTray.js +129 -16
- package/dist/client/settings/SettingsRow.js +1 -1
- package/dist/client/use-chat-threads.js +11 -6
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +52 -0
- package/dist/deploy/build.js +28 -11
- package/dist/extensions/actions.js +22 -1
- package/dist/hosted-harness/actions/get-hosted-harness-config.d.ts +2 -0
- package/dist/hosted-harness/actions/get-hosted-harness-config.js +13 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.d.ts +4 -0
- package/dist/hosted-harness/actions/set-hosted-harness-enabled.js +32 -0
- package/dist/localization/default-messages.d.ts +4 -1
- package/dist/localization/default-messages.js +5 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +12 -0
- package/dist/server/agent-chat-plugin.js +24 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-teams.d.ts +20 -12
- package/dist/server/agent-teams.js +69 -35
- package/dist/server/hosted-harness-policy.d.ts +17 -0
- package/dist/server/hosted-harness-policy.js +38 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +4 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +39 -0
- package/dist/templates/chat/agent-native.config.ts +5 -0
- package/dist/templates/chat/netlify.toml +3 -0
- package/dist/vite/action-types-plugin.js +8 -0
- package/package.json +4 -2
- package/src/templates/chat/agent-native.config.ts +5 -0
- package/src/templates/chat/netlify.toml +3 -0
package/corpus/README.md
CHANGED
|
@@ -19,6 +19,7 @@ AGENT_PROD_CODE_EXECUTION = "sandboxed"
|
|
|
19
19
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
20
20
|
|
|
21
21
|
[context.production.environment]
|
|
22
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
22
23
|
# Analytics owns its schema at release time (`migrate:production` above), so
|
|
23
24
|
# production request functions skip migrations. Preview functions still run
|
|
24
25
|
# request-path migrations because previews do not run the production release
|
|
@@ -651,9 +651,13 @@ function rewriteSqlFunctionCalls(
|
|
|
651
651
|
return match ? result : result + sql.slice(cursor);
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
-
function
|
|
655
|
-
const
|
|
656
|
-
|
|
654
|
+
function coerceDateComparisonOperands(sql: string): string {
|
|
655
|
+
const dateField = "(?:event_date|cohort_date)";
|
|
656
|
+
const qualifiedDateField = `(?:[A-Za-z_][A-Za-z0-9_]*\\.)?${dateField}`;
|
|
657
|
+
const comparisonRe = new RegExp(
|
|
658
|
+
`\\b${qualifiedDateField}\\s*(?:<=|>=|<>|=|<|>)\\s*`,
|
|
659
|
+
"gi",
|
|
660
|
+
);
|
|
657
661
|
let cursor = 0;
|
|
658
662
|
let result = "";
|
|
659
663
|
let match = comparisonRe.exec(sql);
|
|
@@ -685,7 +689,10 @@ function coerceEventDateComparisonOperands(sql: string): string {
|
|
|
685
689
|
}
|
|
686
690
|
result += sql.slice(cursor);
|
|
687
691
|
return result.replace(
|
|
688
|
-
|
|
692
|
+
new RegExp(
|
|
693
|
+
`(\\b${qualifiedDateField}\\s*(?:<=|>=|<>|=|<|>)\\s*)'(\\d{4}-\\d{2}-\\d{2})'`,
|
|
694
|
+
"gi",
|
|
695
|
+
),
|
|
689
696
|
"$1DATE '$2'",
|
|
690
697
|
);
|
|
691
698
|
}
|
|
@@ -813,6 +820,17 @@ function translatePostgresJsonOperators(sql: string): string {
|
|
|
813
820
|
|
|
814
821
|
function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
|
|
815
822
|
let translated = translatePostgresJsonOperators(sql);
|
|
823
|
+
translated = rewriteSqlFunctionCalls(translated, "coalesce", (args) => {
|
|
824
|
+
const uniqueArgs: string[] = [];
|
|
825
|
+
const seen = new Set<string>();
|
|
826
|
+
for (const arg of args) {
|
|
827
|
+
const normalized = arg.trim();
|
|
828
|
+
if (seen.has(normalized)) continue;
|
|
829
|
+
seen.add(normalized);
|
|
830
|
+
uniqueArgs.push(arg);
|
|
831
|
+
}
|
|
832
|
+
return `COALESCE(${uniqueArgs.join(", ")})`;
|
|
833
|
+
});
|
|
816
834
|
translated = translated.replace(
|
|
817
835
|
/\bINTERVAL\s*'(\d+)\s+(day|days|week|weeks|month|months)'/gi,
|
|
818
836
|
(_match, amount: string, unit: string) =>
|
|
@@ -975,7 +993,7 @@ export function renderFirstPartyAnalyticsBigQuerySql(
|
|
|
975
993
|
translateFirstPartyAnalyticsBigQuerySql(normalizedScopeSql);
|
|
976
994
|
const bound = bindSqlArguments(translated, args);
|
|
977
995
|
return addPartitionPrunedEventDeduplication(
|
|
978
|
-
|
|
996
|
+
coerceDateComparisonOperands(qualifyQuerySources(bound, table)),
|
|
979
997
|
table,
|
|
980
998
|
);
|
|
981
999
|
}
|
|
@@ -61,6 +61,10 @@ function hasSuccessfulDashboardSave(
|
|
|
61
61
|
"update-dashboard",
|
|
62
62
|
"mutate-dashboard",
|
|
63
63
|
"compose-dashboard",
|
|
64
|
+
// An extension edit is the whole job when the dashboard panel IS the
|
|
65
|
+
// extension. Leaving it out meant a turn that saved exactly what the user
|
|
66
|
+
// asked for still had to prove itself with a data query.
|
|
67
|
+
"update-extension",
|
|
64
68
|
]);
|
|
65
69
|
return (toolResults ?? []).some((result) => {
|
|
66
70
|
if (result.isError) return false;
|
|
@@ -11,6 +11,9 @@ NITRO_PRESET = "netlify"
|
|
|
11
11
|
NPM_CONFIG_PRODUCTION = "false"
|
|
12
12
|
AGENT_CHAT_DURABLE_BACKGROUND = "true"
|
|
13
13
|
|
|
14
|
+
[context.production.environment]
|
|
15
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
16
|
+
|
|
14
17
|
# Emit the Netlify background function used by Assets' durable agent-chat opt-in.
|
|
15
18
|
# The normal server timeout still protects non-agent image/video HTTP handlers.
|
|
16
19
|
[functions."*"]
|
|
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
|
|
|
10
10
|
NITRO_PRESET = "netlify"
|
|
11
11
|
NPM_CONFIG_PRODUCTION = "false"
|
|
12
12
|
|
|
13
|
+
[context.production.environment]
|
|
14
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
15
|
+
|
|
13
16
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
14
17
|
# The framework does not impose a default agent-run timeout; if a template sets
|
|
15
18
|
# AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
|
|
@@ -385,7 +385,10 @@ export function AppLayout({ children }: AppLayoutProps) {
|
|
|
385
385
|
open={shortcutsHelpOpen}
|
|
386
386
|
onClose={() => setShortcutsHelpOpen(false)}
|
|
387
387
|
/>
|
|
388
|
-
<div
|
|
388
|
+
<div
|
|
389
|
+
className="agent-layout-shell flex h-screen overflow-hidden bg-background"
|
|
390
|
+
data-agent-native-shell-variant="custom"
|
|
391
|
+
>
|
|
389
392
|
<Sidebar
|
|
390
393
|
open={sidebarOpen}
|
|
391
394
|
onClose={() => setSidebarOpen(false)}
|
|
@@ -10,6 +10,9 @@ GTM_CONTAINER_ID = "GTM-N3WSTXZ"
|
|
|
10
10
|
NITRO_PRESET = "netlify"
|
|
11
11
|
NPM_CONFIG_PRODUCTION = "false"
|
|
12
12
|
|
|
13
|
+
[context.production.environment]
|
|
14
|
+
AGENT_NATIVE_HOSTED_HARNESS = "true"
|
|
15
|
+
|
|
13
16
|
# A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
|
|
14
17
|
# The framework does not impose a default agent-run timeout; if a template sets
|
|
15
18
|
# AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { defineAction } from "@agent-native/core";
|
|
11
11
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
12
|
+
import { getRequestUserName } from "@agent-native/core/server/request-context";
|
|
12
13
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
13
14
|
import { and, eq, isNull } from "drizzle-orm";
|
|
14
15
|
import { z } from "zod";
|
|
@@ -93,7 +94,7 @@ export default defineAction({
|
|
|
93
94
|
email: string;
|
|
94
95
|
name?: string;
|
|
95
96
|
isOrganizer?: boolean;
|
|
96
|
-
}> = args.participants ?? [];
|
|
97
|
+
}> = [...(args.participants ?? [])];
|
|
97
98
|
let calendarEventIdLink: string | null = null;
|
|
98
99
|
let source: "calendar" | "adhoc" | "manual" =
|
|
99
100
|
args.source ?? (args.title ? "manual" : "adhoc");
|
|
@@ -190,6 +191,23 @@ export default defineAction({
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// Backfill a missing display name onto the owner's own attendee row —
|
|
195
|
+
// but only when they're already a genuine calendar attendee. Inserting
|
|
196
|
+
// a synthetic row for a non-attendee owner would put their email in
|
|
197
|
+
// meeting_participants indistinguishably from a real attendee, and that
|
|
198
|
+
// table is what the public share payload returns; a meeting made public
|
|
199
|
+
// later would leak an email the owner never actually disclosed. When the
|
|
200
|
+
// owner isn't an attendee, speaker resolution already has a safe
|
|
201
|
+
// fallback (the generic "Me" label) — no snapshot needed for that case.
|
|
202
|
+
const ownerName = getRequestUserName()?.trim() || undefined;
|
|
203
|
+
const ownerParticipant = participantsToInsert.find(
|
|
204
|
+
(participant) =>
|
|
205
|
+
participant.email.trim().toLowerCase() === ownerEmail.toLowerCase(),
|
|
206
|
+
);
|
|
207
|
+
if (ownerParticipant && !ownerParticipant.name?.trim() && ownerName) {
|
|
208
|
+
ownerParticipant.name = ownerName;
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
const visibility =
|
|
194
212
|
args.visibility ??
|
|
195
213
|
(await getDefaultRecordingVisibility(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a persisted meeting contains something a user can reopen.
|
|
3
|
+
*
|
|
4
|
+
* Calendar events are materialized before they are recorded, so the meetings
|
|
5
|
+
* table also contains empty calendar rows. History should keep notes and
|
|
6
|
+
* completed meeting rows even when no recording was linked.
|
|
7
|
+
*/
|
|
8
|
+
export interface MeetingContentFields {
|
|
9
|
+
recordingId?: string | null;
|
|
10
|
+
actualStart?: string | null;
|
|
11
|
+
actualEnd?: string | null;
|
|
12
|
+
summaryMd?: string | null;
|
|
13
|
+
userNotesMd?: string | null;
|
|
14
|
+
bulletsJson?: string | null;
|
|
15
|
+
actionItemsJson?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function meetingRowHasContent(row: MeetingContentFields): boolean {
|
|
19
|
+
return Boolean(
|
|
20
|
+
row.recordingId ||
|
|
21
|
+
row.actualStart ||
|
|
22
|
+
row.actualEnd ||
|
|
23
|
+
(row.summaryMd ?? "").trim() ||
|
|
24
|
+
(row.userNotesMd ?? "").trim() ||
|
|
25
|
+
(row.bulletsJson ?? "[]") !== "[]" ||
|
|
26
|
+
(row.actionItemsJson ?? "[]") !== "[]",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
lt,
|
|
27
27
|
gte,
|
|
28
28
|
lte,
|
|
29
|
+
ne,
|
|
29
30
|
or,
|
|
30
31
|
sql,
|
|
31
32
|
} from "drizzle-orm";
|
|
@@ -48,9 +49,23 @@ import {
|
|
|
48
49
|
} from "../server/lib/calendar-event-meetings.js";
|
|
49
50
|
import { listEvents } from "../server/lib/google-calendar-client.js";
|
|
50
51
|
import { booleanParam } from "./lib/cli-params.js";
|
|
52
|
+
import { meetingRowHasContent } from "./lib/meeting-content.js";
|
|
51
53
|
|
|
52
54
|
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
53
55
|
|
|
56
|
+
/** SQL mirror of `meetingRowHasContent`. Keep history filtering consistent. */
|
|
57
|
+
function meetingHasContentFilter() {
|
|
58
|
+
return or(
|
|
59
|
+
isNotNull(schema.meetings.recordingId),
|
|
60
|
+
isNotNull(schema.meetings.actualStart),
|
|
61
|
+
isNotNull(schema.meetings.actualEnd),
|
|
62
|
+
sql`trim(${schema.meetings.summaryMd}) <> ''`,
|
|
63
|
+
sql`trim(${schema.meetings.userNotesMd}) <> ''`,
|
|
64
|
+
ne(schema.meetings.bulletsJson, "[]"),
|
|
65
|
+
ne(schema.meetings.actionItemsJson, "[]"),
|
|
66
|
+
)!;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
export default defineAction({
|
|
55
70
|
description:
|
|
56
71
|
"List meetings (Granola-style) the current user has access to. Connected calendars are read live; use view='upcoming' / 'past' / 'all' / 'trash' to filter by lifecycle.",
|
|
@@ -64,6 +79,11 @@ export default defineAction({
|
|
|
64
79
|
recordedOnly: booleanParam
|
|
65
80
|
.default(false)
|
|
66
81
|
.describe("Only return persisted meetings that have a linked recording."),
|
|
82
|
+
hasContent: booleanParam
|
|
83
|
+
.default(false)
|
|
84
|
+
.describe(
|
|
85
|
+
"Only return persisted meetings that contain a recording, completed capture, notes, a summary, bullets, or action items.",
|
|
86
|
+
),
|
|
67
87
|
includeLiveCalendar: booleanParam
|
|
68
88
|
.default(true)
|
|
69
89
|
.describe(
|
|
@@ -109,8 +129,9 @@ export default defineAction({
|
|
|
109
129
|
// slice(offset, offset + limit) at the end. To make that final slice
|
|
110
130
|
// correct we must fetch enough rows from BOTH sources to cover the whole
|
|
111
131
|
// offset + limit window before merging — fetching only `limit` would drop
|
|
112
|
-
// events once offset > 0 or the calendar is large. Keep the
|
|
113
|
-
//
|
|
132
|
+
// events once offset > 0 or the calendar is large. Keep the live-event cap
|
|
133
|
+
// so a huge calendar can't blow up the request. Persisted history needs
|
|
134
|
+
// the full sentinel window so pages remain available after 500 rows.
|
|
114
135
|
const windowCount = args.offset + args.limit;
|
|
115
136
|
const upcomingWindowMaxIso = args.upcomingWithinMin
|
|
116
137
|
? new Date(
|
|
@@ -160,6 +181,9 @@ export default defineAction({
|
|
|
160
181
|
if (args.recordedOnly) {
|
|
161
182
|
whereClauses.push(isNotNull(schema.meetings.recordingId));
|
|
162
183
|
}
|
|
184
|
+
if (args.hasContent) {
|
|
185
|
+
whereClauses.push(meetingHasContentFilter());
|
|
186
|
+
}
|
|
163
187
|
|
|
164
188
|
const orderBy =
|
|
165
189
|
args.view === "upcoming"
|
|
@@ -175,7 +199,7 @@ export default defineAction({
|
|
|
175
199
|
.from(schema.meetings)
|
|
176
200
|
.where(and(...whereClauses))
|
|
177
201
|
.orderBy(...orderBy)
|
|
178
|
-
.limit(
|
|
202
|
+
.limit(windowCount + 1)
|
|
179
203
|
.offset(0);
|
|
180
204
|
|
|
181
205
|
// Add a derived `summaryPreview` (first ~100 chars of summaryMd) so the
|
|
@@ -383,13 +407,7 @@ export default defineAction({
|
|
|
383
407
|
if (
|
|
384
408
|
liveEventEmitted &&
|
|
385
409
|
meeting.source === "calendar" &&
|
|
386
|
-
!meeting
|
|
387
|
-
!meeting.actualStart &&
|
|
388
|
-
!meeting.actualEnd &&
|
|
389
|
-
!(meeting.summaryMd ?? "").trim() &&
|
|
390
|
-
!(meeting.userNotesMd ?? "").trim() &&
|
|
391
|
-
(meeting.bulletsJson ?? "[]") === "[]" &&
|
|
392
|
-
(meeting.actionItemsJson ?? "[]") === "[]"
|
|
410
|
+
!meetingRowHasContent(meeting)
|
|
393
411
|
) {
|
|
394
412
|
continue;
|
|
395
413
|
}
|
|
@@ -407,6 +425,10 @@ export default defineAction({
|
|
|
407
425
|
|
|
408
426
|
const meetings = combined.slice(args.offset, args.offset + args.limit);
|
|
409
427
|
|
|
410
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
meetings,
|
|
430
|
+
calendarErrors,
|
|
431
|
+
hasMore: combined.length > args.offset + args.limit,
|
|
432
|
+
};
|
|
411
433
|
},
|
|
412
434
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@/components/ui/tooltip";
|
|
10
10
|
import { cn } from "@/lib/utils";
|
|
11
11
|
|
|
12
|
+
import { formatTranscriptTimestamp } from "../transcript/transcript-segment-row";
|
|
12
13
|
import type { TranscriptSegment } from "./transcript-bubbles";
|
|
13
14
|
|
|
14
15
|
const STOPWORDS = new Set<string>([
|
|
@@ -129,39 +130,61 @@ export function BulletLink({
|
|
|
129
130
|
onJumpTo,
|
|
130
131
|
children,
|
|
131
132
|
}: BulletLinkProps) {
|
|
133
|
+
const t = useT();
|
|
132
134
|
const matchIndex = useMemo(
|
|
133
135
|
() => findBestSegmentMatch(bullet, segments),
|
|
134
136
|
[bullet, segments],
|
|
135
137
|
);
|
|
136
138
|
const hasMatch = matchIndex >= 0;
|
|
139
|
+
const timestamp = hasMatch
|
|
140
|
+
? formatTranscriptTimestamp(segments[matchIndex]!.startMs)
|
|
141
|
+
: null;
|
|
137
142
|
|
|
138
143
|
return (
|
|
139
144
|
<TooltipProvider delayDuration={200}>
|
|
140
145
|
<div className="group flex items-start gap-1.5">
|
|
141
|
-
<div className="flex
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
</
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</
|
|
164
|
-
|
|
146
|
+
<div className="flex min-w-0 flex-1">{children}</div>
|
|
147
|
+
{timestamp ? (
|
|
148
|
+
<Tooltip>
|
|
149
|
+
<TooltipTrigger asChild>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => onJumpTo(matchIndex)}
|
|
153
|
+
aria-label={t("bulletLink.jumpToTranscript", {
|
|
154
|
+
time: timestamp,
|
|
155
|
+
})}
|
|
156
|
+
className={cn(
|
|
157
|
+
"mt-0.5 inline-flex h-5 shrink-0 items-center rounded px-1 font-mono text-[10px] tabular-nums text-muted-foreground/80 opacity-0 transition-opacity",
|
|
158
|
+
"group-hover:opacity-100 [@media(hover:none)]:opacity-100",
|
|
159
|
+
"cursor-pointer hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{timestamp}
|
|
163
|
+
</button>
|
|
164
|
+
</TooltipTrigger>
|
|
165
|
+
<TooltipContent side="top">
|
|
166
|
+
{t("bulletLink.jumpToTranscript", { time: timestamp })}
|
|
167
|
+
</TooltipContent>
|
|
168
|
+
</Tooltip>
|
|
169
|
+
) : (
|
|
170
|
+
<Tooltip>
|
|
171
|
+
<TooltipTrigger asChild>
|
|
172
|
+
{/* A span, not a button — there's no action to disable, just
|
|
173
|
+
a status to announce, and tabIndex keeps it keyboard- and
|
|
174
|
+
screen-reader-reachable (a disabled button wouldn't be). */}
|
|
175
|
+
<span
|
|
176
|
+
tabIndex={0}
|
|
177
|
+
aria-label={t("bulletLink.noMatchingMoment")}
|
|
178
|
+
className="mt-0.5 inline-flex h-5 shrink-0 cursor-default items-center rounded px-1 font-mono text-[10px] tabular-nums text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [@media(hover:none)]:opacity-100"
|
|
179
|
+
>
|
|
180
|
+
--:--
|
|
181
|
+
</span>
|
|
182
|
+
</TooltipTrigger>
|
|
183
|
+
<TooltipContent side="top">
|
|
184
|
+
{t("bulletLink.noMatchingMoment")}
|
|
185
|
+
</TooltipContent>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
)}
|
|
165
188
|
</div>
|
|
166
189
|
</TooltipProvider>
|
|
167
190
|
);
|
|
@@ -29,9 +29,10 @@ interface CanvasEditorProps {
|
|
|
29
29
|
userNotesMd?: string;
|
|
30
30
|
/** Save user notes. Called on blur after edit. */
|
|
31
31
|
onUserNotesChange?: (next: string) => void;
|
|
32
|
-
/** AI-generated summary
|
|
32
|
+
/** AI-generated summary — primary content, so it renders full-strength
|
|
33
|
+
* foreground like user notes. For the "ai" view. */
|
|
33
34
|
summaryMd?: string;
|
|
34
|
-
/** AI-generated bullets
|
|
35
|
+
/** AI-generated bullets — same weight as the summary. For the "ai" view. */
|
|
35
36
|
bullets?: string[];
|
|
36
37
|
/** Save AI summary when the user edits the summary section. */
|
|
37
38
|
onSummaryChange?: (next: string) => void;
|
|
@@ -76,7 +77,7 @@ export function CanvasEditor({
|
|
|
76
77
|
/>
|
|
77
78
|
)}
|
|
78
79
|
|
|
79
|
-
{/* AI bullets —
|
|
80
|
+
{/* AI bullets — with optional BulletLink wrappers */}
|
|
80
81
|
{showAi && bullets.length > 0 && (
|
|
81
82
|
<AiBulletsBlock bullets={bullets} renderBullet={renderBullet} />
|
|
82
83
|
)}
|
|
@@ -181,7 +182,7 @@ function AiSummaryBlock({
|
|
|
181
182
|
return (
|
|
182
183
|
<div className="space-y-1.5">
|
|
183
184
|
<AiTabIndicator />
|
|
184
|
-
<p className="text-sm leading-relaxed whitespace-pre-wrap text-
|
|
185
|
+
<p className="text-sm leading-relaxed whitespace-pre-wrap text-foreground">
|
|
185
186
|
{value}
|
|
186
187
|
</p>
|
|
187
188
|
</div>
|
|
@@ -234,7 +235,7 @@ function AiSummaryBlock({
|
|
|
234
235
|
>
|
|
235
236
|
<p
|
|
236
237
|
className={cn(
|
|
237
|
-
"text-sm leading-relaxed whitespace-pre-wrap text-
|
|
238
|
+
"text-sm leading-relaxed whitespace-pre-wrap text-foreground rounded -mx-1 px-1 group-hover:bg-accent/30",
|
|
238
239
|
)}
|
|
239
240
|
>
|
|
240
241
|
{value}
|
|
@@ -263,7 +264,7 @@ function AiBulletsBlock({
|
|
|
263
264
|
<ul className="space-y-1.5">
|
|
264
265
|
{bullets.map((b, i) => {
|
|
265
266
|
const content = (
|
|
266
|
-
<div className="flex gap-2 text-sm leading-relaxed text-
|
|
267
|
+
<div className="flex gap-2 text-sm leading-relaxed text-foreground">
|
|
267
268
|
<span>•</span>
|
|
268
269
|
<span className="flex-1">{b}</span>
|
|
269
270
|
</div>
|