@telorun/ide-support 0.5.0 → 0.6.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.
@@ -7,9 +7,13 @@ import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
7
7
  * Branches by prefix shape:
8
8
  * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
9
9
  * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
10
- * "<word>" registry search by free-text.
11
- * "<ns>/<name>@<partial>" → version list for that module.
12
- * "http(s)://", "file://" → no suggestions (opaque URLs).
10
+ * "<word>", "oci://…" hub ref autocomplete (fuzzy substring over registered refs).
11
+ * "<ref>@<partial>" → version list for that ref.
12
+ * "http(s)://", "file://" → no suggestions (opaque URLs the author types verbatim).
13
+ *
14
+ * `oci://` is deliberately NOT opaque: without an `@` it routes to the ref
15
+ * search below, whose query is the whole typed prefix — so the hub fuzzy-matches
16
+ * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
13
17
  *
14
18
  * `valueStartColumn` is forwarded onto every result so the host can replace
15
19
  * the whole typed value, not just the trailing word (Monaco / VSCode word
@@ -1 +1 @@
1
- {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/completions/import-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAW3E;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuB7B"}
1
+ {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/completions/import-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAY3E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
@@ -1,6 +1,7 @@
1
- /** Maximum registry hits to surface in a single completion request.
2
- * Keeps the popover scannable when a broad `q=` query matches the catalog. */
3
- const REGISTRY_LIMIT = 50;
1
+ /** Maximum ref hits to surface in a single completion request. Keeps the
2
+ * popover scannable when a broad `q=` query matches many registered refs
3
+ * (the hub already caps `/refs` server-side; this is a client backstop). */
4
+ const REF_LIMIT = 50;
4
5
  /** Caps the number of directory entries we probe with `hasManifest` per
5
6
  * request. Each probe is a host-side filesystem stat; the popover would not
6
7
  * show more than ~50 entries anyway, so probing further only adds latency. */
@@ -13,9 +14,13 @@ const PATH_PROBE_LIMIT = 50;
13
14
  * Branches by prefix shape:
14
15
  * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
15
16
  * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
16
- * "<word>" registry search by free-text.
17
- * "<ns>/<name>@<partial>" → version list for that module.
18
- * "http(s)://", "file://" → no suggestions (opaque URLs).
17
+ * "<word>", "oci://…" hub ref autocomplete (fuzzy substring over registered refs).
18
+ * "<ref>@<partial>" → version list for that ref.
19
+ * "http(s)://", "file://" → no suggestions (opaque URLs the author types verbatim).
20
+ *
21
+ * `oci://` is deliberately NOT opaque: without an `@` it routes to the ref
22
+ * search below, whose query is the whole typed prefix — so the hub fuzzy-matches
23
+ * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
19
24
  *
20
25
  * `valueStartColumn` is forwarded onto every result so the host can replace
21
26
  * the whole typed value, not just the trailing word (Monaco / VSCode word
@@ -33,11 +38,13 @@ export async function importSourceCompletions(prefix, valueStartColumn, adapter)
33
38
  if (isRelativeShape) {
34
39
  return relativePathCompletions(prefix, valueStartColumn, adapter);
35
40
  }
36
- const atIdx = prefix.indexOf("@");
41
+ // The version (or `@sha256:` digest) is the trailing `@`-segment, so split on
42
+ // the LAST `@` — a digest-pinned ref keeps everything before it as the ref.
43
+ const atIdx = prefix.lastIndexOf("@");
37
44
  if (atIdx > 0) {
38
45
  return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
39
46
  }
40
- return registrySearchCompletions(prefix, valueStartColumn, adapter);
47
+ return refSearchCompletions(prefix, valueStartColumn, adapter);
41
48
  }
42
49
  async function relativePathCompletions(prefix, valueStartColumn, adapter) {
43
50
  // Empty prefix → seed `./` and `../` so the user gets traction; otherwise
@@ -95,52 +102,63 @@ async function relativePathCompletions(prefix, valueStartColumn, adapter) {
95
102
  };
96
103
  }));
97
104
  }
98
- async function registrySearchCompletions(prefix, valueStartColumn, adapter) {
99
- // The registry's `q` filter ILIKEs against name / namespace / description
100
- // it doesn't know about the `<namespace>/<name>` shape. Once the user has
101
- // typed a `/`, sending the literal `std/htt` as `q` matches nothing because
102
- // the slash is not in any of those columns. Split here so `q` carries just
103
- // the bit that looks like a name, and apply the namespace constraint
104
- // client-side.
105
- const slashIdx = prefix.indexOf("/");
106
- const namespacePart = slashIdx >= 0 ? prefix.slice(0, slashIdx) : "";
107
- const namePart = slashIdx >= 0 ? prefix.slice(slashIdx + 1) : prefix;
108
- const hits = await adapter.searchRegistry(namePart);
109
- const filtered = namespacePart
110
- ? hits.filter((h) => h.namespace.startsWith(namespacePart))
111
- : hits;
112
- return filtered.slice(0, REGISTRY_LIMIT).map((m) => {
113
- const id = `${m.namespace}/${m.name}@${m.version}`;
105
+ async function refSearchCompletions(prefix, valueStartColumn, adapter) {
106
+ // The whole typed prefix is the fuzzy query the hub matches it as a
107
+ // substring over each registered ref, so no client-side splitting is needed
108
+ // (and `oci://ghcr.io/aws/telo-s3` matches without mangling the `//`).
109
+ const hits = await adapter.searchRefs(prefix);
110
+ return hits.slice(0, REF_LIMIT).map((m) => {
111
+ // Seed the pinned `ref@latestVersion` so the completion is directly usable;
112
+ // the author can still narrow the version afterwards (the `@` re-triggers
113
+ // version completion).
114
+ const id = m.latestVersion ? `${m.ref}@${m.latestVersion}` : m.ref;
115
+ const name = refDisplayName(m.ref);
116
+ // Lead the label with the module name so the interesting part isn't cut off
117
+ // behind the transport/host boilerplate (`oci://ghcr.io/telorun/…`). The
118
+ // full ref moves to `detail`, and `insertText`/`filterText` stay the ref so
119
+ // acceptance still inserts it and a fully-typed ref still filters.
114
120
  return {
115
- label: id,
121
+ label: m.latestVersion ? `${name}@${m.latestVersion}` : name,
116
122
  kind: "module",
117
- detail: m.description ?? "registry module",
123
+ detail: m.description ?? m.ref,
124
+ documentation: m.description ? m.ref : undefined,
118
125
  insertText: id,
119
126
  filterText: id,
120
127
  replaceFromColumn: valueStartColumn,
121
128
  };
122
129
  });
123
130
  }
131
+ /** The `org/name` tail of a location ref: its last two path segments, with the
132
+ * transport scheme (`oci://`, `https://`, …) and registry host dropped.
133
+ * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `std/console`
134
+ * → `std/console`. Falls back to fewer segments (or the whole ref) when there
135
+ * aren't two. */
136
+ function refDisplayName(ref) {
137
+ const withoutScheme = ref.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
138
+ const segments = withoutScheme.split("/").filter(Boolean);
139
+ return segments.slice(-2).join("/") || ref;
140
+ }
124
141
  async function versionCompletions(prefix, atIdx, valueStartColumn, adapter) {
125
- const beforeAt = prefix.slice(0, atIdx);
142
+ const ref = prefix.slice(0, atIdx);
126
143
  const partialVersion = prefix.slice(atIdx + 1);
127
- const slashIdx = beforeAt.indexOf("/");
128
- if (slashIdx <= 0 || slashIdx === beforeAt.length - 1)
144
+ if (ref === "")
129
145
  return [];
130
- const namespace = beforeAt.slice(0, slashIdx);
131
- const name = beforeAt.slice(slashIdx + 1);
132
- const versions = await adapter.listRegistryVersions(namespace, name);
146
+ const versions = await adapter.listVersionsForRef(ref);
133
147
  const matches = versions.filter((v) => v.startsWith(partialVersion));
134
148
  return matches.map((version, idx) => {
135
- const id = `${namespace}/${name}@${version}`;
149
+ const id = `${ref}@${version}`;
150
+ // The ref is already typed and visible on the line, so the label is just the
151
+ // version — no point repeating the full ref on every row. `insertText` /
152
+ // `filterText` stay the full id so acceptance replaces the whole value and
153
+ // the already-typed ref prefix keeps the item in the filtered set.
136
154
  return {
137
- label: id,
155
+ label: version,
138
156
  kind: "value",
139
- detail: idx === 0 ? "latest" : `v${version}`,
157
+ detail: idx === 0 ? "latest" : undefined,
140
158
  insertText: id,
141
159
  filterText: id,
142
160
  replaceFromColumn: valueStartColumn,
143
- // Preserve registry's ordering (newest first) so the latest version is
161
+ // Preserve the hub's ordering (newest first) so the latest version is
144
162
  // suggested at the top regardless of lexical comparison.
145
163
  sortText: String(idx).padStart(4, "0"),
146
164
  };
package/dist/types.d.ts CHANGED
@@ -18,16 +18,22 @@ export interface CompletionResult {
18
18
  * word boundary would not include in the replaced range. */
19
19
  replaceFromColumn?: number;
20
20
  }
21
- export interface RegistryModule {
22
- namespace: string;
23
- name: string;
24
- version: string;
21
+ /** A candidate module ref surfaced by the hub's `/refs` lexical autocomplete.
22
+ * Identity is the location ref, never `namespace/name` — an OCI module has no
23
+ * addressable `namespace/name`. `latestVersion` seeds a pinned `ref@version`
24
+ * insert so a picked completion is directly usable. */
25
+ export interface HubRef {
26
+ ref: string;
27
+ latestVersion: string;
25
28
  description?: string;
26
29
  }
27
30
  /** Host-supplied bridge that lets ide-support reach the filesystem and the
28
- * module registry without depending on Node, Tauri, or vscode APIs. Each
31
+ * federated telo hub without depending on Node, Tauri, or vscode APIs. Each
29
32
  * host (VSCode extension, Telo editor) builds an adapter scoped to the
30
- * currently-edited manifest before calling `buildCompletions`. */
33
+ * currently-edited manifest before calling `buildCompletions`. Hub lookups are
34
+ * ref-keyed: the hub aggregates modules across every transport (OCI, HTTP,
35
+ * direct URL), so completion speaks its `/refs` + `/module/versions` verbs
36
+ * rather than any single registry. */
31
37
  export interface IdeEnvironmentAdapter {
32
38
  /** Subdirectory names within `relPath` (resolved against the manifest's
33
39
  * directory). Returns [] if the path doesn't exist or isn't a directory.
@@ -36,12 +42,15 @@ export interface IdeEnvironmentAdapter {
36
42
  /** True iff `<relPath>/telo.yaml` exists relative to the manifest dir.
37
43
  * Used to mark directories that are valid import targets. */
38
44
  hasManifest(relPath: string): Promise<boolean>;
39
- /** Free-text search against the configured module registry. Matches against
40
- * name, namespace, and description. Empty `query` should return the full
41
- * (capped) catalog. */
42
- searchRegistry(query: string): Promise<RegistryModule[]>;
43
- /** All published versions for a module, newest first. */
44
- listRegistryVersions(namespace: string, name: string): Promise<string[]>;
45
+ /** Fuzzy lexical ref autocomplete against the configured telo hub
46
+ * (`GET /refs?q=`). The query is matched as a substring over every
47
+ * registered ref, so a bare token (`youtrack`) hits the same ref as its full
48
+ * `oci://…` form. Best-effort — hosts swallow network errors and return []. */
49
+ searchRefs(query: string): Promise<HubRef[]>;
50
+ /** All tracked versions for a location ref, newest first
51
+ * (`GET /module/versions?ref=`). The browser cannot call OCI `tags/list`;
52
+ * the hub holds them from ingest. */
53
+ listVersionsForRef(ref: string): Promise<string[]>;
45
54
  }
46
55
  export interface NormalizedDiagnostic {
47
56
  range: Range;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,YAAY,EACV,QAAQ,EACR,KAAK,EACL,kBAAkB,EAClB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjG,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;iEAG6D;IAC7D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;mEAGmE;AACnE,MAAM,WAAW,qBAAqB;IACpC;;2DAEuD;IACvD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpD;kEAC8D;IAC9D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C;;4BAEwB;IACxB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACzD,yDAAyD;IACzD,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE;;;0EAGsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,YAAY,EACV,QAAQ,EACR,KAAK,EACL,kBAAkB,EAClB,aAAa,GACd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjG,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;iEAG6D;IAC7D,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;wDAGwD;AACxD,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;uCAMuC;AACvC,MAAM,WAAW,qBAAqB;IACpC;;2DAEuD;IACvD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpD;kEAC8D;IAC9D,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C;;;oFAGgF;IAChF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7C;;0CAEsC;IACtC,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnE;;;0EAGsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -1,8 +1,9 @@
1
1
  import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
2
2
 
3
- /** Maximum registry hits to surface in a single completion request.
4
- * Keeps the popover scannable when a broad `q=` query matches the catalog. */
5
- const REGISTRY_LIMIT = 50;
3
+ /** Maximum ref hits to surface in a single completion request. Keeps the
4
+ * popover scannable when a broad `q=` query matches many registered refs
5
+ * (the hub already caps `/refs` server-side; this is a client backstop). */
6
+ const REF_LIMIT = 50;
6
7
 
7
8
  /** Caps the number of directory entries we probe with `hasManifest` per
8
9
  * request. Each probe is a host-side filesystem stat; the popover would not
@@ -17,9 +18,13 @@ const PATH_PROBE_LIMIT = 50;
17
18
  * Branches by prefix shape:
18
19
  * "" → relative dirs under the manifest dir, plus `./` / `../` seeds.
19
20
  * "./..", "../", "/..." → subdirs of the typed path (any subdir; existing manifest gets a hint).
20
- * "<word>" registry search by free-text.
21
- * "<ns>/<name>@<partial>" → version list for that module.
22
- * "http(s)://", "file://" → no suggestions (opaque URLs).
21
+ * "<word>", "oci://…" hub ref autocomplete (fuzzy substring over registered refs).
22
+ * "<ref>@<partial>" → version list for that ref.
23
+ * "http(s)://", "file://" → no suggestions (opaque URLs the author types verbatim).
24
+ *
25
+ * `oci://` is deliberately NOT opaque: without an `@` it routes to the ref
26
+ * search below, whose query is the whole typed prefix — so the hub fuzzy-matches
27
+ * `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
23
28
  *
24
29
  * `valueStartColumn` is forwarded onto every result so the host can replace
25
30
  * the whole typed value, not just the trailing word (Monaco / VSCode word
@@ -46,12 +51,14 @@ export async function importSourceCompletions(
46
51
  return relativePathCompletions(prefix, valueStartColumn, adapter);
47
52
  }
48
53
 
49
- const atIdx = prefix.indexOf("@");
54
+ // The version (or `@sha256:` digest) is the trailing `@`-segment, so split on
55
+ // the LAST `@` — a digest-pinned ref keeps everything before it as the ref.
56
+ const atIdx = prefix.lastIndexOf("@");
50
57
  if (atIdx > 0) {
51
58
  return versionCompletions(prefix, atIdx, valueStartColumn, adapter);
52
59
  }
53
60
 
54
- return registrySearchCompletions(prefix, valueStartColumn, adapter);
61
+ return refSearchCompletions(prefix, valueStartColumn, adapter);
55
62
  }
56
63
 
57
64
  async function relativePathCompletions(
@@ -120,32 +127,31 @@ async function relativePathCompletions(
120
127
  );
121
128
  }
122
129
 
123
- async function registrySearchCompletions(
130
+ async function refSearchCompletions(
124
131
  prefix: string,
125
132
  valueStartColumn: number,
126
133
  adapter: IdeEnvironmentAdapter,
127
134
  ): Promise<CompletionResult[]> {
128
- // The registry's `q` filter ILIKEs against name / namespace / description
129
- // it doesn't know about the `<namespace>/<name>` shape. Once the user has
130
- // typed a `/`, sending the literal `std/htt` as `q` matches nothing because
131
- // the slash is not in any of those columns. Split here so `q` carries just
132
- // the bit that looks like a name, and apply the namespace constraint
133
- // client-side.
134
- const slashIdx = prefix.indexOf("/");
135
- const namespacePart = slashIdx >= 0 ? prefix.slice(0, slashIdx) : "";
136
- const namePart = slashIdx >= 0 ? prefix.slice(slashIdx + 1) : prefix;
137
-
138
- const hits = await adapter.searchRegistry(namePart);
139
- const filtered = namespacePart
140
- ? hits.filter((h) => h.namespace.startsWith(namespacePart))
141
- : hits;
142
-
143
- return filtered.slice(0, REGISTRY_LIMIT).map((m) => {
144
- const id = `${m.namespace}/${m.name}@${m.version}`;
135
+ // The whole typed prefix is the fuzzy query the hub matches it as a
136
+ // substring over each registered ref, so no client-side splitting is needed
137
+ // (and `oci://ghcr.io/aws/telo-s3` matches without mangling the `//`).
138
+ const hits = await adapter.searchRefs(prefix);
139
+
140
+ return hits.slice(0, REF_LIMIT).map((m) => {
141
+ // Seed the pinned `ref@latestVersion` so the completion is directly usable;
142
+ // the author can still narrow the version afterwards (the `@` re-triggers
143
+ // version completion).
144
+ const id = m.latestVersion ? `${m.ref}@${m.latestVersion}` : m.ref;
145
+ const name = refDisplayName(m.ref);
146
+ // Lead the label with the module name so the interesting part isn't cut off
147
+ // behind the transport/host boilerplate (`oci://ghcr.io/telorun/…`). The
148
+ // full ref moves to `detail`, and `insertText`/`filterText` stay the ref so
149
+ // acceptance still inserts it and a fully-typed ref still filters.
145
150
  return {
146
- label: id,
151
+ label: m.latestVersion ? `${name}@${m.latestVersion}` : name,
147
152
  kind: "module",
148
- detail: m.description ?? "registry module",
153
+ detail: m.description ?? m.ref,
154
+ documentation: m.description ? m.ref : undefined,
149
155
  insertText: id,
150
156
  filterText: id,
151
157
  replaceFromColumn: valueStartColumn,
@@ -153,32 +159,44 @@ async function registrySearchCompletions(
153
159
  });
154
160
  }
155
161
 
162
+ /** The `org/name` tail of a location ref: its last two path segments, with the
163
+ * transport scheme (`oci://`, `https://`, …) and registry host dropped.
164
+ * `oci://ghcr.io/telorun/telo-console` → `telorun/telo-console`; `std/console`
165
+ * → `std/console`. Falls back to fewer segments (or the whole ref) when there
166
+ * aren't two. */
167
+ function refDisplayName(ref: string): string {
168
+ const withoutScheme = ref.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
169
+ const segments = withoutScheme.split("/").filter(Boolean);
170
+ return segments.slice(-2).join("/") || ref;
171
+ }
172
+
156
173
  async function versionCompletions(
157
174
  prefix: string,
158
175
  atIdx: number,
159
176
  valueStartColumn: number,
160
177
  adapter: IdeEnvironmentAdapter,
161
178
  ): Promise<CompletionResult[]> {
162
- const beforeAt = prefix.slice(0, atIdx);
179
+ const ref = prefix.slice(0, atIdx);
163
180
  const partialVersion = prefix.slice(atIdx + 1);
164
- const slashIdx = beforeAt.indexOf("/");
165
- if (slashIdx <= 0 || slashIdx === beforeAt.length - 1) return [];
181
+ if (ref === "") return [];
166
182
 
167
- const namespace = beforeAt.slice(0, slashIdx);
168
- const name = beforeAt.slice(slashIdx + 1);
169
- const versions = await adapter.listRegistryVersions(namespace, name);
183
+ const versions = await adapter.listVersionsForRef(ref);
170
184
 
171
185
  const matches = versions.filter((v) => v.startsWith(partialVersion));
172
186
  return matches.map((version, idx) => {
173
- const id = `${namespace}/${name}@${version}`;
187
+ const id = `${ref}@${version}`;
188
+ // The ref is already typed and visible on the line, so the label is just the
189
+ // version — no point repeating the full ref on every row. `insertText` /
190
+ // `filterText` stay the full id so acceptance replaces the whole value and
191
+ // the already-typed ref prefix keeps the item in the filtered set.
174
192
  return {
175
- label: id,
193
+ label: version,
176
194
  kind: "value",
177
- detail: idx === 0 ? "latest" : `v${version}`,
195
+ detail: idx === 0 ? "latest" : undefined,
178
196
  insertText: id,
179
197
  filterText: id,
180
198
  replaceFromColumn: valueStartColumn,
181
- // Preserve registry's ordering (newest first) so the latest version is
199
+ // Preserve the hub's ordering (newest first) so the latest version is
182
200
  // suggested at the top regardless of lexical comparison.
183
201
  sortText: String(idx).padStart(4, "0"),
184
202
  };
package/src/types.ts CHANGED
@@ -31,17 +31,23 @@ export interface CompletionResult {
31
31
  replaceFromColumn?: number;
32
32
  }
33
33
 
34
- export interface RegistryModule {
35
- namespace: string;
36
- name: string;
37
- version: string;
34
+ /** A candidate module ref surfaced by the hub's `/refs` lexical autocomplete.
35
+ * Identity is the location ref, never `namespace/name` — an OCI module has no
36
+ * addressable `namespace/name`. `latestVersion` seeds a pinned `ref@version`
37
+ * insert so a picked completion is directly usable. */
38
+ export interface HubRef {
39
+ ref: string;
40
+ latestVersion: string;
38
41
  description?: string;
39
42
  }
40
43
 
41
44
  /** Host-supplied bridge that lets ide-support reach the filesystem and the
42
- * module registry without depending on Node, Tauri, or vscode APIs. Each
45
+ * federated telo hub without depending on Node, Tauri, or vscode APIs. Each
43
46
  * host (VSCode extension, Telo editor) builds an adapter scoped to the
44
- * currently-edited manifest before calling `buildCompletions`. */
47
+ * currently-edited manifest before calling `buildCompletions`. Hub lookups are
48
+ * ref-keyed: the hub aggregates modules across every transport (OCI, HTTP,
49
+ * direct URL), so completion speaks its `/refs` + `/module/versions` verbs
50
+ * rather than any single registry. */
45
51
  export interface IdeEnvironmentAdapter {
46
52
  /** Subdirectory names within `relPath` (resolved against the manifest's
47
53
  * directory). Returns [] if the path doesn't exist or isn't a directory.
@@ -50,12 +56,15 @@ export interface IdeEnvironmentAdapter {
50
56
  /** True iff `<relPath>/telo.yaml` exists relative to the manifest dir.
51
57
  * Used to mark directories that are valid import targets. */
52
58
  hasManifest(relPath: string): Promise<boolean>;
53
- /** Free-text search against the configured module registry. Matches against
54
- * name, namespace, and description. Empty `query` should return the full
55
- * (capped) catalog. */
56
- searchRegistry(query: string): Promise<RegistryModule[]>;
57
- /** All published versions for a module, newest first. */
58
- listRegistryVersions(namespace: string, name: string): Promise<string[]>;
59
+ /** Fuzzy lexical ref autocomplete against the configured telo hub
60
+ * (`GET /refs?q=`). The query is matched as a substring over every
61
+ * registered ref, so a bare token (`youtrack`) hits the same ref as its full
62
+ * `oci://…` form. Best-effort — hosts swallow network errors and return []. */
63
+ searchRefs(query: string): Promise<HubRef[]>;
64
+ /** All tracked versions for a location ref, newest first
65
+ * (`GET /module/versions?ref=`). The browser cannot call OCI `tags/list`;
66
+ * the hub holds them from ingest. */
67
+ listVersionsForRef(ref: string): Promise<string[]>;
59
68
  }
60
69
 
61
70
  export interface NormalizedDiagnostic {