@agent-native/core 0.187.0-nightly-20260924172231 → 0.187.0
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/CHANGELOG.md +11 -0
- package/corpus/templates/analytics/app/i18n/en-US.ts +1 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +101 -11
- package/corpus/templates/analytics/changelog/2026-09-24-session-list-hides-0m-recordings-by-default.md +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/brain/actions/backfill-search-embeddings.ts +337 -0
- package/corpus/templates/brain/actions/get-brain-health.ts +1 -1
- package/corpus/templates/brain/app/lib/brain.ts +18 -0
- package/corpus/templates/brain/jobs/process-ingest-queue.ts +25 -3
- package/corpus/templates/brain/server/lib/brain-health.ts +197 -57
- package/corpus/templates/brain/server/lib/demo.ts +25 -4
- package/corpus/templates/brain/server/lib/hybrid-search.ts +4 -1
- package/corpus/templates/brain/server/lib/ingest-queue.ts +1 -0
- package/corpus/templates/brain/server/lib/search-index.ts +254 -17
- package/corpus/templates/brain/server/lib/search.ts +27 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/content/CHANGELOG.md +22 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +559 -227
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +25 -1
- package/corpus/templates/design/CHANGELOG.md +36 -0
- package/corpus/templates/dispatch/CHANGELOG.md +7 -0
- package/corpus/templates/mail/CHANGELOG.md +17 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.js +44 -11
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/package.json +4 -4
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
availableEmbeddingFamilies,
|
|
4
4
|
defaultEmbeddingFamily,
|
|
5
5
|
type EmbeddingFamily,
|
|
6
|
+
readEmbeddingFamilyAvailability,
|
|
6
7
|
} from "@agent-native/core/embeddings";
|
|
7
8
|
import {
|
|
8
9
|
deletePgVectors,
|
|
@@ -39,6 +40,25 @@ export const BRAIN_MAX_EMBEDDED_BURSTS = 12;
|
|
|
39
40
|
|
|
40
41
|
export type BrainSearchArtifact = z.infer<typeof artifactSchema>;
|
|
41
42
|
|
|
43
|
+
export type BrainEmbeddingReadinessStatus =
|
|
44
|
+
| "ready"
|
|
45
|
+
| "not-configured"
|
|
46
|
+
| "ambiguous"
|
|
47
|
+
| "unavailable";
|
|
48
|
+
|
|
49
|
+
export interface BrainEmbeddingReadiness {
|
|
50
|
+
status: BrainEmbeddingReadinessStatus;
|
|
51
|
+
ready: boolean;
|
|
52
|
+
configuredProviders: string[];
|
|
53
|
+
unavailableProviders: string[];
|
|
54
|
+
configuredFamilies: number;
|
|
55
|
+
provider: string | null;
|
|
56
|
+
model: string | null;
|
|
57
|
+
embeddingSetId: string | null;
|
|
58
|
+
dimensions: number | null;
|
|
59
|
+
warning: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export interface SearchIndexCapture {
|
|
43
63
|
id: string;
|
|
44
64
|
sourceId: string;
|
|
@@ -245,11 +265,225 @@ export function burstRows(
|
|
|
245
265
|
return rows;
|
|
246
266
|
}
|
|
247
267
|
|
|
268
|
+
export function embeddingReadinessFromFamilies(
|
|
269
|
+
families: readonly EmbeddingFamily[],
|
|
270
|
+
unavailableProviders: readonly string[] = [],
|
|
271
|
+
): BrainEmbeddingReadiness {
|
|
272
|
+
const configuredProviders = Array.from(
|
|
273
|
+
new Set(families.map((candidate) => candidate.provider)),
|
|
274
|
+
);
|
|
275
|
+
if (unavailableProviders.length) {
|
|
276
|
+
return {
|
|
277
|
+
status: "unavailable",
|
|
278
|
+
ready: false,
|
|
279
|
+
configuredProviders,
|
|
280
|
+
unavailableProviders: [...unavailableProviders],
|
|
281
|
+
configuredFamilies: families.length,
|
|
282
|
+
provider: null,
|
|
283
|
+
model: null,
|
|
284
|
+
embeddingSetId: null,
|
|
285
|
+
dimensions: null,
|
|
286
|
+
warning:
|
|
287
|
+
"Embedding credential status is temporarily unavailable. Retry before indexing.",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const family = defaultEmbeddingFamily(families);
|
|
291
|
+
if (family) {
|
|
292
|
+
return {
|
|
293
|
+
status: "ready",
|
|
294
|
+
ready: true,
|
|
295
|
+
configuredProviders: [family.provider],
|
|
296
|
+
unavailableProviders: [],
|
|
297
|
+
configuredFamilies: 1,
|
|
298
|
+
provider: family.provider,
|
|
299
|
+
model: family.model,
|
|
300
|
+
embeddingSetId: family.id,
|
|
301
|
+
dimensions: family.dimensions,
|
|
302
|
+
warning: null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
status: families.length ? "ambiguous" : "not-configured",
|
|
307
|
+
ready: false,
|
|
308
|
+
configuredProviders,
|
|
309
|
+
unavailableProviders: [],
|
|
310
|
+
configuredFamilies: families.length,
|
|
311
|
+
provider: null,
|
|
312
|
+
model: null,
|
|
313
|
+
embeddingSetId: null,
|
|
314
|
+
dimensions: null,
|
|
315
|
+
warning: families.length
|
|
316
|
+
? "Configure exactly one embedding provider."
|
|
317
|
+
: "Configure one embedding provider to enable semantic retrieval.",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function readEmbeddingReadiness(): Promise<BrainEmbeddingReadiness> {
|
|
322
|
+
const availability = await readEmbeddingFamilyAvailability();
|
|
323
|
+
return embeddingReadinessFromFamilies(
|
|
324
|
+
availability.families,
|
|
325
|
+
availability.unavailableProviders,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
248
329
|
async function configuredEmbeddingFamily(): Promise<EmbeddingFamily | null> {
|
|
249
330
|
const families = await availableEmbeddingFamilies();
|
|
250
331
|
return defaultEmbeddingFamily(families);
|
|
251
332
|
}
|
|
252
333
|
|
|
334
|
+
export interface CaptureEmbeddingCoverage {
|
|
335
|
+
complete: boolean;
|
|
336
|
+
artifactEmbedded: boolean;
|
|
337
|
+
expectedBursts: number;
|
|
338
|
+
embeddedBursts: number;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function captureAudienceIndexingFailureReason(
|
|
342
|
+
assignments: readonly unknown[],
|
|
343
|
+
): "no-active-audience" | "multiple-audience-assignments" | null {
|
|
344
|
+
if (!assignments.length) return "no-active-audience";
|
|
345
|
+
return assignments.length === 1 ? null : "multiple-audience-assignments";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function captureEmbeddingCoverageFromTargets(input: {
|
|
349
|
+
artifactId: string;
|
|
350
|
+
burstIds: string[];
|
|
351
|
+
embeddings: Array<{ targetType: string; targetId: string }>;
|
|
352
|
+
}): CaptureEmbeddingCoverage {
|
|
353
|
+
const embeddedTargets = new Set(
|
|
354
|
+
input.embeddings.map((row) => `${row.targetType}:${row.targetId}`),
|
|
355
|
+
);
|
|
356
|
+
const artifactEmbedded = embeddedTargets.has(`artifact:${input.artifactId}`);
|
|
357
|
+
const embeddedBursts = input.burstIds.filter((id) =>
|
|
358
|
+
embeddedTargets.has(`burst:${id}`),
|
|
359
|
+
).length;
|
|
360
|
+
return {
|
|
361
|
+
complete: artifactEmbedded && embeddedBursts === input.burstIds.length,
|
|
362
|
+
artifactEmbedded,
|
|
363
|
+
expectedBursts: input.burstIds.length,
|
|
364
|
+
embeddedBursts,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function readCaptureEmbeddingCoverage(
|
|
369
|
+
captureId: string,
|
|
370
|
+
embeddingSetId: string,
|
|
371
|
+
): Promise<CaptureEmbeddingCoverage> {
|
|
372
|
+
const db = getDb();
|
|
373
|
+
const assignments = await db
|
|
374
|
+
.select({
|
|
375
|
+
audienceId: schema.brainCaptureAudiences.audienceId,
|
|
376
|
+
aclHash: schema.brainCaptureAudiences.aclHash,
|
|
377
|
+
})
|
|
378
|
+
.from(schema.brainCaptureAudiences)
|
|
379
|
+
.where(eq(schema.brainCaptureAudiences.captureId, captureId));
|
|
380
|
+
if (captureAudienceIndexingFailureReason(assignments)) {
|
|
381
|
+
return {
|
|
382
|
+
complete: false,
|
|
383
|
+
artifactEmbedded: false,
|
|
384
|
+
expectedBursts: 0,
|
|
385
|
+
embeddedBursts: 0,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const assignment = assignments[0]!;
|
|
389
|
+
const [artifact] = await db
|
|
390
|
+
.select({
|
|
391
|
+
id: schema.brainSearchArtifacts.id,
|
|
392
|
+
contentHash: schema.brainSearchArtifacts.contentHash,
|
|
393
|
+
sensitivityPolicyVersion:
|
|
394
|
+
schema.brainSearchArtifacts.sensitivityPolicyVersion,
|
|
395
|
+
aclHash: schema.brainSearchArtifacts.aclHash,
|
|
396
|
+
indexVersion: schema.brainSearchArtifacts.indexVersion,
|
|
397
|
+
})
|
|
398
|
+
.from(schema.brainSearchArtifacts)
|
|
399
|
+
.innerJoin(
|
|
400
|
+
schema.brainRawCaptures,
|
|
401
|
+
eq(schema.brainSearchArtifacts.captureId, schema.brainRawCaptures.id),
|
|
402
|
+
)
|
|
403
|
+
.where(
|
|
404
|
+
and(
|
|
405
|
+
eq(schema.brainRawCaptures.id, captureId),
|
|
406
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
407
|
+
eq(schema.brainSearchArtifacts.status, "active"),
|
|
408
|
+
eq(schema.brainSearchArtifacts.audienceId, assignment.audienceId),
|
|
409
|
+
eq(schema.brainSearchArtifacts.aclHash, assignment.aclHash),
|
|
410
|
+
eq(
|
|
411
|
+
schema.brainSearchArtifacts.contentHash,
|
|
412
|
+
schema.brainRawCaptures.contentHash,
|
|
413
|
+
),
|
|
414
|
+
eq(
|
|
415
|
+
schema.brainSearchArtifacts.sensitivityPolicyVersion,
|
|
416
|
+
schema.brainRawCaptures.sensitivityPolicyVersion,
|
|
417
|
+
),
|
|
418
|
+
eq(
|
|
419
|
+
schema.brainSearchArtifacts.aclHash,
|
|
420
|
+
schema.brainRawCaptures.audienceAclHash,
|
|
421
|
+
),
|
|
422
|
+
eq(
|
|
423
|
+
schema.brainSearchArtifacts.indexVersion,
|
|
424
|
+
BRAIN_SEARCH_INDEX_VERSION,
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
.limit(1);
|
|
429
|
+
if (!artifact) {
|
|
430
|
+
return {
|
|
431
|
+
complete: false,
|
|
432
|
+
artifactEmbedded: false,
|
|
433
|
+
expectedBursts: 0,
|
|
434
|
+
embeddedBursts: 0,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const bursts = await db
|
|
438
|
+
.select({ id: schema.brainSearchBursts.id })
|
|
439
|
+
.from(schema.brainSearchBursts)
|
|
440
|
+
.where(
|
|
441
|
+
and(
|
|
442
|
+
eq(schema.brainSearchBursts.artifactId, artifact.id),
|
|
443
|
+
eq(schema.brainSearchBursts.indexed, 1),
|
|
444
|
+
eq(schema.brainSearchBursts.contentHash, artifact.contentHash),
|
|
445
|
+
eq(schema.brainSearchBursts.indexVersion, artifact.indexVersion),
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
const targetIds = [artifact.id, ...bursts.map((burst) => burst.id)];
|
|
449
|
+
const embeddings = await db
|
|
450
|
+
.select({
|
|
451
|
+
targetType: schema.brainSearchEmbeddings.targetType,
|
|
452
|
+
targetId: schema.brainSearchEmbeddings.targetId,
|
|
453
|
+
})
|
|
454
|
+
.from(schema.brainSearchEmbeddings)
|
|
455
|
+
.where(
|
|
456
|
+
and(
|
|
457
|
+
eq(schema.brainSearchEmbeddings.status, "active"),
|
|
458
|
+
eq(schema.brainSearchEmbeddings.embeddingSetId, embeddingSetId),
|
|
459
|
+
eq(schema.brainSearchEmbeddings.contentHash, artifact.contentHash),
|
|
460
|
+
eq(
|
|
461
|
+
schema.brainSearchEmbeddings.sensitivityPolicyVersion,
|
|
462
|
+
artifact.sensitivityPolicyVersion,
|
|
463
|
+
),
|
|
464
|
+
eq(schema.brainSearchEmbeddings.aclHash, artifact.aclHash),
|
|
465
|
+
eq(schema.brainSearchEmbeddings.indexVersion, artifact.indexVersion),
|
|
466
|
+
inArray(schema.brainSearchEmbeddings.targetId, targetIds),
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
return captureEmbeddingCoverageFromTargets({
|
|
470
|
+
artifactId: artifact.id,
|
|
471
|
+
burstIds: bursts.map((burst) => burst.id),
|
|
472
|
+
embeddings,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function embedSearchTexts(
|
|
477
|
+
family: EmbeddingFamily | null,
|
|
478
|
+
texts: string[],
|
|
479
|
+
): Promise<number[][] | null> {
|
|
480
|
+
if (!family) return null;
|
|
481
|
+
return family.embed(
|
|
482
|
+
texts.map((text) => ({ text })),
|
|
483
|
+
"document",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
253
487
|
async function indexExternalSearchLanes(input: {
|
|
254
488
|
artifactId: string;
|
|
255
489
|
artifact: BrainSearchArtifact;
|
|
@@ -276,7 +510,6 @@ async function indexExternalSearchLanes(input: {
|
|
|
276
510
|
namespace: SEARCH_NAMESPACE,
|
|
277
511
|
});
|
|
278
512
|
const family = await configuredEmbeddingFamily();
|
|
279
|
-
if (!family || !true) return;
|
|
280
513
|
const targets = [
|
|
281
514
|
{
|
|
282
515
|
targetType: "artifact" as const,
|
|
@@ -291,10 +524,11 @@ async function indexExternalSearchLanes(input: {
|
|
|
291
524
|
text: input.burstBodies[index] ?? "",
|
|
292
525
|
})),
|
|
293
526
|
].filter((target) => target.text.trim());
|
|
294
|
-
const vectors = await
|
|
295
|
-
|
|
296
|
-
|
|
527
|
+
const vectors = await embedSearchTexts(
|
|
528
|
+
family,
|
|
529
|
+
targets.map((target) => target.text),
|
|
297
530
|
);
|
|
531
|
+
if (!family || !vectors) return;
|
|
298
532
|
await ensurePgVectorIndex(dbExec, family.dimensions, {
|
|
299
533
|
namespace: SEARCH_NAMESPACE,
|
|
300
534
|
});
|
|
@@ -633,20 +867,23 @@ export async function indexBrainCapture(captureId: string): Promise<{
|
|
|
633
867
|
})
|
|
634
868
|
.from(schema.brainCaptureAudiences)
|
|
635
869
|
.where(eq(schema.brainCaptureAudiences.captureId, captureId));
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
...capture,
|
|
641
|
-
sensitivityDisposition: capture.sensitivityDisposition,
|
|
642
|
-
},
|
|
643
|
-
audience,
|
|
644
|
-
id: nanoid(),
|
|
645
|
-
now: nowIso(),
|
|
646
|
-
});
|
|
647
|
-
if (result.indexed) indexed += 1;
|
|
870
|
+
const audienceFailure = captureAudienceIndexingFailureReason(audiences);
|
|
871
|
+
if (audienceFailure) {
|
|
872
|
+
await unindexBrainCapture(captureId);
|
|
873
|
+
return { indexed: 0, reason: audienceFailure };
|
|
648
874
|
}
|
|
649
|
-
|
|
875
|
+
const result = await indexCaptureForSearch({
|
|
876
|
+
capture: {
|
|
877
|
+
...capture,
|
|
878
|
+
sensitivityDisposition: capture.sensitivityDisposition,
|
|
879
|
+
},
|
|
880
|
+
audience: audiences[0]!,
|
|
881
|
+
id: nanoid(),
|
|
882
|
+
now: nowIso(),
|
|
883
|
+
});
|
|
884
|
+
return result.indexed
|
|
885
|
+
? { indexed: 1 }
|
|
886
|
+
: { indexed: 0, reason: result.reason ?? "search-index-failed" };
|
|
650
887
|
}
|
|
651
888
|
|
|
652
889
|
export async function unindexBrainCapture(captureId: string): Promise<void> {
|
|
@@ -298,6 +298,33 @@ export function buildSnippet(
|
|
|
298
298
|
return buildSearchSnippet(value, terms, maxLength);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
export function citationEvidenceMatchesCapture(
|
|
302
|
+
citation:
|
|
303
|
+
| {
|
|
304
|
+
quote?: string | null;
|
|
305
|
+
preview?: string | null;
|
|
306
|
+
verbatim?: boolean;
|
|
307
|
+
}
|
|
308
|
+
| null
|
|
309
|
+
| undefined,
|
|
310
|
+
captureContent: string,
|
|
311
|
+
) {
|
|
312
|
+
const redactedContent = redactSensitiveText(captureContent)
|
|
313
|
+
.replace(/\s+/g, " ")
|
|
314
|
+
.trim();
|
|
315
|
+
const quote = citation?.quote?.trim();
|
|
316
|
+
if (quote) return captureContent.includes(quote);
|
|
317
|
+
if (citation?.verbatim !== false) return false;
|
|
318
|
+
const preview = citation.preview
|
|
319
|
+
?.trim()
|
|
320
|
+
.replace(/^\.\.\./, "")
|
|
321
|
+
.replace(/\.\.\.$/, "")
|
|
322
|
+
.trim()
|
|
323
|
+
.replace(/\s+/g, " ");
|
|
324
|
+
if (!preview) return false;
|
|
325
|
+
return redactedContent.includes(preview);
|
|
326
|
+
}
|
|
327
|
+
|
|
301
328
|
export function scoreSearchText(
|
|
302
329
|
fields: {
|
|
303
330
|
title?: string | null;
|
|
@@ -9,6 +9,12 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
9
9
|
|
|
10
10
|
- Calendar opens faster by loading translated catalogs only when you need them.
|
|
11
11
|
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Bookings no longer confirm without their configured Zoom meeting
|
|
15
|
+
- Release a booking slot when Zoom creation never starts
|
|
16
|
+
- Zoom booking timeouts keep the slot reserved to prevent duplicate meetings.
|
|
17
|
+
|
|
12
18
|
## 2026-09-22
|
|
13
19
|
|
|
14
20
|
### Improved
|
|
@@ -7,10 +7,19 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
7
7
|
|
|
8
8
|
### Improved
|
|
9
9
|
|
|
10
|
+
- Clip share sidebars stay expanded in stacked layouts and use a floating, borderless control when collapsed.
|
|
11
|
+
- Clip viewers can collapse the comments, transcript, and Agent sidebar
|
|
12
|
+
- Search results clarify where a match appears in the video.
|
|
13
|
+
- Search results show where a transcript or comment match appears
|
|
10
14
|
- Insights charts load only when opened, keeping the recording library faster.
|
|
11
15
|
|
|
12
16
|
### Fixed
|
|
13
17
|
|
|
18
|
+
- Comment fields grow as you type so longer messages stay visible
|
|
19
|
+
- New recordings stay loading until their share is available
|
|
20
|
+
- Playback errors no longer cover the video player controls
|
|
21
|
+
- Trash recordings are excluded from search results.
|
|
22
|
+
- Opening a trashed recording now links its breadcrumb back to Trash.
|
|
14
23
|
- Long recording menus scroll within the visible space so every action stays reachable.
|
|
15
24
|
|
|
16
25
|
## 2026-09-22
|
|
@@ -28,6 +37,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
28
37
|
|
|
29
38
|
### Fixed
|
|
30
39
|
|
|
40
|
+
- Stopped Clips AI actions now clear the in-progress status.
|
|
31
41
|
- Clips keeps uploaded videos visible while the library refreshes and localizes Loom import failures.
|
|
32
42
|
- Clips recovers dropped uploads after a lost finalization response and clears saving feedback after successful saves.
|
|
33
43
|
- The desktop app stops re-checking flags and meetings every few seconds after your session expires, and resumes when you sign in again.
|
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Content are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-09-23
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Every page in the sidebar now has a "…" menu to rename, duplicate, move, copy its link, open it in a new tab, or move it to Trash, and it shows who last edited the page. Duplicate copies sub-pages too, and Move can take a page and its sub-pages to another workspace after warning you that its sharing will change.
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
|
|
14
|
+
- The sidebar is easier to scan: the page you're on is highlighted wherever it appears, nested pages show guide lines and Trash stays in reach below the page list.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Tables inserted in the visual editor now let you resize columns.
|
|
19
|
+
- Markdown tables with aligned columns now open as editable tables in pages, keeping column alignment and all cell content.
|
|
20
|
+
- The Trash view no longer highlights a document that is not in Trash.
|
|
21
|
+
|
|
22
|
+
## 2026-09-22
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Comments, replies, and suggested-edit decisions now appear immediately without leaving duplicate text in the composer while they save.
|
|
27
|
+
|
|
6
28
|
## 2026-09-21
|
|
7
29
|
|
|
8
30
|
### Improved
|