@javierkaiser/opencode-indexing 0.1.1 → 0.2.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/CHANGELOG.md +157 -2
- package/README.md +192 -21
- package/index.ts +133 -17
- package/install.mjs +229 -128
- package/install.ps1 +7 -1
- package/install.sh +1 -1
- package/package.json +6 -2
- package/src/config.ts +1 -1
- package/src/embedder.ts +339 -99
- package/src/import.ts +30 -9
- package/src/indexer.ts +23 -1
- package/src/registry.ts +105 -19
- package/src/rpc.ts +27 -3
- package/src/search.ts +41 -2
- package/src/settings-commands.ts +6 -4
- package/src/settings.ts +28 -13
- package/src/status.ts +52 -5
- package/src/tools.ts +65 -20
- package/src/types.ts +32 -0
- package/tui/settings-page-data.ts +31 -15
- package/tui/settings-page.tsx +7 -1
- package/tui/workspace-rpc.ts +98 -0
- package/tui.ts +112 -41
package/tui.ts
CHANGED
|
@@ -24,10 +24,13 @@ import type { ThemeLike } from "./tui/page-style.ts"
|
|
|
24
24
|
import { PAGE_ACTIONS, PAGE_ROWS, type SettingsPageRpc } from "./tui/settings-page-data.ts"
|
|
25
25
|
import { footerAction, footerState, type IndexingState } from "./tui/footer.ts"
|
|
26
26
|
import { activeWorkspace, type WorkspaceSources } from "./tui/footer.ts"
|
|
27
|
+
import { bindWorkspace } from "./tui/workspace-rpc.ts"
|
|
27
28
|
import { describeRow, forgetWorkspaceFlow, type WorkspacesRpc } from "./tui/workspaces.ts"
|
|
28
29
|
import { pagePalette } from "./tui/page-style.ts"
|
|
29
30
|
import type { FooterHandle, FooterState } from "./tui/footer-view.tsx"
|
|
30
31
|
import { describeError } from "./src/errors.ts"
|
|
32
|
+
import { getDefaultModelId, isLiveTested, listModelIds } from "./src/registry.ts"
|
|
33
|
+
import type { EmbedderProvider } from "./src/types.ts"
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* Navigable rows on the page.
|
|
@@ -40,11 +43,21 @@ const PAGE_ROW_COUNT = PAGE_ROWS.length
|
|
|
40
43
|
|
|
41
44
|
const ID = "opencode.indexing"
|
|
42
45
|
|
|
46
|
+
/** Sentinel for the "type it yourself" entry in the model chooser. */
|
|
47
|
+
const CUSTOM_MODEL = "\u0000custom-model"
|
|
48
|
+
|
|
43
49
|
type RpcClient = {
|
|
44
50
|
"settings.get": (input: Record<string, never>) => Promise<{ settings: SettingsView }>
|
|
45
51
|
"settings.set": (input: { patch: Record<string, unknown> }) => Promise<{ settings: SettingsView }>
|
|
46
|
-
"settings.test": (input: {
|
|
47
|
-
|
|
52
|
+
"settings.test": (input: {
|
|
53
|
+
target: "qdrant" | "lancedb" | "provider" | "kilo"
|
|
54
|
+
directory?: string
|
|
55
|
+
}) => Promise<{ ok: boolean; message: string }>
|
|
56
|
+
"kilo.discover": (input?: { directory?: string }) => Promise<{
|
|
57
|
+
sources: Array<{ kind: string; name: string; pointsCount: number | null; compatible: boolean; profile: string | null }>
|
|
58
|
+
/** Sources that exist but could not be read. */
|
|
59
|
+
problems?: string[]
|
|
60
|
+
}>
|
|
48
61
|
"status.get": (input: { checkFreshness?: boolean; directory?: string }) => Promise<{ summary: string; recommendation: string; ownKind: string; ownStore: string; ownPoints?: number | null; ownComplete?: boolean | null; kiloCollection?: string | null; directory?: string; ownStoreReachable?: boolean }>
|
|
49
62
|
"index.build": (input: { rebuild?: boolean; skipImport?: boolean; directory?: string }) => Promise<{ summary: string; error?: string }>
|
|
50
63
|
"index.refresh": (input: { maxFiles?: number; directory?: string }) => Promise<{ summary: string; error?: string }>
|
|
@@ -64,18 +77,54 @@ interface SettingsView {
|
|
|
64
77
|
/** Master switch for writing the own index. */
|
|
65
78
|
enabled: boolean
|
|
66
79
|
searchMaxResults: number
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
/** Providers that have a usable credential or base URL configured. */
|
|
81
|
+
configuredProviders: string[]
|
|
69
82
|
settingsFile: string
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
const plugin = {
|
|
73
86
|
id: ID,
|
|
74
87
|
async setup(context: Plugin.Context): Promise<void> {
|
|
88
|
+
/**
|
|
89
|
+
* The workspace the caller is working in.
|
|
90
|
+
*
|
|
91
|
+
* The server plugin serves its own instance directory, so every RPC that
|
|
92
|
+
* reads or writes an index must be told which workspace it is about. Without
|
|
93
|
+
* this the footer would report — and the palette command would index — the
|
|
94
|
+
* service's directory instead of the user's: the ACTIVE tab is what follows
|
|
95
|
+
* the user, and the instance directory is the fallback for a host with tabs
|
|
96
|
+
* switched off.
|
|
97
|
+
*
|
|
98
|
+
* Every accessor is optional: a host may expose neither `ui.tabs` nor
|
|
99
|
+
* `data.session`, and resolving the workspace must degrade rather than
|
|
100
|
+
* throw. Read from a reactive context (the footer slot render) the lookups
|
|
101
|
+
* re-run when the host's tab or session state changes.
|
|
102
|
+
*
|
|
103
|
+
* Declared before `rpc` because `rpc` binds the directory into every
|
|
104
|
+
* workspace-scoped call: a call made before this binding exists would be
|
|
105
|
+
* answered against the service's own directory.
|
|
106
|
+
*/
|
|
107
|
+
const workspaceSources: WorkspaceSources = {
|
|
108
|
+
tabsEnabled: () => context.ui.tabs?.enabled?.() === true,
|
|
109
|
+
tabs: () => context.ui.tabs?.list?.() ?? [],
|
|
110
|
+
sessions: () => context.data?.session?.list?.() ?? [],
|
|
111
|
+
instanceDirectory: () => context.location?.directory,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const workspace = (): string | undefined => activeWorkspace(workspaceSources)
|
|
115
|
+
|
|
75
116
|
// Resolve the RPC subclient lazily: the TUI may start before the server
|
|
76
117
|
// plugin is ready, and calls are retried on first use.
|
|
118
|
+
//
|
|
119
|
+
// Every call goes through `bindWorkspace`, so a workspace-scoped method
|
|
120
|
+
// always names the caller's directory. Passing it per call site is what
|
|
121
|
+
// failed before: the footer did it and the palette did not, and the palette
|
|
122
|
+
// silently read and wrote the service's index instead of the user's.
|
|
77
123
|
const rpc = (): RpcClient =>
|
|
78
|
-
(
|
|
124
|
+
bindWorkspace(
|
|
125
|
+
(context.client as unknown as { rpc: (def: unknown, options?: unknown) => RpcClient }).rpc(IndexingRpc),
|
|
126
|
+
workspace,
|
|
127
|
+
)
|
|
79
128
|
|
|
80
129
|
const toast = (message: string, variant: "success" | "error" | "info" = "info"): void => {
|
|
81
130
|
context.ui.toast.show({ title: "Indexing", message, variant, duration: 5000 })
|
|
@@ -334,13 +383,49 @@ const plugin = {
|
|
|
334
383
|
})
|
|
335
384
|
if (typeof provider !== "string") return
|
|
336
385
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
386
|
+
// Offer the catalog instead of a blank text field. The model list existed
|
|
387
|
+
// all along and was never shown, so the only way to discover a valid model
|
|
388
|
+
// id was to already know it.
|
|
389
|
+
const catalog = listModelIds(provider as EmbedderProvider)
|
|
390
|
+
const defaultModel = getDefaultModelId(provider as EmbedderProvider)
|
|
391
|
+
const tested = isLiveTested(provider as EmbedderProvider)
|
|
392
|
+
const choice = await context.ui.dialog.select({
|
|
393
|
+
title: `Model for ${provider}`,
|
|
394
|
+
options: [
|
|
395
|
+
{
|
|
396
|
+
title: defaultModel ? `${defaultModel} (default)` : "(provider default)",
|
|
397
|
+
value: "",
|
|
398
|
+
description: tested
|
|
399
|
+
? "Leave the model unset so the provider default applies"
|
|
400
|
+
: "Not yet exercised against the real API; see the README",
|
|
401
|
+
},
|
|
402
|
+
...catalog
|
|
403
|
+
.filter((modelId) => modelId !== defaultModel)
|
|
404
|
+
.map((modelId) => ({ title: modelId, value: modelId, description: `${provider} model` })),
|
|
405
|
+
{
|
|
406
|
+
title: "Custom…",
|
|
407
|
+
value: CUSTOM_MODEL,
|
|
408
|
+
description: catalog.length > 0 ? "Type a model id that is not in the list" : "This provider has no catalog; type the model id",
|
|
409
|
+
},
|
|
410
|
+
],
|
|
340
411
|
})
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
412
|
+
if (typeof choice !== "string") return
|
|
413
|
+
|
|
414
|
+
let model = choice
|
|
415
|
+
if (choice === CUSTOM_MODEL) {
|
|
416
|
+
const typed = await context.ui.dialog.prompt({
|
|
417
|
+
title: `Model id for ${provider}`,
|
|
418
|
+
placeholder: defaultModel || "e.g. bge-m3",
|
|
419
|
+
})
|
|
420
|
+
if (typed === undefined) return
|
|
421
|
+
model = typed.trim()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// `model` is always written, empty when the user accepted the default.
|
|
425
|
+
// Omitting it left the previous provider's model in the file, where it
|
|
426
|
+
// outranks the new provider's default: switching to voyage produced
|
|
427
|
+
// `voyage` + `codestral-embed-2505`.
|
|
428
|
+
await patchSettings({ provider, model })
|
|
344
429
|
|
|
345
430
|
// API key for the provider (masked prompt).
|
|
346
431
|
const keyLabel =
|
|
@@ -357,7 +442,7 @@ const plugin = {
|
|
|
357
442
|
: null
|
|
358
443
|
|
|
359
444
|
if (keyLabel) {
|
|
360
|
-
const current =
|
|
445
|
+
const current = settings.configuredProviders.includes(provider)
|
|
361
446
|
const key = await context.ui.dialog.prompt({
|
|
362
447
|
title: `${keyLabel} (leave empty to keep current)`,
|
|
363
448
|
placeholder: current ? "•••••• (set)" : "not set",
|
|
@@ -405,14 +490,24 @@ const plugin = {
|
|
|
405
490
|
|
|
406
491
|
async function kiloMenu(): Promise<void> {
|
|
407
492
|
let sources: Array<{ kind: string; name: string; pointsCount: number | null; compatible: boolean; profile: string | null }> = []
|
|
493
|
+
let problems: string[] = []
|
|
408
494
|
try {
|
|
409
|
-
|
|
495
|
+
const found = await rpc()["kilo.discover"]({})
|
|
496
|
+
sources = found.sources
|
|
497
|
+
problems = found.problems ?? []
|
|
410
498
|
} catch (error) {
|
|
411
499
|
fail(error)
|
|
412
500
|
return
|
|
413
501
|
}
|
|
414
502
|
if (sources.length === 0) {
|
|
415
|
-
|
|
503
|
+
// "Found but unreadable" and "never indexed" look identical from here,
|
|
504
|
+
// and only one of them is fixed by indexing. Say which one it is.
|
|
505
|
+
toast(
|
|
506
|
+
problems.length > 0
|
|
507
|
+
? `No importable Kilo index: ${problems.join(" · ")}`
|
|
508
|
+
: "No Kilo index found for this workspace",
|
|
509
|
+
problems.length > 0 ? "error" : "info",
|
|
510
|
+
)
|
|
416
511
|
return
|
|
417
512
|
}
|
|
418
513
|
const choice = await context.ui.dialog.select({
|
|
@@ -461,7 +556,7 @@ const plugin = {
|
|
|
461
556
|
toast("Running…", "info")
|
|
462
557
|
try {
|
|
463
558
|
if (action === "refresh") {
|
|
464
|
-
reportRun(await rpc()["index.refresh"]({
|
|
559
|
+
reportRun(await rpc()["index.refresh"]({}))
|
|
465
560
|
} else if (action === "build") {
|
|
466
561
|
reportRun(await rpc()["index.build"]({}))
|
|
467
562
|
} else if (action === "build-scratch") {
|
|
@@ -541,30 +636,6 @@ const plugin = {
|
|
|
541
636
|
return route?.type === "plugin" && route.name === SETTINGS_PAGE_NAME
|
|
542
637
|
}
|
|
543
638
|
|
|
544
|
-
/**
|
|
545
|
-
* The workspace the caller is working in.
|
|
546
|
-
*
|
|
547
|
-
* The server plugin serves its own instance directory, so every RPC that
|
|
548
|
-
* reads or writes an index must be told which workspace it is about. Without
|
|
549
|
-
* this the footer would report — and the palette command would index — the
|
|
550
|
-
* service's directory instead of the user's: the ACTIVE tab is what follows
|
|
551
|
-
* the user, and the instance directory is the fallback for a host with tabs
|
|
552
|
-
* switched off.
|
|
553
|
-
*
|
|
554
|
-
* Every accessor is optional: a host may expose neither `ui.tabs` nor
|
|
555
|
-
* `data.session`, and resolving the workspace must degrade rather than
|
|
556
|
-
* throw. Read from a reactive context (the footer slot render) the lookups
|
|
557
|
-
* re-run when the host's tab or session state changes.
|
|
558
|
-
*/
|
|
559
|
-
const workspaceSources: WorkspaceSources = {
|
|
560
|
-
tabsEnabled: () => context.ui.tabs?.enabled?.() === true,
|
|
561
|
-
tabs: () => context.ui.tabs?.list?.() ?? [],
|
|
562
|
-
sessions: () => context.data?.session?.list?.() ?? [],
|
|
563
|
-
instanceDirectory: () => context.location?.directory,
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const workspace = (): string | undefined => activeWorkspace(workspaceSources)
|
|
567
|
-
|
|
568
639
|
/** Opens the dialog editor, holding the page keys off for its duration. */
|
|
569
640
|
async function openEditor(): Promise<void> {
|
|
570
641
|
dialogOpen = true
|
|
@@ -779,8 +850,8 @@ const plugin = {
|
|
|
779
850
|
toast("Indexing started…", "info")
|
|
780
851
|
const summary =
|
|
781
852
|
from.state === "stale"
|
|
782
|
-
? await rpc()["index.refresh"]({
|
|
783
|
-
: await rpc()["index.build"]({ skipImport: false
|
|
853
|
+
? await rpc()["index.refresh"]({})
|
|
854
|
+
: await rpc()["index.build"]({ skipImport: false })
|
|
784
855
|
// A failed run comes back as data, not as a rejection: the reason is far
|
|
785
856
|
// more useful than "something went wrong", and it is already readable.
|
|
786
857
|
reportRun(summary)
|