nebula-notebook 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.
Files changed (142) hide show
  1. package/README.md +110 -16
  2. package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
  3. package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
  4. package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  5. package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  6. package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  7. package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  8. package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  9. package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  10. package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  11. package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  12. package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  13. package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  14. package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
  15. package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  16. package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
  17. package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  18. package/dist/assets/index-B_Bs5koo.js +1 -0
  19. package/dist/assets/index-BtWv4MIT.css +7 -0
  20. package/dist/assets/index-DK4nS74r.js +829 -0
  21. package/dist/assets/index-DeQN03nW.js +81 -0
  22. package/dist/assets/index-DzjpmUak.css +32 -0
  23. package/dist/assets/index-g6MjT-VG.js +1 -0
  24. package/dist/assets/services-shim-BirHVsih.js +33 -0
  25. package/dist/assets/viewlist-uomDf7I7.js +1 -0
  26. package/dist/assets/widgets-X7J3NxEn.css +1 -0
  27. package/dist/index.html +2 -2
  28. package/node-server/dist/auth/auth-middleware.js +22 -4
  29. package/node-server/dist/auth/auth-service.js +37 -7
  30. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  31. package/node-server/dist/cluster/client-registration.js +38 -2
  32. package/node-server/dist/cluster/kernel-proxy.js +43 -12
  33. package/node-server/dist/cluster/server-registry.d.ts +17 -0
  34. package/node-server/dist/cluster/server-registry.js +57 -7
  35. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  36. package/node-server/dist/discovery/conda-locations.js +427 -0
  37. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  38. package/node-server/dist/discovery/discovery-service.js +211 -127
  39. package/node-server/dist/discovery/types.d.ts +0 -1
  40. package/node-server/dist/discovery/types.js +1 -2
  41. package/node-server/dist/environment.d.ts +12 -0
  42. package/node-server/dist/environment.js +104 -0
  43. package/node-server/dist/fs/fs-service.d.ts +86 -8
  44. package/node-server/dist/fs/fs-service.js +640 -99
  45. package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
  46. package/node-server/dist/fs/notebook-formats/percent.js +286 -0
  47. package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
  48. package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
  49. package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
  50. package/node-server/dist/fs/notebook-formats/registry.js +77 -0
  51. package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
  52. package/node-server/dist/fs/notebook-formats/types.js +13 -0
  53. package/node-server/dist/fs/types.d.ts +9 -0
  54. package/node-server/dist/fs/types.js +8 -0
  55. package/node-server/dist/idle-exit.d.ts +52 -0
  56. package/node-server/dist/idle-exit.js +83 -0
  57. package/node-server/dist/index.js +198 -8
  58. package/node-server/dist/kernel/kernel-service.d.ts +130 -2
  59. package/node-server/dist/kernel/kernel-service.js +834 -64
  60. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  61. package/node-server/dist/kernel/kernelspec.js +75 -4
  62. package/node-server/dist/notebook/headless-handler.d.ts +12 -0
  63. package/node-server/dist/notebook/headless-handler.js +125 -29
  64. package/node-server/dist/notebook/operation-router.js +7 -2
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
  66. package/node-server/dist/notebook/undoRedoManager.js +49 -18
  67. package/node-server/dist/output/display-data.js +2 -0
  68. package/node-server/dist/private-tmp.d.ts +14 -0
  69. package/node-server/dist/private-tmp.js +70 -0
  70. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  71. package/node-server/dist/routes/autocomplete.js +294 -0
  72. package/node-server/dist/routes/cluster.js +2 -2
  73. package/node-server/dist/routes/compute.d.ts +8 -0
  74. package/node-server/dist/routes/compute.js +136 -0
  75. package/node-server/dist/routes/fs.js +23 -6
  76. package/node-server/dist/routes/kernel.js +156 -3
  77. package/node-server/dist/routes/notebook.js +37 -3
  78. package/node-server/dist/routes/python.d.ts +1 -2
  79. package/node-server/dist/routes/python.js +139 -3
  80. package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
  81. package/node-server/dist/scheduler/allocation-service.js +285 -0
  82. package/node-server/dist/scheduler/job-template.d.ts +30 -0
  83. package/node-server/dist/scheduler/job-template.js +85 -0
  84. package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
  85. package/node-server/dist/scheduler/mock-scheduler.js +125 -0
  86. package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
  87. package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
  88. package/node-server/dist/scheduler/types.d.ts +118 -0
  89. package/node-server/dist/scheduler/types.js +8 -0
  90. package/node-server/dist/scheduler/util.d.ts +7 -0
  91. package/node-server/dist/scheduler/util.js +20 -0
  92. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  93. package/node-server/dist/terminal/agent-registry.js +246 -0
  94. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  95. package/node-server/dist/terminal/binding-store.js +0 -0
  96. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  97. package/node-server/dist/terminal/pty-manager.js +138 -19
  98. package/node-server/dist/terminal/server.js +155 -23
  99. package/node-server/dist/terminal/types.d.ts +2 -0
  100. package/node-server/dist/update-check.d.ts +20 -0
  101. package/node-server/dist/update-check.js +114 -0
  102. package/node-server/package.json +2 -0
  103. package/node_modules/nebula-autocomplete/README.md +145 -0
  104. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  105. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  106. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  107. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  108. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  109. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  110. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  111. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  112. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  113. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  114. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  115. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  116. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  117. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  118. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  119. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  120. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  121. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  122. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  123. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  124. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  125. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  126. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  127. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  128. package/node_modules/nebula-autocomplete/package.json +58 -0
  129. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  130. package/node_modules/nebula-autocomplete/server.js +3 -0
  131. package/package.json +18 -5
  132. package/scripts/postinstall.cjs +25 -5
  133. package/dist/assets/index-BvrHu37J.js +0 -648
  134. package/dist/assets/index-Dfj_xsKU.css +0 -32
  135. package/node-server/dist/llm/index.d.ts +0 -5
  136. package/node-server/dist/llm/index.js +0 -21
  137. package/node-server/dist/llm/llm-service.d.ts +0 -77
  138. package/node-server/dist/llm/llm-service.js +0 -454
  139. package/node-server/dist/llm/types.d.ts +0 -40
  140. package/node-server/dist/llm/types.js +0 -15
  141. package/node-server/dist/routes/llm.d.ts +0 -8
  142. package/node-server/dist/routes/llm.js +0 -105
@@ -0,0 +1,43 @@
1
+ /**
2
+ * CodeMirror 6 inline ghost-text completion.
3
+ *
4
+ * Renders the pending LLM suggestion as dimmed inline text at the cursor;
5
+ * Tab accepts, Escape dismisses, any edit or cursor move invalidates. Fetching
6
+ * is debounced and superseded requests are aborted.
7
+ *
8
+ * This intentionally does NOT use @codemirror/autocomplete's dropdown — the
9
+ * kernel/static completion sources keep the dropdown; multi-line LLM
10
+ * suggestions render as ghost text alongside it.
11
+ */
12
+ import { EditorState, type Extension } from "@codemirror/state";
13
+ import { EditorView } from "@codemirror/view";
14
+ export interface GhostTextContext {
15
+ prefix: string;
16
+ suffix: string;
17
+ state: EditorState;
18
+ }
19
+ export type GhostTextFetcher = (ctx: GhostTextContext, opts: {
20
+ signal: AbortSignal;
21
+ onChunk: (text: string) => void;
22
+ }) => Promise<string>;
23
+ export interface GhostTextOptions {
24
+ /** Idle time after the last edit before a fetch fires. Default 400 ms. */
25
+ debounceMs?: number;
26
+ /**
27
+ * Skip fetching when the prefix (trimmed) is shorter than this. Default 0:
28
+ * with notebook-level context (other cells) available to the fetcher, even
29
+ * an empty cell is completable — gate in the fetcher, which can see that
30
+ * context, not here. Set >0 only for context-free single-document use.
31
+ */
32
+ minPrefixLength?: number;
33
+ /**
34
+ * When this returns true at fetch time, the fetch is skipped — e.g. while a
35
+ * completion DROPDOWN is open, so the two UIs don't stack and ghost turns
36
+ * aren't burned while the user browses the list. Injected as a predicate so
37
+ * this package needs no dependency on @codemirror/autocomplete.
38
+ */
39
+ holdWhen?: (state: unknown) => boolean;
40
+ }
41
+ export declare function acceptGhostText(view: EditorView): boolean;
42
+ export declare function dismissGhostText(view: EditorView): boolean;
43
+ export declare function ghostText(fetcher: GhostTextFetcher, options?: GhostTextOptions): Extension;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * CodeMirror 6 inline ghost-text completion.
3
+ *
4
+ * Renders the pending LLM suggestion as dimmed inline text at the cursor;
5
+ * Tab accepts, Escape dismisses, any edit or cursor move invalidates. Fetching
6
+ * is debounced and superseded requests are aborted.
7
+ *
8
+ * This intentionally does NOT use @codemirror/autocomplete's dropdown — the
9
+ * kernel/static completion sources keep the dropdown; multi-line LLM
10
+ * suggestions render as ghost text alongside it.
11
+ */
12
+ import { Prec, StateEffect, StateField, } from "@codemirror/state";
13
+ import { Decoration, EditorView, keymap, ViewPlugin, WidgetType, } from "@codemirror/view";
14
+ const setGhost = StateEffect.define();
15
+ class GhostWidget extends WidgetType {
16
+ text;
17
+ constructor(text) {
18
+ super();
19
+ this.text = text;
20
+ }
21
+ eq(other) {
22
+ return other.text === this.text;
23
+ }
24
+ toDOM() {
25
+ const span = document.createElement("span");
26
+ span.className = "cm-ghost-text";
27
+ span.style.opacity = "0.45";
28
+ span.style.whiteSpace = "pre-wrap";
29
+ span.setAttribute("aria-hidden", "true");
30
+ span.textContent = this.text;
31
+ return span;
32
+ }
33
+ get lineBreaks() {
34
+ return (this.text.match(/\n/g) ?? []).length;
35
+ }
36
+ }
37
+ const ghostField = StateField.define({
38
+ create: () => null,
39
+ update(value, tr) {
40
+ for (const e of tr.effects)
41
+ if (e.is(setGhost))
42
+ return e.value;
43
+ // Any document change or cursor move invalidates the suggestion.
44
+ if (tr.docChanged || tr.selection)
45
+ return null;
46
+ return value;
47
+ },
48
+ provide: (field) => EditorView.decorations.from(field, (value) => {
49
+ if (!value || !value.text)
50
+ return Decoration.none;
51
+ return Decoration.set([
52
+ Decoration.widget({
53
+ widget: new GhostWidget(value.text),
54
+ side: 1,
55
+ }).range(value.pos),
56
+ ]);
57
+ }),
58
+ });
59
+ export function acceptGhostText(view) {
60
+ const ghost = view.state.field(ghostField, false);
61
+ if (!ghost || !ghost.text)
62
+ return false;
63
+ view.dispatch({
64
+ changes: { from: ghost.pos, insert: ghost.text },
65
+ selection: { anchor: ghost.pos + ghost.text.length },
66
+ effects: setGhost.of(null),
67
+ userEvent: "input.complete",
68
+ });
69
+ return true;
70
+ }
71
+ export function dismissGhostText(view) {
72
+ const ghost = view.state.field(ghostField, false);
73
+ if (!ghost)
74
+ return false;
75
+ view.dispatch({ effects: setGhost.of(null) });
76
+ return true;
77
+ }
78
+ export function ghostText(fetcher, options = {}) {
79
+ const debounceMs = options.debounceMs ?? 400;
80
+ const minPrefixLength = options.minPrefixLength ?? 0;
81
+ const plugin = ViewPlugin.fromClass(class {
82
+ view;
83
+ timer = null;
84
+ controller = null;
85
+ // In-flight fetches (supersession can briefly overlap two). While > 0 the
86
+ // editor root carries .cm-ai-pending so themes can signal "computing"
87
+ // (e.g. tint the cursor) instead of leaving the wait invisible.
88
+ inflight = 0;
89
+ constructor(view) {
90
+ this.view = view;
91
+ }
92
+ trackFetch(delta) {
93
+ this.inflight += delta;
94
+ this.view.dom.classList.toggle("cm-ai-pending", this.inflight > 0);
95
+ }
96
+ update(update) {
97
+ if (!update.docChanged)
98
+ return;
99
+ // Any user-driven edit triggers a fetch — typing, paste, delete,
100
+ // undo/redo — EXCEPT accepting a suggestion (that would immediately
101
+ // re-fetch on its own output). The kind of edit doesn't matter: the
102
+ // model can complete from whatever state the edit produced.
103
+ const isUserEdit = update.transactions.some((tr) => (tr.isUserEvent("input") && !tr.isUserEvent("input.complete")) ||
104
+ tr.isUserEvent("delete") ||
105
+ tr.isUserEvent("undo") ||
106
+ tr.isUserEvent("redo") ||
107
+ tr.isUserEvent("move"));
108
+ if (!isUserEdit)
109
+ return;
110
+ this.schedule();
111
+ }
112
+ schedule() {
113
+ if (this.timer)
114
+ clearTimeout(this.timer);
115
+ this.controller?.abort();
116
+ this.timer = setTimeout(() => this.fetch(), debounceMs);
117
+ }
118
+ async fetch() {
119
+ const { state } = this.view;
120
+ if (!this.view.hasFocus)
121
+ return;
122
+ if (options.holdWhen?.(state))
123
+ return; // e.g. kernel dropdown open
124
+ const pos = state.selection.main.head;
125
+ if (!state.selection.main.empty)
126
+ return;
127
+ const prefix = state.sliceDoc(0, pos);
128
+ const suffix = state.sliceDoc(pos);
129
+ if (prefix.trim().length < minPrefixLength)
130
+ return;
131
+ this.controller = new AbortController();
132
+ const { signal } = this.controller;
133
+ const startDoc = state.doc;
134
+ const stale = () => signal.aborted || this.view.state.doc !== startDoc;
135
+ let streamed = "";
136
+ this.trackFetch(1);
137
+ try {
138
+ const full = await fetcher({ prefix, suffix, state }, {
139
+ signal,
140
+ onChunk: (text) => {
141
+ if (stale())
142
+ return;
143
+ streamed += text;
144
+ this.view.dispatch({ effects: setGhost.of({ pos, text: streamed }) });
145
+ },
146
+ });
147
+ if (stale())
148
+ return;
149
+ if (full)
150
+ this.view.dispatch({ effects: setGhost.of({ pos, text: full }) });
151
+ }
152
+ catch {
153
+ /* aborted or backend error — ghost text simply doesn't appear */
154
+ }
155
+ finally {
156
+ this.trackFetch(-1);
157
+ }
158
+ }
159
+ destroy() {
160
+ if (this.timer)
161
+ clearTimeout(this.timer);
162
+ this.controller?.abort();
163
+ this.inflight = 0;
164
+ this.view.dom.classList.remove("cm-ai-pending");
165
+ }
166
+ });
167
+ return [
168
+ ghostField,
169
+ plugin,
170
+ Prec.highest(keymap.of([
171
+ { key: "Tab", run: acceptGhostText },
172
+ { key: "Escape", run: dismissGhostText },
173
+ ])),
174
+ ];
175
+ }
@@ -0,0 +1,17 @@
1
+ import type { CompleteOptions, CompletionRequest, CompletionResult, EngineOptions } from "../types.js";
2
+ /**
3
+ * Backend-agnostic autocomplete engine: prompt assembly, LRU result cache,
4
+ * and per-session supersession (a new request aborts the previous one with
5
+ * the same sessionKey).
6
+ */
7
+ export declare class AutocompleteEngine {
8
+ private opts;
9
+ private cache;
10
+ private inflight;
11
+ private turnChain;
12
+ private contextBudget;
13
+ private maxLines;
14
+ constructor(opts: EngineOptions);
15
+ complete(req: CompletionRequest, { signal, onChunk }?: CompleteOptions): Promise<CompletionResult>;
16
+ dispose(): void;
17
+ }
@@ -0,0 +1,135 @@
1
+ import { LruCache } from "./lru.js";
2
+ import { buildPrompt, cacheKey } from "./prompt.js";
3
+ import { createPrefixTrimStreamFilter, createTagStreamFilter, extractCompletionTag, stripFences, trimPrefixOverlap, trimSuffixOverlap, } from "./text.js";
4
+ /**
5
+ * Backend-agnostic autocomplete engine: prompt assembly, LRU result cache,
6
+ * and per-session supersession (a new request aborts the previous one with
7
+ * the same sessionKey).
8
+ */
9
+ export class AutocompleteEngine {
10
+ opts;
11
+ cache;
12
+ inflight = new Map();
13
+ // Per-session dispatch chain (single-flight): a backend turn cannot be
14
+ // cancelled once dispatched — an aborted request's worker still runs its
15
+ // turn to completion. Without this, a typing burst fanned superseded
16
+ // requests out across workers: the pool exhausted, each keystroke cold-
17
+ // spawned another process, and contending spawns snowballed into ~30s
18
+ // waits. Chaining per session keeps at most ONE turn per cell in flight;
19
+ // superseded requests abort while queued and never touch the backend.
20
+ turnChain = new Map();
21
+ contextBudget;
22
+ maxLines;
23
+ constructor(opts) {
24
+ this.opts = opts;
25
+ this.cache = new LruCache(opts.cacheSize ?? 128);
26
+ this.contextBudget = opts.contextBudget ?? 6000;
27
+ this.maxLines = opts.maxLines ?? 10; // field-tested: longer caps don't hurt latency; short ones truncate wanted completions
28
+ }
29
+ async complete(req, { signal, onChunk } = {}) {
30
+ const t0 = performance.now();
31
+ const key = cacheKey(req);
32
+ const cached = this.cache.get(key);
33
+ if (cached !== undefined) {
34
+ onChunk?.(cached);
35
+ return {
36
+ text: cached,
37
+ backend: this.opts.backend.name,
38
+ fromCache: true,
39
+ ttfbMs: 0,
40
+ totalMs: Math.round(performance.now() - t0),
41
+ };
42
+ }
43
+ // Supersede any in-flight request for the same session (cell).
44
+ const session = req.sessionKey;
45
+ let controller;
46
+ if (session !== undefined) {
47
+ this.inflight.get(session)?.abort(new Error("superseded"));
48
+ controller = new AbortController();
49
+ this.inflight.set(session, controller);
50
+ if (signal) {
51
+ const upstream = signal;
52
+ upstream.addEventListener("abort", () => controller.abort(upstream.reason), {
53
+ once: true,
54
+ });
55
+ }
56
+ }
57
+ const effectiveSignal = controller?.signal ?? signal;
58
+ let ttfb = null;
59
+ const rawChunkGuard = (t) => {
60
+ if (ttfb === null)
61
+ ttfb = performance.now() - t0;
62
+ onChunk?.(t);
63
+ };
64
+ // Streamed ghost text goes through the same shaping as the final text so
65
+ // the done event never visibly "snaps" it: strip <completion> tags, then
66
+ // hold back a possible prefix-echo until it either confirms (stripped) or
67
+ // is ruled out (streamed through).
68
+ const chunkGuard = createTagStreamFilter(createPrefixTrimStreamFilter(req.prefix, rawChunkGuard));
69
+ try {
70
+ // Per-request tuning (Advanced settings), clamped to sane bounds.
71
+ const clamp = (v, lo, hi, dflt) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, Math.round(v))) : dflt;
72
+ const prompt = buildPrompt(req, {
73
+ contextBudget: clamp(req.contextBudget, 0, 200_000, this.contextBudget),
74
+ maxLines: clamp(req.maxLines, 1, 40, this.maxLines),
75
+ });
76
+ const diag = { promptChars: prompt.length };
77
+ // Single-flight per session: wait for the previous turn on this cell to
78
+ // settle before dispatching. If we're superseded while queued (the user
79
+ // kept typing), bail here — no backend turn is ever wasted on us.
80
+ const prev = session !== undefined ? this.turnChain.get(session) : undefined;
81
+ const run = (async () => {
82
+ if (prev) {
83
+ const tQueue = performance.now();
84
+ await prev; // never rejects (stored pre-caught)
85
+ diag.queueWaitMs = Math.round(performance.now() - tQueue);
86
+ if (effectiveSignal?.aborted)
87
+ throw new Error("superseded");
88
+ }
89
+ return this.opts.backend.complete(prompt, {
90
+ signal: effectiveSignal,
91
+ onChunk: chunkGuard,
92
+ diag,
93
+ });
94
+ })();
95
+ if (session !== undefined) {
96
+ const link = run.then(() => undefined, () => undefined);
97
+ this.turnChain.set(session, link);
98
+ void link.then(() => {
99
+ if (this.turnChain.get(session) === link)
100
+ this.turnChain.delete(session);
101
+ });
102
+ }
103
+ const raw = await run;
104
+ diag.rawChars = raw.length;
105
+ // Tag-wrapped replies carry whitespace verbatim; untagged replies fall
106
+ // back to the fence-stripping pipeline.
107
+ const tagged = extractCompletionTag(raw);
108
+ const text = trimSuffixOverlap(req.suffix ?? "", trimPrefixOverlap(req.prefix, tagged !== null ? tagged : stripFences(raw)));
109
+ // Cache only non-empty results: an empty completion is a transient
110
+ // model shrug, and caching it pins "no suggestion" onto that exact
111
+ // prefix for the cache's lifetime (observed as instant empty repeats).
112
+ if (text)
113
+ this.cache.set(key, text);
114
+ return {
115
+ text,
116
+ backend: this.opts.backend.name,
117
+ fromCache: false,
118
+ ttfbMs: Math.round(ttfb ?? performance.now() - t0),
119
+ totalMs: Math.round(performance.now() - t0),
120
+ diag,
121
+ };
122
+ }
123
+ finally {
124
+ if (session !== undefined && this.inflight.get(session) === controller) {
125
+ this.inflight.delete(session);
126
+ }
127
+ }
128
+ }
129
+ dispose() {
130
+ for (const c of this.inflight.values())
131
+ c.abort(new Error("engine disposed"));
132
+ this.inflight.clear();
133
+ this.opts.backend.dispose();
134
+ }
135
+ }
@@ -0,0 +1,7 @@
1
+ export declare class LruCache<V> {
2
+ private capacity;
3
+ private map;
4
+ constructor(capacity: number);
5
+ get(key: string): V | undefined;
6
+ set(key: string, value: V): void;
7
+ }
@@ -0,0 +1,26 @@
1
+ export class LruCache {
2
+ capacity;
3
+ map = new Map();
4
+ constructor(capacity) {
5
+ this.capacity = capacity;
6
+ }
7
+ get(key) {
8
+ const v = this.map.get(key);
9
+ if (v !== undefined) {
10
+ this.map.delete(key);
11
+ this.map.set(key, v);
12
+ }
13
+ return v;
14
+ }
15
+ set(key, value) {
16
+ if (this.capacity <= 0)
17
+ return;
18
+ if (this.map.has(key))
19
+ this.map.delete(key);
20
+ this.map.set(key, value);
21
+ if (this.map.size > this.capacity) {
22
+ const oldest = this.map.keys().next().value;
23
+ this.map.delete(oldest);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,12 @@
1
+ import type { CompletionRequest } from "../types.js";
2
+ export interface PromptOptions {
3
+ contextBudget: number;
4
+ maxLines: number;
5
+ }
6
+ /**
7
+ * Build the completion prompt: instruction, cross-cell context (nearest cells
8
+ * kept when over budget), then the active cell with a <CURSOR> marker.
9
+ */
10
+ export declare function buildPrompt(req: CompletionRequest, opts: PromptOptions): string;
11
+ /** Cache key: exact near-cursor text + a cheap digest of the wider context. */
12
+ export declare function cacheKey(req: CompletionRequest): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Does a Claude Code project-transcript directory name belong to a workspace
3
+ * carrying `token`? Claude munges the cwd into the dir name by replacing
4
+ * non-alphanumerics, so we compare alphanumeric-only forms — munge-algorithm
5
+ * independent, and a UUID token cannot collide with a real project path.
6
+ */
7
+ export declare function transcriptDirMatchesToken(dirName: string, token: string): boolean;
8
+ /**
9
+ * Extract the tag-wrapped completion, whitespace preserved VERBATIM.
10
+ *
11
+ * Why tags: models structurally avoid beginning a message with whitespace, so
12
+ * a completion that must START with "\n" (cursor at the end of a finished
13
+ * comment like `# fibonacci`) loses its newline when asked for raw output —
14
+ * verified empirically; no prompt wording fixed it. Inside a tag pair the
15
+ * leading newline survives generation. Returns null when the reply isn't
16
+ * tag-wrapped (fall back to the fence-stripping pipeline).
17
+ */
18
+ export declare function extractCompletionTag(text: string): string | null;
19
+ /**
20
+ * Streaming variant: wraps an onChunk callback so tag delimiters never leak
21
+ * into streamed ghost text, while the inner text (leading whitespace included)
22
+ * streams through as it arrives. If no opening tag shows up within the first
23
+ * 64 chars, assumes an untagged reply and passes everything through.
24
+ */
25
+ export declare function createTagStreamFilter(emit: (t: string) => void): (t: string) => void;
26
+ /**
27
+ * Strip a wrapping markdown code fence, if present. Leading whitespace of an
28
+ * UNFENCED completion is preserved verbatim — it is often meaningful: with
29
+ * the cursor at the end of `# fibonacci` the completion must START with "\n"
30
+ * to put code on the next line, and indentation after `if x:` matters too.
31
+ * (A blanket .trim() here silently glued completions onto comments.)
32
+ */
33
+ export declare function stripFences(text: string): string;
34
+ /**
35
+ * Streaming variant of trimPrefixOverlap: emits the completion as it streams,
36
+ * holding back ONLY while the text so far could still be the beginning of a
37
+ * longer prefix-echo. Without this the ghost text visibly "snaps" when the
38
+ * done event applies the trim that streaming skipped (echo shown, then
39
+ * removed). For non-echo completions the hold is typically zero characters.
40
+ */
41
+ export declare function createPrefixTrimStreamFilter(prefix: string, emit: (t: string) => void): (t: string) => void;
42
+ /**
43
+ * Models sometimes echo the tail of the prefix (e.g. the current line) before
44
+ * continuing. Trim the longest suffix-of-prefix that the completion starts
45
+ * with, so accepting the suggestion never duplicates typed text.
46
+ */
47
+ export declare function trimPrefixOverlap(prefix: string, completion: string): string;
48
+ /**
49
+ * Models also often close constructs that the suffix already closes (quotes,
50
+ * brackets, parens). Trim the longest head-of-suffix that the completion ends
51
+ * with, so accepted text composes with what follows the cursor.
52
+ */
53
+ export declare function trimSuffixOverlap(suffix: string, completion: string): string;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Does a Claude Code project-transcript directory name belong to a workspace
3
+ * carrying `token`? Claude munges the cwd into the dir name by replacing
4
+ * non-alphanumerics, so we compare alphanumeric-only forms — munge-algorithm
5
+ * independent, and a UUID token cannot collide with a real project path.
6
+ */
7
+ export function transcriptDirMatchesToken(dirName, token) {
8
+ const norm = (s) => s.replace(/[^a-z0-9]/gi, "").toLowerCase();
9
+ const needle = norm(token);
10
+ return needle.length > 0 && norm(dirName).includes(needle);
11
+ }
12
+ const OPEN_TAG = "<completion>";
13
+ const CLOSE_TAG = "</completion>";
14
+ /**
15
+ * Extract the tag-wrapped completion, whitespace preserved VERBATIM.
16
+ *
17
+ * Why tags: models structurally avoid beginning a message with whitespace, so
18
+ * a completion that must START with "\n" (cursor at the end of a finished
19
+ * comment like `# fibonacci`) loses its newline when asked for raw output —
20
+ * verified empirically; no prompt wording fixed it. Inside a tag pair the
21
+ * leading newline survives generation. Returns null when the reply isn't
22
+ * tag-wrapped (fall back to the fence-stripping pipeline).
23
+ */
24
+ export function extractCompletionTag(text) {
25
+ const start = text.indexOf(OPEN_TAG);
26
+ if (start === -1)
27
+ return null;
28
+ const inner = text.slice(start + OPEN_TAG.length);
29
+ const end = inner.indexOf(CLOSE_TAG);
30
+ return end === -1 ? inner : inner.slice(0, end);
31
+ }
32
+ /**
33
+ * Streaming variant: wraps an onChunk callback so tag delimiters never leak
34
+ * into streamed ghost text, while the inner text (leading whitespace included)
35
+ * streams through as it arrives. If no opening tag shows up within the first
36
+ * 64 chars, assumes an untagged reply and passes everything through.
37
+ */
38
+ export function createTagStreamFilter(emit) {
39
+ let state = "seeking";
40
+ let buf = "";
41
+ return (chunk) => {
42
+ if (state === "done")
43
+ return;
44
+ if (state === "passthrough")
45
+ return emit(chunk);
46
+ buf += chunk;
47
+ if (state === "seeking") {
48
+ const at = buf.indexOf(OPEN_TAG);
49
+ if (at !== -1) {
50
+ state = "inside";
51
+ buf = buf.slice(at + OPEN_TAG.length);
52
+ }
53
+ else if (buf.length > 64 && !OPEN_TAG.startsWith(buf.slice(-OPEN_TAG.length))) {
54
+ state = "passthrough";
55
+ emit(buf);
56
+ buf = "";
57
+ return;
58
+ }
59
+ else {
60
+ return; // keep buffering — the tag may still be arriving
61
+ }
62
+ }
63
+ // inside: emit everything except a possible partial close tag at the tail
64
+ const close = buf.indexOf(CLOSE_TAG);
65
+ if (close !== -1) {
66
+ if (close > 0)
67
+ emit(buf.slice(0, close));
68
+ state = "done";
69
+ buf = "";
70
+ return;
71
+ }
72
+ // Hold back the longest suffix that could be the start of the close tag.
73
+ let hold = 0;
74
+ for (let n = Math.min(CLOSE_TAG.length - 1, buf.length); n > 0; n--) {
75
+ if (CLOSE_TAG.startsWith(buf.slice(-n))) {
76
+ hold = n;
77
+ break;
78
+ }
79
+ }
80
+ const emittable = buf.slice(0, buf.length - hold);
81
+ if (emittable)
82
+ emit(emittable);
83
+ buf = buf.slice(buf.length - hold);
84
+ };
85
+ }
86
+ /**
87
+ * Strip a wrapping markdown code fence, if present. Leading whitespace of an
88
+ * UNFENCED completion is preserved verbatim — it is often meaningful: with
89
+ * the cursor at the end of `# fibonacci` the completion must START with "\n"
90
+ * to put code on the next line, and indentation after `if x:` matters too.
91
+ * (A blanket .trim() here silently glued completions onto comments.)
92
+ */
93
+ export function stripFences(text) {
94
+ const t = text.trim();
95
+ const open = t.match(/^```[a-zA-Z0-9_-]*\n?/);
96
+ if (open) {
97
+ return t.slice(open[0].length).replace(/\n?```\s*$/, "");
98
+ }
99
+ // No fence: keep leading whitespace, drop only trailing whitespace.
100
+ return text.replace(/\s+$/, "");
101
+ }
102
+ /**
103
+ * Streaming variant of trimPrefixOverlap: emits the completion as it streams,
104
+ * holding back ONLY while the text so far could still be the beginning of a
105
+ * longer prefix-echo. Without this the ghost text visibly "snaps" when the
106
+ * done event applies the trim that streaming skipped (echo shown, then
107
+ * removed). For non-echo completions the hold is typically zero characters.
108
+ */
109
+ export function createPrefixTrimStreamFilter(prefix, emit) {
110
+ const tail = prefix.slice(-200);
111
+ let buf = "";
112
+ let settled = false;
113
+ return (chunk) => {
114
+ if (settled)
115
+ return emit(chunk);
116
+ buf += chunk;
117
+ // Could buf still grow into a LONGER echo of the prefix tail?
118
+ let couldExtend = false;
119
+ for (let n = buf.length + 1; n <= tail.length; n++) {
120
+ if (tail.slice(-n).startsWith(buf)) {
121
+ couldExtend = true;
122
+ break;
123
+ }
124
+ }
125
+ if (couldExtend)
126
+ return; // hold — echo still possible
127
+ // Settle: strip the longest confirmed echo, stream the rest.
128
+ let confirmed = 0;
129
+ for (let n = Math.min(tail.length, buf.length); n > 0; n--) {
130
+ if (buf.startsWith(tail.slice(-n))) {
131
+ confirmed = n;
132
+ break;
133
+ }
134
+ }
135
+ settled = true;
136
+ const out = buf.slice(confirmed);
137
+ buf = "";
138
+ if (out)
139
+ emit(out);
140
+ };
141
+ }
142
+ /**
143
+ * Models sometimes echo the tail of the prefix (e.g. the current line) before
144
+ * continuing. Trim the longest suffix-of-prefix that the completion starts
145
+ * with, so accepting the suggestion never duplicates typed text.
146
+ */
147
+ export function trimPrefixOverlap(prefix, completion) {
148
+ const tail = prefix.slice(-200);
149
+ for (let n = Math.min(tail.length, completion.length); n > 0; n--) {
150
+ if (completion.startsWith(tail.slice(-n))) {
151
+ return completion.slice(n);
152
+ }
153
+ }
154
+ return completion;
155
+ }
156
+ /**
157
+ * Models also often close constructs that the suffix already closes (quotes,
158
+ * brackets, parens). Trim the longest head-of-suffix that the completion ends
159
+ * with, so accepted text composes with what follows the cursor.
160
+ */
161
+ export function trimSuffixOverlap(suffix, completion) {
162
+ const head = suffix.slice(0, 200);
163
+ for (let n = Math.min(head.length, completion.length); n > 0; n--) {
164
+ if (completion.endsWith(head.slice(0, n))) {
165
+ return completion.slice(0, completion.length - n);
166
+ }
167
+ }
168
+ return completion;
169
+ }
@@ -0,0 +1,7 @@
1
+ export { AutocompleteEngine } from "./core/engine.js";
2
+ export { ClaudeBackend, type ClaudeBackendOptions } from "./backends/claude.js";
3
+ export { CodexBackend, type CodexBackendOptions } from "./backends/codex.js";
4
+ export { buildPrompt, cacheKey } from "./core/prompt.js";
5
+ export type { Transport, SshTransport } from "./transport.js";
6
+ export { stripFences, trimPrefixOverlap, trimSuffixOverlap, transcriptDirMatchesToken, } from "./core/text.js";
7
+ export type { CompletionBackend, CompletionRequest, CompletionResult, CompleteOptions, EngineOptions, NotebookCellContext, } from "./types.js";
@@ -0,0 +1,5 @@
1
+ export { AutocompleteEngine } from "./core/engine.js";
2
+ export { ClaudeBackend } from "./backends/claude.js";
3
+ export { CodexBackend } from "./backends/codex.js";
4
+ export { buildPrompt, cacheKey } from "./core/prompt.js";
5
+ export { stripFences, trimPrefixOverlap, trimSuffixOverlap, transcriptDirMatchesToken, } from "./core/text.js";