@agent-native/core 0.184.0 → 0.184.1-nightly-20260922203115
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/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/corpus/templates/content/AGENTS.md +1 -1
- package/corpus/templates/content/actions/_document-search-ranking.ts +191 -0
- package/corpus/templates/content/actions/search-documents.ts +184 -49
- package/corpus/templates/content/changelog/2026-09-21-search-ranks-title-matches-first.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.knowledge.search.md +11 -8
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +10 -3
- package/corpus/templates/design/.generated/bridge/nav.generated.ts +1 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +67 -6
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +16 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +26 -0
- package/corpus/templates/design/changelog/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/corpus/templates/design/changelog/2026-09-22-blocks-can-be-selected-with-one-click-inside-screens.md +6 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +136 -123
- package/corpus/templates/mail/changelog/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-22-apps-start-with-an-app-shaped-skeleton-while-session-data-lo.md +6 -0
- package/dist/client/AppShellSkeleton.d.ts +5 -0
- package/dist/client/AppShellSkeleton.js +69 -0
- package/dist/client/ClientOnly.d.ts +1 -1
- package/dist/client/ClientOnly.js +1 -1
- package/dist/client/LanguagePicker.d.ts +6 -0
- package/dist/client/LanguagePicker.js +128 -0
- package/dist/client/analytics.d.ts +3 -2
- package/dist/client/analytics.js +1 -1
- package/dist/client/app-providers.d.ts +3 -3
- package/dist/client/app-providers.js +53 -23
- package/dist/client/client-status-requests.d.ts +5 -0
- package/dist/client/client-status-requests.js +31 -18
- package/dist/client/i18n.d.ts +1 -6
- package/dist/client/i18n.js +3 -117
- package/dist/client/onboarding/first-run-startup-gate.js +14 -3
- package/dist/client/require-session.d.ts +1 -1
- package/dist/client/require-session.js +3 -3
- package/dist/client/session-replay-context.d.ts +16 -0
- package/dist/client/session-replay-context.js +93 -0
- package/dist/client/session-replay.d.ts +2 -26
- package/dist/client/session-replay.js +1 -64
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +19 -29
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/shared/ssr-session-bootstrap.d.ts +3 -0
- package/dist/shared/ssr-session-bootstrap.js +44 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Read the relevant skill before deeper work:
|
|
|
63
63
|
| `navigate` | Move the UI to a document, comments, media, or settings |
|
|
64
64
|
| `refresh-list` | Repaint the sidebar after an out-of-band mutation |
|
|
65
65
|
| `list-documents` | Document metadata tree, without bodies |
|
|
66
|
-
| `search-documents` | Title
|
|
66
|
+
| `search-documents` | Title-first search with snippets |
|
|
67
67
|
| `get-document` | One document with full content |
|
|
68
68
|
| `pull-document` | Flush live collab state, then read (external edits) |
|
|
69
69
|
| `get-blocks-field-word-count` | Count one exact Blocks field; omit `propertyId` for the primary Content body |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { and, or, sql, type SQL, type SQLWrapper } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ParsedSearchQuery,
|
|
5
|
+
SearchQueryGroup,
|
|
6
|
+
SearchQueryTerm,
|
|
7
|
+
} from "../shared/search-query.js";
|
|
8
|
+
|
|
9
|
+
function escapeLike(value: string) {
|
|
10
|
+
return value.replace(/([\\%_])/g, "\\$1");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function substringMatch(column: SQLWrapper, term: SearchQueryTerm): SQL {
|
|
14
|
+
return sql`${column} ILIKE ${`%${escapeLike(term.text)}%`} ESCAPE '\\'`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeRegex(value: string) {
|
|
18
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function regexTerm(value: string) {
|
|
22
|
+
return escapeRegex(value.trim()).replace(/\s+/g, "[[:space:]]+");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function searchQueryProximityPattern(parsed: ParsedSearchQuery) {
|
|
26
|
+
if (
|
|
27
|
+
parsed.groups.length < 2 ||
|
|
28
|
+
parsed.groups.some(
|
|
29
|
+
(group) => group.terms.length !== 1 || group.terms[0]!.titleOnly,
|
|
30
|
+
)
|
|
31
|
+
) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return parsed.groups
|
|
35
|
+
.map((group) => regexTerm(group.terms[0]!.text))
|
|
36
|
+
.join("(.|\\n){0,240}");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function wordPrefixMatch(column: SQLWrapper, term: SearchQueryTerm): SQL {
|
|
40
|
+
const pattern = regexTerm(term.text);
|
|
41
|
+
return sql`${column} ~* ${`(^|[^[:alnum:]_])${pattern}`}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function groupSubstringMatch(column: SQLWrapper, group: SearchQueryGroup): SQL {
|
|
45
|
+
return or(...group.terms.map((term) => substringMatch(column, term)))!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nonTitleGroupSubstringMatch(
|
|
49
|
+
column: SQLWrapper,
|
|
50
|
+
group: SearchQueryGroup,
|
|
51
|
+
): SQL {
|
|
52
|
+
const terms = group.terms.filter((term) => !term.titleOnly);
|
|
53
|
+
return terms.length
|
|
54
|
+
? or(...terms.map((term) => substringMatch(column, term)))!
|
|
55
|
+
: sql`false`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function groupWordPrefixMatch(
|
|
59
|
+
column: SQLWrapper,
|
|
60
|
+
group: SearchQueryGroup,
|
|
61
|
+
): SQL {
|
|
62
|
+
return or(...group.terms.map((term) => wordPrefixMatch(column, term)))!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function countMatchingGroups(
|
|
66
|
+
column: SQLWrapper,
|
|
67
|
+
groups: SearchQueryGroup[],
|
|
68
|
+
nonTitleOnly = false,
|
|
69
|
+
): SQL<number> {
|
|
70
|
+
if (!groups.length) return sql<number>`0`;
|
|
71
|
+
return sql<number>`(${sql.join(
|
|
72
|
+
groups.map(
|
|
73
|
+
(group) =>
|
|
74
|
+
sql`case when ${nonTitleOnly ? nonTitleGroupSubstringMatch(column, group) : groupSubstringMatch(column, group)} then 1 else 0 end`,
|
|
75
|
+
),
|
|
76
|
+
sql` + `,
|
|
77
|
+
)})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DocumentSearchRankingColumns {
|
|
81
|
+
title: SQLWrapper;
|
|
82
|
+
description: SQLWrapper;
|
|
83
|
+
content: SQLWrapper;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DocumentSearchRankingOptions {
|
|
87
|
+
includeNonTitleFields?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function bodyProximityScore(
|
|
91
|
+
parsed: ParsedSearchQuery,
|
|
92
|
+
content: SQLWrapper,
|
|
93
|
+
): SQL<number> {
|
|
94
|
+
if (
|
|
95
|
+
parsed.groups.length < 2 ||
|
|
96
|
+
parsed.groups.some(
|
|
97
|
+
(group) => group.terms.length !== 1 || group.terms[0]!.titleOnly,
|
|
98
|
+
)
|
|
99
|
+
) {
|
|
100
|
+
return sql<number>`case when true then 0 else 0 end`;
|
|
101
|
+
}
|
|
102
|
+
const phrase = parsed.groups
|
|
103
|
+
.map((group) => group.terms[0]!.text.trim())
|
|
104
|
+
.join(" ");
|
|
105
|
+
return sql<number>`case when ${content} ILIKE ${`%${escapeLike(phrase)}%`} ESCAPE '\\' then 1 else 0 end`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns lexicographic ranking keys. The first key is a protected match tier:
|
|
110
|
+
* no recency or repetition signal can move a body match above a title match.
|
|
111
|
+
*/
|
|
112
|
+
export function documentSearchRanking(
|
|
113
|
+
parsed: ParsedSearchQuery,
|
|
114
|
+
columns: DocumentSearchRankingColumns,
|
|
115
|
+
options: DocumentSearchRankingOptions = {},
|
|
116
|
+
) {
|
|
117
|
+
const groups = parsed.groups;
|
|
118
|
+
const includeNonTitleFields = options.includeNonTitleFields ?? true;
|
|
119
|
+
const normalizedTitle = sql<string>`regexp_replace(lower(trim(coalesce(${columns.title}, ''))), '\\s+', ' ', 'g')`;
|
|
120
|
+
const simpleQueries =
|
|
121
|
+
groups.length > 0 && groups.every((group) => group.terms.length === 1)
|
|
122
|
+
? [groups.map((group) => group.terms[0]!.text.trim()).join(" ")]
|
|
123
|
+
: groups.length === 1
|
|
124
|
+
? groups[0]!.terms.map((term) => term.text.trim())
|
|
125
|
+
: [];
|
|
126
|
+
const normalizedSimpleQueries = simpleQueries.map((query) =>
|
|
127
|
+
query.toLocaleLowerCase().replace(/\s+/g, " "),
|
|
128
|
+
);
|
|
129
|
+
const guardNormalizedTitle = groups.every((group) =>
|
|
130
|
+
group.terms.every((term) => !/\s/.test(term.text)),
|
|
131
|
+
);
|
|
132
|
+
const allTitleSubstrings = groups.length
|
|
133
|
+
? and(...groups.map((group) => groupSubstringMatch(columns.title, group)))!
|
|
134
|
+
: sql`false`;
|
|
135
|
+
const allTitleWordPrefixes = groups.length
|
|
136
|
+
? and(...groups.map((group) => groupWordPrefixMatch(columns.title, group)))!
|
|
137
|
+
: sql`false`;
|
|
138
|
+
const allTitleOrDescription = groups.length
|
|
139
|
+
? and(
|
|
140
|
+
...groups.map((group) =>
|
|
141
|
+
includeNonTitleFields
|
|
142
|
+
? or(
|
|
143
|
+
groupSubstringMatch(columns.title, group),
|
|
144
|
+
nonTitleGroupSubstringMatch(columns.description, group),
|
|
145
|
+
)!
|
|
146
|
+
: groupSubstringMatch(columns.title, group),
|
|
147
|
+
),
|
|
148
|
+
)!
|
|
149
|
+
: sql`false`;
|
|
150
|
+
const matchTier = sql<number>`case
|
|
151
|
+
when ${
|
|
152
|
+
normalizedSimpleQueries.length
|
|
153
|
+
? and(
|
|
154
|
+
guardNormalizedTitle ? allTitleSubstrings : sql`true`,
|
|
155
|
+
or(
|
|
156
|
+
...normalizedSimpleQueries.map(
|
|
157
|
+
(query) => sql`${normalizedTitle} = ${query}`,
|
|
158
|
+
),
|
|
159
|
+
)!,
|
|
160
|
+
)!
|
|
161
|
+
: sql`false`
|
|
162
|
+
} then 5
|
|
163
|
+
when ${
|
|
164
|
+
normalizedSimpleQueries.length
|
|
165
|
+
? and(
|
|
166
|
+
guardNormalizedTitle ? allTitleSubstrings : sql`true`,
|
|
167
|
+
or(
|
|
168
|
+
...normalizedSimpleQueries.map(
|
|
169
|
+
(query) =>
|
|
170
|
+
sql`${normalizedTitle} like ${`${escapeLike(query)}%`} escape '\\'`,
|
|
171
|
+
),
|
|
172
|
+
)!,
|
|
173
|
+
)!
|
|
174
|
+
: sql`false`
|
|
175
|
+
} then 4
|
|
176
|
+
when ${allTitleSubstrings} and ${allTitleWordPrefixes} then 3
|
|
177
|
+
when ${allTitleSubstrings} then 2
|
|
178
|
+
when ${allTitleOrDescription} then 1
|
|
179
|
+
else 0
|
|
180
|
+
end`;
|
|
181
|
+
return {
|
|
182
|
+
matchTier,
|
|
183
|
+
titleCoverage: countMatchingGroups(columns.title, groups),
|
|
184
|
+
descriptionCoverage: includeNonTitleFields
|
|
185
|
+
? countMatchingGroups(columns.description, groups, true)
|
|
186
|
+
: sql<number>`0::integer`,
|
|
187
|
+
bodyProximity: includeNonTitleFields
|
|
188
|
+
? bodyProximityScore(parsed, columns.content)
|
|
189
|
+
: sql<number>`0::integer`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
documentDiscoveryPagination,
|
|
33
33
|
documentDiscoveryWhere,
|
|
34
34
|
} from "./_document-discovery-query.js";
|
|
35
|
+
import {
|
|
36
|
+
documentSearchRanking,
|
|
37
|
+
searchQueryProximityPattern,
|
|
38
|
+
} from "./_document-search-ranking.js";
|
|
35
39
|
|
|
36
40
|
function escapeLike(s: string): string {
|
|
37
41
|
return s.replace(/([\\%_])/g, "\\$1");
|
|
@@ -45,15 +49,21 @@ function escapeLike(s: string): string {
|
|
|
45
49
|
// no-match case. The row is still returned either way.
|
|
46
50
|
function makeSnippet(content: string, query: string, radius = 120) {
|
|
47
51
|
const compact = content.replace(/\s+/g, " ").trim();
|
|
52
|
+
const compactQuery = query.replace(/\s+/g, " ").trim();
|
|
48
53
|
if (!compact) return "";
|
|
49
|
-
|
|
54
|
+
if (!compactQuery) {
|
|
55
|
+
return compact.length <= radius * 2
|
|
56
|
+
? compact
|
|
57
|
+
: `${compact.slice(0, radius * 2).trimEnd()}...`;
|
|
58
|
+
}
|
|
59
|
+
const index = compact.toLowerCase().indexOf(compactQuery.toLowerCase());
|
|
50
60
|
if (index < 0) {
|
|
51
61
|
return compact.length <= radius * 2
|
|
52
62
|
? compact
|
|
53
63
|
: `${compact.slice(0, radius * 2).trimEnd()}...`;
|
|
54
64
|
}
|
|
55
65
|
const start = Math.max(0, index - radius);
|
|
56
|
-
const end = Math.min(compact.length, index +
|
|
66
|
+
const end = Math.min(compact.length, index + compactQuery.length + radius);
|
|
57
67
|
return `${start > 0 ? "..." : ""}${compact.slice(start, end).trim()}${
|
|
58
68
|
end < compact.length ? "..." : ""
|
|
59
69
|
}`;
|
|
@@ -61,7 +71,7 @@ function makeSnippet(content: string, query: string, radius = 120) {
|
|
|
61
71
|
|
|
62
72
|
export default defineAction({
|
|
63
73
|
description:
|
|
64
|
-
'Search one bounded page of access-scoped documents by title and content, or find an exact title within a parent, space, and document type. The query supports Google-style operators: "exact phrase", -excludedTerm, OR between terms (uppercase), intitle:term; bare words combine with AND and %, _ match literally. Returns explicit pagination; follow nextOffset until hasMore is false. Returns metadata and snippets; use get-document for full content.',
|
|
74
|
+
'Search one relevance-ranked, bounded page of access-scoped documents by title and content, or find an exact title within a parent, space, and document type. Exact and partial title matches rank above description and body matches. The query supports Google-style operators: "exact phrase", -excludedTerm, OR between terms (uppercase), intitle:term; bare words combine with AND and %, _ match literally. Returns explicit pagination; follow nextOffset until hasMore is false. Returns metadata and snippets; use get-document for full content.',
|
|
65
75
|
deferLoading: false,
|
|
66
76
|
mcpTool: true,
|
|
67
77
|
schema: z
|
|
@@ -131,6 +141,7 @@ export default defineAction({
|
|
|
131
141
|
]),
|
|
132
142
|
];
|
|
133
143
|
let bodyNeedles: string[] = [];
|
|
144
|
+
let parsedQuery: ReturnType<typeof parseSearchQuery> | null = null;
|
|
134
145
|
const queryTermPredicate = (term: SearchQueryTerm): SQL => {
|
|
135
146
|
const pattern = `%${escapeLike(term.text)}%`;
|
|
136
147
|
const columns =
|
|
@@ -148,6 +159,7 @@ export default defineAction({
|
|
|
148
159
|
const matchPredicates: SQL[] = [];
|
|
149
160
|
if (args.query) {
|
|
150
161
|
const parsed = parseSearchQuery(args.query);
|
|
162
|
+
parsedQuery = parsed;
|
|
151
163
|
if (parsed.empty) {
|
|
152
164
|
// Punctuation-only input (lone `-`, empty quotes) matches nothing by
|
|
153
165
|
// design; report that as a loud empty page rather than every document.
|
|
@@ -169,7 +181,7 @@ export default defineAction({
|
|
|
169
181
|
.map((term) => term.text),
|
|
170
182
|
),
|
|
171
183
|
),
|
|
172
|
-
];
|
|
184
|
+
].slice(0, 256);
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
187
|
const where = documentDiscoveryWhere({
|
|
@@ -204,39 +216,110 @@ export default defineAction({
|
|
|
204
216
|
: undefined,
|
|
205
217
|
),
|
|
206
218
|
});
|
|
207
|
-
const [countRow] = await db
|
|
208
|
-
.select({ count: sql<number>`count(*)` })
|
|
209
|
-
.from(schema.documents)
|
|
210
|
-
.where(where);
|
|
211
|
-
const totalItems = Number(countRow?.count ?? 0);
|
|
212
|
-
|
|
213
219
|
// Project a bounded preview of `content` instead of the full column:
|
|
214
220
|
// document bodies can be multi-MB, and this action only returns a short
|
|
215
221
|
// snippet (use get-document for full content). In free-text mode the
|
|
216
|
-
// preview window is anchored at
|
|
217
|
-
// eligible positive
|
|
218
|
-
// so `makeSnippet`
|
|
222
|
+
// preview window is anchored at an in-body occurrence that keeps the most
|
|
223
|
+
// eligible positive terms in view. The selected term is projected with the
|
|
224
|
+
// window so `makeSnippet` preserves the complete match. Position and query
|
|
225
|
+
// order break ties;
|
|
219
226
|
// title-only matches and exactTitle mode keep the head projection. The
|
|
220
227
|
// true length still comes from SQL `length()` rather than reading `.length`
|
|
221
228
|
// off a truncated string. Mirrors the
|
|
222
229
|
// `substr`/`length` projection style in list-documents.ts; `position`,
|
|
223
230
|
// `substr`, and `length` all work in PostgreSQL and PGlite.
|
|
224
231
|
const normalizedContent = sql<string>`coalesce(${schema.documents.content}, '')`;
|
|
225
|
-
const
|
|
226
|
-
? sql
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
const bodyNeedleArray = bodyNeedles.length
|
|
233
|
+
? sql`array[${sql.join(
|
|
234
|
+
bodyNeedles.map((needle) => sql`${needle}`),
|
|
235
|
+
sql`, `,
|
|
236
|
+
)}]::text[]`
|
|
237
|
+
: undefined;
|
|
238
|
+
const maxOccurrencesPerNeedle = Math.max(
|
|
239
|
+
1,
|
|
240
|
+
Math.floor(256 / Math.max(1, bodyNeedles.length)),
|
|
241
|
+
);
|
|
242
|
+
const proximityPattern =
|
|
243
|
+
bodyNeedleArray && parsedQuery
|
|
244
|
+
? searchQueryProximityPattern(parsedQuery)
|
|
245
|
+
: null;
|
|
246
|
+
const proximityPosition = proximityPattern
|
|
247
|
+
? sql<number>`regexp_instr(${normalizedContent}, ${proximityPattern}, 1, 1, 0, 'i')`
|
|
248
|
+
: undefined;
|
|
249
|
+
// Inspect repeated occurrences so the snippet can prefer a later,
|
|
250
|
+
// denser passage. Cap each needle to keep pathological repeated-token
|
|
251
|
+
// documents from turning one result preview into an unbounded scan.
|
|
252
|
+
const fallbackBodyPosition =
|
|
253
|
+
bodyNeedles.length === 1
|
|
254
|
+
? sql<number>`nullif(position(lower(${bodyNeedles[0]!}) in lower(${normalizedContent})), 0)`
|
|
255
|
+
: bodyNeedleArray
|
|
256
|
+
? sql<number>`(
|
|
257
|
+
with recursive body_occurrence(needle, query_order, match_position, occurrence_number) as (
|
|
258
|
+
select candidate.needle, candidate.query_order,
|
|
259
|
+
position(lower(candidate.needle) in lower(${normalizedContent})), 1
|
|
260
|
+
from unnest(${bodyNeedleArray}) with ordinality as candidate(needle, query_order)
|
|
261
|
+
where position(lower(candidate.needle) in lower(${normalizedContent})) > 0
|
|
262
|
+
union all
|
|
263
|
+
select occurrence.needle, occurrence.query_order,
|
|
264
|
+
occurrence.match_position + position(
|
|
265
|
+
lower(occurrence.needle)
|
|
266
|
+
in substring(lower(${normalizedContent}) from occurrence.match_position + 1)
|
|
267
|
+
), occurrence.occurrence_number + 1
|
|
268
|
+
from body_occurrence as occurrence
|
|
269
|
+
where occurrence.occurrence_number < ${maxOccurrencesPerNeedle} and position(
|
|
270
|
+
lower(occurrence.needle)
|
|
271
|
+
in substring(lower(${normalizedContent}) from occurrence.match_position + 1)
|
|
272
|
+
) > 0
|
|
273
|
+
)
|
|
274
|
+
select candidate.match_position
|
|
275
|
+
from body_occurrence as candidate
|
|
276
|
+
order by (
|
|
277
|
+
select count(*)
|
|
278
|
+
from unnest(${bodyNeedleArray}) as nearby(needle)
|
|
279
|
+
where position(
|
|
280
|
+
lower(nearby.needle)
|
|
281
|
+
in lower(substr(
|
|
282
|
+
${normalizedContent},
|
|
283
|
+
greatest(1, candidate.match_position - 120),
|
|
284
|
+
240 + length(candidate.needle)
|
|
285
|
+
))
|
|
286
|
+
) > 0
|
|
287
|
+
) desc,
|
|
288
|
+
candidate.match_position,
|
|
289
|
+
candidate.query_order
|
|
234
290
|
limit 1
|
|
235
291
|
)`
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
292
|
+
: undefined;
|
|
293
|
+
const selectedBodyPosition =
|
|
294
|
+
proximityPosition && fallbackBodyPosition
|
|
295
|
+
? sql<number>`coalesce(nullif(${proximityPosition}, 0), ${fallbackBodyPosition})`
|
|
296
|
+
: fallbackBodyPosition;
|
|
297
|
+
const selectedBodyNeedle =
|
|
298
|
+
bodyNeedleArray && selectedBodyPosition
|
|
299
|
+
? sql<string>`(
|
|
300
|
+
select candidate.needle
|
|
301
|
+
from unnest(${bodyNeedleArray}) with ordinality as candidate(needle, query_order)
|
|
302
|
+
where lower(substr(${normalizedContent}, ${selectedBodyPosition}, length(candidate.needle))) = lower(candidate.needle)
|
|
303
|
+
order by candidate.query_order
|
|
304
|
+
limit 1
|
|
305
|
+
)`
|
|
306
|
+
: undefined;
|
|
307
|
+
const matchWindow = selectedBodyPosition
|
|
308
|
+
? sql<string>`case when ${selectedBodyPosition} is not null then substr(${normalizedContent}, greatest(1, ${selectedBodyPosition} - 120), least(5000, 240 + coalesce(length(${selectedBodyNeedle}), 0))) else substr(${normalizedContent}, 1, 5000) end`
|
|
239
309
|
: sql<string>`substr(${normalizedContent}, 1, 5000)`;
|
|
310
|
+
const ranking = parsedQuery?.groups.length
|
|
311
|
+
? documentSearchRanking(
|
|
312
|
+
parsedQuery,
|
|
313
|
+
{
|
|
314
|
+
title: schema.documents.title,
|
|
315
|
+
description: schema.documents.description,
|
|
316
|
+
content: normalizedContent,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
includeNonTitleFields: args.searchFields !== "title",
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
: null;
|
|
240
323
|
const docs = await db
|
|
241
324
|
.select({
|
|
242
325
|
id: schema.documents.id,
|
|
@@ -244,11 +327,6 @@ export default defineAction({
|
|
|
244
327
|
title: schema.documents.title,
|
|
245
328
|
description: schema.documents.description,
|
|
246
329
|
icon: schema.documents.icon,
|
|
247
|
-
contentPreview: matchWindow,
|
|
248
|
-
snippetNeedle: selectedBodyNeedle
|
|
249
|
-
? sql<string>`coalesce(${selectedBodyNeedle}, '')`
|
|
250
|
-
: sql<string>`''`,
|
|
251
|
-
contentLength: sql<number>`length(${normalizedContent})`,
|
|
252
330
|
hideFromSearch: schema.documents.hideFromSearch,
|
|
253
331
|
updatedAt: schema.documents.updatedAt,
|
|
254
332
|
sourceKind: schema.documents.sourceKind,
|
|
@@ -264,12 +342,63 @@ export default defineAction({
|
|
|
264
342
|
),
|
|
265
343
|
),
|
|
266
344
|
)} then 'database' else 'page' end`,
|
|
345
|
+
totalItems: sql<number>`count(*) over()`,
|
|
267
346
|
})
|
|
268
347
|
.from(schema.documents)
|
|
269
348
|
.where(where)
|
|
270
|
-
.orderBy(
|
|
349
|
+
.orderBy(
|
|
350
|
+
...(ranking
|
|
351
|
+
? [
|
|
352
|
+
desc(ranking.matchTier),
|
|
353
|
+
desc(ranking.titleCoverage),
|
|
354
|
+
desc(ranking.descriptionCoverage),
|
|
355
|
+
// Phrase coherence is a minor body-only tie-breaker. Bound its
|
|
356
|
+
// full-body scan so broad searches keep predictable latency;
|
|
357
|
+
// protected field tiers still rank the complete result set.
|
|
358
|
+
desc(
|
|
359
|
+
sql<number>`case when count(*) over() <= 1000 then ${ranking.bodyProximity} else 0 end`,
|
|
360
|
+
),
|
|
361
|
+
]
|
|
362
|
+
: []),
|
|
363
|
+
desc(schema.documents.updatedAt),
|
|
364
|
+
asc(schema.documents.id),
|
|
365
|
+
)
|
|
271
366
|
.limit(args.limit)
|
|
272
367
|
.offset(args.offset);
|
|
368
|
+
const previews = docs.length
|
|
369
|
+
? await db
|
|
370
|
+
.select({
|
|
371
|
+
id: schema.documents.id,
|
|
372
|
+
contentPreview: matchWindow,
|
|
373
|
+
snippetNeedle: selectedBodyNeedle
|
|
374
|
+
? sql<string>`coalesce(${selectedBodyNeedle}, '')`
|
|
375
|
+
: sql<string>`''`,
|
|
376
|
+
contentLength: sql<number>`length(${normalizedContent})`,
|
|
377
|
+
})
|
|
378
|
+
.from(schema.documents)
|
|
379
|
+
.where(
|
|
380
|
+
and(
|
|
381
|
+
where,
|
|
382
|
+
inArray(
|
|
383
|
+
schema.documents.id,
|
|
384
|
+
docs.map((doc) => doc.id),
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
: [];
|
|
389
|
+
const previewById = new Map(
|
|
390
|
+
previews.map((preview) => [preview.id, preview]),
|
|
391
|
+
);
|
|
392
|
+
const totalItems = docs.length
|
|
393
|
+
? Number(docs[0]!.totalItems)
|
|
394
|
+
: Number(
|
|
395
|
+
(
|
|
396
|
+
await db
|
|
397
|
+
.select({ count: sql<number>`count(*)` })
|
|
398
|
+
.from(schema.documents)
|
|
399
|
+
.where(where)
|
|
400
|
+
)[0]?.count ?? 0,
|
|
401
|
+
);
|
|
273
402
|
|
|
274
403
|
const parentIds = [
|
|
275
404
|
...new Set(docs.flatMap((doc) => (doc.parentId ? [doc.parentId] : []))),
|
|
@@ -290,24 +419,30 @@ export default defineAction({
|
|
|
290
419
|
const parentById = new Map(parents.map((parent) => [parent.id, parent]));
|
|
291
420
|
|
|
292
421
|
return {
|
|
293
|
-
documents: docs.map((doc) =>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
422
|
+
documents: docs.map((doc) => {
|
|
423
|
+
const preview = previewById.get(doc.id);
|
|
424
|
+
return {
|
|
425
|
+
id: doc.id,
|
|
426
|
+
parentId:
|
|
427
|
+
doc.parentId && parentById.has(doc.parentId) ? doc.parentId : null,
|
|
428
|
+
parentTitle: doc.parentId
|
|
429
|
+
? (parentById.get(doc.parentId)?.title ?? null)
|
|
430
|
+
: null,
|
|
431
|
+
documentType: doc.documentType,
|
|
432
|
+
sourceKind: doc.sourceKind,
|
|
433
|
+
sourceUpdatedAt: doc.sourceUpdatedAt,
|
|
434
|
+
title: doc.title,
|
|
435
|
+
description: doc.description,
|
|
436
|
+
icon: doc.icon,
|
|
437
|
+
snippet: makeSnippet(
|
|
438
|
+
preview?.contentPreview ?? "",
|
|
439
|
+
preview?.snippetNeedle ?? "",
|
|
440
|
+
),
|
|
441
|
+
contentLength: Number(preview?.contentLength) || 0,
|
|
442
|
+
hideFromSearch: parseDocumentHideFromSearch(doc.hideFromSearch),
|
|
443
|
+
updatedAt: doc.updatedAt,
|
|
444
|
+
};
|
|
445
|
+
}),
|
|
311
446
|
pagination: documentDiscoveryPagination({
|
|
312
447
|
offset: args.offset,
|
|
313
448
|
limit: args.limit,
|
|
@@ -59,7 +59,7 @@ Given a stale connected Source result, when it appears, then its freshness state
|
|
|
59
59
|
|
|
60
60
|
## Current evidence
|
|
61
61
|
|
|
62
|
-
Existing search paths are in progress donor substrate, but complete indexed, freshness,
|
|
62
|
+
Existing search paths are in progress donor substrate, but complete indexed, freshness, and agent-parity proof is incomplete. This Capability remains `in_progress`.
|
|
63
63
|
|
|
64
64
|
The command picker uses the shared paginated search Action, with title-only,
|
|
65
65
|
document-type and modified-date predicates applied before pagination. Parent
|
|
@@ -67,13 +67,16 @@ context is independently access-scoped. The query supports a bounded
|
|
|
67
67
|
Google-style operator set: quoted "exact phrases", `-excludedTerm`,
|
|
68
68
|
uppercase `OR` between terms, `intitle:term`, and implicit AND across bare
|
|
69
69
|
words; `%`, `_`, and `*` stay literal, and an all-workspaces scope option
|
|
70
|
-
omits the space filter while keeping server-side access scoping.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
omits the space filter while keeping server-side access scoping. Search ranks
|
|
71
|
+
the complete authorized match set before pagination. Exact and contiguous
|
|
72
|
+
partial titles form protected tiers above title-word, description, and body
|
|
73
|
+
matches; modification time only breaks otherwise equal results. Snippet windows
|
|
74
|
+
prefer context containing more positive terms. Focused tests cover later pages,
|
|
75
|
+
private-parent suppression, hidden-result counts, title tiers, useful snippets,
|
|
76
|
+
type filters, and each operator. A 10,000-document PGlite benchmark covers the
|
|
77
|
+
current warm-query budget. This is bounded lexical search substrate, not proof
|
|
78
|
+
of indexed retrieval across every Source, authoritative author filtering,
|
|
79
|
+
freshness ranking, or policy-governed global search.
|
|
77
80
|
|
|
78
81
|
Local browser verification of the command picker covered 27 authorized matches
|
|
79
82
|
across two result pages, equal-title parent context, keyboard opening, title/body
|
|
@@ -2539,6 +2539,13 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
2539
2539
|
}
|
|
2540
2540
|
return containerScopeAncestor(resolved, scope);
|
|
2541
2541
|
}
|
|
2542
|
+
function plainClickSelectionTarget(hit) {
|
|
2543
|
+
if (!designCanvasBoardSurface) {
|
|
2544
|
+
selectionContainerScope = null;
|
|
2545
|
+
return selectionTargetForHit(hit);
|
|
2546
|
+
}
|
|
2547
|
+
return containerFirstSelectionTarget(hit);
|
|
2548
|
+
}
|
|
2542
2549
|
function clickThroughSelectionTarget(hit, ev) {
|
|
2543
2550
|
if (ev.detail > 1) return null;
|
|
2544
2551
|
if (!selectedEl || !document.documentElement.contains(selectedEl)) {
|
|
@@ -8247,7 +8254,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
8247
8254
|
return;
|
|
8248
8255
|
}
|
|
8249
8256
|
hoveredSpacingHandleKey = "";
|
|
8250
|
-
var resolvedClickTarget = e.metaKey || e.ctrlKey ? selectionTargetForHit(target) :
|
|
8257
|
+
var resolvedClickTarget = e.metaKey || e.ctrlKey ? selectionTargetForHit(target) : plainClickSelectionTarget(target);
|
|
8251
8258
|
var toggled = resolveShiftClickToggleOff(resolvedClickTarget, e);
|
|
8252
8259
|
if (toggled !== void 0) {
|
|
8253
8260
|
postToggledSelection(toggled);
|
|
@@ -16258,7 +16265,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
16258
16265
|
}
|
|
16259
16266
|
if (ev) stopNativeInteraction(ev);
|
|
16260
16267
|
var cycledEl = !readOnly && (e.metaKey || e.ctrlKey) && !e.shiftKey ? stackCycleTarget(e.clientX, e.clientY, selectedEl) : null;
|
|
16261
|
-
var primaryClickTarget = !readOnly && (e.metaKey || e.ctrlKey) ? selectionTargetForHit(hit) : (!readOnly && !e.shiftKey ? clickThroughSelectionTarget(hit, ev) : null) || containerFirstSelectionTarget(hit);
|
|
16268
|
+
var primaryClickTarget = !readOnly && (e.metaKey || e.ctrlKey) ? selectionTargetForHit(hit) : !designCanvasBoardSurface ? plainClickSelectionTarget(hit) : (!readOnly && !e.shiftKey ? clickThroughSelectionTarget(hit, ev) : null) || containerFirstSelectionTarget(hit);
|
|
16262
16269
|
if (cycledEl) {
|
|
16263
16270
|
selectTarget(cycledEl, ev, true);
|
|
16264
16271
|
} else {
|
|
@@ -17205,7 +17212,7 @@ export const editorChromeBridgeScript: string = `"use strict";
|
|
|
17205
17212
|
var lastHoverClientPoint = null;
|
|
17206
17213
|
function resolveHoverTarget(clientX, clientY, deepSelect) {
|
|
17207
17214
|
var rawHit = elementFromEditorPoint(clientX, clientY);
|
|
17208
|
-
return deepSelect ? selectionTargetForHit(rawHit) : containerFirstSelectionTarget(rawHit);
|
|
17215
|
+
return deepSelect || !designCanvasBoardSurface ? selectionTargetForHit(rawHit) : containerFirstSelectionTarget(rawHit);
|
|
17209
17216
|
}
|
|
17210
17217
|
function reresolveHoverAtLastPoint(deepSelect) {
|
|
17211
17218
|
if (!lastHoverClientPoint) return;
|
|
@@ -28,6 +28,7 @@ export const navBridgeScript: string = `"use strict";
|
|
|
28
28
|
if (!t || !t.closest) return;
|
|
29
29
|
var a = t.closest("a[href], [data-screen]");
|
|
30
30
|
if (!a) return;
|
|
31
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
31
32
|
var ds = a.getAttribute && a.getAttribute("data-screen");
|
|
32
33
|
if (!ds) {
|
|
33
34
|
var rawHref = a.getAttribute("href");
|