Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
05bddc4
feat(content): unify workspaces around database-backed files
3mdistal Jul 16, 2026
d4025e2
fix(content): harden workspace sync boundaries
3mdistal Jul 16, 2026
ba2618e
fix(content): preserve reviewed workspace revisions
3mdistal Jul 16, 2026
444188f
fix(content): align workspace authorization context
3mdistal Jul 16, 2026
24c6e0a
fix(content): close local sync race boundaries
3mdistal Jul 16, 2026
4511e98
fix(content): harden workspace migration boundaries
3mdistal Jul 16, 2026
a7ff10b
fix(core): keep Content Cloudflare build deployable
3mdistal Jul 16, 2026
784c55c
Merge remote-tracking branch 'origin/main' into codex/content-global-…
3mdistal Jul 16, 2026
0c081e4
chore: drop consumed workspace changeset
3mdistal Jul 16, 2026
8736074
fix(content): keep workspace setup from showing empty
3mdistal Jul 17, 2026
18a0a89
fix(core): await action mutation success handlers
3mdistal Jul 17, 2026
73bfbaf
chore: drop consumed Cloudflare changeset
3mdistal Jul 17, 2026
1ecef7a
fix(content): complete workspace switching
3mdistal Jul 17, 2026
4192241
Merge remote-tracking branch 'origin/main' into codex/content-global-…
3mdistal Jul 17, 2026
5a3a9bb
fix(content): switch workspaces from Files rows
3mdistal Jul 17, 2026
40fbfe4
fix(content): scope organization Files pagination
3mdistal Jul 17, 2026
37c3edd
fix(content): preserve private workspace boundaries
3mdistal Jul 17, 2026
1b8c2d3
fix(content): restore ordinary-member workspace flows
3mdistal Jul 17, 2026
9dd51e7
Merge remote-tracking branch 'origin/main' into codex/content-global-…
3mdistal Jul 17, 2026
96939d6
fix(content): isolate workspace database caches
3mdistal Jul 17, 2026
75ffe59
fix(core): honor explicit personal context
3mdistal Jul 17, 2026
e94493c
test(core): cover personal context cache
3mdistal Jul 17, 2026
aaedde3
fix(content): switch workspaces from sidebar rows
3mdistal Jul 17, 2026
631c7e5
fix(content): nest Files beneath workspaces
3mdistal Jul 17, 2026
03ee667
fix(content): allow multiple workspace trees
3mdistal Jul 17, 2026
ff77bd6
refine(content): simplify workspace sidebar labels
3mdistal Jul 17, 2026
695568c
refine(content): add workspace creation menus
3mdistal Jul 17, 2026
649a255
refine(content): open workspace pages optimistically
3mdistal Jul 18, 2026
137a92b
refine(content): clarify optimistic page creation
3mdistal Jul 18, 2026
57f237e
fix(content): restore page action menus
3mdistal Jul 18, 2026
784d04c
fix(content): protect system databases from deletion
3mdistal Jul 18, 2026
4b84acf
improve(content): unify page details rail
3mdistal Jul 18, 2026
bc680eb
fix(content): silence utility sheet warning
3mdistal Jul 18, 2026
0ef4bea
fix(content): clarify global workspace navigation
3mdistal Jul 18, 2026
dbd740e
fix(content): open authorized cross-workspace pages
3mdistal Jul 18, 2026
6b30def
fix(content): isolate favorite row actions
3mdistal Jul 18, 2026
be389f8
fix(content): keep favorite menus anchored
3mdistal Jul 18, 2026
7446cfd
fix(content): separate favorite navigation from actions
3mdistal Jul 18, 2026
6429bce
chore(content): allow preview QA signups
3mdistal Jul 18, 2026
a08e672
fix(auth): simplify deploy preview signups
3mdistal Jul 18, 2026
88a356c
fix(auth): bake preview context into server builds
3mdistal Jul 18, 2026
8890f5e
fix(auth): gate serverless route initialization
3mdistal Jul 18, 2026
a50331d
feat(content): add named workspaces and safe folder picker
3mdistal Jul 18, 2026
255990e
fix(content): trust only bridged folder pickers
3mdistal Jul 18, 2026
d10f0d2
fix(content): make workspace catalog drive navigation
3mdistal Jul 18, 2026
1522090
fix(content): protect workspace catalog lifecycle
3mdistal Jul 18, 2026
a9232b2
feat(content): complete Files workspace model
3mdistal Jul 18, 2026
1a6fa25
Merge remote-tracking branch 'origin/main' into codex/content-global-…
3mdistal Jul 19, 2026
d830d52
fix(content): satisfy workspace model release gates
3mdistal Jul 19, 2026
9b1c6c2
fix(content): simplify workspace files database UI
3mdistal Jul 19, 2026
2baf59e
fix(content): hide files system filter state
3mdistal Jul 19, 2026
61953a9
fix(content): make sidebar row actions clickable
3mdistal Jul 19, 2026
ff61ef5
fix(content): derive files sidebar from page parent
3mdistal Jul 19, 2026
6d5d359
fix(content): make files sidebar parent aware
3mdistal Jul 19, 2026
65a6eb5
fix(content): align nested sidebar page icons
3mdistal Jul 19, 2026
6d66b1e
fix(content): move workspace database into breadcrumbs
3mdistal Jul 19, 2026
a7d0f02
fix(content): clarify local folder host support
3mdistal Jul 19, 2026
df510e4
fix(content): restore Chromium folder access
3mdistal Jul 19, 2026
a07e3ca
fix(content): quarantine interrupted folder pickers
3mdistal Jul 19, 2026
49b0a88
fix(content): mirror Files views in sidebar
3mdistal Jul 19, 2026
02d9cf2
improve(content): finish workspace navigation polish
3mdistal Jul 19, 2026
4fd6905
fix(content): satisfy navigation catalog guards
3mdistal Jul 19, 2026
9f34284
feat(content): make favorites a global database
3mdistal Jul 19, 2026
0f4557e
fix(content): refresh favorites membership cache
3mdistal Jul 19, 2026
c24897d
fix(content): localize favorites row actions
3mdistal Jul 19, 2026
9bc544a
fix(content): refine sidebar expansion behavior
3mdistal Jul 19, 2026
d24de08
fix(content): keep workspace creation in context
3mdistal Jul 20, 2026
8c68723
fix(content): support workspace deletion
3mdistal Jul 20, 2026
2390940
test(content): cover workspace deletion parity
3mdistal Jul 20, 2026
37f3dea
refactor(content): share workspace delete action
3mdistal Jul 20, 2026
29d64de
test(content): cover workspace create labels
3mdistal Jul 20, 2026
ebfb4f6
fix(content): restore organization and trash navigation
3mdistal Jul 20, 2026
edaafa4
Merge origin/main into content workspace follow-up
3mdistal Jul 20, 2026
3ef24db
chore: drop released workspace changesets
3mdistal Jul 20, 2026
d9866bf
docs(content): note sidebar disclosure polish
3mdistal Jul 20, 2026
a7a810d
fix(content): align collapsed trash navigation
3mdistal Jul 20, 2026
58705d9
fix(content): share workspace source chooser
3mdistal Jul 20, 2026
64dea8b
fix(content): preserve workspace expansion and nesting
3mdistal Jul 20, 2026
3301823
fix(content): harden workspace and trash boundaries
3mdistal Jul 20, 2026
e7a8f09
fix(content): close final workspace landing gaps
3mdistal Jul 20, 2026
bc41d4e
fix(content): preserve legacy database trash subtrees
3mdistal Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion templates/content/.agents/skills/document-editing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,20 @@ pnpm action update-document --id abc123 --description "Stable guidance for what

### delete-document

Delete a document and all its children recursively.
Move a document and all its children to Trash. IDs, bodies, hierarchy, and
database membership remain intact so the subtree can be restored.

```bash
pnpm action delete-document --id abc123
```

Restore the root subtree, or permanently delete it only after it is in Trash:

```bash
pnpm action restore-document --id abc123
pnpm action permanently-delete-document --id abc123
```

## Comments

Comments are Notion/Google-Docs-style **inline comments**. Selecting text and commenting leaves the passage **highlighted inline** via a ProseMirror decoration overlay — nothing is written into the markdown body, so the document round-trips unchanged. Each thread stores the quoted text plus surrounding context (`anchorPrefix`/`anchorSuffix`) and an approximate `anchorStartOffset`, so the highlight follows the text as the document is edited, disambiguates repeated text, and degrades gracefully (the thread stays in the sidebar) when its text is deleted.
Expand Down
5 changes: 4 additions & 1 deletion templates/content/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ cd templates/content && pnpm action <name> [args]
| `get-content-database` | `--databaseId <id>` or `--documentId <id>` | Get a database with its description, property/option schema guidance, item pages, and computed ancestry context |
| `list-trashed-content-databases` | | List soft-deleted databases visible in the sidebar Trash surface |
| `restore-content-database` | `--databaseId <id>` | Restore a soft-deleted database from the sidebar Trash surface |
| `list-trashed-documents` | | List access-filtered page roots visible in the sidebar Trash surface |
| `restore-document` | `--id <id>` | Restore a page and the subtree moved to Trash with it |
| `permanently-delete-document` | `--id <id>` | Permanently destroy a document subtree already in Trash |
| `get-content-database-source` | `--databaseId <id>` or `--documentId <id>` | Inspect local/no-source or source-backed status, mappings, row identity, freshness, and change sets |
| `attach-content-database-source` | `--databaseId <id>` or `--documentId <id> [--sourceType mock-local\|builder-cms\|local-table\|notion-database] [--sourceName] [--sourceTable] [--relationshipMode items\|details] [--join <json>]` | Attach a source binding; use `items` to add more Builder rows and `details` to match a read-only local/Notion source onto existing rows |
| `list-notion-database-sources` | `[--query <text>] [--limit <1-100>]` | List Notion data sources visible through the current user's OAuth connection; returns IDs/names only, never tokens |
Expand Down Expand Up @@ -175,7 +178,7 @@ cd templates/content && pnpm action <name> [args]
| `reorder-document-property` | `--documentId <id> --propertyId <propertyId> --targetPropertyId <id> [--position before\|after]` | Reorder a property definition within its database (used to reorder Blocks fields on the page) |
| `set-document-discoverability` | `--id <id> --hideFromSearch true\|false [--includeChildren true\|false]` | Hide/show an org-accessible document in Organization/search while keeping link access |
| `move-document` | `--id <id> [--parentId] [--position]` | Move or reorder a document in the page tree |
| `delete-document` | `--id <id>` | Delete with recursive children |
| `delete-document` | `--id <id>` | Move a document and its recursive children to Trash |

Database views follow Notion-style tab labels. When creating or duplicating
views in `viewConfig`, use unique default names (`Table 2`, `SEO copy 2`, etc.)
Expand Down
7 changes: 6 additions & 1 deletion templates/content/actions/_content-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,14 +806,19 @@ async function provisionOwnedContentSpace(
export async function provisionSourceBackedContentSpace(
db: Db,
userEmail: string,
input: { connectionId: string; name: string },
input: {
connectionId: string;
name: string;
propertyValues?: Record<string, unknown>;
},
) {
const connectionId = input.connectionId.trim();
if (!connectionId) throw new Error("Local folder connection ID is required");
return provisionOwnedContentSpace(db, userEmail, {
spaceId: sourceBackedContentSpaceId(userEmail, connectionId),
name: input.name.trim() || "Local folder",
kind: "source_backed",
propertyValues: input.propertyValues,
});
}

Expand Down
17 changes: 15 additions & 2 deletions templates/content/actions/_database-row-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export async function resolveDatabaseRowsForBatch(
eq(schema.contentDatabaseItems.databaseId, database.id),
rowPredicates.length === 1 ? rowPredicates[0] : or(...rowPredicates),
isNull(schema.contentDatabases.deletedAt),
isNull(schema.documents.trashedAt),
),
)
.orderBy(asc(schema.contentDatabaseItems.position));
Expand Down Expand Up @@ -185,9 +186,21 @@ export async function renumberDatabaseRows(
now: string,
) {
const rows = await db
.select()
.select({
id: schema.contentDatabaseItems.id,
documentId: schema.contentDatabaseItems.documentId,
})
.from(schema.contentDatabaseItems)
.where(eq(schema.contentDatabaseItems.databaseId, database.id))
.innerJoin(
schema.documents,
eq(schema.documents.id, schema.contentDatabaseItems.documentId),
)
.where(
and(
eq(schema.contentDatabaseItems.databaseId, database.id),
isNull(schema.documents.trashedAt),
),
)
.orderBy(asc(schema.contentDatabaseItems.position));
if (rows.length === 0) return;

Expand Down
98 changes: 83 additions & 15 deletions templates/content/actions/_database-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import type {
ContentDatabaseResponse,
} from "../shared/api.js";
import { favoriteDocumentIds } from "./_content-favorites.js";
import { listContentOrganizationMemberships } from "./_content-space-access.js";
import {
listContentOrganizationMemberships,
normalizeContentSpaceEmail,
} from "./_content-space-access.js";
import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
import {
applyFederatedOverlayValues,
Expand Down Expand Up @@ -81,6 +84,8 @@ export const contentDatabaseListDocumentSelection = {
sourcePath: schema.documents.sourcePath,
sourceRootPath: schema.documents.sourceRootPath,
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
trashedAt: schema.documents.trashedAt,
trashRootId: schema.documents.trashRootId,
visibility: schema.documents.visibility,
ownerEmail: schema.documents.ownerEmail,
orgId: schema.documents.orgId,
Expand Down Expand Up @@ -300,9 +305,14 @@ export async function getContentDatabaseResponse(

const { limit, offset } = normalizeContentDatabasePageOptions(options);
const userEmail = getRequestUserEmail();
const normalizedUserEmail = userEmail
? normalizeContentSpaceEmail(userEmail)
: null;
const activeOrgId = getRequestOrgId();
const authorizedOrgIds =
database.systemRole === "favorites" && userEmail
(database.systemRole === "favorites" ||
database.systemRole === "workspaces") &&
userEmail
? [
...new Set([
...(await listContentOrganizationMemberships(userEmail)).map(
Expand All @@ -312,6 +322,45 @@ export async function getContentDatabaseResponse(
]),
]
: [];
const workspacesVisibleDocumentIds =
database.systemRole === "workspaces" && normalizedUserEmail
? (
await db
.select({
documentId: schema.contentSpaceCatalogItems.documentId,
ownerEmail: schema.contentSpaces.ownerEmail,
orgId: schema.contentSpaces.orgId,
})
.from(schema.contentSpaceCatalogItems)
.innerJoin(
schema.contentSpaces,
eq(
schema.contentSpaces.id,
schema.contentSpaceCatalogItems.spaceId,
),
)
.where(
and(
eq(
schema.contentSpaceCatalogItems.catalogDatabaseId,
databaseId,
),
eq(
schema.contentSpaceCatalogItems.ownerEmail,
normalizedUserEmail,
),
isNull(schema.contentSpaces.archivedAt),
),
)
)
.filter(
(row) =>
normalizeContentSpaceEmail(row.ownerEmail) ===
normalizedUserEmail ||
(!!row.orgId && authorizedOrgIds.includes(row.orgId)),
)
.map((row) => row.documentId)
: null;
const favoritesVisibleDocumentIds =
database.systemRole === "favorites" && userEmail
? (
Expand All @@ -331,6 +380,7 @@ export async function getContentDatabaseResponse(
}),
),
),
isNull(schema.documents.trashedAt),
documentDiscoveryFilter({
userEmail,
orgIds: authorizedOrgIds,
Expand All @@ -351,6 +401,11 @@ export async function getContentDatabaseResponse(
: undefined;
const visibleItemFilter = and(
eq(schema.contentDatabaseItems.databaseId, databaseId),
sql`exists (
select 1 from ${schema.documents}
where ${schema.documents.id} = ${schema.contentDatabaseItems.documentId}
and ${schema.documents.trashedAt} is null
)`,
organizationFilesItemFilter,
favoritesVisibleDocumentIds
? favoritesVisibleDocumentIds.length > 0
Expand All @@ -360,6 +415,14 @@ export async function getContentDatabaseResponse(
)
: sql`1 = 0`
: undefined,
workspacesVisibleDocumentIds
? workspacesVisibleDocumentIds.length > 0
? inArray(
schema.contentDatabaseItems.documentId,
workspacesVisibleDocumentIds,
)
: sql`1 = 0`
: undefined,
);
const [itemCount] = await db
.select({ count: sql<number>`COUNT(*)` })
Expand Down Expand Up @@ -388,23 +451,28 @@ export async function getContentDatabaseResponse(
schema.documents.id,
items.map((item) => item.documentId),
),
isNull(schema.documents.trashedAt),
database.systemRole === "favorites"
? favoritesVisibleDocumentIds?.length
? inArray(schema.documents.id, favoritesVisibleDocumentIds)
: sql`1 = 0`
: database.systemRole === "files" && database.orgId
? and(
eq(schema.documents.orgId, database.orgId),
or(
eq(schema.documents.visibility, "org"),
eq(schema.documents.visibility, "public"),
),
or(
eq(schema.documents.hideFromSearch, 0),
isNull(schema.documents.hideFromSearch),
),
)
: eq(schema.documents.ownerEmail, database.ownerEmail),
: database.systemRole === "workspaces"
? workspacesVisibleDocumentIds?.length
? inArray(schema.documents.id, workspacesVisibleDocumentIds)
: sql`1 = 0`
: database.systemRole === "files" && database.orgId
? and(
eq(schema.documents.orgId, database.orgId),
or(
eq(schema.documents.visibility, "org"),
eq(schema.documents.visibility, "public"),
),
or(
eq(schema.documents.hideFromSearch, 0),
isNull(schema.documents.hideFromSearch),
),
)
: eq(schema.documents.ownerEmail, database.ownerEmail),
),
)
: [];
Expand Down
3 changes: 3 additions & 0 deletions templates/content/actions/add-database-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export default defineAction({
),
);
if (!database) throw new Error(`Database "${databaseId}" not found`);
if (database.systemRole === "workspaces") {
throw new Error("Use create-content-space to add a workspace");
}

const access = await assertAccess(
"document",
Expand Down
3 changes: 2 additions & 1 deletion templates/content/actions/connect-local-folder-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default defineAction({
.optional()
.default(false)
.describe("Create a separate private Content space for this folder"),
propertyValues: z.record(z.string(), z.unknown()).optional(),
truthPolicy: truthPolicySchema.describe(
"Whether Content, the folder, or reviewed conflict resolution is authoritative",
),
Expand Down Expand Up @@ -130,7 +131,7 @@ export default defineAction({
const provisioned = await provisionSourceBackedContentSpace(
db,
userEmail,
{ connectionId, name: label },
{ connectionId, name: label, propertyValues: args.propertyValues },
);
targetSpaceId = provisioned.spaceId;
targetDatabaseId = provisioned.filesDatabaseId;
Expand Down
Loading
Loading