@gmickel/gno 2.6.0 → 2.7.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/README.md +25 -30
- package/assets/skill/SKILL.md +5 -3
- package/assets/skill/cli-reference.md +9 -2
- package/assets/skill/mcp-reference.md +2 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.zip → gno-browser-clipper-v2.7.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +109 -18
- package/spec/mcp.md +36 -2
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +1 -1
- package/spec/output-schemas/collection-list.schema.json +2 -2
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
- package/spec/output-schemas/memory-remember.schema.json +2 -2
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +110 -16
- package/src/cli/commands/ask.ts +31 -12
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +53 -1
- package/src/core/capture-sync.ts +9 -2
- package/src/core/host-paths.ts +49 -0
- package/src/core/memory-remember.ts +4 -3
- package/src/core/request-receipts.ts +63 -9
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +145 -27
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +48 -18
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +21 -2
- package/src/llm/inference-scope.ts +18 -0
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +9 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +1 -3
- package/src/mcp/tools/index.ts +11 -4
- package/src/mcp/tools/memory-remember.ts +1 -1
- package/src/mcp/tools/status.ts +23 -6
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +116 -0
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +17 -2
- package/src/serve/public/lib/request-intent.ts +8 -0
- package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +16 -11
- package/src/serve/public/pages/DocView.tsx +10 -5
- package/src/serve/public/pages/DocumentEditor.tsx +91 -14
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/resident-runtime.ts +33 -1
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/sessions.ts +61 -5
- package/src/serve/server.ts +15 -12
- package/src/serve/status-model.ts +26 -6
- package/src/serve/status.ts +4 -7
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +24 -3
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +0 -1
|
@@ -9,6 +9,7 @@ import type { Database } from "bun:sqlite";
|
|
|
9
9
|
|
|
10
10
|
import type { EmbeddingPort } from "../llm/types";
|
|
11
11
|
import type { VectorIndexPort } from "../store/vector";
|
|
12
|
+
import type { BackgroundIssue } from "./status-model";
|
|
12
13
|
|
|
13
14
|
import { embedBacklog } from "../embed";
|
|
14
15
|
import {
|
|
@@ -24,6 +25,15 @@ import { createVectorStatsPort } from "../store/vector";
|
|
|
24
25
|
const DEBOUNCE_MS = 30_000; // 30 seconds
|
|
25
26
|
const MAX_WAIT_MS = 300_000; // 5 minutes
|
|
26
27
|
const BATCH_SIZE = 32;
|
|
28
|
+
/** Consecutive failed passes after which automatic retries stop (parked). */
|
|
29
|
+
export const MAX_FAILED_PASSES = 5;
|
|
30
|
+
/** A pass running longer than this is reported as overrunning. */
|
|
31
|
+
export const PASS_OVERRUN_MS = 15 * 60_000;
|
|
32
|
+
|
|
33
|
+
/** Delay before the automatic retry that follows `failures` failed passes. */
|
|
34
|
+
export function failedPassRetryDelayMs(failures: number): number {
|
|
35
|
+
return DEBOUNCE_MS * 2 ** Math.max(0, failures - 1);
|
|
36
|
+
}
|
|
27
37
|
|
|
28
38
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
39
|
// Types
|
|
@@ -35,6 +45,12 @@ export interface EmbedSchedulerState {
|
|
|
35
45
|
nextRunAt?: number;
|
|
36
46
|
lastRunAt?: number;
|
|
37
47
|
lastResult?: EmbedResult;
|
|
48
|
+
/** Start of the pass in flight. */
|
|
49
|
+
runningSince?: number;
|
|
50
|
+
/** Failed passes since the last clean pass (0 when healthy). */
|
|
51
|
+
consecutiveFailures: number;
|
|
52
|
+
/** Automatic retries stopped; chunks stay pending for new work or `gno embed`. */
|
|
53
|
+
parked: boolean;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export interface EmbedResult {
|
|
@@ -52,6 +68,47 @@ export interface EmbedSchedulerDeps {
|
|
|
52
68
|
getModelUri: () => string;
|
|
53
69
|
onEmbedded?: (result: EmbedResult) => void;
|
|
54
70
|
embedBacklogFn?: typeof embedBacklog;
|
|
71
|
+
/**
|
|
72
|
+
* Shared writer lease for one page of background writes; null when another
|
|
73
|
+
* writer holds it, which defers the rest of the pass instead of waiting.
|
|
74
|
+
*/
|
|
75
|
+
acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Project scheduler state onto status issues; empty while healthy. */
|
|
79
|
+
export function embedSchedulerIssues(
|
|
80
|
+
state: EmbedSchedulerState,
|
|
81
|
+
now = Date.now()
|
|
82
|
+
): BackgroundIssue[] {
|
|
83
|
+
const issues: BackgroundIssue[] = [];
|
|
84
|
+
if (state.parked || state.consecutiveFailures > 0) {
|
|
85
|
+
issues.push({
|
|
86
|
+
job: "embed",
|
|
87
|
+
state: state.parked ? "parked" : "failing",
|
|
88
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
89
|
+
runningSeconds: null,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
state.runningSince !== undefined &&
|
|
94
|
+
now - state.runningSince > PASS_OVERRUN_MS
|
|
95
|
+
) {
|
|
96
|
+
issues.push({
|
|
97
|
+
job: "embed",
|
|
98
|
+
state: "overrunning",
|
|
99
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
100
|
+
runningSeconds: Math.floor((now - state.runningSince) / 1000),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface PassOutcome {
|
|
107
|
+
result: EmbedResult;
|
|
108
|
+
/** Present when the pass failed; the message is logged once per retry step. */
|
|
109
|
+
failure?: string;
|
|
110
|
+
/** Another writer held the lease; the rest of the backlog waits for a rerun. */
|
|
111
|
+
deferred?: boolean;
|
|
55
112
|
}
|
|
56
113
|
|
|
57
114
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -86,6 +143,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
86
143
|
getModelUri,
|
|
87
144
|
onEmbedded,
|
|
88
145
|
embedBacklogFn = embedBacklog,
|
|
146
|
+
acquireWriteLease,
|
|
89
147
|
} = deps;
|
|
90
148
|
|
|
91
149
|
// State
|
|
@@ -99,6 +157,9 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
99
157
|
let currentRun: Promise<EmbedResult | null> | null = null;
|
|
100
158
|
let lastRunAt: number | null = null;
|
|
101
159
|
let lastResult: EmbedResult | null = null;
|
|
160
|
+
let runningSince: number | null = null;
|
|
161
|
+
let consecutiveFailures = 0;
|
|
162
|
+
let parked = false;
|
|
102
163
|
const controller = new AbortController();
|
|
103
164
|
|
|
104
165
|
const stats = createVectorStatsPort(db);
|
|
@@ -110,14 +171,14 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
110
171
|
* 2. Filtering by docId would require joining through documents table
|
|
111
172
|
* 3. Simpler to just embed all backlog when triggered
|
|
112
173
|
*/
|
|
113
|
-
async function runEmbed(): Promise<
|
|
174
|
+
async function runEmbed(): Promise<PassOutcome> {
|
|
114
175
|
// Resolve dependencies at execution time (survives context reloads)
|
|
115
176
|
const embedPort = getEmbedPort();
|
|
116
177
|
const vectorIndex = getVectorIndex();
|
|
117
178
|
const modelUri = getModelUri();
|
|
118
179
|
|
|
119
180
|
if (!embedPort || !vectorIndex) {
|
|
120
|
-
return { embedded: 0, errors: 0 };
|
|
181
|
+
return { result: { embedded: 0, errors: 0 } };
|
|
121
182
|
}
|
|
122
183
|
|
|
123
184
|
let result: Awaited<ReturnType<typeof embedBacklogFn>>;
|
|
@@ -130,6 +191,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
130
191
|
vectorIndex,
|
|
131
192
|
modelUri,
|
|
132
193
|
batchSize: BATCH_SIZE,
|
|
194
|
+
acquireWriteTurn: acquireWriteLease,
|
|
133
195
|
identityStillCurrent: () =>
|
|
134
196
|
getEmbedPort() === embedPort &&
|
|
135
197
|
getVectorIndex() === vectorIndex &&
|
|
@@ -138,14 +200,21 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
138
200
|
)
|
|
139
201
|
);
|
|
140
202
|
} catch (cause) {
|
|
141
|
-
if (controller.signal.aborted)
|
|
142
|
-
|
|
203
|
+
if (controller.signal.aborted)
|
|
204
|
+
return { result: { embedded: 0, errors: 0 } };
|
|
205
|
+
// A worker exit aborts the whole pass (a deadline fails only its page).
|
|
206
|
+
// It is a failed pass like any other, never a silently dropped rejection.
|
|
207
|
+
return {
|
|
208
|
+
result: { embedded: 0, errors: 0 },
|
|
209
|
+
failure: cause instanceof Error ? cause.message : String(cause),
|
|
210
|
+
};
|
|
143
211
|
}
|
|
144
212
|
|
|
145
213
|
if (!result.ok) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
214
|
+
return {
|
|
215
|
+
result: { embedded: 0, errors: 0 },
|
|
216
|
+
failure: result.error.message,
|
|
217
|
+
};
|
|
149
218
|
}
|
|
150
219
|
if (
|
|
151
220
|
getEmbedPort() !== embedPort ||
|
|
@@ -153,21 +222,42 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
153
222
|
getModelUri() !== modelUri
|
|
154
223
|
)
|
|
155
224
|
needsRerun = true;
|
|
156
|
-
if (
|
|
157
|
-
|
|
225
|
+
if (result.value.deferred) needsRerun = true;
|
|
226
|
+
if (result.value.embedded > 0) onEmbedded?.(result.value);
|
|
227
|
+
const contended = result.value.contentionErrors ?? 0;
|
|
228
|
+
const deferred = result.value.deferred === true;
|
|
229
|
+
// Provider failures and contended checkpoints remain durably pending.
|
|
230
|
+
return result.value.errors > 0 || contended > 0
|
|
231
|
+
? {
|
|
232
|
+
result: result.value,
|
|
233
|
+
deferred,
|
|
234
|
+
failure: `${result.value.errors} embedding errors, ${contended} contended writes`,
|
|
235
|
+
}
|
|
236
|
+
: { result: result.value, deferred };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Count a failed pass and schedule its bounded retry, logging each step once. */
|
|
240
|
+
function recordFailure(message: string): void {
|
|
241
|
+
consecutiveFailures += 1;
|
|
242
|
+
if (parked) return;
|
|
243
|
+
if (consecutiveFailures >= MAX_FAILED_PASSES) {
|
|
244
|
+
parked = true;
|
|
158
245
|
console.error(
|
|
159
|
-
`[embed-scheduler] ${
|
|
246
|
+
`[embed-scheduler] Embed pass failed ${consecutiveFailures} times (${message}); automatic retries parked. Pending chunks stay queued for new changes or \`gno embed\`.`
|
|
160
247
|
);
|
|
161
|
-
|
|
248
|
+
return;
|
|
162
249
|
}
|
|
163
|
-
|
|
164
|
-
|
|
250
|
+
const delay = failedPassRetryDelayMs(consecutiveFailures);
|
|
251
|
+
console.error(
|
|
252
|
+
`[embed-scheduler] Embed pass failed (${consecutiveFailures}/${MAX_FAILED_PASSES}): ${message}; retrying in ${Math.round(delay / 1000)}s`
|
|
253
|
+
);
|
|
254
|
+
scheduleRun(delay);
|
|
165
255
|
}
|
|
166
256
|
|
|
167
257
|
/**
|
|
168
258
|
* Schedule or reschedule the debounced embed run.
|
|
169
259
|
*/
|
|
170
|
-
function scheduleRun(): void {
|
|
260
|
+
function scheduleRun(retryDelay?: number): void {
|
|
171
261
|
if (disposed) {
|
|
172
262
|
return;
|
|
173
263
|
}
|
|
@@ -180,7 +270,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
180
270
|
|
|
181
271
|
// Calculate delay
|
|
182
272
|
const now = Date.now();
|
|
183
|
-
let delay = DEBOUNCE_MS;
|
|
273
|
+
let delay = retryDelay ?? DEBOUNCE_MS;
|
|
184
274
|
|
|
185
275
|
// Check max-wait
|
|
186
276
|
if (firstPendingAt !== null) {
|
|
@@ -226,17 +316,38 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
226
316
|
pendingCount = 0;
|
|
227
317
|
firstPendingAt = null;
|
|
228
318
|
|
|
229
|
-
|
|
319
|
+
runningSince = Date.now();
|
|
320
|
+
let outcome: PassOutcome;
|
|
230
321
|
try {
|
|
231
|
-
|
|
322
|
+
outcome = await runEmbed();
|
|
232
323
|
lastRunAt = Date.now();
|
|
233
|
-
lastResult = result;
|
|
324
|
+
lastResult = outcome.result;
|
|
234
325
|
} finally {
|
|
235
326
|
running = false;
|
|
327
|
+
runningSince = null;
|
|
236
328
|
}
|
|
329
|
+
const result = outcome.result;
|
|
237
330
|
|
|
238
|
-
// Check if we need to rerun (notifications arrived while running)
|
|
239
331
|
// Must be AFTER running=false so scheduleRun() actually schedules
|
|
332
|
+
if (outcome.failure !== undefined && !disposed) {
|
|
333
|
+
// A failed pass never reruns immediately: the bounded retry covers work
|
|
334
|
+
// that arrived meanwhile, and once parked only fresh work earns a pass.
|
|
335
|
+
needsRerun = false;
|
|
336
|
+
recordFailure(outcome.failure);
|
|
337
|
+
// Parked stops retries of failed work, never a lease-deferred remainder.
|
|
338
|
+
if (parked && (pendingCount > 0 || outcome.deferred)) {
|
|
339
|
+
firstPendingAt ??= Date.now();
|
|
340
|
+
scheduleRun();
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
// Only a pass that reached the end of the backlog cleanly clears failures.
|
|
345
|
+
if (outcome.failure === undefined && !outcome.deferred) {
|
|
346
|
+
consecutiveFailures = 0;
|
|
347
|
+
parked = false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if we need to rerun (notifications arrived while running)
|
|
240
351
|
if ((needsRerun || pendingCount > 0) && !disposed) {
|
|
241
352
|
needsRerun = false;
|
|
242
353
|
// Set firstPendingAt if we have pending work
|
|
@@ -313,7 +424,10 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
313
424
|
const state: EmbedSchedulerState = {
|
|
314
425
|
pendingDocCount: pendingCount,
|
|
315
426
|
running,
|
|
427
|
+
consecutiveFailures,
|
|
428
|
+
parked,
|
|
316
429
|
};
|
|
430
|
+
if (runningSince !== null) state.runningSince = runningSince;
|
|
317
431
|
|
|
318
432
|
// Use accurate nextRunAt from timer scheduling
|
|
319
433
|
if (nextRunAt !== null) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST boundary for host paths: a caller the request-locality rule judges
|
|
3
|
+
* remote never receives an `absPath` field from any `/api/*` JSON response,
|
|
4
|
+
* nor the host `path` fields of the status, collection, connector, and
|
|
5
|
+
* document-mutation routes (plus `configPath`/`dbPath` on status and the
|
|
6
|
+
* `file://` `uri` of a document save). Same-host callers (the loopback Web
|
|
7
|
+
* UI) keep them for Reveal, "Open original", and the Collections page.
|
|
8
|
+
* Routes that stream original source bytes are exempt: their body is the
|
|
9
|
+
* owner's file, not an API envelope.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
HOST_PATH_FIELDS,
|
|
14
|
+
OWNER_CONFIG_PATH_FIELDS,
|
|
15
|
+
withoutFields,
|
|
16
|
+
} from "../core/host-paths";
|
|
17
|
+
import {
|
|
18
|
+
isLocalClientRequest,
|
|
19
|
+
type RequestPeerServer,
|
|
20
|
+
} from "./request-locality";
|
|
21
|
+
|
|
22
|
+
const SOURCE_BYTE_ROUTES = new Set(["/api/doc-asset"]);
|
|
23
|
+
const OWNER_CONFIG_FIELDS: ReadonlySet<string> = new Set([
|
|
24
|
+
...HOST_PATH_FIELDS,
|
|
25
|
+
...OWNER_CONFIG_PATH_FIELDS,
|
|
26
|
+
]);
|
|
27
|
+
/** Document mutations answer with the file's host `path`. */
|
|
28
|
+
const DOC_PATH_FIELDS: ReadonlySet<string> = new Set([
|
|
29
|
+
...HOST_PATH_FIELDS,
|
|
30
|
+
"path",
|
|
31
|
+
]);
|
|
32
|
+
/** A document save also names the file by a `file://` `uri`. */
|
|
33
|
+
const DOC_SAVE_FIELDS: ReadonlySet<string> = new Set([
|
|
34
|
+
...DOC_PATH_FIELDS,
|
|
35
|
+
"uri",
|
|
36
|
+
]);
|
|
37
|
+
const ROUTE_FIELDS = new Map<string, ReadonlySet<string>>([
|
|
38
|
+
["/api/status", OWNER_CONFIG_FIELDS],
|
|
39
|
+
["/api/collections", OWNER_CONFIG_FIELDS],
|
|
40
|
+
["/api/collections/:name", OWNER_CONFIG_FIELDS],
|
|
41
|
+
["/api/connectors", OWNER_CONFIG_FIELDS],
|
|
42
|
+
["/api/connectors/install", OWNER_CONFIG_FIELDS],
|
|
43
|
+
["/api/docs", DOC_PATH_FIELDS],
|
|
44
|
+
["/api/docs/:id", DOC_SAVE_FIELDS],
|
|
45
|
+
["/api/docs/:id/rename", DOC_PATH_FIELDS],
|
|
46
|
+
["/api/docs/:id/move", DOC_PATH_FIELDS],
|
|
47
|
+
["/api/docs/:id/duplicate", DOC_PATH_FIELDS],
|
|
48
|
+
["/api/docs/:id/trash", DOC_PATH_FIELDS],
|
|
49
|
+
["/api/docs/:id/editable-copy", DOC_PATH_FIELDS],
|
|
50
|
+
["/api/folders", DOC_PATH_FIELDS],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
type RouteHandler = (
|
|
54
|
+
req: Request,
|
|
55
|
+
server: RequestPeerServer
|
|
56
|
+
) => Response | Promise<Response>;
|
|
57
|
+
|
|
58
|
+
/** The JSON response with every key in `fields` removed. */
|
|
59
|
+
export async function redactResponseHostPaths(
|
|
60
|
+
response: Response,
|
|
61
|
+
fields: ReadonlySet<string>
|
|
62
|
+
): Promise<Response> {
|
|
63
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
64
|
+
if (!contentType.includes("application/json") || response.body === null) {
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
const body = [...fields].some((field) => text.includes(`"${field}"`))
|
|
69
|
+
? JSON.stringify(withoutFields(JSON.parse(text) as unknown, fields))
|
|
70
|
+
: text;
|
|
71
|
+
const headers = new Headers(response.headers);
|
|
72
|
+
headers.delete("content-length");
|
|
73
|
+
return new Response(body, {
|
|
74
|
+
status: response.status,
|
|
75
|
+
statusText: response.statusText,
|
|
76
|
+
headers,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const redacting =
|
|
81
|
+
(handler: RouteHandler, fields: ReadonlySet<string>): RouteHandler =>
|
|
82
|
+
async (req, server) => {
|
|
83
|
+
const response = await handler(req, server);
|
|
84
|
+
return isLocalClientRequest(req, server)
|
|
85
|
+
? response
|
|
86
|
+
: redactResponseHostPaths(response, fields);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const isMethodTable = (route: unknown): route is Record<string, unknown> =>
|
|
90
|
+
route !== null &&
|
|
91
|
+
typeof route === "object" &&
|
|
92
|
+
Object.getPrototypeOf(route) === Object.prototype;
|
|
93
|
+
|
|
94
|
+
/** Wrap every `/api/*` handler in the remote host-path redaction. */
|
|
95
|
+
export function withRemoteHostPathRedaction<T extends Record<string, unknown>>(
|
|
96
|
+
routes: T
|
|
97
|
+
): T {
|
|
98
|
+
const wrapped: Record<string, unknown> = { ...routes };
|
|
99
|
+
for (const [path, route] of Object.entries(routes)) {
|
|
100
|
+
if (!path.startsWith("/api/") || SOURCE_BYTE_ROUTES.has(path)) continue;
|
|
101
|
+
const fields = ROUTE_FIELDS.get(path) ?? HOST_PATH_FIELDS;
|
|
102
|
+
if (typeof route === "function") {
|
|
103
|
+
wrapped[path] = redacting(route as RouteHandler, fields);
|
|
104
|
+
} else if (isMethodTable(route)) {
|
|
105
|
+
wrapped[path] = Object.fromEntries(
|
|
106
|
+
Object.entries(route).map(([method, handler]) => [
|
|
107
|
+
method,
|
|
108
|
+
typeof handler === "function"
|
|
109
|
+
? redacting(handler as RouteHandler, fields)
|
|
110
|
+
: handler,
|
|
111
|
+
])
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return wrapped as T;
|
|
116
|
+
}
|
|
@@ -105,9 +105,11 @@ export function BootstrapStatus({
|
|
|
105
105
|
</CardHeader>
|
|
106
106
|
<CardContent className="space-y-2 text-sm">
|
|
107
107
|
<p>Total cache: {bootstrap.cache.totalSizeLabel}</p>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
{bootstrap.cache.path && (
|
|
109
|
+
<p className="font-mono text-muted-foreground text-xs">
|
|
110
|
+
{bootstrap.cache.path}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
111
113
|
</CardContent>
|
|
112
114
|
</Card>
|
|
113
115
|
|
|
@@ -54,7 +54,8 @@ export interface CollectionModelDetails {
|
|
|
54
54
|
modelSources?: Record<ModelRole, ModelSource>;
|
|
55
55
|
models?: Partial<Record<ModelRole, string>>;
|
|
56
56
|
name: string;
|
|
57
|
-
path
|
|
57
|
+
/** Host path; omitted for remote callers. */
|
|
58
|
+
path?: string;
|
|
58
59
|
pattern?: string;
|
|
59
60
|
egressPolicy?: {
|
|
60
61
|
schemaVersion: "1.0";
|
|
@@ -110,7 +111,7 @@ const CODE_EXT_HINTS = [
|
|
|
110
111
|
] as const;
|
|
111
112
|
|
|
112
113
|
function collectionLooksCodeHeavy(collection: CollectionModelDetails): boolean {
|
|
113
|
-
const path = collection.path.toLowerCase();
|
|
114
|
+
const path = (collection.path ?? "").toLowerCase();
|
|
114
115
|
if (
|
|
115
116
|
CODE_PATH_HINTS.some(
|
|
116
117
|
(hint) => path.endsWith(hint) || path.includes(`${hint}/`)
|
|
@@ -284,17 +285,19 @@ export function CollectionModelDialog({
|
|
|
284
285
|
active preset for the rest of the workspace.
|
|
285
286
|
</DialogDescription>
|
|
286
287
|
</div>
|
|
287
|
-
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
{collection?.path && (
|
|
289
|
+
<div className="hidden shrink-0 max-w-[200px] space-y-1 border-border/15 border-l pl-4 lg:block">
|
|
290
|
+
<p className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
291
|
+
Path
|
|
292
|
+
</p>
|
|
293
|
+
<p
|
|
294
|
+
className="break-all font-mono text-[10px] leading-relaxed text-muted-foreground/50"
|
|
295
|
+
title={collection.path}
|
|
296
|
+
>
|
|
297
|
+
{collection.path}
|
|
298
|
+
</p>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
298
301
|
</div>
|
|
299
302
|
</DialogHeader>
|
|
300
303
|
|
|
@@ -13,6 +13,8 @@ export function CollectionsEmptyState({
|
|
|
13
13
|
suggestedCollections,
|
|
14
14
|
onAddCollection,
|
|
15
15
|
}: CollectionsEmptyStateProps) {
|
|
16
|
+
// A remote caller's suggestions carry no host path to prefill.
|
|
17
|
+
const quickPicks = suggestedCollections.filter(({ path }) => path);
|
|
16
18
|
return (
|
|
17
19
|
<div className="mx-auto max-w-3xl py-16 text-center">
|
|
18
20
|
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
|
@@ -29,7 +31,7 @@ export function CollectionsEmptyState({
|
|
|
29
31
|
<FolderPlusIcon className="mr-2 size-4" />
|
|
30
32
|
Add Folder
|
|
31
33
|
</Button>
|
|
32
|
-
{
|
|
34
|
+
{quickPicks.map((suggestion) => (
|
|
33
35
|
<Button
|
|
34
36
|
key={suggestion.path}
|
|
35
37
|
onClick={() => onAddCollection(suggestion.path)}
|
|
@@ -40,9 +42,9 @@ export function CollectionsEmptyState({
|
|
|
40
42
|
))}
|
|
41
43
|
</div>
|
|
42
44
|
|
|
43
|
-
{
|
|
45
|
+
{quickPicks.length > 0 && (
|
|
44
46
|
<div className="grid gap-3 text-left md:grid-cols-2">
|
|
45
|
-
{
|
|
47
|
+
{quickPicks.map((suggestion) => (
|
|
46
48
|
<div
|
|
47
49
|
className="rounded-xl border border-border/60 bg-background/70 p-4"
|
|
48
50
|
key={`${suggestion.path}-detail`}
|
|
@@ -235,6 +235,8 @@ export function FirstRunWizard({
|
|
|
235
235
|
? steps[activeIndex + 1]
|
|
236
236
|
: null;
|
|
237
237
|
const progressValue = getStepProgress(onboarding);
|
|
238
|
+
// A remote caller's suggestions carry no host path to prefill.
|
|
239
|
+
const quickPicks = onboarding.suggestedCollections.filter(({ path }) => path);
|
|
238
240
|
const isShowingRecommended = activeStep?.id === recommendedStepId;
|
|
239
241
|
const runRecommendedAction = () => {
|
|
240
242
|
if (!activeStep) {
|
|
@@ -277,7 +279,7 @@ export function FirstRunWizard({
|
|
|
277
279
|
</Button>
|
|
278
280
|
</div>
|
|
279
281
|
|
|
280
|
-
{
|
|
282
|
+
{quickPicks.length > 0 && (
|
|
281
283
|
<div className="space-y-3">
|
|
282
284
|
<div className="flex items-center justify-between gap-3">
|
|
283
285
|
<div>
|
|
@@ -295,7 +297,7 @@ export function FirstRunWizard({
|
|
|
295
297
|
<Badge variant="outline">Quick picks</Badge>
|
|
296
298
|
</div>
|
|
297
299
|
<div className="grid gap-3 md:grid-cols-2">
|
|
298
|
-
{
|
|
300
|
+
{quickPicks.map((suggestion) => (
|
|
299
301
|
<button
|
|
300
302
|
className="rounded-xl border border-border/70 bg-background/80 p-4 text-left shadow-sm transition-all hover:border-primary/35 hover:bg-background hover:shadow-md"
|
|
301
303
|
key={suggestion.path}
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
} from "../../../../sessions/types";
|
|
9
9
|
import { apiFetch } from "../../hooks/use-api";
|
|
10
10
|
import { buildDocDeepLink } from "../../lib/deep-links";
|
|
11
|
+
import { renderSnippet } from "../../lib/snippet";
|
|
11
12
|
import { Badge } from "../ui/badge";
|
|
12
13
|
import { Button } from "../ui/button";
|
|
13
14
|
import { Input } from "../ui/input";
|
|
14
|
-
import { renderSessionSnippet } from "./snippet";
|
|
15
15
|
|
|
16
16
|
interface SessionSearchResult {
|
|
17
17
|
docid: string;
|
|
@@ -274,7 +274,7 @@ export function SessionSearch({ collections, navigate }: SessionSearchProps) {
|
|
|
274
274
|
{result.title || result.uri}
|
|
275
275
|
</a>
|
|
276
276
|
<p className="line-clamp-3 break-words text-muted-foreground text-sm">
|
|
277
|
-
{
|
|
277
|
+
{renderSnippet(result.snippet)}
|
|
278
278
|
</p>
|
|
279
279
|
</li>
|
|
280
280
|
);
|