@mrclrchtr/supi-tree-sitter 1.4.0 → 1.5.0

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.
Files changed (30) hide show
  1. package/README.md +18 -6
  2. package/node_modules/@mrclrchtr/supi-core/README.md +107 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +44 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +85 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +76 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +186 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/index.ts +85 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  21. package/package.json +8 -3
  22. package/src/api.ts +5 -1
  23. package/src/index.ts +5 -1
  24. package/src/session/runtime.ts +3 -2
  25. package/src/session/service-registry.ts +30 -0
  26. package/src/session/session.ts +16 -8
  27. package/src/tool/action-specs.ts +92 -0
  28. package/src/tool/guidance.ts +12 -3
  29. package/src/tree-sitter.ts +111 -61
  30. package/src/types.ts +13 -2
@@ -0,0 +1,86 @@
1
+ // Shared registry utility for SuPi extensions.
2
+ //
3
+ // Provides a globalThis-backed registry pattern so that all jiti module instances
4
+ // (resolved through different node_modules symlinks) share the same Map.
5
+ // Without this, each symlink path gets its own module copy and its own Map,
6
+ // so registrations from one instance are invisible to consumers in another.
7
+
8
+ import * as path from "node:path";
9
+
10
+ const SYMBOL_PREFIX = "@mrclrchtr/supi-core/";
11
+
12
+ function getGlobalRegistryMap<T>(name: string): Map<string, T> {
13
+ const key = Symbol.for(SYMBOL_PREFIX + name);
14
+ let map = (globalThis as Record<symbol, unknown>)[key] as Map<string, T> | undefined;
15
+ if (!map) {
16
+ map = new Map<string, T>();
17
+ (globalThis as Record<symbol, unknown>)[key] = map;
18
+ }
19
+ return map;
20
+ }
21
+
22
+ /**
23
+ * Create a named registry backed by `globalThis` + `Symbol.for`.
24
+ *
25
+ * The registry is lazily initialized on first access and shared across all
26
+ * jiti module instances via the global symbol namespace.
27
+ *
28
+ * @typeParam T - The value type stored in the registry.
29
+ * @param name - Unique registry name (used to construct the `Symbol.for` key).
30
+ * @returns An object with `register`, `getAll`, and `clear` functions.
31
+ */
32
+ export function createRegistry<T>(name: string) {
33
+ const getMap = (): Map<string, T> => getGlobalRegistryMap<T>(name);
34
+
35
+ return {
36
+ /**
37
+ * Register a value by id. Duplicate ids silently replace the previous registration.
38
+ */
39
+ register: (id: string, value: T): void => {
40
+ getMap().set(id, value);
41
+ },
42
+
43
+ /**
44
+ * Get all registered values in registration order.
45
+ */
46
+ getAll: (): T[] => {
47
+ return Array.from(getMap().values());
48
+ },
49
+
50
+ /**
51
+ * Clear all entries from the registry (primarily for tests).
52
+ */
53
+ clear: (): void => {
54
+ getMap().clear();
55
+ },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a named session-state registry keyed by normalized cwd.
61
+ *
62
+ * This helper is intended for session-scoped runtime services that should be
63
+ * shared across duplicate jiti module instances while keeping package-specific
64
+ * state unions and convenience wrappers local to the calling package.
65
+ */
66
+ export function createSessionStateRegistry<TState>(name: string) {
67
+ const getMap = (): Map<string, TState> => getGlobalRegistryMap<TState>(name);
68
+ const normalizeCwd = (cwd: string): string => path.resolve(cwd);
69
+
70
+ return {
71
+ /** Get the current state for one session cwd. */
72
+ get: (cwd: string): TState | undefined => {
73
+ return getMap().get(normalizeCwd(cwd));
74
+ },
75
+
76
+ /** Store the current state for one session cwd. */
77
+ set: (cwd: string, state: TState): void => {
78
+ getMap().set(normalizeCwd(cwd), state);
79
+ },
80
+
81
+ /** Clear the current state for one session cwd. */
82
+ clear: (cwd: string): void => {
83
+ getMap().delete(normalizeCwd(cwd));
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,29 @@
1
+ // Generic session-file tree-walking utilities.
2
+
3
+ import type { FileEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
4
+
5
+ /**
6
+ * Resolve the active branch path using PI's append-only tree semantics.
7
+ *
8
+ * The active branch is the path from the **last entry** (current leaf)
9
+ * back to the root via `parentId`. This follows PI's tree structure where
10
+ * entries are append-only and the last entry in the file is always the
11
+ * current leaf of the active branch.
12
+ */
13
+ export function getActiveBranchEntries(entries: FileEntry[]): SessionEntry[] {
14
+ const sessionEntries = entries.filter((e): e is SessionEntry => e.type !== "session");
15
+ const byId = new Map(sessionEntries.map((entry) => [entry.id, entry]));
16
+ const leaf = sessionEntries.at(-1);
17
+ if (!leaf) return [];
18
+
19
+ const path: SessionEntry[] = [];
20
+ const visited = new Set<string>();
21
+ let current: SessionEntry | undefined = leaf;
22
+ while (current) {
23
+ if (visited.has(current.id)) break;
24
+ visited.add(current.id);
25
+ path.unshift(current);
26
+ current = current.parentId ? byId.get(current.parentId) : undefined;
27
+ }
28
+ return path;
29
+ }
@@ -0,0 +1,15 @@
1
+ // /supi-settings command registration.
2
+ //
3
+ // Thin wrapper that registers the command and delegates to openSettingsOverlay.
4
+
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { openSettingsOverlay } from "./settings-ui.ts";
7
+
8
+ export function registerSettingsCommand(pi: ExtensionAPI): void {
9
+ pi.registerCommand("supi-settings", {
10
+ description: "Manage SuPi extension settings",
11
+ handler: async (_args, ctx) => {
12
+ openSettingsOverlay(ctx);
13
+ },
14
+ });
15
+ }
@@ -0,0 +1,41 @@
1
+ // Settings registry for SuPi extensions.
2
+ //
3
+ // Extensions declare their settings via `registerSettings()` during their
4
+ // factory function. The generic settings UI reads them via `getRegisteredSettings()`.
5
+
6
+ import type { SettingItem } from "@earendil-works/pi-tui";
7
+ import { createRegistry } from "../registry-utils.ts";
8
+
9
+ export type SettingsScope = "project" | "global";
10
+
11
+ export interface SettingsSection {
12
+ /** Extension identifier — e.g. "lsp", "claude-md" */
13
+ id: string;
14
+ /** Human-readable label shown in the UI */
15
+ label: string;
16
+ /** Load current SettingItem[] for the given scope */
17
+ loadValues: (scope: SettingsScope, cwd: string) => SettingItem[];
18
+ /** Persist a change back to config */
19
+ persistChange: (scope: SettingsScope, cwd: string, settingId: string, value: string) => void;
20
+ }
21
+
22
+ const registry = createRegistry<SettingsSection>("settings-registry");
23
+
24
+ /**
25
+ * Register a settings section for an extension.
26
+ * Call during the extension factory function (not async handlers).
27
+ * Duplicate ids replace the previous registration.
28
+ */
29
+ export function registerSettings(section: SettingsSection): void {
30
+ registry.register(section.id, section);
31
+ }
32
+
33
+ /** Get all registered settings sections in registration order. */
34
+ export function getRegisteredSettings(): SettingsSection[] {
35
+ return registry.getAll();
36
+ }
37
+
38
+ /** Clear the registry — used by tests. */
39
+ export function clearRegisteredSettings(): void {
40
+ registry.clear();
41
+ }
@@ -0,0 +1,226 @@
1
+ // Generic settings overlay for SuPi extensions.
2
+ //
3
+ // Uses pi-tui's SettingsList with scope toggle (Tab), extension grouping,
4
+ // and search. Each extension declares its settings via registerSettings().
5
+
6
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ Container,
10
+ Input,
11
+ Key,
12
+ matchesKey,
13
+ type SettingItem,
14
+ SettingsList,
15
+ Text,
16
+ } from "@earendil-works/pi-tui";
17
+ import {
18
+ getRegisteredSettings,
19
+ type SettingsScope,
20
+ type SettingsSection,
21
+ } from "./settings-registry.ts";
22
+
23
+ // ── Input submenu component ──────────────────────────────────
24
+
25
+ /**
26
+ * Creates a pi-tui Input-backed submenu component with enter-to-confirm
27
+ * and escape-to-cancel handling.
28
+ *
29
+ * @param currentValue - Initial value for the text input.
30
+ * @param label - Label text displayed above the input.
31
+ * @param done - Callback invoked with the confirmed value, or undefined on cancel.
32
+ */
33
+ export function createInputSubmenu(
34
+ currentValue: string,
35
+ label: string,
36
+ done: (selectedValue?: string) => void,
37
+ ): {
38
+ render: (width: number) => string[];
39
+ invalidate: () => void;
40
+ handleInput: (data: string) => boolean;
41
+ } {
42
+ const input = new Input();
43
+ input.setValue(currentValue);
44
+
45
+ return {
46
+ render: (_width: number) => {
47
+ const lines = [` ${label}`];
48
+ lines.push(...input.render(_width));
49
+ lines.push(" enter confirm • esc cancel");
50
+ return lines;
51
+ },
52
+ invalidate: () => {
53
+ input.invalidate();
54
+ },
55
+ handleInput: (data: string) => {
56
+ if (matchesKey(data, Key.escape)) {
57
+ done();
58
+ return true;
59
+ }
60
+ if (matchesKey(data, Key.enter)) {
61
+ done(input.getValue());
62
+ return true;
63
+ }
64
+ input.handleInput(data);
65
+ return true;
66
+ },
67
+ };
68
+ }
69
+
70
+ // ── Types ────────────────────────────────────────────────────
71
+
72
+ interface OverlayState {
73
+ scope: SettingsScope;
74
+ cwd: string;
75
+ }
76
+
77
+ // ── Pure helpers ─────────────────────────────────────────────
78
+
79
+ function getScopeLabel(scope: SettingsScope): string {
80
+ return scope === "project" ? "Project" : "Global";
81
+ }
82
+
83
+ function buildFlatItems(
84
+ sections: SettingsSection[],
85
+ scope: SettingsScope,
86
+ cwd: string,
87
+ ): SettingItem[] {
88
+ const items: SettingItem[] = [];
89
+ for (const section of sections) {
90
+ const sectionItems = section.loadValues(scope, cwd);
91
+ for (const item of sectionItems) {
92
+ items.push({
93
+ ...item,
94
+ id: `${section.id}.${item.id}`,
95
+ label: `${section.label}: ${item.label}`,
96
+ });
97
+ }
98
+ }
99
+ return items;
100
+ }
101
+
102
+ function findSectionAndId(
103
+ sections: SettingsSection[],
104
+ flatId: string,
105
+ ): { section: SettingsSection; itemId: string } | null {
106
+ const dotIndex = flatId.indexOf(".");
107
+ if (dotIndex === -1) return null;
108
+ const sectionId = flatId.slice(0, dotIndex);
109
+ const itemId = flatId.slice(dotIndex + 1);
110
+ const section = sections.find((s) => s.id === sectionId);
111
+ if (!section) return null;
112
+ return { section, itemId };
113
+ }
114
+
115
+ // ── Component ────────────────────────────────────────────────
116
+
117
+ interface SettingsOverlayDeps {
118
+ state: OverlayState;
119
+ container: Container;
120
+ settingsList: SettingsList | null;
121
+ tui: Parameters<Parameters<ExtensionContext["ui"]["custom"]>[0]>[0];
122
+ theme: Parameters<Parameters<ExtensionContext["ui"]["custom"]>[0]>[1];
123
+ done: () => void;
124
+ }
125
+
126
+ function createSettingsList(deps: SettingsOverlayDeps): SettingsList {
127
+ const sections = getRegisteredSettings();
128
+ const items = buildFlatItems(sections, deps.state.scope, deps.state.cwd);
129
+ const onChange = (flatId: string, newValue: string) => {
130
+ const found = findSectionAndId(sections, flatId);
131
+ if (found) {
132
+ found.section.persistChange(deps.state.scope, deps.state.cwd, found.itemId, newValue);
133
+ }
134
+ // Re-read all values to reflect persisted changes, but keep the list
135
+ // instance (and its selectedIndex) intact.
136
+ const updatedItems = buildFlatItems(sections, deps.state.scope, deps.state.cwd);
137
+ for (const updated of updatedItems) {
138
+ const existing = items.find((i) => i.id === updated.id);
139
+ if (existing && existing.currentValue !== updated.currentValue) {
140
+ settingsList.updateValue(updated.id, updated.currentValue);
141
+ }
142
+ }
143
+ deps.tui.requestRender();
144
+ };
145
+ const settingsList = new SettingsList(
146
+ items,
147
+ Math.min(items.length + 4, 20),
148
+ getSettingsListTheme(),
149
+ onChange,
150
+ () => deps.done(),
151
+ { enableSearch: true },
152
+ );
153
+ return settingsList;
154
+ }
155
+
156
+ function rebuildSettingsList(deps: SettingsOverlayDeps): SettingsList {
157
+ const settingsList = createSettingsList(deps);
158
+ deps.settingsList = settingsList;
159
+
160
+ deps.container.clear();
161
+ deps.container.addChild(createHeaderComponent(deps));
162
+ deps.container.addChild(settingsList);
163
+
164
+ return settingsList;
165
+ }
166
+
167
+ function createHeaderComponent(deps: SettingsOverlayDeps): Text {
168
+ const { theme, state } = deps;
169
+ const scopeLabel = getScopeLabel(state.scope);
170
+ const otherScope = state.scope === "project" ? "Global" : "Project";
171
+ const headerText = new Text(
172
+ `${theme.fg("accent", theme.bold("SuPi Settings"))} ${theme.fg("text", `Scope: ${scopeLabel}`)} ${theme.fg("dim", `(tab → ${otherScope})`)}`,
173
+ 0,
174
+ 0,
175
+ );
176
+ return headerText;
177
+ }
178
+
179
+ function handleScopeToggle(deps: SettingsOverlayDeps): void {
180
+ deps.state.scope = deps.state.scope === "project" ? "global" : "project";
181
+ rebuildSettingsList(deps);
182
+ deps.tui.requestRender();
183
+ }
184
+
185
+ // ── Entry point ──────────────────────────────────────────────
186
+
187
+ export function openSettingsOverlay(ctx: ExtensionContext): void {
188
+ const sections = getRegisteredSettings();
189
+ if (sections.length === 0) {
190
+ ctx.ui.notify("No settings registered by SuPi extensions", "info");
191
+ return;
192
+ }
193
+
194
+ void ctx.ui.custom<void>((tui, theme, _kb, done) => {
195
+ const state: OverlayState = { scope: "project", cwd: ctx.cwd };
196
+ const container = new Container();
197
+
198
+ const deps: SettingsOverlayDeps = {
199
+ state,
200
+ container,
201
+ settingsList: null,
202
+ tui,
203
+ theme,
204
+ done,
205
+ };
206
+
207
+ rebuildSettingsList(deps);
208
+
209
+ const component = {
210
+ render: (width: number) => container.render(width),
211
+ invalidate: () => container.invalidate(),
212
+ handleInput: (data: string) => {
213
+ if (matchesKey(data, Key.tab)) {
214
+ handleScopeToggle(deps);
215
+ return true;
216
+ }
217
+ // Delegate input to the settings list (always set after rebuildSettingsList)
218
+ deps.settingsList?.handleInput?.(data);
219
+ deps.tui.requestRender();
220
+ return true;
221
+ },
222
+ };
223
+
224
+ return component;
225
+ });
226
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared terminal title formatting and signaling utilities.
3
+ *
4
+ * Centralized place for pi title convention (π prefix), completion (✓)
5
+ * and waiting (●) indicators, and the audible terminal bell.
6
+ */
7
+ import path from "node:path";
8
+
9
+ /** Unicode checkmark shown when the agent finishes a turn. */
10
+ export const DONE_SYMBOL = "\u2713";
11
+ /** Unicode dot shown when waiting for user input. */
12
+ export const WAITING_SYMBOL = "\u25CF";
13
+
14
+ /** Minimal UI surface needed for title operations. */
15
+ export interface TitleTarget {
16
+ ui: {
17
+ setTitle?(title: string): void;
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Format pi's canonical terminal title from session name and cwd.
23
+ * Falls back gracefully when either is missing.
24
+ *
25
+ * @example
26
+ * formatTitle("my-session", "/home/projects/foo") // "π - my-session - foo"
27
+ * formatTitle(undefined, "/home/projects/foo") // "π - foo"
28
+ * formatTitle("my-session") // "π - my-session"
29
+ * formatTitle() // "π"
30
+ */
31
+ export function formatTitle(sessionName?: string, cwd?: string): string {
32
+ const base = cwd ? path.basename(cwd) : undefined;
33
+ if (sessionName && base) return `π - ${sessionName} - ${base}`;
34
+ if (sessionName) return `π - ${sessionName}`;
35
+ if (base) return `π - ${base}`;
36
+ return "π";
37
+ }
38
+
39
+ /** Sound the audible terminal bell (ASCII BEL). */
40
+ export function signalBell(): void {
41
+ process.stdout.write("\x07");
42
+ }
43
+
44
+ /**
45
+ * Set the terminal title to indicate the agent is waiting for user input.
46
+ * Prefixes with ● and sounds the terminal bell.
47
+ */
48
+ export function signalWaiting(ctx: TitleTarget, title: string): void {
49
+ ctx.ui.setTitle?.(`${WAITING_SYMBOL} ${title}`);
50
+ signalBell();
51
+ }
52
+
53
+ /**
54
+ * Set the terminal title to indicate the agent turn has completed.
55
+ * Prefixes with ✓ and sounds the terminal bell.
56
+ */
57
+ export function signalDone(ctx: TitleTarget, title: string): void {
58
+ ctx.ui.setTitle?.(`${DONE_SYMBOL} ${title}`);
59
+ signalBell();
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "SuPi Tree-sitter extension — structural AST analysis for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,8 @@
23
23
  "!__tests__"
24
24
  ],
25
25
  "dependencies": {
26
- "web-tree-sitter": "^0.26.8"
26
+ "web-tree-sitter": "^0.26.8",
27
+ "@mrclrchtr/supi-core": "1.5.0"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "@earendil-works/pi-ai": "*",
@@ -43,9 +44,13 @@
43
44
  },
44
45
  "pi": {
45
46
  "extensions": [
46
- "./src/extension.ts"
47
+ "./src/extension.ts",
48
+ "node_modules/@mrclrchtr/supi-core/src/extension.ts"
47
49
  ]
48
50
  },
51
+ "bundledDependencies": [
52
+ "@mrclrchtr/supi-core"
53
+ ],
49
54
  "main": "src/api.ts",
50
55
  "exports": {
51
56
  "./api": "./src/api.ts",
package/src/api.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Public tree-sitter session factory and shared types.
1
+ // Public tree-sitter session factory, shared session service access, and shared types.
2
2
 
3
+ export { getSessionTreeSitterService } from "./session/service-registry.ts";
3
4
  export { createTreeSitterSession } from "./session/session.ts";
4
5
  export type {
5
6
  CalleesAtResult,
@@ -9,8 +10,11 @@ export type {
9
10
  NodeAtResult,
10
11
  OutlineItem,
11
12
  QueryCapture,
13
+ SessionTreeSitterService,
14
+ SessionTreeSitterServiceState,
12
15
  SourceRange,
13
16
  SupportedExtension,
14
17
  TreeSitterResult,
18
+ TreeSitterService,
15
19
  TreeSitterSession,
16
20
  } from "./types.ts";
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Public session factory and re-exports for @mrclrchtr/supi-tree-sitter.
1
+ // Public session factory, shared session service access, and re-exports for @mrclrchtr/supi-tree-sitter.
2
2
 
3
+ export { getSessionTreeSitterService } from "./session/service-registry.ts";
3
4
  export { createTreeSitterSession } from "./session/session.ts";
4
5
  export type {
5
6
  CalleesAtResult,
@@ -9,8 +10,11 @@ export type {
9
10
  NodeAtResult,
10
11
  OutlineItem,
11
12
  QueryCapture,
13
+ SessionTreeSitterService,
14
+ SessionTreeSitterServiceState,
12
15
  SourceRange,
13
16
  SupportedExtension,
14
17
  TreeSitterResult,
18
+ TreeSitterService,
15
19
  TreeSitterSession,
16
20
  } from "./types.ts";
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { resolveToolPath } from "@mrclrchtr/supi-core/api";
5
6
  import type { Language, Parser, Tree } from "web-tree-sitter";
6
7
  import { nodeToRange } from "../coordinates.ts";
7
8
  import { detectGrammar, resolveGrammarWasmPath } from "../language.ts";
@@ -113,7 +114,7 @@ export class TreeSitterRuntime {
113
114
  grammarId: GrammarId;
114
115
  }>
115
116
  > {
116
- const resolvedPath = path.resolve(this.cwd, filePath);
117
+ const resolvedPath = resolveToolPath(this.cwd, filePath);
117
118
 
118
119
  // Check language support first
119
120
  const grammarId = detectGrammar(filePath);
@@ -212,7 +213,7 @@ export class TreeSitterRuntime {
212
213
 
213
214
  /** Resolve a file path from cwd. */
214
215
  resolvePath(filePath: string): string {
215
- return path.resolve(this.cwd, filePath);
216
+ return resolveToolPath(this.cwd, filePath);
216
217
  }
217
218
 
218
219
  /** Dispose all held parser resources. */
@@ -0,0 +1,30 @@
1
+ // Shared session-scoped Tree-sitter service registry.
2
+ // Peer extensions can import `getSessionTreeSitterService` from the package API
3
+ // to reuse the active structural runtime without creating duplicate sessions.
4
+
5
+ import { createSessionStateRegistry } from "@mrclrchtr/supi-core/api";
6
+ import type { SessionTreeSitterService, SessionTreeSitterServiceState } from "../types.ts";
7
+
8
+ const registry = createSessionStateRegistry<SessionTreeSitterServiceState>(
9
+ "supi-tree-sitter/session-registry",
10
+ );
11
+
12
+ /** Publish the shared Tree-sitter service for one session cwd. */
13
+ export function setSessionTreeSitterService(cwd: string, service: SessionTreeSitterService): void {
14
+ registry.set(cwd, { kind: "ready", service });
15
+ }
16
+
17
+ /** Acquire the shared Tree-sitter service state for one session cwd. */
18
+ export function getSessionTreeSitterService(cwd: string): SessionTreeSitterServiceState {
19
+ return (
20
+ registry.get(cwd) ?? {
21
+ kind: "unavailable",
22
+ reason: "No Tree-sitter session initialized for this workspace",
23
+ }
24
+ );
25
+ }
26
+
27
+ /** Remove the shared Tree-sitter service for one session cwd. */
28
+ export function clearSessionTreeSitterService(cwd: string): void {
29
+ registry.clear(cwd);
30
+ }
@@ -1,4 +1,4 @@
1
- // Session factory — creates a TreeSitterSession bound to a working directory.
1
+ // Session factory — creates runtime-backed Tree-sitter services and owned sessions.
2
2
 
3
3
  import { detectGrammar, isJsTsGrammar } from "../language.ts";
4
4
  import {
@@ -16,17 +16,13 @@ import type {
16
16
  OutlineItem,
17
17
  QueryCapture,
18
18
  TreeSitterResult,
19
+ TreeSitterService,
19
20
  TreeSitterSession,
20
21
  } from "../types.ts";
21
22
  import { TreeSitterRuntime } from "./runtime.ts";
22
23
 
23
- /**
24
- * Create a new Tree-sitter session bound to the given working directory.
25
- * The session owns parser/grammar reuse and must be disposed when done.
26
- */
27
- export function createTreeSitterSession(cwd: string): TreeSitterSession {
28
- const runtime = new TreeSitterRuntime(cwd);
29
-
24
+ /** Create a runtime-backed structural service without taking ownership of disposal. */
25
+ export function createTreeSitterService(runtime: TreeSitterRuntime): TreeSitterService {
30
26
  return {
31
27
  async canParse(file: string) {
32
28
  const result = await runtime.parseFile(file);
@@ -104,7 +100,19 @@ export function createTreeSitterSession(cwd: string): TreeSitterSession {
104
100
  ): Promise<TreeSitterResult<CalleesAtResult>> {
105
101
  return lookupCalleesAt(runtime, file, line, character);
106
102
  },
103
+ };
104
+ }
107
105
 
106
+ /**
107
+ * Create a new Tree-sitter session bound to the given working directory.
108
+ * The session owns parser/grammar reuse and must be disposed when done.
109
+ */
110
+ export function createTreeSitterSession(cwd: string): TreeSitterSession {
111
+ const runtime = new TreeSitterRuntime(cwd);
112
+ const service = createTreeSitterService(runtime);
113
+
114
+ return {
115
+ ...service,
108
116
  dispose() {
109
117
  runtime.dispose();
110
118
  },