@agent-native/core-corpus 0.212.1 → 0.213.1
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/.agents/skills/onboarding-journeys/SKILL.md +29 -14
- package/corpus/templates/analytics/CHANGELOG.md +11 -0
- package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
- package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
- package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
- package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
- package/corpus/templates/analytics/package.json +6 -0
- package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
- package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
- package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
- package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
- package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
- package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
- package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -4
- package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
- package/corpus/templates/clips/server/lib/recordings.ts +4 -19
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +8 -0
- package/corpus/templates/content/actions/_content-space-access.ts +95 -6
- package/corpus/templates/content/actions/_database-utils.ts +20 -16
- package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
- package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
- package/corpus/templates/content/actions/_github-source.ts +141 -77
- package/corpus/templates/content/actions/check-github-connection.ts +15 -0
- package/corpus/templates/content/actions/create-github-collection.ts +180 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
- package/corpus/templates/content/actions/link-github-folder.ts +104 -36
- package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
- package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +172 -19
- package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
- package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
- package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
- package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
- package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -1
- package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
- package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
- package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
- package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
- package/corpus/templates/content/docs/product/roadmap.md +1 -1
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +14 -0
- package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
- package/corpus/templates/content/shared/api.ts +3 -0
- package/corpus/templates/content/shared/github-source.ts +68 -0
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
- package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
- package/corpus/templates/design/shared/journey-canvas.ts +6 -4
- package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
- package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
- package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
- package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ContentDatabaseSource } from "@shared/api";
|
|
2
|
+
import { GITHUB_SOURCE_TYPE, githubFolderName } from "@shared/github-source";
|
|
3
|
+
|
|
4
|
+
import { githubRelativeTime } from "./github-source-text";
|
|
5
|
+
import { dbText } from "./text";
|
|
6
|
+
|
|
7
|
+
/** The collection the Sources panel is about, as GitHub linking needs it. */
|
|
8
|
+
export interface GitHubCollectionPlace {
|
|
9
|
+
databaseId: string;
|
|
10
|
+
documentId: string;
|
|
11
|
+
/** Where a new collection beside this one goes. */
|
|
12
|
+
spaceId?: string;
|
|
13
|
+
parentId: string | null;
|
|
14
|
+
systemRole: string | null;
|
|
15
|
+
pageCount: number;
|
|
16
|
+
canEdit: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type GitHubBlockedReason =
|
|
20
|
+
| "files"
|
|
21
|
+
| "system"
|
|
22
|
+
| "editors-only"
|
|
23
|
+
| "other-source"
|
|
24
|
+
| "has-pages";
|
|
25
|
+
|
|
26
|
+
/** Whether this collection can take a GitHub folder, and if not, why. */
|
|
27
|
+
export type GitHubAvailability =
|
|
28
|
+
| { kind: "linked"; source: ContentDatabaseSource }
|
|
29
|
+
| { kind: "available" }
|
|
30
|
+
| { kind: "connect" }
|
|
31
|
+
| {
|
|
32
|
+
kind: "blocked";
|
|
33
|
+
reason: GitHubBlockedReason;
|
|
34
|
+
/** The other app the collection's pages come from. */
|
|
35
|
+
sourceName?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The app a source's pages come from, as people call it. */
|
|
39
|
+
export function sourceAppName(source: ContentDatabaseSource): string {
|
|
40
|
+
if (source.sourceType === "builder-cms") return "Builder";
|
|
41
|
+
if (source.sourceType === "notion-database") return "Notion";
|
|
42
|
+
return source.sourceName;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks the collection before the GitHub connection, so nobody is sent
|
|
47
|
+
* through GitHub's sign-in for a collection that could never link. The server
|
|
48
|
+
* checks again when linking, for changes made in between.
|
|
49
|
+
*/
|
|
50
|
+
export function githubAvailability(
|
|
51
|
+
place: GitHubCollectionPlace,
|
|
52
|
+
sources: ContentDatabaseSource[],
|
|
53
|
+
connected: boolean | null | undefined,
|
|
54
|
+
): GitHubAvailability {
|
|
55
|
+
const linked = sources.find(
|
|
56
|
+
(source) => source.sourceType === GITHUB_SOURCE_TYPE,
|
|
57
|
+
);
|
|
58
|
+
if (linked) return { kind: "linked", source: linked };
|
|
59
|
+
if (place.systemRole === "files") return { kind: "blocked", reason: "files" };
|
|
60
|
+
if (place.systemRole) return { kind: "blocked", reason: "system" };
|
|
61
|
+
if (!place.canEdit) return { kind: "blocked", reason: "editors-only" };
|
|
62
|
+
const other = sources[0];
|
|
63
|
+
if (other) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "blocked",
|
|
66
|
+
reason: "other-source",
|
|
67
|
+
sourceName: sourceAppName(other),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (place.pageCount > 0) return { kind: "blocked", reason: "has-pages" };
|
|
71
|
+
// An unknown connection is left to the dialog, which asks again.
|
|
72
|
+
return connected === false ? { kind: "connect" } : { kind: "available" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a blocked collection can point to a new one instead. */
|
|
76
|
+
export function githubOffersNewCollection(
|
|
77
|
+
availability: GitHubAvailability,
|
|
78
|
+
place: GitHubCollectionPlace,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
availability.kind === "blocked" &&
|
|
82
|
+
availability.reason !== "editors-only" &&
|
|
83
|
+
!!place.spaceId
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The line under "GitHub" in Sources, always visible. */
|
|
88
|
+
export function githubRowDescription(
|
|
89
|
+
availability: GitHubAvailability,
|
|
90
|
+
now = Date.now(),
|
|
91
|
+
): string {
|
|
92
|
+
switch (availability.kind) {
|
|
93
|
+
case "linked": {
|
|
94
|
+
const status = availability.source.metadata.github;
|
|
95
|
+
const folder = status
|
|
96
|
+
? githubFolderName(status)
|
|
97
|
+
: availability.source.sourceName;
|
|
98
|
+
return status?.syncedAt
|
|
99
|
+
? dbText("githubRowLinked", {
|
|
100
|
+
folder,
|
|
101
|
+
time: githubRelativeTime(status.syncedAt, now),
|
|
102
|
+
})
|
|
103
|
+
: dbText("githubRowLinkedNotUpdated", { folder });
|
|
104
|
+
}
|
|
105
|
+
case "available":
|
|
106
|
+
return dbText("githubRowAvailable");
|
|
107
|
+
case "connect":
|
|
108
|
+
return dbText("githubRowConnectFirst");
|
|
109
|
+
case "blocked":
|
|
110
|
+
switch (availability.reason) {
|
|
111
|
+
case "files":
|
|
112
|
+
return dbText("githubRowFiles");
|
|
113
|
+
case "system":
|
|
114
|
+
return dbText("githubRowSystem");
|
|
115
|
+
case "editors-only":
|
|
116
|
+
return dbText("githubRowEditorsOnly");
|
|
117
|
+
case "other-source":
|
|
118
|
+
return dbText("githubRowOtherSource", {
|
|
119
|
+
source: availability.sourceName ?? "",
|
|
120
|
+
});
|
|
121
|
+
case "has-pages":
|
|
122
|
+
return dbText("githubRowEmptyOnly");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The full reason a blocked collection can't take a folder. */
|
|
128
|
+
export function githubBlockedExplanation(
|
|
129
|
+
reason: GitHubBlockedReason,
|
|
130
|
+
place: GitHubCollectionPlace,
|
|
131
|
+
sourceName?: string,
|
|
132
|
+
): string {
|
|
133
|
+
switch (reason) {
|
|
134
|
+
case "files":
|
|
135
|
+
return dbText("githubExplainFiles");
|
|
136
|
+
case "system":
|
|
137
|
+
return dbText("githubExplainSystem");
|
|
138
|
+
case "editors-only":
|
|
139
|
+
return dbText("githubExplainEditorsOnly");
|
|
140
|
+
case "other-source":
|
|
141
|
+
return dbText("githubExplainOtherSource", { source: sourceName ?? "" });
|
|
142
|
+
case "has-pages":
|
|
143
|
+
return place.pageCount === 1
|
|
144
|
+
? dbText("githubExplainHasPagesOne")
|
|
145
|
+
: dbText("githubExplainHasPages", { count: place.pageCount });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { actionErrorMessage } from "@agent-native/core/client/hooks";
|
|
2
|
+
import {
|
|
3
|
+
GITHUB_SOURCE_MAX_FILES,
|
|
4
|
+
parseGitHubFolderUrl,
|
|
5
|
+
type GitHubLinkBlockedReason,
|
|
6
|
+
type GitHubSourceProblemCode,
|
|
7
|
+
type GitHubSourceSkipReason,
|
|
8
|
+
} from "@shared/github-source";
|
|
9
|
+
|
|
10
|
+
import { dbText, type DatabaseMessageKey } from "./text";
|
|
11
|
+
|
|
12
|
+
const PROBLEM_KEYS: Record<GitHubSourceProblemCode, DatabaseMessageKey> = {
|
|
13
|
+
connection: "githubProblemConnection",
|
|
14
|
+
forbidden: "githubProblemForbidden",
|
|
15
|
+
repository_not_found: "githubProblemRepositoryNotFound",
|
|
16
|
+
branch_not_found: "githubProblemBranchNotFound",
|
|
17
|
+
folder_not_found: "githubProblemFolderNotFound",
|
|
18
|
+
rate_limited: "githubProblemPaused",
|
|
19
|
+
too_large: "githubProblemTooLarge",
|
|
20
|
+
unavailable: "githubProblemPaused",
|
|
21
|
+
sync_failed: "githubProblemPaused",
|
|
22
|
+
storage_unavailable: "githubProblemStorageUnavailable",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SKIP_KEYS: Record<GitHubSourceSkipReason, DatabaseMessageKey> = {
|
|
26
|
+
mdx: "githubSkipMdx",
|
|
27
|
+
"too-large": "githubSkipTooLarge",
|
|
28
|
+
"not-text": "githubSkipNotText",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const BLOCKED_KEYS: Record<GitHubLinkBlockedReason, DatabaseMessageKey> = {
|
|
32
|
+
"collection-has-rows": "githubLinkBlockedHasRows",
|
|
33
|
+
"already-linked": "githubLinkBlockedLinked",
|
|
34
|
+
"system-collection": "githubLinkBlockedSystem",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Where a linked folder lives, for problem text that names it. */
|
|
38
|
+
export interface GitHubProblemPlace {
|
|
39
|
+
branch?: string;
|
|
40
|
+
path?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A linked folder's problem, as the status line and update toasts say it. */
|
|
44
|
+
export function githubProblemText(
|
|
45
|
+
code: GitHubSourceProblemCode,
|
|
46
|
+
place: GitHubProblemPlace = {},
|
|
47
|
+
): string {
|
|
48
|
+
return dbText(PROBLEM_KEYS[code] ?? "githubProblemPaused", {
|
|
49
|
+
branch: place.branch ?? "",
|
|
50
|
+
folder: lastSegment(place.path ?? ""),
|
|
51
|
+
max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Problems that mean nothing comes in until someone acts. */
|
|
56
|
+
export function githubProblemStopsUpdates(
|
|
57
|
+
code: GitHubSourceProblemCode,
|
|
58
|
+
): boolean {
|
|
59
|
+
return (
|
|
60
|
+
code === "connection" ||
|
|
61
|
+
code === "forbidden" ||
|
|
62
|
+
code === "repository_not_found" ||
|
|
63
|
+
code === "branch_not_found" ||
|
|
64
|
+
code === "folder_not_found" ||
|
|
65
|
+
code === "too_large"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The problem a `GITHUB_<CODE>` action error stands for, so the dialog can say
|
|
71
|
+
* it in the reader's language. Null for any other error.
|
|
72
|
+
*/
|
|
73
|
+
export function githubProblemFromErrorCode(
|
|
74
|
+
errorCode: unknown,
|
|
75
|
+
): GitHubSourceProblemCode | null {
|
|
76
|
+
if (typeof errorCode !== "string" || !errorCode.startsWith("GITHUB_")) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const code = errorCode.slice("GITHUB_".length).toLowerCase();
|
|
80
|
+
return code in PROBLEM_KEYS ? (code as GitHubSourceProblemCode) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether an action error means GitHub needs connecting first. */
|
|
84
|
+
export function githubErrorNeedsConnection(error: unknown): boolean {
|
|
85
|
+
const code = (error as { errorCode?: unknown } | null)?.errorCode;
|
|
86
|
+
// A missing or ungranted connection comes from the shared provider runtime
|
|
87
|
+
// as `connection_required`; a rejected one, from GitHub.
|
|
88
|
+
return (
|
|
89
|
+
code === "connection_required" ||
|
|
90
|
+
githubProblemFromErrorCode(code) === "connection"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The collection-level reason a link was refused, from its action error. */
|
|
95
|
+
export function githubBlockedFromErrorCode(
|
|
96
|
+
errorCode: unknown,
|
|
97
|
+
): GitHubLinkBlockedReason | null {
|
|
98
|
+
if (errorCode === "COLLECTION_NOT_EMPTY") return "collection-has-rows";
|
|
99
|
+
if (errorCode === "SOURCE_ALREADY_LINKED") return "already-linked";
|
|
100
|
+
if (errorCode === "COLLECTION_NOT_LINKABLE") return "system-collection";
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A link-time error in plain words, naming what the person typed: a typo in
|
|
106
|
+
* the link reads as a typo, not as something that went missing.
|
|
107
|
+
*/
|
|
108
|
+
export function githubLinkErrorText(error: unknown, url: string): string {
|
|
109
|
+
const errorCode = (error as { errorCode?: unknown } | null)?.errorCode;
|
|
110
|
+
const blocked = githubBlockedFromErrorCode(errorCode);
|
|
111
|
+
if (blocked) return githubBlockedText(blocked);
|
|
112
|
+
const problem = githubProblemFromErrorCode(errorCode);
|
|
113
|
+
if (!problem) {
|
|
114
|
+
return (
|
|
115
|
+
actionErrorMessage(error) ??
|
|
116
|
+
(error instanceof Error ? error.message : dbText("githubLinkUnavailable"))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const parsed = parseGitHubFolderUrl(url);
|
|
120
|
+
const repo = parsed.ok ? parsed.repo : "";
|
|
121
|
+
const [branch = "", ...folder] = parsed.ok ? parsed.refPath : [];
|
|
122
|
+
switch (problem) {
|
|
123
|
+
case "repository_not_found":
|
|
124
|
+
return dbText("githubLinkRepositoryNotFound", { repo });
|
|
125
|
+
case "forbidden":
|
|
126
|
+
return dbText("githubLinkForbidden", { repo });
|
|
127
|
+
case "branch_not_found":
|
|
128
|
+
return dbText("githubLinkBranchNotFound", { branch, repo });
|
|
129
|
+
case "folder_not_found":
|
|
130
|
+
return dbText("githubLinkFolderNotFound", {
|
|
131
|
+
folder: folder.join("/"),
|
|
132
|
+
repo,
|
|
133
|
+
});
|
|
134
|
+
case "too_large":
|
|
135
|
+
return dbText("githubLinkTooLarge", {
|
|
136
|
+
max: GITHUB_SOURCE_MAX_FILES.toLocaleString(pageLocale()),
|
|
137
|
+
});
|
|
138
|
+
default:
|
|
139
|
+
return dbText("githubLinkUnavailable");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** What's wrong with a link before anything is sent, if anything. */
|
|
144
|
+
export function githubUrlProblem(
|
|
145
|
+
url: string,
|
|
146
|
+
): { text: string; folderUrl: string | null; folder: string | null } | null {
|
|
147
|
+
const parsed = parseGitHubFolderUrl(url);
|
|
148
|
+
if (parsed.ok) return null;
|
|
149
|
+
if (parsed.reason !== "file") {
|
|
150
|
+
return {
|
|
151
|
+
text: dbText("githubLinkInvalidUrl"),
|
|
152
|
+
folderUrl: null,
|
|
153
|
+
folder: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const folder = fileLinkFolder(url);
|
|
157
|
+
return {
|
|
158
|
+
text: dbText("githubLinkFileUrl"),
|
|
159
|
+
folderUrl: folder?.url ?? null,
|
|
160
|
+
folder: folder?.name ?? null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The folder a file link points into: `…/blob/main/docs/faq.md` becomes
|
|
166
|
+
* `…/tree/main/docs`, named "docs", or the repository at its root.
|
|
167
|
+
*/
|
|
168
|
+
export function fileLinkFolder(
|
|
169
|
+
url: string,
|
|
170
|
+
): { url: string; name: string } | null {
|
|
171
|
+
let parsed: URL;
|
|
172
|
+
try {
|
|
173
|
+
parsed = new URL(
|
|
174
|
+
/^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : `https://${url}`,
|
|
175
|
+
);
|
|
176
|
+
// coercion-ok: a link that isn't a URL has no folder to offer; the caller shows the invalid-link text.
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
181
|
+
const [owner, repo, kind, ...rest] = segments;
|
|
182
|
+
if (!owner || !repo || kind !== "blob" || rest.length < 2) return null;
|
|
183
|
+
const folderSegments = rest.slice(0, -1);
|
|
184
|
+
const folderUrl = `https://github.com/${owner}/${repo}/tree/${folderSegments.join("/")}`;
|
|
185
|
+
// The first segment is the branch, so a file at the root names the repository.
|
|
186
|
+
const name =
|
|
187
|
+
folderSegments.length > 1
|
|
188
|
+
? decodeURIComponent(folderSegments[folderSegments.length - 1]!)
|
|
189
|
+
: decodeURIComponent(repo);
|
|
190
|
+
return parseGitHubFolderUrl(folderUrl).ok ? { url: folderUrl, name } : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function githubSkipReasonText(reason: GitHubSourceSkipReason): string {
|
|
194
|
+
return dbText(SKIP_KEYS[reason] ?? "githubSkipNotText");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function githubBlockedText(reason: GitHubLinkBlockedReason): string {
|
|
198
|
+
return dbText(BLOCKED_KEYS[reason] ?? "githubLinkBlockedLinked");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The seven-character commit name GitHub shows. */
|
|
202
|
+
export function shortCommit(sha: string): string {
|
|
203
|
+
return sha.slice(0, 7);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function lastSegment(path: string): string {
|
|
207
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
208
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The page's language, for numbers, dates and lists. */
|
|
212
|
+
export function pageLocale(): string {
|
|
213
|
+
return typeof document === "undefined" || !document.documentElement.lang
|
|
214
|
+
? "en-US"
|
|
215
|
+
: document.documentElement.lang;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Not in the TypeScript lib this app targets, though every supported
|
|
219
|
+
// browser has it.
|
|
220
|
+
const ListFormat = (
|
|
221
|
+
Intl as typeof Intl & {
|
|
222
|
+
ListFormat?: new (
|
|
223
|
+
locale: string,
|
|
224
|
+
options: { style: "long"; type: "conjunction" },
|
|
225
|
+
) => { format(items: string[]): string };
|
|
226
|
+
}
|
|
227
|
+
).ListFormat;
|
|
228
|
+
|
|
229
|
+
/** "API reference, FAQ and Getting started", in the page's language. */
|
|
230
|
+
export function githubList(items: string[]): string {
|
|
231
|
+
if (!ListFormat) return items.join(", ");
|
|
232
|
+
return new ListFormat(pageLocale(), {
|
|
233
|
+
style: "long",
|
|
234
|
+
type: "conjunction",
|
|
235
|
+
}).format(items);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const RELATIVE_UNITS: Array<[Intl.RelativeTimeFormatUnit, number]> = [
|
|
239
|
+
["year", 365 * 24 * 60 * 60],
|
|
240
|
+
["month", 30 * 24 * 60 * 60],
|
|
241
|
+
["week", 7 * 24 * 60 * 60],
|
|
242
|
+
["day", 24 * 60 * 60],
|
|
243
|
+
["hour", 60 * 60],
|
|
244
|
+
["minute", 60],
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
/** "2 minutes ago", "just now" and the like, in the page's language. */
|
|
248
|
+
export function githubRelativeTime(iso: string, now = Date.now()): string {
|
|
249
|
+
const at = Date.parse(iso);
|
|
250
|
+
if (!Number.isFinite(at)) return "";
|
|
251
|
+
const format = new Intl.RelativeTimeFormat(pageLocale(), {
|
|
252
|
+
numeric: "auto",
|
|
253
|
+
});
|
|
254
|
+
const seconds = Math.round((at - now) / 1000);
|
|
255
|
+
for (const [unit, size] of RELATIVE_UNITS) {
|
|
256
|
+
if (Math.abs(seconds) >= size) {
|
|
257
|
+
return format.format(Math.round(seconds / size), unit);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return dbText("githubJustNow");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A frontmatter example the way a writer reads it: "Yes", "Sep 15, 2026". */
|
|
264
|
+
export function githubExampleText(type: string, example: string): string {
|
|
265
|
+
if (type === "checkbox") {
|
|
266
|
+
if (example === "true") return dbText("githubYes");
|
|
267
|
+
if (example === "false") return dbText("githubNo");
|
|
268
|
+
}
|
|
269
|
+
if (type === "date") {
|
|
270
|
+
const at = Date.parse(example);
|
|
271
|
+
if (Number.isFinite(at)) {
|
|
272
|
+
return new Intl.DateTimeFormat(pageLocale(), {
|
|
273
|
+
dateStyle: "medium",
|
|
274
|
+
// Frontmatter dates are calendar days, so they mustn't shift by zone.
|
|
275
|
+
timeZone: /^\d{4}-\d{2}-\d{2}$/.test(example) ? "UTC" : undefined,
|
|
276
|
+
}).format(at);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return example;
|
|
280
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { useLocation, useNavigation } from "react-router";
|
|
27
27
|
import { toast } from "sonner";
|
|
28
28
|
|
|
29
|
+
import { GitHubLinkDialogHost } from "@/components/editor/database/GitHubLinkDialog";
|
|
29
30
|
import { DocumentEditor } from "@/components/editor/DocumentEditor";
|
|
30
31
|
import { DocumentEditorSkeleton } from "@/components/editor/DocumentEditorSkeleton";
|
|
31
32
|
import { DocumentSidebar } from "@/components/sidebar/DocumentSidebar";
|
|
@@ -497,6 +498,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
497
498
|
</>
|
|
498
499
|
)}
|
|
499
500
|
</div>
|
|
501
|
+
<GitHubLinkDialogHost />
|
|
500
502
|
</HeaderActionsProvider>
|
|
501
503
|
);
|
|
502
504
|
}
|
|
@@ -30,12 +30,14 @@ const DOCUMENT_MUTATIONS = new Set([
|
|
|
30
30
|
"delete-document",
|
|
31
31
|
"delete-document-property",
|
|
32
32
|
"delete-content-database",
|
|
33
|
+
"disconnect-content-database-source",
|
|
33
34
|
"execute-content-trash-purge",
|
|
34
35
|
"permanently-delete-document",
|
|
35
36
|
"duplicate-document-property",
|
|
36
37
|
"edit-document",
|
|
37
38
|
"execute-builder-source-batch",
|
|
38
39
|
"execute-builder-source-execution",
|
|
40
|
+
"create-github-collection",
|
|
39
41
|
"import-content-source",
|
|
40
42
|
"link-github-folder",
|
|
41
43
|
"migrate-content-database-rows",
|
|
@@ -72,12 +74,14 @@ const DATABASE_RESULT_MUTATIONS = new Set([
|
|
|
72
74
|
"delete-content-database",
|
|
73
75
|
"delete-document",
|
|
74
76
|
"delete-document-property",
|
|
77
|
+
"disconnect-content-database-source",
|
|
75
78
|
"duplicate-database-item",
|
|
76
79
|
"duplicate-database-items",
|
|
77
80
|
"duplicate-document-property",
|
|
78
81
|
"edit-document",
|
|
79
82
|
"execute-builder-source-batch",
|
|
80
83
|
"execute-builder-source-execution",
|
|
84
|
+
"create-github-collection",
|
|
81
85
|
"import-content-source",
|
|
82
86
|
"link-github-folder",
|
|
83
87
|
"migrate-content-database-rows",
|
|
@@ -103,6 +107,7 @@ const DATABASE_PRESENTATION_MUTATIONS = new Set([
|
|
|
103
107
|
]);
|
|
104
108
|
|
|
105
109
|
const DATABASE_LIFECYCLE_MUTATIONS = new Set([
|
|
110
|
+
"create-github-collection",
|
|
106
111
|
"delete-content-database",
|
|
107
112
|
"execute-content-trash-purge",
|
|
108
113
|
"permanently-delete-document",
|
|
@@ -115,6 +120,7 @@ const DATABASE_LIFECYCLE_MUTATIONS = new Set([
|
|
|
115
120
|
|
|
116
121
|
const DOCUMENT_DISCOVERY_MUTATIONS = new Set([
|
|
117
122
|
"create-document",
|
|
123
|
+
"create-github-collection",
|
|
118
124
|
"import-content",
|
|
119
125
|
"link-github-folder",
|
|
120
126
|
"refresh-content-database-source",
|
|
@@ -1837,7 +1837,9 @@ export function useRefreshContentDatabaseSource(documentId: string) {
|
|
|
1837
1837
|
RefreshContentDatabaseSourceRequest
|
|
1838
1838
|
>("refresh-content-database-source", {
|
|
1839
1839
|
skipActionQueryInvalidation: true,
|
|
1840
|
-
|
|
1840
|
+
// A GitHub refresh that fails stores the problem on the source first, so
|
|
1841
|
+
// the status line rereads after a failure too.
|
|
1842
|
+
onSettled: () => {
|
|
1841
1843
|
invalidateContentDatabaseSourceRefreshQueries(queryClient, documentId);
|
|
1842
1844
|
},
|
|
1843
1845
|
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useActionMutation,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
} from "@agent-native/core/client/hooks";
|
|
5
|
+
import type { ContentDatabase, ContentDatabaseSource } from "@shared/api";
|
|
6
|
+
import type {
|
|
7
|
+
GitHubFolderPreview,
|
|
8
|
+
GitHubLinkBlockedReason,
|
|
9
|
+
GitHubSourceProblemCode,
|
|
10
|
+
} from "@shared/github-source";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { useLocation } from "react-router";
|
|
13
|
+
|
|
14
|
+
import { documentQueryFilter } from "../lib/document-query";
|
|
15
|
+
import {
|
|
16
|
+
contentPlacementTargets,
|
|
17
|
+
contentSpaceFilesDatabaseId,
|
|
18
|
+
invalidateContentQueries,
|
|
19
|
+
} from "./use-content-action-mutation";
|
|
20
|
+
import { contentSyncInvalidatePredicate } from "./use-db-sync";
|
|
21
|
+
|
|
22
|
+
type GitHubFolderAction = "link-github-folder" | "sync-github-folder";
|
|
23
|
+
|
|
24
|
+
/** What a sync did, as the link and sync actions report it. */
|
|
25
|
+
export interface GitHubFolderSyncSummary {
|
|
26
|
+
state: "unchanged" | "synced" | "partial" | "busy" | "error";
|
|
27
|
+
commitSha: string | null;
|
|
28
|
+
created: number;
|
|
29
|
+
updated: number;
|
|
30
|
+
renamed: number;
|
|
31
|
+
trashed: number;
|
|
32
|
+
remaining: number;
|
|
33
|
+
problem: { code: GitHubSourceProblemCode; message: string } | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LinkGitHubFolderRequest {
|
|
37
|
+
databaseId: string;
|
|
38
|
+
url: string;
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type LinkGitHubFolderResponse =
|
|
43
|
+
| {
|
|
44
|
+
dryRun: true;
|
|
45
|
+
database: ContentDatabase;
|
|
46
|
+
preview: GitHubFolderPreview;
|
|
47
|
+
canLink: boolean;
|
|
48
|
+
blocked: string | null;
|
|
49
|
+
blockedReason: GitHubLinkBlockedReason | null;
|
|
50
|
+
summary: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
dryRun: false;
|
|
54
|
+
database: ContentDatabase;
|
|
55
|
+
preview: GitHubFolderPreview;
|
|
56
|
+
sync: GitHubFolderSyncSummary;
|
|
57
|
+
source: ContentDatabaseSource | null;
|
|
58
|
+
summary: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface CreateGitHubCollectionRequest {
|
|
62
|
+
url: string;
|
|
63
|
+
spaceId: string;
|
|
64
|
+
parentId?: string | null;
|
|
65
|
+
dryRun?: boolean;
|
|
66
|
+
idempotencyKey?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type CreateGitHubCollectionResponse =
|
|
70
|
+
| {
|
|
71
|
+
dryRun: true;
|
|
72
|
+
preview: GitHubFolderPreview;
|
|
73
|
+
title: string;
|
|
74
|
+
summary: string;
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
dryRun: false;
|
|
78
|
+
database: ContentDatabase;
|
|
79
|
+
preview: GitHubFolderPreview;
|
|
80
|
+
title: string;
|
|
81
|
+
sync: GitHubFolderSyncSummary;
|
|
82
|
+
source: ContentDatabaseSource | null;
|
|
83
|
+
summary: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export interface SyncGitHubFolderResponse {
|
|
87
|
+
database: ContentDatabase;
|
|
88
|
+
sync: GitHubFolderSyncSummary;
|
|
89
|
+
source: ContentDatabaseSource | null;
|
|
90
|
+
summary: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Linking or syncing a folder changes rows, Page lists and Trash. Other tabs
|
|
95
|
+
* refresh from the action's change event, but a tab ignores its own events,
|
|
96
|
+
* so this tab refreshes the same queries that event would.
|
|
97
|
+
*/
|
|
98
|
+
export function useRefreshAfterGitHubFolder() {
|
|
99
|
+
const queryClient = useQueryClient();
|
|
100
|
+
const { pathname } = useLocation();
|
|
101
|
+
return (actionName: GitHubFolderAction) => {
|
|
102
|
+
const spoiled = contentSyncInvalidatePredicate(queryClient, pathname);
|
|
103
|
+
const events = [{ source: "action", key: actionName }];
|
|
104
|
+
void queryClient.invalidateQueries({
|
|
105
|
+
predicate: (query) => spoiled(query, events),
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Refreshes this tab after a folder action that changes rows, after a failure
|
|
112
|
+
* too: a link can be saved before its first sync fails.
|
|
113
|
+
*/
|
|
114
|
+
function useGitHubFolderMutation<TData, TVariables>(
|
|
115
|
+
actionName: GitHubFolderAction,
|
|
116
|
+
changesRows: (variables: TVariables) => boolean,
|
|
117
|
+
) {
|
|
118
|
+
const refresh = useRefreshAfterGitHubFolder();
|
|
119
|
+
return useActionMutation<TData, TVariables>(actionName, {
|
|
120
|
+
skipActionQueryInvalidation: true,
|
|
121
|
+
onSettled: (_data, _error, variables) => {
|
|
122
|
+
if (changesRows(variables as TVariables)) refresh(actionName);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useLinkGitHubFolder() {
|
|
128
|
+
return useGitHubFolderMutation<
|
|
129
|
+
LinkGitHubFolderResponse,
|
|
130
|
+
LinkGitHubFolderRequest
|
|
131
|
+
>("link-github-folder", (variables) => !variables.dryRun);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function useSyncGitHubFolder() {
|
|
135
|
+
return useGitHubFolderMutation<
|
|
136
|
+
SyncGitHubFolderResponse,
|
|
137
|
+
{ databaseId: string }
|
|
138
|
+
>("sync-github-folder", () => true);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Makes a collection from a GitHub folder. The new collection shows in the
|
|
143
|
+
* sidebar and under its parent, so those lists refresh as a new collection's
|
|
144
|
+
* would, and its rows refresh like a link's.
|
|
145
|
+
*/
|
|
146
|
+
export function useCreateGitHubCollection() {
|
|
147
|
+
const queryClient = useQueryClient();
|
|
148
|
+
const { pathname } = useLocation();
|
|
149
|
+
return useActionMutation<
|
|
150
|
+
CreateGitHubCollectionResponse,
|
|
151
|
+
CreateGitHubCollectionRequest
|
|
152
|
+
>("create-github-collection", {
|
|
153
|
+
skipActionQueryInvalidation: true,
|
|
154
|
+
onSuccess: (data, variables) => {
|
|
155
|
+
if (data.dryRun) return;
|
|
156
|
+
const parentId = variables.parentId ?? null;
|
|
157
|
+
void queryClient.invalidateQueries(
|
|
158
|
+
documentQueryFilter(data.database.documentId),
|
|
159
|
+
);
|
|
160
|
+
invalidateContentQueries(queryClient, [
|
|
161
|
+
...contentPlacementTargets(queryClient, {
|
|
162
|
+
documentIds: [data.database.documentId],
|
|
163
|
+
parentIds: [parentId],
|
|
164
|
+
databaseId: parentId
|
|
165
|
+
? undefined
|
|
166
|
+
: contentSpaceFilesDatabaseId(queryClient, variables.spaceId),
|
|
167
|
+
}),
|
|
168
|
+
["action", "list-documents"],
|
|
169
|
+
["action", "list-content-databases"],
|
|
170
|
+
]);
|
|
171
|
+
const spoiled = contentSyncInvalidatePredicate(queryClient, pathname);
|
|
172
|
+
const events = [{ source: "action", key: "create-github-collection" }];
|
|
173
|
+
void queryClient.invalidateQueries({
|
|
174
|
+
predicate: (query) => spoiled(query, events),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** How long a connection answer holds before the Sources panel asks again. */
|
|
181
|
+
const CONNECTION_STALE_MS = 60_000;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether the person has a GitHub connection Content can read through, so a
|
|
185
|
+
* Collection's Sources can say "Connect your GitHub account first" before any
|
|
186
|
+
* typing. `connected` is null when GitHub couldn't be asked.
|
|
187
|
+
*/
|
|
188
|
+
export function useGitHubConnection(enabled: boolean) {
|
|
189
|
+
return useActionQuery<{ connected: boolean | null }>(
|
|
190
|
+
"check-github-connection",
|
|
191
|
+
{},
|
|
192
|
+
{
|
|
193
|
+
enabled,
|
|
194
|
+
retry: false,
|
|
195
|
+
staleTime: CONNECTION_STALE_MS,
|
|
196
|
+
resources: [],
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|