@intentic/extension-api 1.222.0 → 1.224.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.
package/src/background.ts CHANGED
@@ -2,7 +2,7 @@ import type { Ref } from "vue";
2
2
  import type { Disposable, IntenticApi } from "./api.js";
3
3
  import { sandboxRef, sandboxScopeGuard } from "./scope.js";
4
4
 
5
- /* WHAT AN EXTENSION DOES WHILE NONE OF IT IS ON SCREEN — the two pieces every surface that badges a rail tile
5
+ /* WHAT AN EXTENSION DOES WHILE NONE OF IT IS ON SCREEN, the two pieces every surface that badges a rail tile
6
6
  * turned out to need, and had been writing out by hand.
7
7
  *
8
8
  * A tile has to be able to say something before it is opened. That rules out the view's own query, which stops
@@ -12,7 +12,7 @@ import { sandboxRef, sandboxScopeGuard } from "./scope.js";
12
12
  * Seven modules across six extensions arrived at the identical shape for that timer, and it carries five rules
13
13
  * that are each invisible until they are broken:
14
14
  *
15
- * never reject it runs detached, so a throw is an unhandled rejection with no caller to report to — and
15
+ * never reject it runs detached, so a throw is an unhandled rejection with no caller to report to, and
16
16
  * that includes reading the host handle, which throws before activate() has bound one
17
17
  * skip when down an unreachable daemon is not news; asking it is a failed request per tick, forever
18
18
  * guard the await a read issued before a sandbox switch must not write its answer into the box after it
@@ -31,7 +31,7 @@ export interface SandboxPoll<T> {
31
31
  readonly state: Ref<T>;
32
32
  // Begin polling. Push the Disposable onto `context.subscriptions` and the clock stops with the extension.
33
33
  start(): Disposable;
34
- // Read now, off-cycle — for the moments that change the answer and should not wait out the interval: a
34
+ // Read now, off-cycle, for the moments that change the answer and should not wait out the interval: a
35
35
  // connection appearing, a draft published, a run discarded.
36
36
  refresh(): void;
37
37
  }
@@ -46,14 +46,14 @@ export interface SandboxPollOptions<T> {
46
46
  readonly everyMs: number;
47
47
  // The value before anything has been read, rebuilt on every sandbox switch (sandboxRef).
48
48
  readonly initial: () => T;
49
- /* The read. Gets the api and the value currently held — the second for a poll that ACCUMULATES rather than
49
+ /* The read. Gets the api and the value currently held, the second for a poll that ACCUMULATES rather than
50
50
  * replaces, where one failed source must leave its own last answer standing beside the others. Throwing is
51
51
  * fine and means "nothing changed": the value in hand is kept.
52
52
  */
53
53
  readonly read: (api: IntenticApi, previous: T) => Promise<T>;
54
54
  /* Whether `start()` reads immediately as well as on the interval. Default true, because a tile that only
55
55
  * badges a minute after login is a tile nobody trusts. Set false when the poll has nothing to ask until
56
- * something else tells it what to ask about — deployments learns its connections from `detect()`. */
56
+ * something else tells it what to ask about, deployments learns its connections from `detect()`. */
57
57
  readonly immediate?: boolean;
58
58
  // For a value that owns something the garbage collector will not take back; see sandboxRef.
59
59
  readonly dispose?: (previous: T) => void;
@@ -75,7 +75,7 @@ export const sandboxPoll = <T>(options: SandboxPollOptions<T>): SandboxPoll<T> =
75
75
  }
76
76
  state.value = next;
77
77
  } catch {
78
- // Whatever went wrong — an unbound host, a refused route, a daemon mid-boot — the answer is the
78
+ // Whatever went wrong, an unbound host, a refused route, a daemon mid-boot, the answer is the
79
79
  // same: leave the last value standing. "We could not ask" is not "there is nothing there".
80
80
  }
81
81
  };
@@ -97,12 +97,12 @@ export const sandboxPoll = <T>(options: SandboxPollOptions<T>): SandboxPoll<T> =
97
97
  *
98
98
  * The rail's bar is that a badge means "something happened here that you do not already know about". Meeting it
99
99
  * needs somewhere to record what they DO know, and three extensions independently chose the same home: a JSON
100
- * object under `.intentic`, keyed by whatever identifies the thing. That is the right home — it survives a
101
- * reload, it is shared across the owner's browsers, and it needs no setting nobody would ever type — but each
100
+ * object under `.intentic`, keyed by whatever identifies the thing. That is the right home, it survives a
101
+ * reload, it is shared across the owner's browsers, and it needs no setting nobody would ever type, but each
102
102
  * of them then hand-wrote the same tolerant reader and the same careful write.
103
103
  *
104
104
  * KEY → MARK, where the mark is what makes the entry STALE. That is the whole vocabulary, and it covers both
105
- * the ledgers that compare (a chore's evidence digest, a story's verdict — the same key with a different mark
105
+ * the ledgers that compare (a chore's evidence digest, a story's verdict, the same key with a different mark
106
106
  * is news again) and the ones that only ask whether a key is present at all (a document set reviewed once).
107
107
  * A presence-only ledger writes the acknowledgement time as its mark, which nothing reads and a human opening
108
108
  * the file is glad of.
@@ -113,18 +113,18 @@ export const sandboxPoll = <T>(options: SandboxPollOptions<T>): SandboxPoll<T> =
113
113
  *
114
114
  * BOTH WRITES ANSWER "did this take effect", which is the question the caller's NEXT line depends on. Marking
115
115
  * something seen is almost always followed by folding it out of the badge locally, so the tile clears on the
116
- * spot rather than at the next poll — and that fold is a write into sandbox-scoped state, so it must not happen
116
+ * spot rather than at the next poll, and that fold is a write into sandbox-scoped state, so it must not happen
117
117
  * when the acknowledgement itself was abandoned because the owner switched sandbox mid-operation. It would
118
118
  * silence the NEW box's badge for a fact about the old one. `false` means only that: the scope moved. A ledger
119
119
  * that already said what you asked it to say answers `true`, because it does. */
120
120
  export interface SandboxLedger {
121
121
  // Everything acknowledged so far. Absent, unparseable or not-an-object all read as nothing.
122
122
  read(): Promise<Readonly<Record<string, string>>>;
123
- /* Record these, leaving every other entry alone — the ordinary acknowledgement. No write happens when
123
+ /* Record these, leaving every other entry alone, the ordinary acknowledgement. No write happens when
124
124
  * nothing moved: the file push would otherwise cost every connected browser a refetch for a file whose
125
125
  * content is identical. */
126
126
  mark(entries: Readonly<Record<string, string>>): Promise<boolean>;
127
- /* Make these the WHOLE ledger, dropping anything not named. For a ledger whose keys go out of scope — a
127
+ /* Make these the WHOLE ledger, dropping anything not named. For a ledger whose keys go out of scope, a
128
128
  * run that has scrolled past the scan window can never be seen again, and merging forever would grow the
129
129
  * file without bound. Same no-op-when-unchanged rule as `mark`.
130
130
  */
@@ -142,7 +142,7 @@ export const sandboxLedger = (host: () => IntenticApi, path: string): SandboxLed
142
142
  return Object.fromEntries(Object.entries(parsed ?? {}).filter((entry): entry is [string, string] => typeof entry[1] === `string`));
143
143
  };
144
144
 
145
- /* One writer for both verbs, and the scope guard lives HERE rather than at the call site — this is the
145
+ /* One writer for both verbs, and the scope guard lives HERE rather than at the call site, this is the
146
146
  * only thing in an extension's background work that damages state on DISK when a sandbox switch lands
147
147
  * mid-operation. Reading one workspace's acknowledgements and writing them into the tree of the workspace
148
148
  * the owner has just moved to is bookkeeping filed in the wrong place, which no later poll corrects. */
package/src/diff.ts CHANGED
@@ -1,22 +1,22 @@
1
- /* WHAT AN EXTENSION HANDS THE HOST TO OPEN A DIFF — the argument to `api.workspace.openDiff`.
1
+ /* WHAT AN EXTENSION HANDS THE HOST TO OPEN A DIFF, the argument to `api.workspace.openDiff`.
2
2
  *
3
3
  * A diff belongs in the editor area beside the files it is about, in the same tab strip as everything else the
4
4
  * user has open. That strip is the host's, so an extension that has computed a before/after pair has nowhere to
5
5
  * put it: a view renders inside its own frame, and a document provider inside its own tab. This is the way out,
6
- * and it is the whole shape of the contribution — the extension says what changed, the host owns the tab, the
6
+ * and it is the whole shape of the contribution, the extension says what changed, the host owns the tab, the
7
7
  * viewer, the close orchestration and the dirty-buffer bookkeeping.
8
8
  *
9
9
  * It lives in the PUBLIC api package rather than in the app because the app is downstream of it: the workspace's
10
10
  * own review surfaces build the identical payload, and having two spellings of it is how the two would drift. */
11
11
 
12
12
  // Git's vocabulary for what happened to a file. "conflicted" is git's unmerged state (`U`) and is not a kind of
13
- // modification — there is no stage 0 for such a path, so nothing a commit could record. The host renders each of
13
+ // modification, there is no stage 0 for such a path, so nothing a commit could record. The host renders each of
14
14
  // these as its own letter and colour.
15
15
  export type ChangeStatus = "added" | "modified" | "deleted" | "renamed" | "type-changed" | "conflicted";
16
16
 
17
17
  /* A binary diff ships no text, so its two sides ride the payload as daemon URLs its BYTES are fetched from
18
- * rather than as content. An absent side means that side does not exist — an added file has no before, a deleted
19
- * one no after — and the viewer then gives the side that does exist the whole pane instead of drawing an empty
18
+ * rather than as content. An absent side means that side does not exist, an added file has no before, a deleted
19
+ * one no after, and the viewer then gives the side that does exist the whole pane instead of drawing an empty
20
20
  * half beside it. */
21
21
  export interface DiffRawSides {
22
22
  readonly beforeRaw?: string;
@@ -24,33 +24,33 @@ export interface DiffRawSides {
24
24
  }
25
25
 
26
26
  export interface DiffPayload extends DiffRawSides {
27
- /* The diff SOURCE's identity — a commit sha, a snapshot id, `working:<repo>`. Together with `scope` and
27
+ /* The diff SOURCE's identity, a commit sha, a snapshot id, `working:<repo>`. Together with `scope` and
28
28
  * `path` it is the tab's identity, so re-opening the same file at the same commit focuses the tab that is
29
29
  * already open rather than stacking a second copy of it. Pick something stable and collision-free; prefixing
30
30
  * with the extension's own id is the safe habit. */
31
31
  readonly key: string;
32
- // Which repo (or snapshot scope) the path is relative to — the other half of the tab identity.
32
+ // Which repo (or snapshot scope) the path is relative to, the other half of the tab identity.
33
33
  readonly scope: string;
34
34
  // The tab's label. Short: the strip is narrow, and "file.ts @ a1b2c3d" reads better than a full path.
35
35
  readonly label: string;
36
36
  readonly status: ChangeStatus;
37
37
  readonly path: string;
38
- // The two sides as text. Absent where the side does not exist, or where the content is binary/oversized —
38
+ // The two sides as text. Absent where the side does not exist, or where the content is binary/oversized,
39
39
  // `binary` and `truncated` are what the viewer renders instead of an empty pane.
40
40
  readonly before?: string;
41
41
  readonly after?: string;
42
42
  readonly binary?: boolean;
43
43
  readonly truncated?: boolean;
44
44
  // What the row that opened this diff already knew about its size, carried onto the tab's toolbar. Absent
45
- // where the source has no numstat to give (a binary file, a change list without line counts) — the toolbar
45
+ // where the source has no numstat to give (a binary file, a change list without line counts), the toolbar
46
46
  // then renders nothing rather than a zero.
47
47
  readonly additions?: number;
48
48
  readonly deletions?: number;
49
- /* THE CONTENT IS STILL COMING — open the tab now and fill it in when it lands (`workspace.fillDiff`).
49
+ /* THE CONTENT IS STILL COMING, open the tab now and fill it in when it lands (`workspace.fillDiff`).
50
50
  *
51
51
  * Reading a file the source has to compute or fetch is a wait, and a wait that has nowhere to be drawn gets
52
52
  * drawn on the click instead: the row is clicked, nothing on screen changes, and the reader clicks it again.
53
- * Everything the tab needs to exist — its identity, its label, the status letter and the line counts — is
53
+ * Everything the tab needs to exist, its identity, its label, the status letter and the line counts, is
54
54
  * known before the content is, so the tab opens on the click and the panes fill underneath it.
55
55
  *
56
56
  * Absent means the payload IS the content, which is what an extension handing over an already-computed
package/src/engines.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Minimal matcher for a manifest's `engines.intentic` range against the host's extension API version: an exact
2
- // version ("0.1.0") or a caret range ("^0.1", "^1", "^1.2.3"). Caret follows semver — same major at or above
2
+ // version ("0.1.0") or a caret range ("^0.1", "^1", "^1.2.3"). Caret follows semver, same major at or above
3
3
  // the floor, and while the major is 0 the minor is breaking too. Anything unparseable fails closed: the loader
4
4
  // reports the extension incompatible rather than activating on a guess.
5
5
 
package/src/facts.ts CHANGED
@@ -1,15 +1,15 @@
1
- /* The stable DETECTION facts — the subset of the daemon's wire schemas an extension's `detect()` reads to decide
1
+ /* The stable DETECTION facts, the subset of the daemon's wire schemas an extension's `detect()` reads to decide
2
2
  * when to activate a view. This is deliberately narrow (evidence over identity) so activation logic doesn't
3
3
  * couple to the daemon's fuller summaries. It is NOT the data plane: an activated extension reads real data over
4
- * `api.sandbox.request/json` against the `@intentic/sandbox-contract` schemas — the first-party wire contract,
4
+ * `api.sandbox.request/json` against the `@intentic/sandbox-contract` schemas, the first-party wire contract,
5
5
  * gated per-route by the manifest's `permissions.sandbox` allowlist. (Because every first-party extension is
6
6
  * in-repo and compiled together, a wire change there is caught by the compiler and fixed atomically, so there is
7
- * no separate "stable data API" to promote — only detection is version-stable.) The daemon's own summaries
7
+ * no separate "stable data API" to promote, only detection is version-stable.) The daemon's own summaries
8
8
  * (PanelSummary, CapabilitySummary) are structural supersets and flow into `detect()` unmapped, but only THESE
9
9
  * fields are guaranteed to it. Optional fields carry `| undefined` so zod-inferred wire types assign under
10
10
  * exactOptionalPropertyTypes. */
11
11
 
12
- // Daemon-computed content facts for one discovered repository under /work (`repo` is its root-relative dir) —
12
+ // Daemon-computed content facts for one discovered repository under /work (`repo` is its root-relative dir),
13
13
  // evidence over identity: a repo is served because of what it CONTAINS (deploy.config.ts, pnpm-workspace.yaml
14
14
  // + turbo.json, .intentic/ui), not what it happens to be named.
15
15
  export interface RepoFacts {
@@ -23,18 +23,18 @@ export interface RepoFacts {
23
23
  readonly directoryUi: boolean;
24
24
  readonly monorepo: boolean;
25
25
  readonly vitest: boolean;
26
- // Whether the repo describes its features as user stories (a docs/user-stories directory) — the one fact
26
+ // Whether the repo describes its features as user stories (a docs/user-stories directory), the one fact
27
27
  // here that is language-agnostic, and the evidence an acceptance-testing surface activates on.
28
28
  readonly userStories: boolean;
29
29
  /* Whether the repo carries architecture documentation (a docs/architecture directory).
30
30
  *
31
31
  * Here so that a surface which READS documentation can tell, without asking the file routes, which repos
32
- * have any. The alternative was a read per repo on a poll — an answer the daemon already has from the same
32
+ * have any. The alternative was a read per repo on a poll, an answer the daemon already has from the same
33
33
  * one-pass scan that produces every other fact on this interface. */
34
34
  readonly docs: boolean;
35
35
  }
36
36
 
37
- // One connected capability's secret-free echo — `kind` is an open string: new kinds appear without an API bump,
37
+ // One connected capability's secret-free echo, `kind` is an open string: new kinds appear without an API bump,
38
38
  // and matching on one couples the extension to that kind's continued existence.
39
39
  export interface CapabilityFacts {
40
40
  readonly id: string;
package/src/host.ts CHANGED
@@ -2,7 +2,7 @@ import type { IntenticApi } from "./api.js";
2
2
 
3
3
  /* THE AMBIENT HOST HANDLE, one slot per extension. `activate(api)` binds it once, before any view renders, so
4
4
  * the extension's composables reach the authenticated daemon transport, cache scoping and workspace facts
5
- * through `host()` — the way `vscode.*` is ambient to a VSCode extension. Not app internals: everything flows
5
+ * through `host()`, the way `vscode.*` is ambient to a VSCode extension. Not app internals: everything flows
6
6
  * through the public IntenticApi.
7
7
  *
8
8
  * A FACTORY rather than a module-level slot here, and that is the whole reason this lives in the API package
package/src/protocol.ts CHANGED
@@ -1,17 +1,17 @@
1
- /* THE HALF OF THIS PACKAGE THAT RUNS WITHOUT A BROWSER — the protocol version and the matcher that compares a
1
+ /* THE HALF OF THIS PACKAGE THAT RUNS WITHOUT A BROWSER, the protocol version and the matcher that compares a
2
2
  * manifest's `engines.intentic` against it.
3
3
  *
4
4
  * The root barrel is the EXTENSION-facing surface, and an extension runs in the app, so the barrel is free to
5
5
  * reach for vue: `scope.ts` imports `ref` as a value, not a type. The daemon is the other consumer, and it
6
- * needs exactly these two names — to refuse an incompatible extension (backend-supervisor), to stamp the
6
+ * needs exactly these two names, to refuse an incompatible extension (backend-supervisor), to stamp the
7
7
  * version it provides (backend-host-main), to write an engines range into a scaffold. It has no vue and no
8
8
  * reason to grow one, but `export *` loads every re-exported module eagerly, so importing the barrel for
9
- * `extensionApiVersion` alone dragged `vue` into a Node process that could not resolve it — the daemon crashed
9
+ * `extensionApiVersion` alone dragged `vue` into a Node process that could not resolve it, the daemon crashed
10
10
  * on boot and the sandbox never became healthy.
11
11
  *
12
12
  * Hence this entry point. `@intentic/extension-api/protocol` is what host-side Node code imports; the root
13
13
  * barrel keeps exporting both names too, so nothing about the published extension surface changes. Type-only
14
- * imports from the root (`ExtensionServerApi` and friends) stay where they are — those are erased at compile
14
+ * imports from the root (`ExtensionServerApi` and friends) stay where they are, those are erased at compile
15
15
  * time and never reach the loader. */
16
16
 
17
17
  export { satisfiesEngines } from "./engines.js";
package/src/route.ts CHANGED
@@ -1,4 +1,4 @@
1
- /* The URL-query rules behind `api.route`, as pure functions — the same split `permissions.ts` uses: the host owns
1
+ /* The URL-query rules behind `api.route`, as pure functions, the same split `permissions.ts` uses: the host owns
2
2
  * the router, this owns the rule, and the rule is testable without one.
3
3
  *
4
4
  * A view's internal navigation lives in the QUERY because the path is already spoken for: `/ext/:ext/:key?` has
@@ -11,7 +11,7 @@ export type RawQuery = Readonly<Record<string, string | readonly (string | null)
11
11
  /* Flatten a router query to the scalar record extensions read. A repeated key takes its FIRST value rather than
12
12
  * its last: a view's state is singular, and the first occurrence is the one a hand-written or shared link means.
13
13
  * A valueless key (`?draft`) reads as the empty string, which is falsy-ish for the caller to test but never
14
- * `undefined` — absent and present-but-empty are different answers. */
14
+ * `undefined`, absent and present-but-empty are different answers. */
15
15
  export const flattenQuery = (query: RawQuery): Record<string, string> => {
16
16
  const out: Record<string, string> = {};
17
17
  for (const [key, value] of Object.entries(query)) {
@@ -25,7 +25,7 @@ export const flattenQuery = (query: RawQuery): Record<string, string> => {
25
25
  return out;
26
26
  };
27
27
 
28
- /* Merge a patch into the live query. `undefined` REMOVES its key — that is how a view says "I am no longer on a
28
+ /* Merge a patch into the live query. `undefined` REMOVES its key, that is how a view says "I am no longer on a
29
29
  * page" without leaving `?doc=` behind, so the tidy URL is the one you get by default rather than one you have to
30
30
  * construct. Every key the patch does not mention is carried through untouched, which is the whole invariant:
31
31
  * a documentation view setting `doc` must not drop the terminal's or another view's parameters. */
package/src/scope.ts CHANGED
@@ -5,11 +5,11 @@ import { ref, type Ref } from "vue";
5
5
  * Three tiers of client state exist in this app, and each needs a different answer to "what happens on a
6
6
  * switch". Cached server state is keyed by `api.sandbox.key(...)`, so it is answered by construction. State
7
7
  * inside a mounted component dies with the component. THIS is the third tier: module state owned by
8
- * `activate()` — the badge counts, the document-presence maps, the poll results that must survive the view
8
+ * `activate()`, the badge counts, the document-presence maps, the poll results that must survive the view
9
9
  * being unmounted, because a badge you only see once you have already navigated to the view is pointless.
10
10
  *
11
11
  * Nothing owned it. A rail tile filled by a ten-minute timer therefore kept the PREVIOUS sandbox's number for
12
- * up to ten minutes after a switch, under the new sandbox's name — a badge is a claim addressed to the reader,
12
+ * up to ten minutes after a switch, under the new sandbox's name, a badge is a claim addressed to the reader,
13
13
  * and one describing a workspace they are no longer looking at is worse than no badge at all. It was not one
14
14
  * extension's mistake either: every extension that badges had the identical shape, which is the signature of a
15
15
  * missing primitive rather than of carelessness.
@@ -19,7 +19,7 @@ import { ref, type Ref } from "vue";
19
19
  *
20
20
  * A MODULE-LEVEL REGISTRY IS CORRECT HERE, and that is worth saying because `hostSlot` in this same package
21
21
  * warns against exactly that. The shell publishes ONE instance of this module to every bundle
22
- * (extension-host/hostModules.ts), so a slot held here is shared by all of them — which made it wrong for a
22
+ * (extension-host/hostModules.ts), so a slot held here is shared by all of them, which made it wrong for a
23
23
  * per-extension host handle and makes it right for this: one switch empties every extension's scope, and no
24
24
  * extension can be missed. */
25
25
 
@@ -29,7 +29,7 @@ interface Registered {
29
29
 
30
30
  const registered: Registered[] = [];
31
31
 
32
- /* Which scope the extensions are in, counted rather than named — this module cannot see the sandbox id, and
32
+ /* Which scope the extensions are in, counted rather than named, this module cannot see the sandbox id, and
33
33
  * does not need to. All any caller asks is "is this still the scope I started in", and a counter answers that
34
34
  * without this package having to know what a sandbox is. */
35
35
  let generation = 0;
@@ -40,9 +40,9 @@ let generation = 0;
40
40
  * const unseen = sandboxRef<readonly ChoreVerdict[]>(() => []);
41
41
  *
42
42
  * It is an ordinary `Ref` in every other respect: read it in a `badge()` or a `detect()` and the host's own
43
- * computed re-renders the tile when it changes, exactly as before. READ, not write — see `sandboxValue`.
43
+ * computed re-renders the tile when it changes, exactly as before. READ, not write, see `sandboxValue`.
44
44
  *
45
- * `dispose` is for state that owns something the garbage collector will not take back — an object URL, a
45
+ * `dispose` is for state that owns something the garbage collector will not take back, an object URL, a
46
46
  * subscription. It is handed the value being dropped, once, at the moment the scope closes. Most callers need
47
47
  * none: a list of verdicts is released by being replaced. */
48
48
  export const sandboxRef = <T>(initial: () => T, dispose?: (previous: T) => void): Ref<T> => {
@@ -56,18 +56,18 @@ export const sandboxRef = <T>(initial: () => T, dispose?: (previous: T) => void)
56
56
  return state;
57
57
  };
58
58
 
59
- // A sandbox-scoped box that nothing observes. Same shape as a `Ref` on purpose — `.value`, read and written —
59
+ // A sandbox-scoped box that nothing observes. Same shape as a `Ref` on purpose, `.value`, read and written,
60
60
  // so moving state between the two is one word at the declaration and nothing at the call sites.
61
61
  export interface SandboxValue<T> {
62
62
  value: T;
63
63
  }
64
64
 
65
- /* MODULE STATE FOR ONE SANDBOX THAT NOTHING RENDERS — `sandboxRef`'s lifetime without its reactivity, for the
65
+ /* MODULE STATE FOR ONE SANDBOX THAT NOTHING RENDERS, `sandboxRef`'s lifetime without its reactivity, for the
66
66
  * bookkeeping a background poll keeps for ITSELF: which connections to ask about next round, the cursor a
67
67
  * fetch resumes from, the id a retry belongs to.
68
68
  *
69
69
  * It exists because of where `detect()` and `badge()` are called from. Both run INSIDE the host's render
70
- * computed — that is the whole mechanism by which a tile repaints when a poll lands — and a `Ref` WRITTEN from
70
+ * computed, that is the whole mechanism by which a tile repaints when a poll lands, and a `Ref` WRITTEN from
71
71
  * inside a computed is that computed mutating its own dependency. Vue re-runs it, the write happens again, and
72
72
  * the rail recurses until Vue abandons the flush mid-frame. What the reader sees then is not one broken tile:
73
73
  * every update queued behind the rail is dropped with the flush, so the whole window stops answering, and the
@@ -76,7 +76,7 @@ export interface SandboxValue<T> {
76
76
  * So the division is by AUDIENCE, not by lifetime: `sandboxRef` for what a tile SHOWS, `sandboxValue` for what
77
77
  * a poll REMEMBERS. Both are emptied on a switch by the same door, and writing this one from a render callback
78
78
  * is safe precisely because there is nothing to invalidate. When a poll's own bookkeeping later turns out to
79
- * be worth showing, promoting it is a one-word change — and the promotion is the moment to check that nothing
79
+ * be worth showing, promoting it is a one-word change, and the promotion is the moment to check that nothing
80
80
  * writes it from `detect()`.
81
81
  *
82
82
  * `dispose` behaves exactly as it does on `sandboxRef`. */
@@ -94,7 +94,7 @@ export const sandboxValue = <T>(initial: () => T, dispose?: (previous: T) => voi
94
94
  /* THE GUARD FOR WORK THAT WAS ALREADY IN FLIGHT WHEN THE SWITCH HAPPENED.
95
95
  *
96
96
  * Emptying the refs is not enough on its own. A poll that issued its request under the old sandbox resolves a
97
- * moment after the switch, and writes the old box's answer into the fresh scope — the same wrong badge, just
97
+ * moment after the switch, and writes the old box's answer into the fresh scope, the same wrong badge, just
98
98
  * harder to reproduce. There is no way for this module to cancel that request, so it offers the one thing the
99
99
  * caller needs instead: a way to ask, after the await, whether the answer is still wanted.
100
100
  *
@@ -111,7 +111,7 @@ export const sandboxScopeGuard = (): (() => boolean) => {
111
111
  return () => taken === generation;
112
112
  };
113
113
 
114
- /* THE HOST'S DOOR, called by the shell when the active sandbox changes — not by extensions, which have nothing
114
+ /* THE HOST'S DOOR, called by the shell when the active sandbox changes, not by extensions, which have nothing
115
115
  * to reset and no business resetting each other's. */
116
116
  export const resetSandboxScope = (): void => {
117
117
  generation += 1;
package/src/server.ts CHANGED
@@ -1,49 +1,49 @@
1
- /* THE SERVER HALF an extension programs against — the daemon-side twin of IntenticApi (api.ts).
1
+ /* THE SERVER HALF an extension programs against, the daemon-side twin of IntenticApi (api.ts).
2
2
  *
3
- * A manifest `server` bundle exports `activateServer(api, context)`, and the daemon's BACKEND HOST — one
4
- * separate supervised node process shared by every enabled extension with a backend — imports the bundle and
3
+ * A manifest `server` bundle exports `activateServer(api, context)`, and the daemon's BACKEND HOST, one
4
+ * separate supervised node process shared by every enabled extension with a backend, imports the bundle and
5
5
  * calls it. A separate process rather than the daemon itself because loaded code can never be unloaded: the
6
6
  * off switch, an upgrade to a new sha and a live-edited workspace extension all require the process holding
7
7
  * the old code to die, and that process must never be the daemon (chat, terminals and file sync live there).
8
8
  * A shared process rather than one per extension because the trust model is full trust (install is owner-only
9
- * and sha-pinned) — isolation between extensions would buy robustness nobody is billed for.
9
+ * and sha-pinned), isolation between extensions would buy robustness nobody is billed for.
10
10
  *
11
11
  * Full trust is also why this surface is deliberately small. The backend runs in the sandbox container as the
12
- * same user the daemon does, so the workspace is reachable with plain `node:fs` — the api hands over PATHS,
12
+ * same user the daemon does, so the workspace is reachable with plain `node:fs`, the api hands over PATHS,
13
13
  * not a file service. What it does mediate is the two things a path cannot carry: the extension's route
14
14
  * namespace (mount), and its reach into the daemon's own routes (daemon.*, gated by the manifest's
15
- * `permissions.daemon` — the daemon refuses undeclared routes, same grammar and same honesty rule as the UI
15
+ * `permissions.daemon`, the daemon refuses undeclared routes, same grammar and same honesty rule as the UI
16
16
  * half's `permissions.sandbox`). */
17
17
 
18
18
  // One request into this extension's namespace. The host strips the `/x/<id>` prefix before dispatch, so the
19
- // handler sees the extension's OWN paths — the same paths its contract declares and its UI half calls.
19
+ // handler sees the extension's OWN paths, the same paths its contract declares and its UI half calls.
20
20
  // `undefined` means "not mine": the host answers 404 without the extension having to speak HTTP for it.
21
21
  export type BackendRouteHandler = (request: Request) => Promise<Response | undefined>;
22
22
 
23
23
  export interface ExtensionServerApi {
24
- // The host's @intentic/extension-api version — what `engines.intentic` was checked against.
24
+ // The host's @intentic/extension-api version, what `engines.intentic` was checked against.
25
25
  readonly apiVersion: string;
26
- // The workspace root (absolute). The backend reads and writes under it with node's own fs — full trust
26
+ // The workspace root (absolute). The backend reads and writes under it with node's own fs, full trust
27
27
  // means no file service in between. Durable state belongs in workspace files (the same rule the UI half
28
28
  // lives by): it survives restarts, is shared across browsers, and the agent editing it out-of-band is the
29
29
  // product.
30
30
  readonly workspaceRoot: string;
31
- // This extension's own checkout (absolute) — where its bundled assets sit.
31
+ // This extension's own checkout (absolute), where its bundled assets sit.
32
32
  readonly extensionDir: string;
33
33
  // A line in the daemon's log, attributed to this extension.
34
34
  readonly log: (message: string) => void;
35
35
  readonly routes: {
36
- /* Serve this extension's route namespace. The daemon proxies /x/<id>/* here — through its ordinary
36
+ /* Serve this extension's route namespace. The daemon proxies /x/<id>/* here, through its ordinary
37
37
  * auth (an owner's browser, a member at the route's role floor), so a backend never sees an
38
38
  * unauthenticated request and never sees a credential. One handler per extension: the extension owns
39
39
  * its whole namespace, and how it routes inside it (an oRPC handler over its own contract, a plain
40
40
  * switch) is its own business. A second mount replaces the first. */
41
41
  mount(handler: BackendRouteHandler): void;
42
42
  };
43
- /* The authenticated transport to the daemon's own routes — the backend's `api.sandbox`. Auth is a minted
43
+ /* The authenticated transport to the daemon's own routes, the backend's `api.sandbox`. Auth is a minted
44
44
  * per-extension token injected here; the daemon's gate checks every call against the manifest's
45
45
  * `permissions.daemon` allowlist, so a backend's reach into the core is declared, reviewable and refusable
46
- * exactly like the UI half's. The extension's own namespace needs no declaration — but there is also no
46
+ * exactly like the UI half's. The extension's own namespace needs no declaration, but there is also no
47
47
  * reason to dial yourself over HTTP. */
48
48
  readonly daemon: {
49
49
  request(path: string, init?: RequestInit): Promise<Response>;
@@ -52,13 +52,13 @@ export interface ExtensionServerApi {
52
52
  }
53
53
 
54
54
  export interface ExtensionServerContext {
55
- // The extension's routing id — its /x/<id> namespace segment (the capability entry id for a git-installed
55
+ // The extension's routing id, its /x/<id> namespace segment (the capability entry id for a git-installed
56
56
  // extension, publisher.name otherwise; the same id the UI half sees as ExtensionSummary.id).
57
57
  readonly extensionId: string;
58
58
  }
59
59
 
60
60
  // The shape of the manifest `server` bundle's default export (or its named exports): `activateServer` runs
61
- // once per backend-host start, after the engines check. There is no deactivate — retirement IS the host
61
+ // once per backend-host start, after the engines check. There is no deactivate, retirement IS the host
62
62
  // process ending, which is the one teardown that cannot leak.
63
63
  export interface ExtensionServerModule {
64
64
  activateServer(api: ExtensionServerApi, context: ExtensionServerContext): void | Promise<void>;
package/src/stream.ts CHANGED
@@ -1,9 +1,9 @@
1
- /* Reading a daemon SSE/ndjson stream — the transport half of `sandbox.request().body`. The daemon emits SSE
1
+ /* Reading a daemon SSE/ndjson stream, the transport half of `sandbox.request().body`. The daemon emits SSE
2
2
  * frames (blank-line separated, each a `data: <JSON>` line, an oRPC event-iterator failure as `event: error`),
3
3
  * so consuming a streamed apply/plan/provision means reframing + JSON-parsing. Pure (ReadableStream in, async
4
- * records out), no deps — extensions bundle it; the shim path never touches it. */
4
+ * records out), no deps, extensions bundle it; the shim path never touches it. */
5
5
 
6
- // A silent daemon — one that accepts the stream then sends nothing and never closes — would park reader.read()
6
+ // A silent daemon, one that accepts the stream then sends nothing and never closes, would park reader.read()
7
7
  // forever, hanging the consumer. A live daemon heartbeats (≤1s) over the stream, so no bytes for this long means
8
8
  // the connection is dead: cancel the reader and end the generator instead of waiting indefinitely.
9
9
  const SSE_IDLE_MS = 120_000;
package/src/surface.json CHANGED
@@ -467,5 +467,71 @@
467
467
  "sandboxValue",
468
468
  "satisfiesEngines"
469
469
  ]
470
+ },
471
+ "2.9.0": {
472
+ "manifest": [
473
+ "$schema",
474
+ "art",
475
+ "category",
476
+ "contributes",
477
+ "engines",
478
+ "entry",
479
+ "icon",
480
+ "logo",
481
+ "name",
482
+ "permissions",
483
+ "publisher",
484
+ "server",
485
+ "version"
486
+ ],
487
+ "contributes": [
488
+ "agent",
489
+ "automationTemplates",
490
+ "bin",
491
+ "capabilities",
492
+ "commands",
493
+ "documents",
494
+ "environment",
495
+ "files",
496
+ "listener",
497
+ "processes",
498
+ "settings",
499
+ "viewers",
500
+ "views"
501
+ ],
502
+ "api": [
503
+ "apiVersion",
504
+ "chat",
505
+ "commands",
506
+ "documents",
507
+ "href",
508
+ "models",
509
+ "navigate",
510
+ "processes",
511
+ "route",
512
+ "sandbox",
513
+ "settings",
514
+ "terminal",
515
+ "theme",
516
+ "viewers",
517
+ "views",
518
+ "workspace"
519
+ ],
520
+ "listener": ["automation", "events", "provider"],
521
+ "sandboxApi": ["fetch", "json", "key", "origin", "reachable", "request", "role", "rpc"],
522
+ "moduleExports": [
523
+ "extensionApiVersion",
524
+ "flattenQuery",
525
+ "hostSlot",
526
+ "mergeQuery",
527
+ "readDaemonStream",
528
+ "resetSandboxScope",
529
+ "sandboxLedger",
530
+ "sandboxPoll",
531
+ "sandboxRef",
532
+ "sandboxScopeGuard",
533
+ "sandboxValue",
534
+ "satisfiesEngines"
535
+ ]
470
536
  }
471
537
  }
package/src/version.ts CHANGED
@@ -1,56 +1,62 @@
1
- // The extension API's protocol version — the value `engines.intentic` ranges are matched against at load, and
1
+ // The extension API's protocol version, the value `engines.intentic` ranges are matched against at load, and
2
2
  // the value the host reports as IntenticApi.apiVersion. Bumped ONLY with the published package: additive
3
3
  // surface = minor, breaking = major. This package is the one deliberate exception to the repo's no-legacy rule.
4
4
  //
5
5
  // 1.0.0 rather than 0.5.0, and the reason is the drift that forced that bump. While the major was 0 the caret
6
- // matcher treats the MINOR as breaking (engines.ts), so every addition invalidated every declared range — which
6
+ // matcher treats the MINOR as breaking (engines.ts), so every addition invalidated every declared range, which
7
7
  // made bumping expensive enough that two surface changes (connectors → capabilities, api.documents.open) shipped
8
8
  // without one, and the number stopped being true. At 1.x an addition is 1.1.0 and costs authors nothing, so the
9
9
  // bump that keeps this honest is the cheap one. surface-guard.test.ts is what makes it non-optional.
10
10
  // 2.0.0 makes listener contributions self-describing: the former `eventTypes` array became labelled `events`
11
- // plus the automation-editor vocabulary the provider owns. That is intentionally breaking — a 1.x listener
11
+ // plus the automation-editor vocabulary the provider owns. That is intentionally breaking, a 1.x listener
12
12
  // manifest cannot honestly promise that a generic host can configure it.
13
13
  // 2.1.0 adds the backend half: a manifest `server` bundle (activateServer, run by the daemon's backend host
14
- // under /x/<id>/…) and `permissions.daemon` beside `permissions.sandbox`. Additive — a 2.0 manifest is a 2.1
14
+ // under /x/<id>/…) and `permissions.daemon` beside `permissions.sandbox`. Additive, a 2.0 manifest is a 2.1
15
15
  // manifest that ships no backend.
16
16
  // 2.2.0 opens the automation composer: `contributes.automationTemplates` lets a pack ship the starting points
17
17
  // for its own service, and a `listener` may declare a second narrowing field (`automation.branchField`) for a
18
18
  // source whose events carry one. Both fold into the daemon's trigger catalogue, so a pack that knows something
19
19
  // worth waking on says so itself instead of being written into the automations surface. Additive.
20
- // 2.3.0 adds `api.sandbox.role()` — the signed-in user's trust tier, for affordance gating (the daemon floors
20
+ // 2.3.0 adds `api.sandbox.role()`, the signed-in user's trust tier, for affordance gating (the daemon floors
21
21
  // every route regardless). Surfaced when the drafts queue moved out of the app: approve/reject are
22
22
  // maintainer-and-up, and no extension could say so. Additive. The surface guard grew a `sandboxApi` member
23
23
  // list with this release, so additions below the top-level grain are recorded from here on.
24
24
  // 2.4.0 gives the manifest an authoring schema: `$schema` is a declared field, and every contribution point now
25
25
  // carries the sentence that explains it, generated out to intentic-extension.schema.json. An editor pointed at
26
- // that URL completes the fields, shows what each one does, and marks a key nothing declares — which used to be
26
+ // that URL completes the fields, shows what each one does, and marks a key nothing declares, which used to be
27
27
  // the one class of mistake nothing caught, because zod strips what it does not know rather than refusing it.
28
28
  // Additive: a manifest that names no schema is unchanged.
29
29
  // 2.5.0 lets an extension bring its own picture: `art` carries a complete SVG document inline, above the
30
30
  // simple-icons `logo` and the host's `icon` in the same ladder. The two tiers before it could say "this is
31
- // Slack" or "this is a server", and nothing could say "this is mine" — so a page listing seven unfamiliar
31
+ // Slack" or "this is a server", and nothing could say "this is mine", so a page listing seven unfamiliar
32
32
  // extensions drew seven near-identical glyphs, which is the shape of a directory rather than of a shelf. Inline
33
33
  // rather than a URL because the row is drawn before any code is cloned: a link would put a stranger's server in
34
- // the render path, track who is browsing, and rot after approval. Additive — a manifest that ships no drawing
34
+ // the render path, track who is browsing, and rot after approval. Additive, a manifest that ships no drawing
35
35
  // falls to exactly the mark it had before.
36
36
  // 2.6.0 gives module state an owner across a sandbox switch: `sandboxRef` declares state that belongs to ONE
37
37
  // sandbox and `sandboxScopeGuard` protects the write of a read that was already in flight when the switch
38
- // happened (scope.ts). The tier existed and nothing cleared it — every extension that badges a rail tile from a
38
+ // happened (scope.ts). The tier existed and nothing cleared it, every extension that badges a rail tile from a
39
39
  // timer kept the previous sandbox's count under the new sandbox's name, which is the one thing a badge must
40
40
  // never do. Additive: an extension that keeps no module state is unchanged, and the host resets the scope
41
41
  // whether or not anything registered.
42
42
  // 2.7.0 adds the two halves of work an extension does while none of it is on screen (background.ts):
43
43
  // `sandboxPoll`, the timer behind a rail badge, and `sandboxLedger`, the file recording what the owner has
44
- // already seen. Seven modules across six extensions had hand-written the same poller — five invisible rules
45
- // each, and six of the seven had the sandbox-switch one wrong — and three had hand-written the same tolerant
44
+ // already seen. Seven modules across six extensions had hand-written the same poller, five invisible rules
45
+ // each, and six of the seven had the sandbox-switch one wrong, and three had hand-written the same tolerant
46
46
  // reader and careful write over the same shape of file. Additive: nothing is removed, and an extension that
47
47
  // keeps polling by hand still runs.
48
48
  // 2.8.0 splits sandbox-scoped module state by AUDIENCE: `sandboxValue` is `sandboxRef`'s lifetime without its
49
49
  // reactivity, for what a background poll remembers for itself (which connections to ask about next round, the
50
50
  // cursor a fetch resumes from) rather than what a tile shows. `detect()` and `badge()` both run inside the
51
- // host's render computed, so a `Ref` written from either is a computed mutating its own dependency — Vue
51
+ // host's render computed, so a `Ref` written from either is a computed mutating its own dependency. Vue
52
52
  // re-runs it, it writes again, and the rail recurses until the flush is abandoned mid-frame, dropping every
53
53
  // unrelated update queued behind it. The symptom is a window that stops answering, blamed on whichever
54
54
  // component the loop was noticed in, so the fix has to be a box nothing observes rather than a rule to
55
55
  // remember. Additive: `sandboxRef` is unchanged, and both are emptied on a switch by the same door.
56
- export const extensionApiVersion = "2.8.0";
56
+ // 2.9.0 adds `api.href` — the same app path the host would navigate to, as a browser address. A view that can
57
+ // only call `navigate` has to draw every destination it offers as a <button>, and a button is not a link: no
58
+ // address under the pointer, nothing in the browser's own right-click menu, nothing to copy, and Ctrl/⌘-click
59
+ // moving the tab the reader is in instead of opening a second one. Six views across three packs had each drawn
60
+ // a place that way, so the fix has to be reachable from the API rather than repeated per pack. Additive:
61
+ // `navigate` is unchanged, and it is still what a plain click calls.
62
+ export const extensionApiVersion = "2.9.0";