opencode-rag-plugin 1.21.2 → 1.22.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.
@@ -32,7 +32,11 @@ export function registerUiCommand(program) {
32
32
  const port = parseInt(options.port ?? String(config.ui?.port ?? 3210), 10);
33
33
  const openBrowser = options.open !== false && (config.ui?.openBrowser ?? true);
34
34
  const { startWebUi } = await import("../../web/server.js");
35
- const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config);
35
+ const { findConfigFile } = await import("../../core/config.js");
36
+ const configPath = options.config
37
+ ? path.resolve(cwd, options.config)
38
+ : findConfigFile(cwd);
39
+ const server = await startWebUi(storePath, port, cwd, config.embedding.vectorDimension ?? 384, config, configPath);
36
40
  const url = `http://127.0.0.1:${server.port}/?token=${server.token}`;
37
41
  logCliInfo(logFilePath, "ui", `\n${c.heading("OpenCodeRAG Web UI")}`);
38
42
  logCliInfo(logFilePath, "ui", ` ${c.label("URL:")} ${c.value(url)}`);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { RagConfig } from "../core/config.js";
5
5
  import { type FileManifest } from "../core/manifest.js";
6
- import { type ExcludeMatcher } from "../core/exclude.js";
6
+ import { type ExcludeMatcher, type IncludedMatcher } from "../core/exclude.js";
7
7
  import { DescriptionCache } from "../core/desc-cache.js";
8
8
  import { type ImageVisionProvider } from "../chunker/image.js";
9
9
  /** Metadata and extracted content for a single workspace file discovered during scanning. */
@@ -28,7 +28,7 @@ interface Logger {
28
28
  * Recursively walk a directory tree and collect paths matching the given extension set,
29
29
  * respecting exclusion lists and configurable limits for max directories and results.
30
30
  */
31
- export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: ExcludeMatcher, excludeFiles?: ExcludeMatcher, rootDir?: string, logger?: Logger, dirCount?: {
31
+ export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: ExcludeMatcher, excludeFiles?: ExcludeMatcher, includeDirs?: IncludedMatcher, rootDir?: string, logger?: Logger, dirCount?: {
32
32
  value: number;
33
33
  }, maxDirs?: number, maxResults?: number): Promise<string[]>;
34
34
  /**
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import pLimit from "p-limit";
7
7
  import { computeFileHash, computeDescriptionConfigHash, normalizeFilePath } from "../core/manifest.js";
8
- import { createExcludeMatcher } from "../core/exclude.js";
8
+ import { createExcludeMatcher, createIncludeMatcher } from "../core/exclude.js";
9
9
  import { DescriptionCache } from "../core/desc-cache.js";
10
10
  import { createImageVisionProvider, } from "../chunker/image.js";
11
11
  import * as pdfExtractor from "./pdf.js";
@@ -17,13 +17,14 @@ import * as imageExtractor from "./image.js";
17
17
  * Recursively walk a directory tree and collect paths matching the given extension set,
18
18
  * respecting exclusion lists and configurable limits for max directories and results.
19
19
  */
20
- export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, rootDir = dir, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
20
+ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, includeDirs, rootDir = dir, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
21
21
  const results = [];
22
22
  const entries = await fs.readdir(dir, { withFileTypes: true });
23
23
  for (const entry of entries) {
24
24
  const fullPath = path.join(dir, entry.name);
25
25
  if (entry.isDirectory()) {
26
- if (excludeDirs.excluded(path.relative(rootDir, fullPath)))
26
+ const rel = path.relative(rootDir, fullPath);
27
+ if ((includeDirs && !includeDirs.included(rel)) || excludeDirs.excluded(rel))
27
28
  continue;
28
29
  if (dirCount) {
29
30
  dirCount.value++;
@@ -39,7 +40,7 @@ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, root
39
40
  logger?.warn(`Exceeded ${maxResults} matching files — truncating walk at ${fullPath}`);
40
41
  return results;
41
42
  }
42
- results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, rootDir, logger, dirCount, maxDirs, maxResults)));
43
+ results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, includeDirs, rootDir, logger, dirCount, maxDirs, maxResults)));
43
44
  }
44
45
  else if (entry.isFile()) {
45
46
  if (results.length >= maxResults) {
@@ -104,6 +105,7 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
104
105
  }
105
106
  const excludeDirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
106
107
  const excludeFileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
108
+ const includeDirMatcher = createIncludeMatcher(config.indexing.includeDirs ?? []);
107
109
  let files;
108
110
  if (filterPaths && filterPaths.length > 0) {
109
111
  files = filterPaths
@@ -116,14 +118,14 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
116
118
  const rel = path.relative(cwd, fp);
117
119
  if (rel.startsWith(".."))
118
120
  return false;
119
- return !excludeDirMatcher.excluded(rel) && !excludeFileMatcher.excluded(rel);
121
+ return includeDirMatcher.included(rel) && !excludeDirMatcher.excluded(rel) && !excludeFileMatcher.excluded(rel);
120
122
  });
121
123
  }
122
124
  else {
123
125
  logger?.info("Walking directory tree...");
124
126
  const walkStart = Date.now();
125
127
  const dirCount = { value: 0 };
126
- files = await walkFiles(cwd, extensions, excludeDirMatcher, excludeFileMatcher, cwd, logger, dirCount);
128
+ files = await walkFiles(cwd, extensions, excludeDirMatcher, excludeFileMatcher, includeDirMatcher, cwd, logger, dirCount);
127
129
  const walkSec = ((Date.now() - walkStart) / 1000).toFixed(1);
128
130
  logger?.info(`Found ${files.length} matching files in ${walkSec}s (${dirCount.value} dirs traversed)`);
129
131
  }
@@ -183,6 +183,8 @@ export interface TuiConfig {
183
183
  fileListKeybinding: string;
184
184
  /** Keybinding to toggle the chunk viewer panel. */
185
185
  chunksKeybinding: string;
186
+ /** Keybinding to open the RAG settings dialog. Must be distinguishable by the terminal (see docs). */
187
+ settingsKeybinding: string;
186
188
  }
187
189
  /** Configuration for the standalone MCP (Model Context Protocol) server. */
188
190
  export interface McpConfig {
@@ -265,6 +267,21 @@ export interface RagConfig {
265
267
  * patterns are anchored to the workspace root.
266
268
  */
267
269
  excludeFiles?: string[];
270
+ /**
271
+ * Workspace-relative folder paths to **restrict indexing to**.
272
+ *
273
+ * When non-empty, only files inside these folders (and their
274
+ * subfolders) are indexed; files directly in the workspace root are
275
+ * NOT indexed. `excludeDirs`/`excludeFiles` still apply inside the
276
+ * included folders. Empty or omitted = whole workspace (default).
277
+ *
278
+ * Semantics:
279
+ * - Entries are **anchored to the workspace root** (unlike
280
+ * `excludeDirs`, plain names do NOT match at any depth).
281
+ * - Supports globs (`docs/**`, `src/{a,b}`); use `/` as separator
282
+ * (converted automatically). Matching is case-insensitive.
283
+ */
284
+ includeDirs?: string[];
268
285
  /** Number of overlapping lines between adjacent chunks. */
269
286
  chunkOverlap: number;
270
287
  /** Minimum file size in bytes to index (0 = no minimum). */
@@ -415,3 +432,15 @@ export declare function loadConfig(filePath: string, validate?: boolean): RagCon
415
432
  * @param dimension - The vector dimension to persist.
416
433
  */
417
434
  export declare function persistProbedDimension(configPath: string, dimension: number): void;
435
+ /**
436
+ * Set a value at a dotted path inside the config JSON file, creating
437
+ * intermediate objects as needed, and write it back with 2-space
438
+ * indentation. Reads/writes the raw file, so comments or formatting of
439
+ * unrelated keys are preserved as parsed (the file is re-serialized).
440
+ *
441
+ * @param configPath - Absolute path to the config JSON file.
442
+ * @param path - Dotted path segments, e.g. `["indexing", "includeDirs"]`.
443
+ * @param value - Value to write at the target path.
444
+ * @returns `true` when the file was updated, `false` on any error.
445
+ */
446
+ export declare function updateConfigValue(configPath: string, path: string[], value: unknown): boolean;
@@ -294,6 +294,7 @@ export const DEFAULT_CONFIG = {
294
294
  tui: {
295
295
  fileListKeybinding: "ctrl+enter",
296
296
  chunksKeybinding: "ctrl+alt+enter",
297
+ settingsKeybinding: "ctrl+shift+r",
297
298
  },
298
299
  logging: {
299
300
  level: "info",
@@ -617,4 +618,37 @@ export function persistProbedDimension(configPath, dimension) {
617
618
  // best-effort
618
619
  }
619
620
  }
621
+ /**
622
+ * Set a value at a dotted path inside the config JSON file, creating
623
+ * intermediate objects as needed, and write it back with 2-space
624
+ * indentation. Reads/writes the raw file, so comments or formatting of
625
+ * unrelated keys are preserved as parsed (the file is re-serialized).
626
+ *
627
+ * @param configPath - Absolute path to the config JSON file.
628
+ * @param path - Dotted path segments, e.g. `["indexing", "includeDirs"]`.
629
+ * @param value - Value to write at the target path.
630
+ * @returns `true` when the file was updated, `false` on any error.
631
+ */
632
+ export function updateConfigValue(configPath, path, value) {
633
+ try {
634
+ let raw = readFileSync(configPath, "utf-8");
635
+ if (raw.charCodeAt(0) === 0xfeff)
636
+ raw = raw.slice(1);
637
+ const data = JSON.parse(raw);
638
+ let target = data;
639
+ for (let i = 0; i < path.length - 1; i++) {
640
+ const key = path[i];
641
+ if (!target[key] || typeof target[key] !== "object") {
642
+ target[key] = {};
643
+ }
644
+ target = target[key];
645
+ }
646
+ target[path[path.length - 1]] = value;
647
+ writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
648
+ return true;
649
+ }
650
+ catch {
651
+ return false;
652
+ }
653
+ }
620
654
  //# sourceMappingURL=config.js.map
@@ -1,4 +1,18 @@
1
1
  export interface ExcludeMatcher {
2
2
  excluded(relPath: string): boolean;
3
3
  }
4
+ export interface IncludedMatcher {
5
+ included(relPath: string): boolean;
6
+ }
4
7
  export declare function createExcludeMatcher(patterns: string[]): ExcludeMatcher;
8
+ /**
9
+ * Create a matcher for `indexing.includeDirs`.
10
+ *
11
+ * Semantics differ from {@link createExcludeMatcher}: every pattern is
12
+ * **anchored to the workspace root** — it is matched as a glob against each
13
+ * ancestor prefix of the path, so `docs` includes `<root>/docs` and all its
14
+ * contents, but never a nested `docs` elsewhere in the tree. The workspace
15
+ * root itself (`""`) is always included so a walk can descend into it.
16
+ * Empty pattern lists include everything.
17
+ */
18
+ export declare function createIncludeMatcher(patterns: string[]): IncludedMatcher;
@@ -42,4 +42,41 @@ export function createExcludeMatcher(patterns) {
42
42
  }
43
43
  return { excluded };
44
44
  }
45
+ /**
46
+ * Create a matcher for `indexing.includeDirs`.
47
+ *
48
+ * Semantics differ from {@link createExcludeMatcher}: every pattern is
49
+ * **anchored to the workspace root** — it is matched as a glob against each
50
+ * ancestor prefix of the path, so `docs` includes `<root>/docs` and all its
51
+ * contents, but never a nested `docs` elsewhere in the tree. The workspace
52
+ * root itself (`""`) is always included so a walk can descend into it.
53
+ * Empty pattern lists include everything.
54
+ */
55
+ export function createIncludeMatcher(patterns) {
56
+ const matchers = [];
57
+ for (const pattern of patterns) {
58
+ const pat = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
59
+ if (!pat)
60
+ continue;
61
+ matchers.push(new Minimatch(pat, { nocase: true, dot: true }));
62
+ }
63
+ if (matchers.length === 0) {
64
+ return { included: () => true };
65
+ }
66
+ function included(relPath) {
67
+ const normalized = relPath.replace(/\\/g, "/");
68
+ if (!normalized)
69
+ return true;
70
+ let prefix = "";
71
+ for (const seg of normalized.split("/")) {
72
+ prefix = prefix ? `${prefix}/${seg}` : seg;
73
+ for (const mm of matchers) {
74
+ if (mm.match(prefix) || mm.match(`${prefix}/`))
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ return { included };
81
+ }
45
82
  //# sourceMappingURL=exclude.js.map
@@ -49,6 +49,7 @@ export interface RuntimeOverrides {
49
49
  tui?: {
50
50
  fileListKeybinding?: string;
51
51
  chunksKeybinding?: string;
52
+ settingsKeybinding?: string;
52
53
  };
53
54
  }
54
55
  /** Load runtime overrides from the store directory. Returns empty object if none exist. */
@@ -149,6 +149,7 @@ export function applyRuntimeOverrides(cfg, overrides) {
149
149
  ...(merged.tui ?? {}),
150
150
  fileListKeybinding: overrides.tui.fileListKeybinding ?? merged.tui?.fileListKeybinding ?? DEFAULT_CONFIG.tui.fileListKeybinding,
151
151
  chunksKeybinding: overrides.tui.chunksKeybinding ?? merged.tui?.chunksKeybinding ?? DEFAULT_CONFIG.tui.chunksKeybinding,
152
+ settingsKeybinding: overrides.tui.settingsKeybinding ?? merged.tui?.settingsKeybinding ?? DEFAULT_CONFIG.tui.settingsKeybinding,
152
153
  };
153
154
  }
154
155
  return merged;
@@ -28,7 +28,9 @@ export declare function createWatchPassScheduler(runPass: (filterPaths?: string[
28
28
  /**
29
29
  * Build a predicate that returns `true` for paths that should be ignored by
30
30
  * a file watcher (store directory, manifest file, and configured exclude
31
- * directories).
31
+ * directories). When `indexing.includeDirs` is non-empty, any path outside
32
+ * the included folders is ignored as well, so out-of-scope changes never
33
+ * trigger re-index passes.
32
34
  *
33
35
  * @param cwd - Workspace root directory.
34
36
  * @param config - RAG configuration containing `indexing.excludeDirs`.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import path from "node:path";
5
5
  import { manifestPathFor } from "../core/manifest.js";
6
- import { createExcludeMatcher } from "../core/exclude.js";
6
+ import { createExcludeMatcher, createIncludeMatcher } from "../core/exclude.js";
7
7
  /**
8
8
  * Create a scheduler that debounces calls to a re-index pass. While a pass is
9
9
  * running, subsequent notifications queue a single rerun. Useful for watching
@@ -108,7 +108,9 @@ export function createWatchPassScheduler(runPass, onError, debounceMs = 300) {
108
108
  /**
109
109
  * Build a predicate that returns `true` for paths that should be ignored by
110
110
  * a file watcher (store directory, manifest file, and configured exclude
111
- * directories).
111
+ * directories). When `indexing.includeDirs` is non-empty, any path outside
112
+ * the included folders is ignored as well, so out-of-scope changes never
113
+ * trigger re-index passes.
112
114
  *
113
115
  * @param cwd - Workspace root directory.
114
116
  * @param config - RAG configuration containing `indexing.excludeDirs`.
@@ -120,6 +122,7 @@ export function createWatchIgnore(cwd, config, storePath) {
120
122
  const manifestPath = manifestPathFor(storePath);
121
123
  const dirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
122
124
  const fileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
125
+ const includeMatcher = createIncludeMatcher(config.indexing.includeDirs ?? []);
123
126
  // Prefix check with a trailing separator so sibling dirs like
124
127
  // `<storePath>2` are NOT ignored; case-insensitive on win32 so a
125
128
  // differently-cased store path cannot cause self-triggering watch loops.
@@ -136,7 +139,7 @@ export function createWatchIgnore(cwd, config, storePath) {
136
139
  const relative = path.relative(cwd, resolved);
137
140
  if (!relative || relative.startsWith(".."))
138
141
  return false;
139
- return dirMatcher.excluded(relative) || fileMatcher.excluded(relative);
142
+ return !includeMatcher.included(relative) || dirMatcher.excluded(relative) || fileMatcher.excluded(relative);
140
143
  };
141
144
  }
142
145
  //# sourceMappingURL=watch.js.map
@@ -122,8 +122,7 @@ function dedupeSimilar(results, threshold) {
122
122
  if (sim > threshold) {
123
123
  const [keepIdx, removeIdx] = kept[i].score >= kept[j].score ? [i, j] : [j, i];
124
124
  const removedId = kept[removeIdx].chunk.id;
125
- kept.splice(removeIdx, 1);
126
- kept[keepIdx] = {
125
+ const keeper = {
127
126
  ...kept[keepIdx],
128
127
  optimized: {
129
128
  ...kept[keepIdx].optimized,
@@ -133,6 +132,14 @@ function dedupeSimilar(results, threshold) {
133
132
  ],
134
133
  },
135
134
  };
135
+ if (removeIdx < keepIdx) {
136
+ kept.splice(removeIdx, 1);
137
+ kept[keepIdx - 1] = keeper;
138
+ }
139
+ else {
140
+ kept[keepIdx] = keeper;
141
+ kept.splice(removeIdx, 1);
142
+ }
136
143
  changed = true;
137
144
  break;
138
145
  }
package/dist/tui.js CHANGED
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { loadRuntimeOverrides, saveRuntimeOverride } from "./core/runtime-overrides.js";
10
10
  import { PROVIDER_DEFAULTS } from "./core/provider-defaults.js";
11
- import { loadConfig } from "./core/config.js";
11
+ import { loadConfig, updateConfigValue } from "./core/config.js";
12
12
  import { setPendingRagInjection } from "./core/rag-injection-flag.js";
13
13
  /** Cached plugin version string from package.json. */
14
14
  let _version;
@@ -162,6 +162,7 @@ function renderSidebar(theme, version, status, tuiConfig, tokenStats) {
162
162
  : `Watcher idle \u00B7 last ${formatRelativeTime(watcher.lastRunAt)}`;
163
163
  const fileListKey = tuiConfig?.fileListKeybinding ?? "ctrl+enter";
164
164
  const chunksKey = tuiConfig?.chunksKeybinding ?? "ctrl+alt+enter";
165
+ const settingsKey = tuiConfig?.settingsKeybinding ?? "ctrl+shift+r";
165
166
  return box({
166
167
  width: "100%",
167
168
  flexDirection: "column",
@@ -186,7 +187,7 @@ function renderSidebar(theme, version, status, tuiConfig, tokenStats) {
186
187
  text({ fg: theme.text }, [statusLine]),
187
188
  text({ fg: theme.textMuted }, [timeLine]),
188
189
  text({ fg: watcher.running ? theme.accent : theme.textMuted }, [watcherLine]),
189
- text({ fg: theme.textMuted }, ["Ctrl+Shift+R → Settings"]),
190
+ text({ fg: theme.textMuted }, [`${formatKeybinding(settingsKey)} → Settings`]),
190
191
  text({ fg: theme.textMuted }, [`${formatKeybinding(fileListKey)} → Add File List`]),
191
192
  text({ fg: theme.textMuted }, [`${formatKeybinding(chunksKey)} → Add Chunks`]),
192
193
  ...(tokenStats && tokenStats.queries > 0 ? [
@@ -275,26 +276,6 @@ function resolveProviderBaseUrl(provider) {
275
276
  * Write a single config value at a dotted path into the opencode-rag.json file.
276
277
  * Creates intermediate objects as needed.
277
278
  */
278
- function saveConfigValue(configPath, path, value) {
279
- try {
280
- let raw = readFileSync(configPath, "utf-8");
281
- if (raw.charCodeAt(0) === 0xfeff)
282
- raw = raw.slice(1);
283
- const data = JSON.parse(raw);
284
- let target = data;
285
- for (let i = 0; i < path.length - 1; i++) {
286
- const key = path[i];
287
- if (!target[key] || typeof target[key] !== "object") {
288
- target[key] = {};
289
- }
290
- target = target[key];
291
- }
292
- target[path[path.length - 1]] = value;
293
- writeFileSync(configPath, JSON.stringify(data, null, 2), "utf-8");
294
- }
295
- catch {
296
- }
297
- }
298
279
  /**
299
280
  * Mirror the watcher status file that the server plugin maintains, so the
300
281
  * sidebar reflects a watcher toggle from the settings dialog immediately:
@@ -334,12 +315,12 @@ function saveModelSelection(storePath, configPath, selectionValue, path, provide
334
315
  const ragProvider = providerIdToRagProvider(providerId);
335
316
  const baseUrl = provider ? resolveProviderBaseUrl(provider) : "";
336
317
  saveRuntimeOverride(storePath, [section, "provider"], ragProvider);
337
- saveConfigValue(configPath, [section, "provider"], ragProvider);
318
+ updateConfigValue(configPath, [section, "provider"], ragProvider);
338
319
  saveRuntimeOverride(storePath, [section, "model"], modelId);
339
- saveConfigValue(configPath, [section, "model"], modelId);
320
+ updateConfigValue(configPath, [section, "model"], modelId);
340
321
  if (baseUrl) {
341
322
  saveRuntimeOverride(storePath, [section, "baseUrl"], baseUrl);
342
- saveConfigValue(configPath, [section, "baseUrl"], baseUrl);
323
+ updateConfigValue(configPath, [section, "baseUrl"], baseUrl);
343
324
  }
344
325
  const apiKey = provider?.options?.apiKey ?? "";
345
326
  if (apiKey) {
@@ -609,6 +590,12 @@ function buildSettingCategories(cfg, ro, providers) {
609
590
  label: "Keybindings",
610
591
  description: "Configure keyboard shortcuts",
611
592
  entries: [
593
+ {
594
+ path: ["tui", "settingsKeybinding"],
595
+ label: "Open settings",
596
+ type: "string",
597
+ currentValue: tuiRo.settingsKeybinding ?? tuiCfg.settingsKeybinding ?? "ctrl+shift+r",
598
+ },
612
599
  {
613
600
  path: ["tui", "fileListKeybinding"],
614
601
  label: "Add file list",
@@ -704,7 +691,7 @@ async function openSettingsDialog(api) {
704
691
  else if (entry.type === "boolean") {
705
692
  const newVal = !entry.currentValue;
706
693
  saveRuntimeOverride(storePath, entry.path, newVal);
707
- saveConfigValue(configPath, entry.path, newVal);
694
+ updateConfigValue(configPath, entry.path, newVal);
708
695
  api.ui.toast({
709
696
  variant: "success",
710
697
  title: "Settings",
@@ -730,7 +717,7 @@ async function openSettingsDialog(api) {
730
717
  const num = parseFloat(input);
731
718
  if (!isNaN(num)) {
732
719
  saveRuntimeOverride(storePath, entry.path, num);
733
- saveConfigValue(configPath, entry.path, num);
720
+ updateConfigValue(configPath, entry.path, num);
734
721
  api.ui.toast({
735
722
  variant: "success",
736
723
  title: "Settings",
@@ -763,7 +750,7 @@ async function openSettingsDialog(api) {
763
750
  return;
764
751
  }
765
752
  saveRuntimeOverride(storePath, entry.path, parsed);
766
- saveConfigValue(configPath, entry.path, parsed);
753
+ updateConfigValue(configPath, entry.path, parsed);
767
754
  api.ui.toast({
768
755
  variant: "success",
769
756
  title: "Settings",
@@ -792,7 +779,7 @@ async function openSettingsDialog(api) {
792
779
  value: String(entry.currentValue),
793
780
  onConfirm: (input) => {
794
781
  saveRuntimeOverride(storePath, entry.path, input);
795
- saveConfigValue(configPath, entry.path, input);
782
+ updateConfigValue(configPath, entry.path, input);
796
783
  api.ui.toast({
797
784
  variant: "success",
798
785
  title: "Settings",
@@ -828,7 +815,7 @@ async function openSettingsDialog(api) {
828
815
  }
829
816
  else if (input) {
830
817
  saveRuntimeOverride(storePath, entry.path, input);
831
- saveConfigValue(configPath, entry.path, input);
818
+ updateConfigValue(configPath, entry.path, input);
832
819
  entry.currentValue = input;
833
820
  }
834
821
  api.onSettingsChanged?.();
@@ -971,10 +958,11 @@ const plugin = {
971
958
  // ignore
972
959
  }
973
960
  }
974
- // Register keybinding for settings dialog
961
+ // Register keybinding for settings dialog (configurable)
975
962
  try {
963
+ const settingsKey = tuiConfig?.settingsKeybinding ?? "ctrl+shift+r";
976
964
  api.keymap.registerLayer({
977
- bindings: [{ key: "ctrl+shift+r", cmd: "opencode-rag:settings" }],
965
+ bindings: [{ key: settingsKey, cmd: "opencode-rag:settings" }],
978
966
  commands: [
979
967
  {
980
968
  name: "opencode-rag:settings",
package/dist/web/api.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import type { IncomingMessage, ServerResponse } from "node:http";
5
5
  import { LanceDbStore } from "../vectorstore/lancedb.js";
6
6
  import { KeywordIndex } from "../retriever/keyword-index.js";
7
- import type { RagConfig } from "../core/config.js";
7
+ import { type RagConfig } from "../core/config.js";
8
8
  import { type EmbeddingProvider } from "../core/interfaces.js";
9
9
  /** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
10
10
  interface ApiResponse {
@@ -26,9 +26,12 @@ interface ApiResponse {
26
26
  * @param storePath - Filesystem path to the store directory (used by eval endpoints).
27
27
  * @param cwd - Optional workspace root for resolving file paths.
28
28
  * @param cfg - Active RAG configuration (used by quirk endpoints).
29
+ * @param getEmbedder - Lazily-created embedder for /api/retrieve and reindex passes.
30
+ * @param token - Per-run auth token required on every request.
31
+ * @param configPath - Path to the opencode-rag.json config file, used by PUT /api/config.
29
32
  * @returns An async handler that returns `true` when a route matched or `false` otherwise.
30
33
  */
31
- export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
34
+ export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string, configPath?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
32
35
  /**
33
36
  * Perform token-usage analysis for a single evaluation session.
34
37
  *