limn-review 0.1.169 → 0.1.170
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/dist/api/plugins/rate-limit.js +1 -0
- package/dist/reference-docs/retrieve.js +2 -2
- package/dist/reference-docs/routes.js +24 -1
- package/dist/reference-docs/search.js +335 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/reference-docs-search.js +6 -0
- package/package.json +1 -1
- package/public/assets/AiFixTab-DgMYZv-8.js +1 -0
- package/public/assets/{ClaudeReviewTab-BNfmEfZY.js → ClaudeReviewTab-BRmx3_1s.js} +6 -6
- package/public/assets/{PrBotBehaviourTab-5acbInfj.js → PrBotBehaviourTab-CDoyrffn.js} +1 -1
- package/public/assets/index-CU7IZhWc.js +87 -0
- package/public/assets/index-CttgPvGo.css +1 -0
- package/public/index.html +2 -2
- package/public/assets/AiFixTab-CLg4ncIF.js +0 -3
- package/public/assets/index-CViygrci.css +0 -1
- package/public/assets/index-DVFWpfMV.js +0 -89
|
@@ -433,6 +433,7 @@ function tierFor(method, path) {
|
|
|
433
433
|
// POST /api/docs/chat ONE Sonnet 5 agent answer → ai + ai_hourly
|
|
434
434
|
// POST /api/docs/story-alignment ONE Haiku shortlist + ONE Sonnet 5 read of a story → ai + ai_hourly
|
|
435
435
|
// GET /api/docs/story-alignment the stored runs (DB + disk) → read
|
|
436
|
+
// GET /api/docs/search full-text search of the library (DB only) → read
|
|
436
437
|
// everything else (list, tree, section, stream, cancel, delete, chats, settings) → read
|
|
437
438
|
// The upload's bytes ride the same `ai` bucket: twenty 25 MB files a minute is far past a person
|
|
438
439
|
// adding reference material, and a script hits the hourly window long before the disk notices.
|
|
@@ -16,7 +16,7 @@ import { db, schema } from '../db/client.js';
|
|
|
16
16
|
export const RETRIEVE_PASSAGES = 200;
|
|
17
17
|
/** The most query terms sent to MATCH (a long story keeps its first ones). */
|
|
18
18
|
export const RETRIEVE_MAX_TERMS = 120;
|
|
19
|
-
const STOPWORDS = new Set(('the and for are but not you all any can had her was one our out has have him his how its may new now ' +
|
|
19
|
+
export const STOPWORDS = new Set(('the and for are but not you all any can had her was one our out has have him his how its may new now ' +
|
|
20
20
|
'old see two way who did get let put say she too use that with this from they will would there their ' +
|
|
21
21
|
'what about which when make like than then them these some into more other only also could should ' +
|
|
22
22
|
'each must been were being does done such very just over under after before where while your ' +
|
|
@@ -73,7 +73,7 @@ export function rankSections(hits, k, perDocCap) {
|
|
|
73
73
|
return out.sort((a, b) => a.rank - b.rank || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
74
74
|
}
|
|
75
75
|
/** Does the workspace have any passage of a current build? */
|
|
76
|
-
async function workspaceIndexed(accountId, workspaceId) {
|
|
76
|
+
export async function workspaceIndexed(accountId, workspaceId) {
|
|
77
77
|
const c = schema.referenceDocChunks;
|
|
78
78
|
const d = schema.referenceDocs;
|
|
79
79
|
const rows = await db
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { REFERENCE_DOCS_CHAT_MAX_QUESTION_CHARS, REFERENCE_DOC_CRAWL_MAX_DEPTH, REFERENCE_DOC_CRAWL_MAX_PAGES, REFERENCE_DOC_MAX_BYTES, } from '../shared/index.js';
|
|
1
|
+
import { REFERENCE_DOCS_CHAT_MAX_QUESTION_CHARS, REFERENCE_DOC_CRAWL_MAX_DEPTH, REFERENCE_DOC_CRAWL_MAX_PAGES, REFERENCE_DOC_MAX_BYTES, DOCS_SEARCH_MAX_QUERY_CHARS, DOCS_SEARCH_MAX_RESULTS, DOCS_SEARCH_PAGE_SIZE, } from '../shared/index.js';
|
|
2
2
|
import { resolveWorkspaceScope } from '../db/queries.js';
|
|
3
3
|
import { AGENTIC_AI_ENABLED } from '../review/claude-review/manager.js';
|
|
4
4
|
import { CONVERSATION_ID, DocsChatError, askDocs } from './chat.js';
|
|
@@ -9,6 +9,7 @@ import { cancelBuild, enqueueBuild, subscribeDocBuild } from './manager.js';
|
|
|
9
9
|
import { parseLibraryPath, uniqueDocSlug } from './paths.js';
|
|
10
10
|
import { conversationRows, deleteConversation, deleteDocRow, getDocBySlug, getDocRow, insertDocRow, isBuilding, listConversations, listDocRows, readDocsInReview, requeueDoc, takenSlugs, toChatMessage, toReferenceDoc, workspaceOwned, writeDocsInReview, } from './store.js';
|
|
11
11
|
import { UrlFetchError, checkPageUrl } from './url-fetch.js';
|
|
12
|
+
import { searchDocs } from './search.js';
|
|
12
13
|
import { deleteWikiPages } from './wiki/pages.js';
|
|
13
14
|
import { noteDocsUsed } from './wiki/refresh.js';
|
|
14
15
|
import { registerWikiDocRoutes } from './wiki/routes.js';
|
|
@@ -29,6 +30,7 @@ import { providerName } from '../ai/prompt-tools.js';
|
|
|
29
30
|
// GET /api/docs/:id/stream SSE progress while a build runs → read
|
|
30
31
|
// GET /api/docs/tree?workspace=&doc= INDEX.md (+ one doc's CONTENTS.md) → read
|
|
31
32
|
// GET /api/docs/section?workspace=&path= one library file, path-validated → read
|
|
33
|
+
// GET /api/docs/search?workspace=&q=&sort=&page= full-text search, one result per section → read
|
|
32
34
|
// POST /api/docs/chat one Sonnet answer → ai + ai_hourly
|
|
33
35
|
// GET /api/docs/chats?workspace= conversations → read
|
|
34
36
|
// GET /api/docs/chats/:conversationId?workspace= one conversation → read
|
|
@@ -410,6 +412,27 @@ export function registerReferenceDocsRoutes(app, ctx) {
|
|
|
410
412
|
next: at >= 0 && at < sections.length - 1 ? sections[at + 1].path : null,
|
|
411
413
|
};
|
|
412
414
|
});
|
|
415
|
+
// ---- searching (DB only: the FTS5 index; search.ts) ----
|
|
416
|
+
app.get('/api/docs/search', {
|
|
417
|
+
schema: {
|
|
418
|
+
querystring: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
properties: {
|
|
421
|
+
workspace: { type: 'string', maxLength: 20 },
|
|
422
|
+
q: { type: 'string', maxLength: DOCS_SEARCH_MAX_QUERY_CHARS },
|
|
423
|
+
sort: { type: 'string', enum: ['relevance', 'newest'] },
|
|
424
|
+
page: { type: 'integer', minimum: 1, maximum: Math.ceil(DOCS_SEARCH_MAX_RESULTS / DOCS_SEARCH_PAGE_SIZE) },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
}, async (req, reply) => {
|
|
429
|
+
if (!AGENTIC_AI_ENABLED)
|
|
430
|
+
return featureOff(reply);
|
|
431
|
+
const accountId = ctx.accountIdOf(req);
|
|
432
|
+
const q = req.query;
|
|
433
|
+
const workspaceId = await workspaceOf(accountId, q.workspace);
|
|
434
|
+
return searchDocs({ accountId, workspaceId, query: (q.q ?? '').trim(), sort: q.sort ?? 'relevance', page: q.page ?? 1 });
|
|
435
|
+
});
|
|
413
436
|
// ---- the chat ----
|
|
414
437
|
app.post('/api/docs/chat', {
|
|
415
438
|
schema: {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
2
|
+
import { DOCS_SEARCH_MAX_RESULTS, DOCS_SEARCH_PAGE_SIZE, } from '../shared/index.js';
|
|
3
|
+
import { db, schema } from '../db/client.js';
|
|
4
|
+
import { STOPWORDS, workspaceIndexed } from './retrieve.js';
|
|
5
|
+
// SEARCH THE LIBRARY (docs/REFERENCE-DOCS.md § Search). The reader's search box, over the SAME FTS5
|
|
6
|
+
// index as `retrieve.ts` — whose OR-joined query (wide recall for a model) is NOT changed by this file.
|
|
7
|
+
//
|
|
8
|
+
// • THE QUERY IS UNTRUSTED. FTS5 syntax never reaches MATCH raw: the text is cut into plain word
|
|
9
|
+
// terms (letters and digits only), each one quoted (`searchClauses`). `"quoted words"` and a
|
|
10
|
+
// hyphenated word ("co-change") become one quoted multi-word string — FTS5's phrase. Quoting does
|
|
11
|
+
// not stop stemming: the porter tokenizer still stems inside the quotes.
|
|
12
|
+
// • EVERY CLAUSE MUST MATCH THE SECTION, not one passage (`searchMatchPlan`): passages are found and
|
|
13
|
+
// ranked by the OR of the clauses, then kept only where each clause matches some passage of the
|
|
14
|
+
// same section. Most sections are several passages, so a passage-level AND missed real hits.
|
|
15
|
+
// • Only passages of a document's CURRENT build count; account + workspace predicates on every row.
|
|
16
|
+
// • One result per SECTION, by its best passage. At most DOCS_SEARCH_MAX_RESULTS sections are
|
|
17
|
+
// counted; past that the total says so (`totalCapped`).
|
|
18
|
+
// • RANKING (`orderSearchHits`, pure): relevance = bm25 (lower is better), and scores within
|
|
19
|
+
// SEARCH_TIE (5%) of a run's best count as EQUAL — that run is ordered newest document first.
|
|
20
|
+
// `newest` = the document's added date, newest first, then relevance.
|
|
21
|
+
// • THE SNIPPET is built here from FTS5's `highlight()` (control-character markers), as plain text
|
|
22
|
+
// runs flagged matched or not — the SPA renders them without HTML.
|
|
23
|
+
/** Scores within this share of a run's best are a tie, ordered by recency. */
|
|
24
|
+
export const SEARCH_TIE = 0.05;
|
|
25
|
+
/** The most clauses (words or phrases) sent to MATCH. */
|
|
26
|
+
export const SEARCH_MAX_CLAUSES = 32;
|
|
27
|
+
/** The snippet's length in words, and how many words of lead-in before the first match. */
|
|
28
|
+
export const SNIPPET_WORDS = 36;
|
|
29
|
+
const SNIPPET_LEAD = 8;
|
|
30
|
+
const OPEN = '\u0001';
|
|
31
|
+
const CLOSE = '\u0002';
|
|
32
|
+
/** Two-letter words too common to narrow a search (retrieve.ts's STOPWORDS are three letters and up). */
|
|
33
|
+
const SHORT_STOPWORDS = new Set('an as at be by do if in is it no of on or so to up us we'.split(' '));
|
|
34
|
+
const isStopword = (w) => STOPWORDS.has(w) || SHORT_STOPWORDS.has(w);
|
|
35
|
+
const wordsOf = (s) => s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
36
|
+
/** The reader's text → clauses. Quoted runs are phrases; bare stopwords drop out unless nothing else is left. */
|
|
37
|
+
export function searchClauses(text) {
|
|
38
|
+
const parts = text.split('"');
|
|
39
|
+
// An unclosed quote: its text is read as bare words.
|
|
40
|
+
const closed = parts.length % 2 === 1 ? parts.length : parts.length - 1;
|
|
41
|
+
const phrases = [];
|
|
42
|
+
const bare = [];
|
|
43
|
+
for (let i = 0; i < parts.length; i++) {
|
|
44
|
+
const part = parts[i];
|
|
45
|
+
if (i % 2 === 1 && i < closed) {
|
|
46
|
+
const w = wordsOf(part);
|
|
47
|
+
if (w.length > 0)
|
|
48
|
+
phrases.push(w);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const token of part.split(/\s+/)) {
|
|
52
|
+
const w = wordsOf(token);
|
|
53
|
+
if (w.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
bare.push(w); // a hyphenated word stays one clause, matched as a phrase
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const meaningful = bare.filter((w) => w.length > 1 || (!isStopword(w[0]) && (w[0].length > 1 || /\p{N}/u.test(w[0]))));
|
|
59
|
+
const all = [...phrases, ...(meaningful.length > 0 || phrases.length > 0 ? meaningful : bare)];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const clauses = [];
|
|
62
|
+
for (const c of all) {
|
|
63
|
+
const key = c.join(' ');
|
|
64
|
+
if (seen.has(key))
|
|
65
|
+
continue;
|
|
66
|
+
seen.add(key);
|
|
67
|
+
clauses.push(c);
|
|
68
|
+
if (clauses.length >= SEARCH_MAX_CLAUSES)
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
return { clauses };
|
|
72
|
+
}
|
|
73
|
+
const quoted = (c) => `"${c.join(' ')}"`;
|
|
74
|
+
/**
|
|
75
|
+
* The MATCH expressions the search runs: `any` (every clause quoted, OR-joined) finds and ranks the
|
|
76
|
+
* passages and marks their words; `each` (one per clause, only when there are two or more) proves a
|
|
77
|
+
* SECTION has every clause somewhere — a section is cut into passages of ~220 words, so one passage
|
|
78
|
+
* need not hold them all. '' / [] ⇒ nothing to search.
|
|
79
|
+
*/
|
|
80
|
+
export function searchMatchPlan(text) {
|
|
81
|
+
const { clauses } = searchClauses(text);
|
|
82
|
+
return { any: clauses.map(quoted).join(' OR '), each: clauses.length > 1 ? clauses.map(quoted) : [] };
|
|
83
|
+
}
|
|
84
|
+
const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : a.docId - b.docId);
|
|
85
|
+
/** Is `rank` within SEARCH_TIE of `best`? (bm25 is negative; compare magnitudes.) */
|
|
86
|
+
export function isTie(rank, best) {
|
|
87
|
+
return Math.abs(rank - best) <= SEARCH_TIE * Math.abs(best);
|
|
88
|
+
}
|
|
89
|
+
/** The hits in reading order. Pure and deterministic. */
|
|
90
|
+
export function orderSearchHits(hits, sort) {
|
|
91
|
+
if (sort === 'newest')
|
|
92
|
+
return [...hits].sort((a, b) => b.addedAt - a.addedAt || a.rank - b.rank || byPath(a, b));
|
|
93
|
+
const ranked = [...hits].sort((a, b) => a.rank - b.rank || byPath(a, b));
|
|
94
|
+
const out = [];
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < ranked.length) {
|
|
97
|
+
const best = ranked[i].rank;
|
|
98
|
+
let j = i + 1;
|
|
99
|
+
while (j < ranked.length && isTie(ranked[j].rank, best))
|
|
100
|
+
j++;
|
|
101
|
+
out.push(...ranked.slice(i, j).sort((a, b) => b.addedAt - a.addedAt || a.rank - b.rank || byPath(a, b)));
|
|
102
|
+
i = j;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** `highlight()` output → the plain text and its marked ranges. Stray markers are dropped. */
|
|
107
|
+
export function parseHighlighted(marked) {
|
|
108
|
+
let text = '';
|
|
109
|
+
const ranges = [];
|
|
110
|
+
let start = -1;
|
|
111
|
+
for (const ch of marked) {
|
|
112
|
+
if (ch === OPEN) {
|
|
113
|
+
if (start < 0)
|
|
114
|
+
start = text.length;
|
|
115
|
+
}
|
|
116
|
+
else if (ch === CLOSE) {
|
|
117
|
+
if (start >= 0 && text.length > start)
|
|
118
|
+
ranges.push([start, text.length]);
|
|
119
|
+
start = -1;
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
text += ch;
|
|
123
|
+
}
|
|
124
|
+
return { text, ranges };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Markdown's marks out of a passage, so a quote reads as text: emphasis, code ticks, backslash escapes,
|
|
128
|
+
* link brackets, heading hashes, table rules; a table's cell walls become " · ". The highlight markers are control
|
|
129
|
+
* characters none of these touch.
|
|
130
|
+
*/
|
|
131
|
+
export function plainSnippetText(s) {
|
|
132
|
+
return s
|
|
133
|
+
.replace(/\\([\\`*_{}[\]()#+\-.!|>~])/g, '$1')
|
|
134
|
+
.replace(/\*\*|__|`/g, '')
|
|
135
|
+
// Single-star emphasis around words (`*no fixture*`); a lone star (`5 * 3`) stays.
|
|
136
|
+
.replace(/(^|[\s(])\*(?=\S)([^*\n]*?\S)\*(?=$|[\s).,;:!?])/g, '$1$2')
|
|
137
|
+
.replace(/\[([^\]\n]*)\]/g, '$1')
|
|
138
|
+
.replace(/(^|\n)#{1,6}[ \t]+/g, '$1')
|
|
139
|
+
.replace(/:?-{3,}:?/g, '')
|
|
140
|
+
.replace(/\s*\|(?:\s*\|)*\s*/g, ' · ')
|
|
141
|
+
.replace(/^(?:\s*·\s*)+|(?:\s*·\s*)+$/g, '');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The snippet of one passage: its body (the indexed prefix — document › trail › section and the
|
|
145
|
+
* summary — left off), whitespace collapsed, cut to SNIPPET_WORDS words around the densest run of
|
|
146
|
+
* matches, "…" where it was cut. Plus every matched word of the WHOLE passage, lower-cased.
|
|
147
|
+
*/
|
|
148
|
+
export function buildSnippet(highlighted) {
|
|
149
|
+
const split = highlighted.indexOf('\n\n');
|
|
150
|
+
const prefix = split >= 0 ? highlighted.slice(0, split) : '';
|
|
151
|
+
const bodyMarked = plainSnippetText(split >= 0 ? highlighted.slice(split + 2) : highlighted).replace(/\s+/g, ' ').trim();
|
|
152
|
+
const body = parseHighlighted(bodyMarked);
|
|
153
|
+
const head = parseHighlighted(prefix);
|
|
154
|
+
const matchedWords = [
|
|
155
|
+
...new Set([...head.ranges.map(([a, b]) => head.text.slice(a, b)), ...body.ranges.map(([a, b]) => body.text.slice(a, b))].map((w) => w.toLowerCase())),
|
|
156
|
+
];
|
|
157
|
+
// Word spans of the body.
|
|
158
|
+
const spans = [];
|
|
159
|
+
for (const m of body.text.matchAll(/\S+/g))
|
|
160
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
161
|
+
const isMatch = spans.map(([a, b]) => body.ranges.some(([s, e]) => s < b && e > a));
|
|
162
|
+
let from = 0;
|
|
163
|
+
if (spans.length > SNIPPET_WORDS) {
|
|
164
|
+
let bestCount = -1;
|
|
165
|
+
for (let i = 0; i < spans.length; i++) {
|
|
166
|
+
if (!isMatch[i])
|
|
167
|
+
continue;
|
|
168
|
+
const s = Math.max(0, Math.min(i - SNIPPET_LEAD, spans.length - SNIPPET_WORDS));
|
|
169
|
+
let n = 0;
|
|
170
|
+
for (let k = s; k < s + SNIPPET_WORDS; k++)
|
|
171
|
+
if (isMatch[k])
|
|
172
|
+
n++;
|
|
173
|
+
if (n > bestCount) {
|
|
174
|
+
bestCount = n;
|
|
175
|
+
from = s;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const to = Math.min(spans.length, from + SNIPPET_WORDS);
|
|
180
|
+
if (spans.length === 0)
|
|
181
|
+
return { snippet: [], matchedWords };
|
|
182
|
+
const startChar = spans[from][0];
|
|
183
|
+
const endChar = spans[to - 1][1];
|
|
184
|
+
const parts = [];
|
|
185
|
+
const push = (text, match) => {
|
|
186
|
+
if (text === '')
|
|
187
|
+
return;
|
|
188
|
+
const last = parts[parts.length - 1];
|
|
189
|
+
if (last != null && last.match === match)
|
|
190
|
+
last.text += text;
|
|
191
|
+
else
|
|
192
|
+
parts.push({ text, match });
|
|
193
|
+
};
|
|
194
|
+
if (from > 0)
|
|
195
|
+
push('… ', false);
|
|
196
|
+
let at = startChar;
|
|
197
|
+
for (const [s0, e0] of body.ranges) {
|
|
198
|
+
const s = Math.max(s0, startChar);
|
|
199
|
+
const e = Math.min(e0, endChar);
|
|
200
|
+
if (e <= s)
|
|
201
|
+
continue;
|
|
202
|
+
push(body.text.slice(at, s), false);
|
|
203
|
+
push(body.text.slice(s, e), true);
|
|
204
|
+
at = e;
|
|
205
|
+
}
|
|
206
|
+
push(body.text.slice(at, endChar), false);
|
|
207
|
+
if (to < spans.length)
|
|
208
|
+
push(' …', false);
|
|
209
|
+
return { snippet: parts, matchedWords };
|
|
210
|
+
}
|
|
211
|
+
/** One page of results. DB only. */
|
|
212
|
+
export async function searchDocs(a) {
|
|
213
|
+
const { any: match, each } = searchMatchPlan(a.query);
|
|
214
|
+
const page = Math.max(1, Math.floor(a.page));
|
|
215
|
+
const base = {
|
|
216
|
+
workspaceId: a.workspaceId,
|
|
217
|
+
query: a.query,
|
|
218
|
+
sort: a.sort,
|
|
219
|
+
page,
|
|
220
|
+
pageSize: DOCS_SEARCH_PAGE_SIZE,
|
|
221
|
+
total: 0,
|
|
222
|
+
totalCapped: false,
|
|
223
|
+
results: [],
|
|
224
|
+
indexed: true,
|
|
225
|
+
};
|
|
226
|
+
const c = schema.referenceDocChunks;
|
|
227
|
+
const d = schema.referenceDocs;
|
|
228
|
+
// Stage 1: every SECTION with a passage matching ANY clause, by its best such passage (bm25, then
|
|
229
|
+
// reading order); then only the sections holding EVERY clause (stage 1b); at most one more than the
|
|
230
|
+
// cap, so the total can say "more". FTS5's MATCH and bm25() have no query-builder form; every value
|
|
231
|
+
// is a bound parameter.
|
|
232
|
+
const anyRows = match === ''
|
|
233
|
+
? []
|
|
234
|
+
: await db.all(sql `
|
|
235
|
+
select docId, path, chunkId, rank, addedAt from (
|
|
236
|
+
select docId, path, chunkId, rank, addedAt,
|
|
237
|
+
row_number() over (partition by docId, path order by rank, ordinal) as rn
|
|
238
|
+
from (
|
|
239
|
+
select ${c.docId} as docId, ${c.sectionPath} as path, ${c.id} as chunkId, ${c.ordinal} as ordinal,
|
|
240
|
+
bm25(reference_doc_chunks_fts) as rank, ${d.createdAt} as addedAt
|
|
241
|
+
from reference_doc_chunks_fts
|
|
242
|
+
join ${c} on ${c.id} = reference_doc_chunks_fts.rowid
|
|
243
|
+
join ${d} on ${d.id} = ${c.docId} and ${d.accountId} = ${c.accountId} and ${d.builtAt} = ${c.builtAt}
|
|
244
|
+
where reference_doc_chunks_fts match ${match}
|
|
245
|
+
and ${c.accountId} = ${a.accountId} and ${c.workspaceId} = ${a.workspaceId}
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
where rn = 1
|
|
249
|
+
order by rank, path`);
|
|
250
|
+
// Stage 1b (two or more clauses): the sections where each clause matches some passage, intersected.
|
|
251
|
+
let rows = anyRows;
|
|
252
|
+
if (each.length > 0 && anyRows.length > 0) {
|
|
253
|
+
let keep = null;
|
|
254
|
+
for (const clause of each) {
|
|
255
|
+
const secs = await db.all(sql `
|
|
256
|
+
select distinct ${c.docId} as docId, ${c.sectionPath} as path
|
|
257
|
+
from reference_doc_chunks_fts
|
|
258
|
+
join ${c} on ${c.id} = reference_doc_chunks_fts.rowid
|
|
259
|
+
join ${d} on ${d.id} = ${c.docId} and ${d.accountId} = ${c.accountId} and ${d.builtAt} = ${c.builtAt}
|
|
260
|
+
where reference_doc_chunks_fts match ${clause}
|
|
261
|
+
and ${c.accountId} = ${a.accountId} and ${c.workspaceId} = ${a.workspaceId}`);
|
|
262
|
+
const next = new Set();
|
|
263
|
+
for (const r of secs) {
|
|
264
|
+
const key = `${r.docId}\u0000${r.path}`;
|
|
265
|
+
if (keep == null || keep.has(key))
|
|
266
|
+
next.add(key);
|
|
267
|
+
}
|
|
268
|
+
keep = next;
|
|
269
|
+
if (keep.size === 0)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
rows = anyRows.filter((r) => keep?.has(`${r.docId}\u0000${r.path}`) === true);
|
|
273
|
+
}
|
|
274
|
+
rows = rows.slice(0, DOCS_SEARCH_MAX_RESULTS + 1);
|
|
275
|
+
if (rows.length === 0)
|
|
276
|
+
return { ...base, indexed: await workspaceIndexed(a.accountId, a.workspaceId) };
|
|
277
|
+
const totalCapped = rows.length > DOCS_SEARCH_MAX_RESULTS;
|
|
278
|
+
const hits = orderSearchHits(rows.slice(0, DOCS_SEARCH_MAX_RESULTS), a.sort);
|
|
279
|
+
const pageHits = hits.slice((page - 1) * DOCS_SEARCH_PAGE_SIZE, page * DOCS_SEARCH_PAGE_SIZE);
|
|
280
|
+
const out = { ...base, total: hits.length, totalCapped };
|
|
281
|
+
if (pageHits.length === 0)
|
|
282
|
+
return out;
|
|
283
|
+
// Stage 2: the page's passages, highlighted, and their documents.
|
|
284
|
+
const ids = pageHits.map((h) => h.chunkId);
|
|
285
|
+
const marked = await db.all(sql `
|
|
286
|
+
select ${c.id} as id, ${c.anchor} as anchor, highlight(reference_doc_chunks_fts, 0, ${OPEN}, ${CLOSE}) as hl
|
|
287
|
+
from reference_doc_chunks_fts
|
|
288
|
+
join ${c} on ${c.id} = reference_doc_chunks_fts.rowid
|
|
289
|
+
where reference_doc_chunks_fts match ${match}
|
|
290
|
+
and ${c.accountId} = ${a.accountId} and ${c.workspaceId} = ${a.workspaceId}
|
|
291
|
+
and ${c.id} in (${sql.join(ids.map((id) => sql `${id}`), sql `, `)})`);
|
|
292
|
+
const byChunk = new Map(marked.map((m) => [m.id, m]));
|
|
293
|
+
const docIds = [...new Set(pageHits.map((h) => h.docId))];
|
|
294
|
+
const docs = await db
|
|
295
|
+
.select({
|
|
296
|
+
id: d.id,
|
|
297
|
+
title: d.title,
|
|
298
|
+
sourceKind: d.sourceKind,
|
|
299
|
+
sourceName: d.sourceName,
|
|
300
|
+
sourceUrl: d.sourceUrl,
|
|
301
|
+
wiki: d.wiki,
|
|
302
|
+
createdAt: d.createdAt,
|
|
303
|
+
sections: d.sections,
|
|
304
|
+
})
|
|
305
|
+
.from(d)
|
|
306
|
+
.where(and(eq(d.accountId, a.accountId), eq(d.workspaceId, a.workspaceId), inArray(d.id, docIds)))
|
|
307
|
+
.execute();
|
|
308
|
+
const byDoc = new Map(docs.map((r) => [r.id, r]));
|
|
309
|
+
const results = [];
|
|
310
|
+
for (const h of pageHits) {
|
|
311
|
+
const doc = byDoc.get(h.docId);
|
|
312
|
+
const m = byChunk.get(h.chunkId);
|
|
313
|
+
if (doc == null || m == null)
|
|
314
|
+
continue;
|
|
315
|
+
const sec = (doc.sections ?? []).find((s) => s.path === h.path);
|
|
316
|
+
const { snippet, matchedWords } = buildSnippet(m.hl);
|
|
317
|
+
results.push({
|
|
318
|
+
docId: doc.id,
|
|
319
|
+
docTitle: doc.title,
|
|
320
|
+
sourceKind: doc.sourceKind,
|
|
321
|
+
sourceName: doc.sourceName,
|
|
322
|
+
sourceUrl: doc.sourceUrl,
|
|
323
|
+
wikiProvider: doc.wiki?.provider ?? null,
|
|
324
|
+
docAddedAt: doc.createdAt.toISOString(),
|
|
325
|
+
path: h.path,
|
|
326
|
+
sectionTitle: sec?.title ?? doc.title,
|
|
327
|
+
trail: sec?.trail ?? [],
|
|
328
|
+
anchor: m.anchor,
|
|
329
|
+
snippet,
|
|
330
|
+
matchedWords,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return { ...out, results };
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=search.js.map
|
package/dist/shared/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export * from './reference-docs.js';
|
|
|
27
27
|
export * from './reference-docs-wiki.js';
|
|
28
28
|
export * from './reference-docs-offers.js';
|
|
29
29
|
export * from './reference-docs-quote.js';
|
|
30
|
+
export * from './reference-docs-search.js';
|
|
30
31
|
export * from './sprint-members.js';
|
|
31
32
|
export * from './story-docs-alignment.js';
|
|
32
33
|
export * from './ai-budget.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Results per page. */
|
|
2
|
+
export const DOCS_SEARCH_PAGE_SIZE = 20;
|
|
3
|
+
/** The most sections one search counts; past it the total reads "2000+". */
|
|
4
|
+
export const DOCS_SEARCH_MAX_RESULTS = 2000;
|
|
5
|
+
/** The longest query text the route accepts. */
|
|
6
|
+
export const DOCS_SEARCH_MAX_QUERY_CHARS = 500;
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{d as Ie,r as x,j as e}from"./tanstack-BnbsafB1.js";import{aV as V,aU as Xe,cH as je,cO as Ze,co as es,cF as ss,cq as Se,aO as ts,cG as O,A as Ne,aT as Te,U as ne,w as ns,a5 as is,an as ie,bp as Ee,a1 as _e,e as Me,f as K,a4 as as,aR as Be,i as Le,q as rs,k as ls,a as cs,C as os,dk as us,bU as ds,aW as ms,dc as hs,dd as xs,Z as ps,H as fs,T as gs,t as z,bC as bs,cE as js,cD as Ns,cn as vs,bh as ws,bV as ks,az as ys,bg as Rs,b5 as Ps,c3 as Ue,bF as Cs,cj as $e,ck as As,cx as Fs,cM as Is,cL as Ss,d5 as Ts,d6 as Es,aS as _s,aP as Ms,cm as Bs,cl as Ls,cy as Us,d2 as $s,au as Os,_ as Ds,aQ as Hs,bJ as Ws,bS as ae,W as qs,u as Ys,L as Ks,S as zs,b$ as Oe,cW as Gs,ai as ve,v as Vs,J as Js}from"./index-CU7IZhWc.js";import{r as Qs,t as Xs,S as Zs,C as et,T as st,F as tt,f as nt}from"./ClaudeReviewTab-BRmx3_1s.js";import"./markdown-CcoLFczy.js";import"./highlight-DIEV0qGG.js";import"./vis-C3BCkP5U.js";const it="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3",we="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3",ke="inline-flex items-center rounded-ui bg-ui-raise px-1.5 py-px font-mono text-meta text-ui-ink";function at(s){return s instanceof Le||s instanceof Error?s.message:"Something went wrong"}function De({plan:s}){return e.jsx("ul",{className:"list-disc space-y-0.5 pl-5 text-body text-ui-ink",children:s.summary.map((t,n)=>e.jsx("li",{children:t},n))})}function ye(s,t){var n;return((n=s==null?void 0:s.find(a=>a.ref===t))==null?void 0:n.title)??null}function rt({plan:s,items:t}){return e.jsxs("div",{className:"mt-2 space-y-3 text-meta text-ui-ink",children:[s.steps.length>0&&e.jsx("ol",{className:"list-decimal space-y-2 pl-5",children:s.steps.map((n,a)=>e.jsxs("li",{children:[e.jsx("div",{className:"font-medium text-ui-ink",children:n.title}),n.files.length>0&&e.jsx("div",{className:"mt-0.5 font-mono text-meta text-ui-mute",children:n.files.join(", ")}),e.jsx("p",{className:"mt-0.5 whitespace-pre-wrap",children:n.change}),n.refs.length>0&&e.jsx("div",{className:"mt-1 flex flex-wrap gap-1",children:n.refs.map(i=>e.jsx("span",{className:ke,children:i},i))})]},a))}),s.notAddressed.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"mb-1 font-semibold text-ui-mute",children:"Will not fix"}),e.jsx("ul",{className:"space-y-1",children:s.notAddressed.map(n=>e.jsxs("li",{children:[e.jsx("span",{className:ke,children:n.ref})," ",ye(t,n.ref)&&e.jsxs("span",{className:"font-medium",children:[ye(t,n.ref),": "]}),n.reason]},n.ref))})]}),s.notCovered.length>0&&e.jsxs("p",{children:[e.jsx("span",{className:"font-semibold text-ui-mute",children:"Not in the plan: "}),s.notCovered.join(", ")]})]})}function lt({pr:s,fix:t,liveStatus:n,passRunning:a,auth:i}){var w,p;const l=t.plan,o=V(t.plan??t),g=Xe((t.plan??t).provider,(t.plan??t).model),c=je(g,"plan"),m=je(g,"fix"),v=Be(c),F=Ze(s.id),u=es(s.id),k=ss(s.id),M=Se(s.id),C=Ie({mutationKey:ts(s.id)})>0,B=O(N=>N.noteAiFixRun),[E,I]=x.useState(""),[y,D]=x.useState(!1);if(!l)return null;const R=l.plan,_=l.status==="plan_ready"&&!a,L=E.trim(),P=L.length>Ne,r=()=>{!_||C||L===""||P||F.mutate({fixId:t.id,text:L},{onSuccess:()=>I("")})},d=()=>{B({prId:s.id,repoFullName:s.repoFullName,prNumber:s.number,prTitle:s.title}),u.mutate(t.id)},f=F.error??u.error??k.error;return e.jsxs("div",{className:"mt-3 rounded-ui bg-ui-surface p-3",children:[e.jsxs("div",{className:"mb-1 flex items-center gap-1 text-meta font-semibold text-ui-ink",children:["Plan",R&&l.revision>1&&e.jsxs("span",{className:"font-normal text-ui-mute",children:["· version ",l.revision]}),l.model&&e.jsxs("span",{className:"font-normal text-ui-mute",children:["· ",Te(l.provider,l.model,l.effort)]}),e.jsxs(ne,{title:"Plan first",children:[e.jsxs("p",{children:[v," reads the code and writes a plan. It changes nothing."]}),e.jsxs("p",{children:["Tell ",v," what to change and it rewrites the plan, taking the whole conversation into account. Each message runs ",v," again."]}),e.jsxs("p",{children:["When you approve, ",v," builds the fix from this plan. You still review the diff and press Push."]})]})]}),R?e.jsxs(e.Fragment,{children:[e.jsx(De,{plan:R}),e.jsxs("button",{type:"button",className:"hit mt-2 inline-flex items-center gap-1 text-meta text-ui-mute hover:text-ui-ink","aria-expanded":y,onClick:()=>D(N=>!N),children:[e.jsx(ns,{dir:y?"down":"right"}),"Details"]}),y&&e.jsx(rt,{plan:R,items:t.reviewItems})]}):!a&&e.jsx("p",{className:"text-meta text-ui-mute",children:"No plan yet."}),t.error&&e.jsx("p",{className:"mt-2 rounded-ui bg-ui-raise p-2 text-meta text-ui-warn",children:t.error}),e.jsx(is,{transcript:l.transcript,plannedBy:o}),a&&e.jsxs("div",{className:"mt-3",children:[e.jsx(ie,{active:!0,label:R?"Updating the plan":"Making the plan",value:Ee(n),timeConstantSec:30}),e.jsxs("div",{className:"mt-1 flex items-center gap-2 text-meta text-ui-mute",children:[_e[((w=n==null?void 0:n.progress)==null?void 0:w.phase)??""]??"Working…",e.jsx("button",{type:"button",className:we,onClick:()=>M.mutate(),children:"Cancel"})]}),e.jsx(Me,{lines:(p=n==null?void 0:n.progress)==null?void 0:p.recentActivity,className:"mt-1"})]}),_&&R&&e.jsxs("div",{className:"mt-3",children:[e.jsx(K,{kind:"plan",auth:i,meter:!1,followUp:!0,provider:c,children:e.jsx(as,{name:v,draft:E,setDraft:I,busy:C,maxChars:Ne,onSend:r})}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[e.jsx(K,{kind:"fix",auth:i,followUp:!0,provider:m,children:e.jsx("button",{type:"button","data-ai-run":!0,className:it,disabled:C,onClick:d,children:"Approve and build"})}),e.jsx("button",{type:"button",className:we,disabled:C,onClick:()=>k.mutate(t.id),children:"Discard plan"})]})]}),f!=null&&e.jsx("p",{className:"mt-1 text-meta text-ui-danger",children:at(f)})]})}function He({fix:s}){var n;const t=((n=s.plan)==null?void 0:n.status)==="approved"?s.plan.plan:null;return t?e.jsxs("div",{className:"mb-2 rounded-ui bg-ui-surface p-2",children:[e.jsx("div",{className:"mb-1 text-meta font-semibold text-ui-mute",children:"Built from your plan"}),e.jsx(De,{plan:t})]}):null}function ct(s,t,n){var a;return s??((t==null?void 0:t.status)==="succeeded"?t.id:((a=n.find(i=>i.status==="succeeded"))==null?void 0:a.id)??null)}function ot(s,t,n){return t==null||n==null||t===n.id?null:s!=null?"Uses the review you picked, not the latest.":n.status==="failed"?"Uses the last review that finished. The latest one failed.":n.status==="running"||n.status==="queued"?"Uses the last review that finished. A newer one is running.":"Uses the last review that finished."}const Re=os,ut=ls;function dt(s){return s.kind==="finding"&&s.findingId!=null?`claude-finding-${s.findingId}`:null}function mt(s){const t=O(n=>n.openClaudeReview);return n=>{t({id:s.id,number:s.number,title:s.title,repoFullName:s.repoFullName,authorLogin:null,authorDisplayName:null,authorAvatarUrl:null});const a=Date.now()+3e3,i=()=>{const l=document.getElementById(n);if(l){l.scrollIntoView({behavior:"smooth",block:"center"});return}Date.now()<a&&requestAnimationFrame(i)};requestAnimationFrame(i)}}function Pe({refId:s,item:t,onJump:n}){const a=t?`${cs[t.kind]} ${s}`:s,i=e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"shrink-0",children:a}),t&&e.jsxs("span",{className:"min-w-0 truncate font-normal",children:["· ",t.title]})]}),l=t?dt(t):null;return l?e.jsx("button",{type:"button",onClick:()=>n(l),"aria-label":`${a}: ${(t==null?void 0:t.title)??""}. Show in AI Review`,className:`${Re} max-w-[22rem] hover:bg-ui-hover`,children:i}):e.jsx("span",{className:`${Re} max-w-[22rem]`,children:i})}function ht(s){var t;switch(s.seed){case"review":{const n=((t=s.reviewItems)==null?void 0:t.length)??0;return`Fix from ${s.sourceReviewId==null?"the CI check":"the AI review"}${n>0?` (${n} item${n===1?"":"s"})`:""}.`}case"plain":return"Fix from your instruction.";case"comments":return"Fix from picked comments (no longer offered).";case"ci_analysis":return"Fix from the CI analysis (no longer offered).";default:return""}}function xt({pr:s,fix:t}){const n=V(t),a=mt(s),i=t.changeReport,l=new Map((t.reviewItems??[]).map(c=>[c.ref,c])),o=(t.reviewItems??[]).filter(c=>!c.included);if(!i&&o.length===0)return null;const g=l.size>0;return e.jsxs("div",{className:"mt-2 text-meta",children:[e.jsx(rs,{report:i,filesChanged:t.filesChanged,by:n,refsInfo:g?e.jsx("p",{children:"Each tag is a review item the change answers. A finding tag opens it in the AI Review tab."}):null,renderRef:c=>e.jsx(Pe,{refId:c,item:l.get(c),onJump:a},c)}),o.length>0&&e.jsxs("section",{children:[e.jsxs("div",{className:ut,children:["Left out",e.jsx(ne,{title:"Left out",children:e.jsxs("p",{children:["These review items did not fit in what ",n," was sent, so it never saw them."]})})]}),e.jsx("div",{className:"flex flex-wrap gap-1",children:o.map(c=>e.jsx(Pe,{refId:c.ref,item:c,onJump:a},c.ref))})]})]})}const pt=["findings","earlier_findings","judged_threads","untouched_threads","ci_failures","sonar","style_bots","story"],ft={findings:"Claude’s findings",earlier_findings:"Earlier findings still open",judged_threads:"Threads Claude says need a fix",untouched_threads:"Unanswered threads",ci_failures:"CI failures",sonar:"SonarCloud issues",style_bots:"Style bot comments",story:"Ticket gaps"};function Ce(s,t="Claude"){return s==="findings"?`${t}’s findings`:s==="judged_threads"?`Threads ${t} says need a fix`:ft[s]}function G(s,t){return t[s.key]??s.defaultIncluded}function gt(s,t){return s.items.filter(n=>G(n,t)).map(n=>n.key)}function We(s,t){const n=new Set;let a=0;for(const i of s.items)if(G(i,t)){if(a>0&&a+i.chars>s.budgetChars){n.add(i.key);continue}a+=i.chars}return n}function bt(s,t){const n={};for(const a of s.items)n[a.key]=a.section===t;return n}const jt={blocker:"Blocker",warning:"Warning",nit:"Nit",question:"Question",praise:"Praise"},Ae={blocker:"bg-ui-danger/10 text-ui-danger",warning:"bg-ui-raise text-ui-warn",nit:"bg-ui-raise text-ui-mute"},qe="whitespace-nowrap rounded-ui bg-ui-raise px-1 py-px text-meta text-ui-warn",Z=new Set;function Nt(s,t){return s?t!=null?`${s}:${t}`:s:null}function vt({item:s,picked:t,cut:n,disabled:a,onToggle:i}){return e.jsxs("span",{className:"inline-flex shrink-0 items-center gap-1.5 pt-0.5",children:[e.jsx("label",{className:"flex cursor-pointer items-center touch:-my-3 touch:min-h-10 touch:min-w-10 touch:justify-center",children:e.jsx(z,{checked:t,disabled:a,onChange:l=>i(l.target.checked),"aria-label":`Include in the fix: ${s.label}`})}),t&&n&&e.jsx("span",{className:qe,children:"Won’t fit"})]})}function wt({item:s,picked:t,cut:n,disabled:a,onToggle:i}){const l=Nt(s.path,s.line);return e.jsx("li",{children:e.jsxs("label",{className:`flex cursor-pointer items-start gap-2 rounded-ui border px-2 py-1.5 text-meta ${t?"border-ui-line bg-ui-sel":"border-ui-line"}`,children:[e.jsx(z,{className:"mt-0.5",checked:t,disabled:a,onChange:o=>i(o.target.checked)}),e.jsxs("span",{className:"min-w-0 flex-1",children:[e.jsxs("span",{className:"flex flex-wrap items-center gap-1.5",children:[s.severity&&Ae[s.severity]&&e.jsx("span",{className:`rounded-ui px-1 py-px text-meta font-medium ${Ae[s.severity]}`,children:jt[s.severity]}),e.jsx("span",{className:"break-words text-ui-ink",children:s.label}),t&&n&&e.jsx("span",{className:qe,children:"Won’t fit"})]}),(l||s.detail)&&e.jsxs("span",{className:"mt-0.5 block break-words text-meta text-ui-mute",children:[l&&e.jsx("span",{className:"font-mono",children:l}),l&&s.detail?" · ":"",s.detail]})]})]})})}function kt({preview:s,overrides:t,onChange:n,disabled:a,pr:i,review:l}){const o=We(s,t),g=new Map;for(const r of s.items){const d=g.get(r.section)??[];d.push(r),g.set(r.section,d)}const c=(r,d)=>{const f={...t};for(const w of r)f[w]=d;n(f)},m=l!=null&&l.id===s.sourceReviewId?l:null,v=Qs(l),F=Xs(l),u=x.useMemo(()=>m!=null?us(m):null,[m]),k=x.useMemo(()=>new Map(((u==null?void 0:u.findings)??[]).map(r=>[r.id,r])),[u]),M=x.useMemo(()=>{var r;return new Map((((r=u==null?void 0:u.followUp)==null?void 0:r.items)??[]).map(d=>[d.priorFindingId,d]))},[u]),C=x.useMemo(()=>u!=null?ds(u):new Map,[u]),B=x.useMemo(()=>u!=null?ms(u):new Set,[u]),E=x.useMemo(()=>{var d;const r=new Set;for(const f of s.items)((d=f.card)==null?void 0:d.kind)==="story"&&r.add(f.card.ticketReviewId);return[...r].sort((f,w)=>f-w)},[s.items]),I=hs(i.id,E.length>0),y=x.useMemo(()=>{var d;const r=new Map;for(const f of((d=I.data)==null?void 0:d.tickets)??[])f.review!=null&&r.set(f.review.id,f.review);return r},[I.data]),D=x.useMemo(()=>I.data==null?[]:E.filter(r=>!y.has(r)),[I.data,E,y]),R=xs(D),_=x.useMemo(()=>{const r=new Map(y);for(const d of R)r.set(d.id,d);return r},[y,R]);if(s.items.length===0)return e.jsx("p",{className:"text-meta text-ui-mute",children:"The review found nothing to fix."});const L=(m==null?void 0:m.headSha)??i.headSha,P=r=>{var H,S;const d=G(r,t),f=o.has(r.key),w=j=>c([r.key],j),p=e.jsx(vt,{item:r,picked:d,cut:f,disabled:a,onToggle:w}),N=r.card;switch(N==null?void 0:N.kind){case"finding":{const j=k.get(N.findingId);if(j==null)break;return e.jsx(tt,{authorName:nt(l,j),prId:i.id,finding:j,editable:!1,prUrl:i.githubUrl,repoFullName:i.repoFullName,headSha:L,posting:!1,postError:null,inChangeset:j.fileInDiff,priorStatus:C.get(j.id),alreadyPosted:B.has(j.id),actions:!1,selection:p,docReferences:(m==null?void 0:m.docReferences)??null},r.key)}case"earlier_finding":{const j=M.get(N.priorFindingId);if(j==null)break;return e.jsx(gs,{item:j,muted:!1,findingsById:k,headMoved:((H=u==null?void 0:u.followUp)==null?void 0:H.headMoved)??!1,changedPaths:Z,actions:!1,selection:p,by:v},r.key)}case"thread":return e.jsx(st,{t:N.assessment,muted:!1,actions:!1,selection:p,notCheckedNote:N.inReview?void 0:`${F} didn’t check this one.`,by:F},r.key);case"ci":return e.jsx(et,{f:N.item,suggestion:N.item.suggestion,changedPaths:Z,selection:p},r.key);case"sonar":return e.jsx(Zs,{issue:N.issue,selection:p},r.key);case"story":{const j=_.get(N.ticketReviewId),T=j==null?void 0:j.items.find(U=>U.id===N.itemId);if(j==null||T==null)break;const b={review:j,viewedPrId:i.id,changedPaths:Z,actions:!1,selection:p};if(T.status==="missing")return e.jsx(ps,{item:T,...b},r.key);const h=(S=j.assessment)==null?void 0:S.criteria.find(U=>U.ref===T.ref);if(h==null||h.status==="met")break;return e.jsx(fs,{c:h,item:T,...b},r.key)}}return e.jsx(wt,{item:r,picked:d,cut:f,disabled:a,onToggle:w},r.key)};return e.jsxs("div",{className:"space-y-3",children:[pt.filter(r=>g.has(r)).map(r=>{const d=g.get(r),f=d.filter(p=>G(p,t)).length,w=f===d.length;return e.jsxs("section",{"aria-label":Ce(r,v),children:[e.jsxs("div",{className:"mb-1 flex items-center gap-2",children:[e.jsx("h4",{className:"text-meta font-semibold text-ui-ink",children:Ce(r,v)}),e.jsxs("span",{className:"text-meta text-ui-mute",children:[f," of ",d.length]}),e.jsx("button",{type:"button",className:"hit ml-auto text-meta text-ui-accent hover:underline disabled:opacity-50",disabled:a,onClick:()=>c(d.map(p=>p.key),!w),children:w?"Untick all":"Tick all"})]}),e.jsx("ul",{className:"space-y-1.5",children:d.map(P)})]},r)}),o.size>0&&e.jsxs("p",{className:"text-meta text-ui-warn",children:[o.size," ticked ",o.size===1?"item does":"items do"," not fit in one fix and will be left out. Untick something to make room."]})]})}const yt="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3",Fe="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3";function Ye(s){return{prId:s.id,repoId:s.repoId,repoFullName:s.repoFullName,prNumber:s.number,prTitle:s.title,githubUrl:s.githubUrl}}function Rt(s){const t=bs({mergeable:s.mergeable,mergeStateStatus:s.mergeStateStatus,isDraft:s.isDraft,inMergeQueue:s.inMergeQueue===!0}).verdict;return js({state:s.state,verdict:t,viewerCanPush:s.viewerCanPush})}function Pt({pr:s}){const t=Ns(s.id),n=vs(),a=ws(t),i=s.baseRefName??"the base branch";x.useEffect(()=>{ks(s.id)},[s.id]);const l=Ye(s),o=(t==null?void 0:t.phase)==="succeeded"?t.resolution:null;return e.jsxs("div",{className:"mb-3 rounded-ui bg-ui-surface p-3",children:[e.jsxs("div",{className:"flex items-center gap-1.5 text-body font-medium text-ui-ink",children:[e.jsx(ys,{size:13,className:"text-ai-signal"}),"This branch conflicts with ",i]}),e.jsxs("p",{className:"mt-1 text-meta text-ui-mute",children:[n," reads the code and picks a result for each conflict. You check its choices in the resolver before anything is pushed."]}),a&&t!=null?e.jsxs("div",{className:"mt-2",children:[e.jsx(ie,{active:!0,label:`Resolving conflicts with ${n}`,timeConstantSec:60}),e.jsxs("div",{className:"mt-1 flex items-center gap-2 text-meta text-ui-mute",children:[Rs(t.phase,n),e.jsx("button",{type:"button",className:Fe,onClick:()=>Ps(s.id),children:"Cancel"})]})]}):e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[e.jsx(K,{kind:"resolve",children:e.jsx("button",{type:"button",className:yt,onClick:()=>Ue(l,{providerName:n}),children:o!=null?"Run again":`Resolve merge conflicts with ${n}`})}),o!=null&&e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:Fe,onClick:()=>Cs(l),children:"Open the resolver"}),e.jsxs("span",{className:"text-meta text-ui-mute",children:[V(o)," decided ",o.choices.length," of ",o.decidableTotal," change",o.decidableTotal===1?"":"s","."]})]}),(t==null?void 0:t.phase)==="failed"&&t.error!=null&&e.jsx("span",{className:"text-meta text-ui-danger",children:t.error}),(t==null?void 0:t.phase)==="cancelled"&&e.jsx("span",{className:"text-meta text-ui-mute",children:"Cancelled."})]})]})}const se="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3";function te(s){return s instanceof Le||s instanceof Error?s.message:"Something went wrong"}const ee="inline-flex h-7 items-center whitespace-nowrap rounded-ui bg-ui-raise px-2.5 text-meta font-medium text-ui-ink hover:bg-ui-hover disabled:cursor-not-allowed disabled:opacity-50 touch:h-10 touch:px-3";function Ct({children:s,info:t}){return e.jsxs("div",{className:"flex items-center gap-1.5 px-4 pb-2 pt-4 text-title text-ui-ink md:px-5",children:[e.jsx("h3",{children:s}),t]})}function Ot({pr:s}){const t=$e().enabled,n=O(c=>c.aiFixTabFocus),a=O(c=>c.consumeAiFixTabFocus),[i,l]=x.useState(null),[o,g]=x.useState(null);return x.useEffect(()=>{n&&n.prId===s.id&&(n.reviewId!=null&&l(n.reviewId),n.only!=null&&g(n.only),a())},[n,s.id,a]),t?e.jsx("div",{className:"pb-6",children:e.jsx(At,{pr:s,handoffReviewId:i,onlySection:o,onOnlySectionApplied:()=>g(null)})}):e.jsx(e.Fragment,{})}function At({pr:s,handoffReviewId:t,onlySection:n,onOnlySectionApplied:a}){var oe,ue,de,me,he,xe,pe,fe,ge,be;const{data:i,isLoading:l}=As(s.id,!0),{data:o,isLoading:g}=Fs(s.id),c=(o==null?void 0:o.review)??null,m=ct(t,c,(o==null?void 0:o.history)??[]),v=ot(t,m,c),F=O(A=>A.setPrDetailTab),u=Is("fix",Js),k=Be(u.chosen.provider),M=Ss().data,C=Ts(s.id),B=Es(s.id),E=Se(s.id),I=$e(),y=_s(M,u.chosen.provider,"plan"),[D,R]=x.useState(!1),_=D&&y,L=O(A=>A.noteAiFixRun),P=Ie({mutationKey:Ms(s.id)})>0,r=((oe=i==null?void 0:i.fix)==null?void 0:oe.status)??null,d=r==="planned",f=d&&((de=(ue=i==null?void 0:i.fix)==null?void 0:ue.plan)==null?void 0:de.status)==="planning",w=r==="running"||r==="queued"||P||f,{status:p}=Bs(s.id,w),N=(p==null?void 0:p.status)??r??"idle",H=N==="running"||N==="queued",S=H&&!d,j=d&&(f||H),T=d&&(((he=(me=i==null?void 0:i.fix)==null?void 0:me.plan)==null?void 0:he.status)==="planning"||((pe=(xe=i==null?void 0:i.fix)==null?void 0:xe.plan)==null?void 0:pe.status)==="plan_ready"),b=(i==null?void 0:i.fix)??null,h=Ls(s.id,m,(i==null?void 0:i.enabled)!==!1&&(m!=null||!g)),U=m==null&&h.data!=null&&h.data.sourceReviewId==null&&h.data.items.length>0,Y=m!=null||U,ze=Us(m!=null&&(c==null?void 0:c.id)!==m?m:null),Ge=m!=null&&(c==null?void 0:c.id)===m?c:ze.data??null,[J,Q]=x.useState({});x.useEffect(()=>Q({}),[m]);const[Ve,W]=x.useState(!1);x.useEffect(()=>{n==null||!h.data||(Q(bt(h.data,n)),W(!0),a())},[n,h.data,a]);const re=!T&&(Ve||!b||b.status!=="succeeded"),X=Rt(s),[le,Je]=x.useState(!1),q=$s(s.id,s.repoFullName),$=h.data?gt(h.data,J):[],ce=h.data?We(h.data,J).size:0,Qe=()=>{if(!Y||!h.data||$.length===0)return;const A={model:u.chosen.model,provider:u.chosen.provider,seed:"review",...m!=null?{sourceReviewId:m}:{},include:$,...q.names.length>0?{checks:q.names}:{}};_?B.mutate(A,{onSuccess:()=>W(!1)}):(L({prId:s.id,repoFullName:s.repoFullName,prNumber:s.number,prTitle:s.title}),C.mutate(A,{onSuccess:()=>W(!1)})),le&&X&&Ue(Ye(s),{model:Hs(u.chosen.model),providerName:I.providerName})};return e.jsxs("div",{children:[e.jsx(Ct,{info:e.jsxs(ne,{title:"AI Fix",children:[e.jsxs("p",{children:[k," reads the PR's code and edits files in a private copy. It has no shell. Tick Run tests to let it run the project's own test, lint and type checks in a sandbox."]}),e.jsx("p",{children:"A fix starts from an AI review. Pick what it should work on: the review's findings, earlier findings still open, threads the review says need a fix, unanswered threads, CI failures and ticket gaps are ticked; style bot comments are not."}),e.jsx("p",{children:"With no AI review, a fix can start from a finished CI check: it works on the failures the check says this PR can fix."}),e.jsxs("p",{children:["Tick Plan first to read ",k,"'s plan, steer it and approve it before anything changes."]}),e.jsxs("p",{children:[k," reports what it changed in each file and what it left alone, and why."]}),e.jsx("p",{children:"Nothing is pushed until you press Push. An automatic fix on your own PR is pushed for you only when Push automatically is on in Settings."})]}),children:"AI Fix"}),e.jsxs("div",{className:"px-4",children:[X&&e.jsx(Pt,{pr:s}),(i==null?void 0:i.enabled)===!1?e.jsx("p",{className:"text-body text-ui-mute",children:"The agentic fixer is turned off."}):e.jsxs(e.Fragment,{children:[!S&&!T&&!Y&&!g&&!h.isLoading&&e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("p",{className:"text-meta text-ui-mute",children:(c==null?void 0:c.status)==="running"||(c==null?void 0:c.status)==="queued"?"A fix starts from an AI review. One is running.":"A fix starts from a finished AI review."}),e.jsx("button",{type:"button",className:se,onClick:()=>F(s.id,"claude_review"),children:"Open AI Review"})]}),!S&&!T&&Y&&!re&&e.jsx("button",{type:"button",className:ee,onClick:()=>W(!0),children:"New fix"}),!S&&Y&&re&&e.jsxs("div",{children:[v&&e.jsx("p",{className:"mb-2 text-meta text-ui-mute",children:v}),U&&e.jsx("p",{className:"mb-2 text-meta text-ui-mute",children:"No AI review yet. This fix works on the CI check’s failures."}),h.isLoading&&e.jsx("p",{className:"text-meta text-ui-mute",children:"Loading…"}),h.isError&&m!=null&&e.jsx("p",{className:"text-meta text-ui-danger",children:te(h.error)}),h.data&&e.jsx(kt,{preview:h.data,overrides:J,onChange:Q,disabled:P,pr:s,review:Ge}),h.data&&h.data.items.length>0&&X&&e.jsxs("label",{className:"mt-3 flex cursor-pointer items-start gap-2 text-meta text-ui-ink",children:[e.jsx(z,{className:"mt-0.5",checked:le,disabled:P,onChange:A=>Je(A.target.checked)}),e.jsxs("span",{children:["Also resolve merge conflicts with ",s.baseRefName??"the base branch",e.jsx("span",{className:"block text-meta text-ui-mute",children:"You check the merge in the resolver. It lands as its own commit, apart from the fix."})]})]}),h.data&&h.data.items.length>0&&e.jsx(Os,{state:q,disabled:P}),h.data&&h.data.items.length>0&&e.jsxs("div",{className:"mt-3 flex flex-wrap items-center gap-2 max-md:sticky max-md:bottom-0 max-md:z-20 max-md:-mx-4 max-md:bg-ui-surface max-md:px-4 max-md:py-3",children:[e.jsx(Ds,{state:u,onPick:u.pick,disabled:P}),y&&e.jsxs("label",{className:"flex cursor-pointer items-center gap-1.5 text-meta text-ui-ink",children:[e.jsx(z,{checked:_,disabled:P,onChange:A=>R(A.target.checked)}),"Plan first"]}),e.jsx(K,{kind:"fix",auth:i==null?void 0:i.auth,anyProvider:!0,provider:u.chosen.provider,children:e.jsx("button",{type:"button",className:se,disabled:P||$.length===0||q.on&&q.query.isLoading,onClick:Qe,children:_?"Make a plan":"Start fix"})}),e.jsx("span",{className:"text-meta text-ui-mute",children:$.length===0?"Nothing ticked.":`${$.length-ce} item${$.length-ce===1?"":"s"}`}),(b==null?void 0:b.status)==="succeeded"&&e.jsx("button",{type:"button",className:ee,onClick:()=>W(!1),children:"Close"})]})]}),S&&e.jsx("button",{type:"button",className:ee,onClick:()=>E.mutate(),children:"Cancel"}),(C.isError||B.isError)&&e.jsx("p",{className:"mt-1 text-meta text-ui-danger",children:te(C.error??B.error)}),b&&d&&e.jsx(lt,{pr:s,fix:b,liveStatus:p,passRunning:j,auth:i==null?void 0:i.auth}),S&&e.jsxs("div",{className:"mt-3",children:[b&&e.jsx(He,{fix:b}),e.jsx(ie,{active:!0,label:"Running AI fix",value:Ee(p),timeConstantSec:40}),e.jsxs("div",{className:"mt-1 flex items-center gap-2 text-meta text-ui-mute",children:[(b==null?void 0:b.trigger)==="auto"&&e.jsx(Ke,{}),_e[((fe=p==null?void 0:p.progress)==null?void 0:fe.phase)??""]??"Working…"]}),e.jsx(Me,{lines:(ge=p==null?void 0:p.progress)==null?void 0:ge.recentActivity,runKey:(b==null?void 0:b.id)??null,className:"mt-1"})]}),!S&&b&&!d&&e.jsx(It,{pr:s,fix:b,headInfo:(i==null?void 0:i.headInfo)??null,viewerCanPush:(i==null?void 0:i.viewerCanPush)??!1}),l&&!b&&e.jsx("p",{className:"mt-3 text-meta text-ui-mute",children:"Loading…"})]}),e.jsx(Tt,{history:(i==null?void 0:i.history)??[],currentFixId:((be=i==null?void 0:i.fix)==null?void 0:be.id)??null})]})]})}function Ke(){return e.jsx("span",{className:"inline-flex items-center rounded-ui bg-ui-raise px-1.5 py-px text-meta font-medium text-ui-mute",children:"Auto fix"})}function Ft(s){return Te(s.provider,s.model,s.effort)}function It({pr:s,fix:t,headInfo:n,viewerCanPush:a}){var o,g,c;const i=x.useMemo(()=>Ws(t.patch),[t.patch]);if(t.status==="failed"&&((o=t.plan)==null?void 0:o.status)==="failed")return e.jsxs("div",{className:"mt-3 rounded-ui border border-ui-danger/40 bg-ui-danger/10 p-2 text-meta text-ui-danger",children:[V(t.plan??t)," could not make a plan: ",t.error??"unknown error"]});if(t.status==="cancelled"&&((g=t.plan)==null?void 0:g.status)==="discarded")return e.jsx("p",{className:"mt-3 text-meta text-ui-mute",children:"The plan was discarded."});if(t.status==="failed")return e.jsxs("div",{className:"mt-3 rounded-ui border border-ui-danger/40 bg-ui-danger/10 p-2 text-meta text-ui-danger",children:["The fix run failed: ",t.error??"unknown error"]});if(t.status==="cancelled")return e.jsx("p",{className:"mt-3 text-meta text-ui-mute",children:"The fix run was cancelled."});if(t.status!=="succeeded")return e.jsx(e.Fragment,{});const l=!t.patch||t.filesChanged.length===0;return e.jsxs("div",{className:"mt-3 border-t border-ui-line pt-3",children:[e.jsxs("p",{className:"mb-1 text-meta text-ui-mute",children:[t.trigger==="auto"&&e.jsxs(e.Fragment,{children:[e.jsx(Ke,{})," "]}),ht(t)," · ",Ft(t),t.finishedAt&&e.jsxs(e.Fragment,{children:[" · ",ae(t.finishedAt)]})]}),t.pushedAt==null&&t.error&&e.jsx("p",{className:"mb-2 rounded-ui bg-ui-raise p-2 text-meta text-ui-warn",children:t.error}),e.jsx(He,{fix:t}),t.summary&&e.jsx("div",{className:"prose prose-sm max-w-none dark:prose-invert",children:e.jsx(qs,{children:t.summary})}),e.jsx(xt,{pr:s,fix:t}),e.jsx(Ys,{checks:t.checks}),l?e.jsx("p",{className:"mt-2 text-meta text-ui-mute",children:"No files changed."}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-1 mt-2 flex items-center justify-between gap-3 text-meta text-ui-mute",children:[e.jsxs("span",{children:[t.filesChanged.length," file",t.filesChanged.length===1?"":"s"," changed",!((c=t.checks)!=null&&c.ran)&&" · Not built or tested here."]}),e.jsx(Ks,{})]}),e.jsx("div",{className:"overflow-hidden rounded-ui bg-ui-surface text-ui-ink",children:e.jsx(zs,{files:i})}),e.jsx(Et,{pr:s,fix:t,headInfo:n,viewerCanPush:a})]})]})}function St({fix:s}){return e.jsxs("div",{className:"mb-2 rounded-ui bg-ui-raise p-2 text-meta text-ui-success",children:[e.jsxs("div",{children:["Pushed to ",e.jsx("span",{className:"font-mono",children:s.pushedBranch}),s.pushedPrUrl&&e.jsxs(e.Fragment,{children:[" · ",e.jsxs("a",{href:Oe(s.pushedPrUrl),target:"_blank",rel:"noreferrer",className:"hit underline",children:["PR #",s.pushedPrNumber]})]}),s.pushedAt&&e.jsxs(e.Fragment,{children:[" · ",ae(s.pushedAt)]})]}),s.commitMessage&&e.jsx("div",{className:"mt-1 font-mono text-meta text-ui-success",children:s.commitMessage})]})}function Tt({history:s,currentFixId:t}){const n=s.filter(a=>a.pushedAt!=null&&a.id!==t);return n.length===0?null:e.jsxs("div",{className:"mt-6",children:[e.jsx("div",{className:"mb-2 text-title text-ui-ink",children:"Earlier fixes pushed"}),e.jsx("ul",{className:"space-y-2",children:n.map(a=>e.jsxs("li",{className:"text-meta",children:[e.jsx("div",{className:"font-mono text-ui-ink",children:a.commitMessage??"(no commit message)"}),e.jsxs("div",{className:"mt-0.5 text-meta text-ui-faint",children:[a.pushedBranch&&e.jsx("span",{className:"font-mono",children:a.pushedBranch}),a.pushedPrUrl&&e.jsxs(e.Fragment,{children:[" · ",e.jsxs("a",{href:Oe(a.pushedPrUrl),target:"_blank",rel:"noreferrer",className:"hit underline",children:["PR #",a.pushedPrNumber]})]}),a.pushedAt&&e.jsxs(e.Fragment,{children:[" · ",ae(a.pushedAt)]})]})]},a.id))})]})}function Et({pr:s,fix:t,headInfo:n,viewerCanPush:a}){const i=Gs(s.id),l=(n==null?void 0:n.canPushSameBranch)??!1,[o,g]=x.useState(l?"existing":"new"),[c,m]=x.useState((n==null?void 0:n.suggestedBranch)??""),v=x.useRef(!1);if(x.useEffect(()=>{!v.current&&(n!=null&&n.suggestedBranch)&&(m(n.suggestedBranch),v.current=!0,n.canPushSameBranch||g("new"))},[n]),t.pushedAt!=null)return e.jsx(St,{fix:t});if(!a)return e.jsx("p",{className:"mt-2 text-meta text-ui-mute",children:"You need write access to push this fix."});const F=o==="new"&&c.trim().length===0,u=i.isPending,k=()=>{i.mutate({fixId:t.id,body:{target:o,branch:o==="new"?c.trim():void 0}})};return e.jsxs("div",{className:"mt-3 rounded-ui bg-ui-surface p-2",children:[t.commitMessage&&e.jsx("div",{className:"mb-2 rounded-ui bg-ui-surface px-2 py-1 font-mono text-meta text-ui-ink",title:"Commit message",children:t.commitMessage}),e.jsxs("label",{className:`flex items-center gap-2 text-meta ${l?"":"opacity-40"}`,title:l?void 0:"The PR head is a fork you cannot push to — use a new branch",children:[e.jsx(ve,{className:"mt-0",checked:o==="existing",disabled:!l||u,onChange:()=>g("existing")}),"Push to the PR branch",(n==null?void 0:n.headRef)&&e.jsxs("span",{className:"font-mono text-ui-faint",children:["(",n.headRef,")"]})]}),e.jsxs("label",{className:"mt-1 flex items-center gap-2 text-meta",children:[e.jsx(ve,{className:"mt-0",checked:o==="new",disabled:u,onChange:()=>g("new")}),"New branch + open a PR"]}),o==="new"&&e.jsx("input",{type:"text",className:"mt-1 w-full rounded-ui bg-ui-raise px-2 py-1 font-mono text-meta ring-1 ring-inset ring-ui-line focus:outline-none focus:ring-2 focus:ring-ui-accent touch:min-h-10",value:c,disabled:u,onChange:M=>m(M.target.value),placeholder:"branch-name"}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[e.jsx("button",{type:"button",className:se,disabled:u||F,onClick:k,children:u?"Pushing…":o==="new"?"Push + open PR":"Push"}),e.jsx(Vs,{checks:t.checks})]}),i.isError&&e.jsx("div",{className:"mt-2 text-meta text-ui-danger",children:te(i.error)})]})}export{Ot as AiFixTab};
|