@intentic/extension-api 1.247.0 → 1.249.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/api.ts CHANGED
@@ -4,247 +4,136 @@ import type { Component } from "vue";
4
4
  import type { DiffPayload } from "./diff.js";
5
5
  import type { CapabilityFacts, RepoFacts } from "./facts.js";
6
6
 
7
- /* The host API an extension programs against. There is no ambient global: the implementation arrives as the
8
- * `activate(api, context)` argument, and everything an extension registers is returned as a Disposable pushed
9
- * onto context.subscriptions so deactivation can unwind it. */
7
+ // The host API an extension programs against; there is no ambient global. Arrives as
8
+ // activate(api, context); everything registered returns a Disposable pushed onto context.subscriptions.
10
9
 
11
10
  export interface Disposable {
12
11
  dispose(): void;
13
12
  }
14
13
 
15
- // One sidebar element a view contributes: routed at /ext/<viewId>/<key> (the key segment is dropped when it
16
- // equals the view id, a singleton view links to /ext/<viewId>), rendered by the view's component with
17
- // `repo` (+ props) bound.
14
+ // One sidebar element a view contributes, routed at /ext/<viewId>/<key> and rendered by the view's
15
+ // component with `repo` (+ props) bound.
18
16
  export interface Activation {
19
- // Stable per-view key (usually the repo name), the route segment, so deep links survive reloads.
17
+ // Stable per-view key (usually the repo name); the route segment, so deep links survive reloads.
20
18
  readonly key: string;
21
19
  readonly title: string;
22
- // An icon name from the host's icon set; absent ⇒ the rail renders the title's initials.
20
+ // An icon name from the host's icon set; absent renders the title's initials.
23
21
  readonly icon?: string | undefined;
24
- // The repo this element is rooted at, the fallback-dedup subject and the component's `repo` prop. Absent
25
- // for capability-driven elements, which aren't rooted at any repo.
22
+ // Absent for capability-driven elements, which have no repo to root at.
26
23
  readonly repo?: string | undefined;
27
24
  readonly props?: Record<string, unknown> | undefined;
28
25
  }
29
26
 
30
- // What a sidebar element may say on its own tile without being opened. The rail is glanced at, not read, so
31
- // this is deliberately the smallest useful vocabulary: a number and how alarmed to be about it.
32
- //
33
- // A badge is a claim on the user's attention, so the bar is the same one the core surfaces already hold
34
- // themselves to: it must mean "something happened here that you don't already know about", never "here is a
35
- // statistic". A count that is lit most of the day teaches the user to stop seeing the rail.
36
- //
37
- // AND IT IS WHAT PUTS THE TILE THERE. Beyond the host's few permanent areas, a rail tile holds its seat while
38
- // this is non-empty (or while the reader has pinned it, or is standing in it) and is reached through the rail's
39
- // More menu otherwise, which lists every area whether or not it is seated, as does the palette. So `detect()`
40
- // says whether the area EXISTS, which is what makes its route, its menu rows and its command work, and this says
41
- // whether it is currently worth one of the column's roughly nine seats. Badging all day now costs a seat as well
42
- // as the reader's trust.
27
+ // What a sidebar tile may say before it's opened: a number and how alarmed to be. Non-empty also keeps
28
+ // the tile seated in the rail instead of the More menu.
43
29
  export interface ViewBadge {
44
- // How many, for work whose SIZE is what the user acts on, two files to review and two hundred are
45
- // different afternoons. Omitted or 0 ⇒ no number. The host renders anything above 99 as "99+".
30
+ // How many; omitted or 0 means no number. The host renders anything above 99 as "99+".
46
31
  readonly count?: number | undefined;
47
- // A glyph from the host's icon set, rendered INSTEAD of a number, for a pending action whose size changes
48
- // nothing about what the user does with it: one click either way. "There is committed work here waiting to
49
- // be sent" is the whole message, and a number beside it would be read in the unit `count` established,
50
- // so the amount goes in the tooltip and the glyph carries the kind. A badge with neither renders nothing.
32
+ // A glyph shown instead of a number, for a pending action whose size doesn't matter. Neither renders nothing.
51
33
  readonly mark?: string | undefined;
52
- // THE FIRST QUESTION A BADGE ANSWERS IS "DO I OWE THIS ANYTHING?", and until `neutral` existed there was no
53
- // way to say no. A count that means "three things are alive in here" (open browsers, running services) was
54
- // drawn exactly like one that means "three things are waiting on you", same pill, same tint, same digit,
55
- // so a reader who chased one and found an inventory learned that badges do not repay being chased. That is
56
- // the failure this vocabulary exists to prevent, arriving through the door it left open.
57
- //
58
- // `neutral` is an INVENTORY: true most of the day, nothing owed, quiet ink. `info` is the resting tone for
59
- // work the user can act on (unread agents, uncommitted changes); `warning` marks a risk they are carrying
60
- // (an exposed port); `danger` means something is BROKEN, reach for it sparingly, its whole value is that it
61
- // is rare enough to still mean something. Absent still means `info`, so a badge that says nothing about its
62
- // tone is still assumed to be asking for something.
34
+ // neutral: an inventory, nothing owed.
35
+ // info: the resting tone for actionable work.
36
+ // warning: a risk being carried.
37
+ // danger: something is broken; use sparingly.
38
+ // Absent means info.
63
39
  readonly tone?: "neutral" | "info" | "warning" | "danger" | undefined;
64
- // Say what happened and how much, not just the number the user can already see. The host renders it
65
- // AFTER the view's own name, "Agents · 3 need you" on the rail, the chip's text in the mobile menu, so
66
- // phrase it as the continuation of a label, not as a standalone sentence that repeats the view.
40
+ // What happened and how much; rendered after the view's name, so phrase it as a continuation, not a sentence.
67
41
  readonly tooltip?: string | undefined;
68
42
  }
69
43
 
70
- /* ONE CACHED READ, described rather than performed, the currency both `ViewRegistration.warm` and
71
- * `api.sandbox.fetch` deal in.
72
- *
73
- * It is deliberately the shape a vue-query `useQuery` already takes, because that is the point: the entry a
74
- * view warms, the entry its badge fills from a timer and the entry the view's own `useQuery` observes are ONE
75
- * entry, and they are one entry because all three name it the same way. Two of the three used to be separate
76
- * reads of the same route in this app's own extensions, the tile fetched the report every ten minutes and kept
77
- * it privately, and the view it badged for started from nothing every time it was opened.
78
- *
79
- * The caching terms are optional and yours: `staleTime` is how long an answer stays believable without a
80
- * refetch (Infinity for something only an invalidation can make wrong), `gcTime` how long it survives with
81
- * nothing observing it. Both default to the host's. */
44
+ // One cached read, shared by `ViewRegistration.warm` and `api.sandbox.fetch`. `staleTime`/`gcTime` are
45
+ // optional; both default to the host's.
82
46
  export interface HostQuery<T = unknown> {
83
- // MUST be scoped by api.sandbox.key(...), so nothing bleeds across a sandbox switch.
47
+ // Must be scoped by api.sandbox.key(...), so nothing bleeds across a sandbox switch.
84
48
  readonly queryKey: readonly unknown[];
85
49
  readonly queryFn: () => Promise<T>;
86
50
  readonly staleTime?: number | undefined;
87
51
  readonly gcTime?: number | undefined;
88
52
  }
89
53
 
90
- // A view's runtime registration, for third-party extensions, `id`, `label` and `surface` must match a
91
- // `contributes.views` entry in the approved manifest or the host refuses the registration.
54
+ // A view's runtime registration; `id`, `label` and `surface` must match a `contributes.views` entry in
55
+ // the approved manifest.
92
56
  export interface ViewRegistration {
93
57
  readonly id: string;
94
- // The view family's human name (distinct from an Activation's per-repo `title`), labels the directory
95
- // panel's surface switch when a repo activates several.
58
+ // The view family's human name, distinct from an Activation's per-repo `title`.
96
59
  readonly label: string;
97
- // Where the view's activations mount. `rail` is the always-visible left column, a place the user ACTS
98
- // from, so a tile there must earn a permanently occupied slot. `directory` is a per-repo panel opened from
99
- // the Workspace tree. `sandbox` is a tab on the Sandbox hub, where the subject is the box itself (its
100
- // logs, its status, its consumption), inspected occasionally rather than worked in, so it costs a tab in
101
- // a scrolling word-labelled strip instead of an icon in the rail's fixed budget.
60
+ // rail: the always-visible left column, a permanently seated tile.
61
+ // directory: a per-repo panel opened from the Workspace tree.
62
+ // sandbox: a tab on the Sandbox hub, for inspecting the box itself.
102
63
  readonly surface: "rail" | "directory" | "sandbox";
103
- /* Evidence-based detection over the public facts, one activation per sidebar element. Called on every
104
- * facts poll; a throwing detect contributes nothing that round.
105
- *
106
- * MUST NOT WRITE REACTIVE STATE. This runs inside the host's render computed, so a `sandboxRef` (or any
107
- * other `Ref`) written here is a computed mutating its own dependency: Vue re-runs it, it writes again,
108
- * and the rail recurses until the frame is abandoned, taking every unrelated update queued behind it, so
109
- * the symptom is a window that stops responding rather than one misbehaving tile.
110
- *
111
- * It IS the right place to notice things, though, it is the only callback that sees the live facts, so
112
- * for the bookkeeping that notice produces (which connections a poller should ask about next round) use
113
- * `sandboxValue`, which has the same lifetime and no observers. Same rule for `badge` below. */
64
+ // Evidence-based detection over the public facts; a throwing detect contributes nothing that round.
65
+ // Must not write reactive state (runs inside the host's render computed).
114
66
  readonly detect: (repos: readonly RepoFacts[], capabilities: readonly CapabilityFacts[]) => Activation[];
115
- // What this activation's tile should say without being opened. Read inside the host's own computed, so
116
- // reading a ref here re-renders the tile when it changes, no push channel needed. Called on every render
117
- // of every surface that draws tiles, so it must be cheap and pure: derive from state the extension already
118
- // keeps, never fetch, and never write a ref (see detect above, same computed, same recursion). A throwing
119
- // badge simply yields none.
120
- //
121
- // Requires `badge: true` on the manifest's matching contributes.views entry, the host drops the function
122
- // otherwise, because a tile that can interrupt the user is a contribution the owner must have approved.
123
- // The source has to stay alive while the view is UNMOUNTED (a badge you only see once you have already
124
- // navigated to the view is pointless), so it belongs in module state owned by activate(), not in the view.
67
+ // Read inside the host's computed, so a ref read here re-renders the tile; called on every render, so
68
+ // keep it cheap, pure and non-writing. Requires `badge: true` on the manifest entry.
125
69
  readonly badge?: ((activation: Activation) => ViewBadge | undefined) | undefined;
126
- /* WHAT THIS VIEW WOULD LIKE IN HAND BEFORE ANYONE OPENS IT, read ahead by the host's background loader in
127
- * the gaps between what the user is already doing, so the tile opens with content instead of a skeleton.
128
- *
129
- * A rail tile is at the far end of the loader's priority order (the user is not there, they might GO
130
- * there), so this is a wish and never a guarantee: on a workspace busy enough that nothing is spare, none
131
- * of it is read and the view costs exactly what it cost before. Nothing here is user-visible, nothing
132
- * retries, and a failed warm is simply a warm that did not happen.
133
- *
134
- * DECLARE THE QUERY, not a function that fetches it, that is the whole reason this takes a HostQuery. The
135
- * host's own wishes used to carry a cache key and a separate "how to read it" callback, and for most of
136
- * them the callback fetched the data and returned it to its caller without ever filing it under that key.
137
- * The wish could then never be satisfied, and since the loader always takes the first unsatisfied wish, one
138
- * of them was enough to park it for the whole session. Handing over the query makes the two halves the same
139
- * object. Use the SAME key your view's `useQuery` reads (api.sandbox.key(...)), or you are warming an entry
140
- * nothing will look in.
141
- *
142
- * Called on every beat of the loader, so it must be cheap and pure, derive from state you already keep,
143
- * never fetch. A throwing warm contributes nothing that beat. */
70
+ // Declare the query (not a fetcher) the loader should read ahead of need; use the same key your view's
71
+ // `useQuery` reads. A wish, never a guarantee; called every loader beat, so keep it cheap and pure.
144
72
  readonly warm?: (() => readonly HostQuery[]) | undefined;
145
73
  // A fallback view's activations are dropped for repos already claimed by a non-fallback one.
146
74
  readonly fallback?: true | undefined;
147
- // An AUXILIARY view adds a surface BESIDE whatever else serves the repo instead of replacing it, a test
148
- // runner, a docs browser. Its activations render and mark the directory manageable exactly like any other,
149
- // but they do not claim the repo, so the fallback view (the raw dev-server preview) survives alongside.
150
- // Claiming is for a view that subsumes the fallback: `apps` renders the preview URLs itself, so dropping
151
- // the preview tile beside it is right; a test runner renders no preview, so dropping it would be a loss.
75
+ // An auxiliary view adds a surface beside the repo's other views instead of claiming it.
152
76
  readonly auxiliary?: true | undefined;
153
77
  // Lazily imported root component, rendered with `repo` (+ props) bound.
154
78
  readonly view: () => Promise<Component>;
155
79
  }
156
80
 
157
- // A custom file viewer's runtime registration, `id` must match a `contributes.viewers` entry in the approved
158
- // manifest (the host reads the file extensions + fetch kind from there). The host resolves an open file to this
159
- // viewer, gets its content, and renders `component` with `{ path, text?, blob?, src? }` bound, which of the
160
- // three content props is filled is decided by the manifest's `fetch` (see ViewerContributionSchema).
81
+ // A custom file viewer's registration; `id` must match a `contributes.viewers` manifest entry. The host
82
+ // resolves an open file to it and renders `component` with the fetched content bound.
161
83
  export interface ViewerRegistration {
162
84
  readonly id: string;
163
85
  readonly component: () => Promise<Component>;
164
86
  }
165
87
 
166
- // What a directory row offers when a provider has a document for it: the icon the Workspace tree draws on that
167
- // row, and what the tab it opens is called. `icon` is an open string like Activation.icon, a name outside the
168
- // host's set renders nothing rather than failing the registration.
88
+ // What a directory row offers when a provider has a document for it: the tree's icon, and the tab's
89
+ // title. An unknown `icon` renders nothing rather than failing the registration.
169
90
  export interface DocumentOffer {
170
91
  readonly icon: string;
171
- // Names the ACTION on the row ("Open architecture doc"), since that is what a tooltip on an icon is read as.
92
+ // Names the action on the row ("Open architecture doc"); read as a tooltip on an icon.
172
93
  readonly tooltip: string;
173
- // The tab's label. Short: the strip already shows the directory's own name beside it.
94
+ // The tab's label; keep it short, the strip already shows the directory's name beside it.
174
95
  readonly title: string;
175
- /* Whether the row keeps this icon when the pointer is elsewhere. A tree row's icons are revealed on hover,
176
- * because a permanent column of them is what stops the eye reading names, but that rule assumes an icon is
177
- * an ACTION you already know you want. An offer that is EVIDENCE is the opposite case: "there is a page about
178
- * this package" is a fact nobody can act on until they see it, and finding it by sweeping fifty-five rows with
179
- * the mouse is not finding it. Such an offer sets this, and the row carries it dimmed until hover.
180
- *
181
- * Left off (the default) by an offer every directory of its kind gets, a repo's git history is always there,
182
- * so a permanent glyph states nothing and costs the same attention. */
96
+ // Keeps the icon visible outside hover, for evidence rather than an action found by sweeping.
183
97
  readonly evidence?: boolean;
184
98
  }
185
99
 
186
- /* A DOCUMENT PROVIDER, an extension's answer to "there is something to READ about this directory".
187
- *
188
- * PATH-KEYED, which is the whole reason it is not a `view`. `detect()` on a ViewRegistration answers per REPO
189
- * off the daemon's facts, and that is the wrong grain for a document: a monorepo is one repo with fifty-five
190
- * documented packages. So this asks per directory instead, and the Workspace tree, not the rail, not a routed
191
- * area, is where the answer lands.
192
- *
193
- * The host owns the tab. A provider says "yes, and here is what to call it"; opening it mounts `view` with the
194
- * path bound, in the editor area beside the files it describes. That placement is the point: documentation about
195
- * a package belongs next to the package, not behind a navigation away from it. */
100
+ // An extension's answer to "there is something to read about this directory", keyed by path rather
101
+ // than repo. The host owns the tab; `view` mounts beside the files it describes.
196
102
  export interface DocumentProviderRegistration {
197
103
  // Must match a `contributes.documents` entry in the approved manifest.
198
104
  readonly id: string;
199
- /* Whether this provider has a document for a workspace path (root-relative; "" is the workspace root), and
200
- * what the row should offer if so. Called for every visible directory row on every render of the tree, so it
201
- * must be a LOOKUP and never a fetch, derive it from state the extension already keeps. Reading a ref in
202
- * here is what repaints the tree when documents land, the same contract (and the same reason) as
203
- * ViewRegistration.badge; and like badge, that state has to outlive the view being unmounted, so it belongs
204
- * in module state owned by activate(). A throwing detect simply offers nothing for that row. */
105
+ // Whether this provider has a document for a path ("" is the workspace root). Called on every tree
106
+ // render, so it must be a lookup, never a fetch; a throwing detect offers nothing.
205
107
  readonly detect: (path: string) => DocumentOffer | undefined;
206
108
  // Lazily imported component, rendered with `path` bound.
207
109
  readonly view: () => Promise<Component>;
208
110
  }
209
111
 
210
- /* EVERYTHING THAT DECIDES WHO SERVES A TURN, as one value. The label is here because a view that shows a chosen
211
- * model without showing the list would otherwise have to keep a catalog of its own, which is exactly the
212
- * duplication `api.models` exists to end.
213
- *
214
- * Everything after the label is optional because they are pins, and the unpinned state is the one most callers
215
- * want: absent means "whatever the daemon resolves", which is what keeps a saved choice working after an account
216
- * is disconnected or a harness gains a provider. A caller that only cares which model runs can ignore all of
217
- * them and lose nothing. */
112
+ // Everything that decides who serves a turn. `label` lets a view show the choice without its own
113
+ // catalog; everything after it is an optional pin (absent means the daemon resolves it).
218
114
  export interface PickedModel {
219
- // An `AgentProvider`, `claude`, `codex`, a configured model endpoint's id, an installed ACP agent's id.
220
- // Open on purpose: the set grows with what the sandbox has connected, and an extension only carries it.
115
+ // An AgentProvider, a model endpoint id, or an installed ACP agent's id; the set grows with what's connected.
221
116
  readonly provider: string;
222
117
  readonly model: string;
223
118
  readonly label: string;
224
- /* WHICH CONNECTED ACCOUNT of that provider runs the turn, by its daemon-minted id, absent ⇒ whichever comes
225
- * first. It is on the pick rather than left to the daemon because the surfaces that start UNATTENDED runs are
226
- * the ones that need it: nobody is watching at 6am, so a first account that has run out of headroom (or whose
227
- * organization switched the plan off) is a run that errors every time until someone reads the row. */
119
+ // Which connected account runs the turn; absent means whichever comes first.
228
120
  readonly account?: string | undefined;
229
- /* What the shell calls that account, the sign-in identity, which is the only part of it the owner
230
- * recognises ("Claude" is what three unrenamed accounts are all called).
231
- *
232
- * Absent means the shell cannot name it, which covers BOTH a pin whose credential has been disconnected and
233
- * an account list this sandbox has not been read for yet. Do not render the first from the second: they are
234
- * the same absence, and a view that reads it as "this automation is broken" says so about every row while the
235
- * daemon is merely still starting. Show the name when there is one, and nothing when there isn't. */
121
+ // What the shell calls that account; absent covers both a disconnected pin and an unread account list.
236
122
  readonly accountLabel?: string | undefined;
237
- // `native` or `claude-code`, the agentic loop, an axis of its own since codex/grok run the same subscription
238
- // model ids under either. Absent ⇒ native, which for every other provider is the only answer there is.
123
+ // `native` or `claude-code`; absent means native, the only answer for most providers.
239
124
  readonly harness?: string | undefined;
240
- /* HOW HARD THAT MODEL THINKS, one of the provider's own reasoning tiers ("low", "high", "max"), absent ⇒ the
241
- * model's own default. Pass it on the turn you start (`effort`) exactly as you pass `model`: the sandbox
242
- * fills a pinned entry's tier in only for a run that named NO model, so a run your caret re-pointed and that
243
- * dropped this would quietly fall back to the provider's default tier. */
125
+ // One of the provider's reasoning tiers ("low", "high", "max"); absent means the model's own default.
244
126
  readonly effort?: string | undefined;
245
- // What the shell calls that tier ("X-High"), for a view that shows the choice. Absent whenever `effort` is,
246
- // and for a runtime that owns its own reasoning settings and publishes no scale.
127
+ // What the shell calls that tier ("X-High"); absent whenever `effort` is.
247
128
  readonly effortLabel?: string | undefined;
129
+ /* WHETHER THE MODEL REASONS BEFORE IT ANSWERS, where that is a choice it offers. Three states and not two:
130
+ * absent means nobody said, and the turn goes out with no thinking field so the model's own default decides,
131
+ * which is NOT the same as `false`. The distinction is load-bearing — Claude refuses its top effort tier
132
+ * beside thinking explicitly disabled — so pass it on the turn exactly as it arrives. */
133
+ readonly thinking?: boolean | undefined;
134
+ /* WHETHER THE WORK IS BOUGHT AT THE FASTER RATE, for a higher price. A request rather than a promise: the
135
+ * harness answers, and may decline (the plan, the account's own settings, the model). Absent ⇒ standard. */
136
+ readonly fast?: boolean | undefined;
248
137
  }
249
138
 
250
139
  export type SettingValue = string | number | boolean;
@@ -257,30 +146,22 @@ export interface ProcessStatus {
257
146
  }
258
147
 
259
148
  export interface IntenticApi {
260
- // The host's @intentic/extension-api version, what `engines.intentic` was checked against.
149
+ // The host's @intentic/extension-api version, checked against `engines.intentic`.
261
150
  readonly apiVersion: string;
262
151
  readonly views: {
263
152
  register(view: ViewRegistration): Disposable;
264
153
  };
265
- // Custom file viewers (contributes.viewers), the host owns the fetch + open-file lifecycle and renders the
266
- // registered component with the file's content; the extension only renders. See ViewerRegistration.
154
+ // Custom file viewers (contributes.viewers); the host owns the fetch and open-file lifecycle and
155
+ // renders the registered component. See ViewerRegistration.
267
156
  readonly viewers: {
268
157
  register(viewer: ViewerRegistration): Disposable;
269
158
  };
270
- // Per-directory documents (contributes.documents), the extension says which directories it can explain and
271
- // renders one; the host draws the tree's affordance and owns the tab. See DocumentProviderRegistration.
159
+ // Per-directory documents (contributes.documents); the extension says which directories it can
160
+ // explain, the host draws the tree affordance and owns the tab. See DocumentProviderRegistration.
272
161
  readonly documents: {
273
162
  register(provider: DocumentProviderRegistration): Disposable;
274
- /* Open one of THIS extension's documents for a directory, as if its row icon had been clicked.
275
- *
276
- * The row is the ordinary way in, so this is for the directories that have no row: the workspace root,
277
- * which the tree renders the contents of rather than a line for. Without it a command contributed
278
- * alongside a document provider, "Show Git History" in the palette, has nothing it can actually open.
279
- *
280
- * `id` must be one of this extension's registered providers, and the provider must have an offer for
281
- * `path` (the same `detect()` the tree asks); a provider that has nothing to say about the directory
282
- * opens nothing rather than an empty tab. The title and glyph come from that offer, so the tab reads
283
- * exactly as it would have from the row. */
163
+ // Opens one of this extension's documents for a path with no row of its own (e.g. the workspace root).
164
+ // `id` must be a registered provider with an offer for `path`, or nothing opens.
284
165
  open(id: string, path: string): void;
285
166
  };
286
167
  readonly commands: {
@@ -294,159 +175,78 @@ export interface IntenticApi {
294
175
  set(key: string, value: SettingValue): Promise<void>;
295
176
  onDidChange(listener: (key: string) => void): Disposable;
296
177
  };
297
- // The authenticated transport to the sandbox daemon's routes, auth is injected host-side; an extension
298
- // never sees tokens. Reach is scoped: every door here is gated by the manifest's `permissions.sandbox`
299
- // allowlist, so a call to an undeclared method+path throws rather than reaching the whole daemon.
178
+ // The authenticated transport to the sandbox daemon; auth is injected host-side. Every door is gated
179
+ // by the manifest's `permissions.sandbox` allowlist.
300
180
  readonly sandbox: {
301
- /* THE DAEMON, TYPED, the same contract the daemon implements, so a call names a procedure instead of
302
- * building a URL. `rpc.git.stashApply({ repo, ref, pop })` carries the declared input shape and answers
303
- * the declared output shape, both checked at build time.
304
- *
305
- * This is the door to reach for. `request`/`json` below take a path string, which means every caller
306
- * re-derives what this already knows: the method, the escaping, the query encoding, and the shape of the
307
- * answer, the last of those as an unchecked assertion that keeps compiling long after the daemon's reply
308
- * has changed underneath it. Thirteen extensions between them hand-wrote a hundred such calls and
309
- * re-validated half the responses against the very schemas the contract had already declared.
310
- *
311
- * Gated identically, and on the same evidence: the host resolves the procedure to its method and concrete
312
- * path and checks THAT against `permissions.sandbox`, so a manifest neither gains nor loses reach by an
313
- * extension switching doors, and the usage record stays comparable across both. */
181
+ // The daemon's contract, typed: a call names a procedure instead of a URL, checked at build time.
182
+ // Gated the same way as `request`/`json`, on the resolved method and path.
314
183
  readonly rpc: ContractRouterClient<typeof sandboxContract>;
315
184
  request(path: string, init?: RequestInit): Promise<Response>;
316
185
  json<T>(path: string, init?: RequestInit): Promise<T>;
317
- /* READ THROUGH THE HOST'S CACHE, from outside a component, the door for the module-level timers that
318
- * badge a rail tile, which is where `useQuery` cannot reach.
319
- *
320
- * Concurrent callers of one key share a single request, and a caller inside `staleTime` is answered
321
- * from cache with no round trip at all. Which is what makes this worth using over `json`: a badge that
322
- * polls a route on its own timer and hands the answer only to itself makes the view it badges for pay
323
- * for the same read again on open. Through here, the badge's poll IS the view's first paint.
324
- *
325
- * The route is gated exactly as `json` is, `queryFn` is your function, and whatever it calls carries
326
- * its own manifest check. */
186
+ // Reads through the host's cache from outside a component (e.g. a module-level badge timer);
187
+ // concurrent callers of one key share a request. Gated exactly as `json` is.
327
188
  fetch<T>(query: HostQuery<T>): Promise<T>;
328
- // Whether the active sandbox is currently reachable, reactive when read inside a computed, so it
329
- // drives host-provided vue-query `enabled` options.
189
+ // Whether the active sandbox is reachable; reactive when read inside a computed.
330
190
  reachable(): boolean;
331
- // A cache key scoped to the ACTIVE sandbox, the required prefix for every host-provided vue-query
332
- // key, so caches never bleed across a sandbox switch.
191
+ // A cache key scoped to the active sandbox; required prefix for every host-provided vue-query key.
333
192
  key(...parts: readonly string[]): readonly unknown[];
334
- // The daemon's base URL (its public tunnel origin), for building externally-shareable URLs like webhook
335
- // endpoints. Undefined until the sandbox has registered its address. Not needed for `request`/`json`
336
- // (those take a path and inject auth), only when the raw origin must be shown to the user.
193
+ // The daemon's public tunnel origin, for externally-shareable URLs; undefined until the sandbox is registered.
337
194
  origin(): string | undefined;
338
- /* THE SIGNED-IN USER'S TRUST TIER on the active sandbox, `owner`, `maintainer`, `collaborator` or
339
- * `viewer`, reactive when read inside a computed, like `reachable`. For AFFORDANCES ONLY: every route
340
- * is independently floored by the daemon, so what this gates is whether an Approve button renders, never
341
- * whether the call would succeed. A view that shows a viewer buttons the daemon will refuse teaches them
342
- * that buttons lie; this is how a view says less instead.
343
- *
344
- * The first consumer is the approvals queue (approve/reject are maintainer-and-up), and it existed as a
345
- * private composable before it was public API, which is the pattern this package's history warns about:
346
- * a surface only its own app needs is a surface nobody else can build the same feature on. */
195
+ // The signed-in user's trust tier, reactive like `reachable`. For affordances only: every route is
196
+ // still floored by the daemon independently.
347
197
  role(): "owner" | "maintainer" | "collaborator" | "viewer";
348
198
  };
349
199
  readonly workspace: {
350
200
  repos(): readonly RepoFacts[];
351
201
  capabilities(): readonly CapabilityFacts[];
352
202
  onDidChange(listener: () => void): Disposable;
353
- /* A REF MOVED IN ONE OF THESE REPOS, a commit, a branch, a checkout, a rebase, an aborted merge.
354
- *
355
- * Separate from `contributes.files` because no file contribution could ever carry it: the daemon's
356
- * watcher descent-ignores `.git`, so a changed ref produces no `workspaceChanged` path to match a prefix
357
- * against. The daemon diffs the git dirs itself and pushes the repos that moved, exactly as it does for
358
- * the repo SET (which the same watcher cannot see either, and for the same reason).
359
- *
360
- * This matters most for work the user did not do: an agent commits, rebases or lands out-of-band, with no
361
- * HTTP mutation in this browser to hang an invalidate on. Without this a git surface is only ever as fresh
362
- * as the last thing the user clicked. `repos` are root-relative ids ("root" is the workspace repo itself).
363
- */
203
+ // Fires when a ref moves in a repo (commit, branch, checkout, rebase) — a change `.git` watching can't
204
+ // see as a file path. `repos` are root-relative ids.
364
205
  onDidChangeRefs(listener: (repos: readonly string[]) => void): Disposable;
365
- /* ONE OF YOUR OWN `contributes.files` PATHS WAS WRITTEN, scoped to that declaration: the listener is
366
- * registered with your manifest's prefixes, so it never fires for a write you did not claim and you never
367
- * write the prefix matching yourself. `paths` are the matching ones, workspace-root-relative.
368
- *
369
- * The declaration already made the daemon's push evict the query keys it names, and for anything the
370
- * user is LOOKING at that is the whole story. It is not the story for what an extension does while none
371
- * of it is on screen: an eviction only reaches a query something is observing, and a rail badge is by
372
- * definition read with nothing mounted, so the tile went on saying whatever it last said until its own
373
- * timer came round. Every badge in the workspace was therefore as fresh as its interval, up to ten
374
- * minutes for the slowest, and a queue the owner had just emptied kept claiming its old count.
375
- *
376
- * You are unlikely to need this directly: `sandboxPoll` already wakes on it (background.ts), which is
377
- * what turns a declared file binding into a badge that moves with the write. Reach for it when you keep
378
- * background state some other way.
379
- *
380
- * FIRES WITH YOUR WHOLE DECLARATION when the host cannot say what moved: past a few hundred paths the
381
- * daemon stops listing them, and a reconnected stream may have missed frames outright. Both are "assume
382
- * yours changed", which is the safe direction for a badge, one re-read against a badge that is silently
383
- * wrong for as long as the connection was away. */
206
+ // Fires for a write under your declared `contributes.files` prefixes; `paths` are the matching ones.
207
+ // Fires with your whole declaration when the host can't say what moved.
384
208
  onDidChangeFiles(listener: (paths: readonly string[]) => void): Disposable;
385
- /* OPEN A DIFF IN THE EDITOR AREA, the host's tab strip, beside the files the diff is about.
386
- *
387
- * The shell owns the strip, the viewer, the close orchestration and the edit-buffer bookkeeping; the
388
- * extension owns only the question of what changed. Re-opening the same `key`+`scope`+`path` focuses the
389
- * tab that is already open rather than stacking a second copy, see DiffPayload for how that identity is
390
- * built. On mobile, where there is no strip, the host navigates to the diff instead.
391
- */
209
+ // Opens a diff in the editor area, beside the files it's about. Re-opening the same key+scope+path
210
+ // focuses the existing tab instead of stacking a new one.
392
211
  openDiff(payload: DiffPayload): void;
393
- /* FILL A DIFF OPENED WITH `pending`, the second half of opening a tab before its content exists.
394
- *
395
- * Refreshes, never opens: a tab the user has since closed or replaced takes nothing, and the content is
396
- * simply dropped. That is what makes the pending open safe to use for a slow source, a reader who has
397
- * moved on to another file is never yanked back to this one, and never has it appear under them.
398
- */
212
+ // Fills a diff opened with `pending`. Refreshes only: a tab the user has since closed or replaced takes
213
+ // nothing.
399
214
  fillDiff(payload: DiffPayload): void;
400
- /* READING AND WRITING WORKSPACE FILES, the daemon's file routes, without the encoding.
401
- *
402
- * Extensions keep their durable state in the workspace rather than in settings: an acceptance run's
403
- * reports, a documentation set's staging tree, the "what has the rail badge already shown" file each of
404
- * them keeps. That is the right home, it survives a reload, it is shared across the owner's browsers,
405
- * and the agent writing into it out-of-band is the whole point, but it left every extension spelling
406
- * `sandbox.json(\`/workspace/file?path=${encodeURIComponent(path)}\`)` and then parsing the envelope out
407
- * of the answer. Three extensions had five byte-identical copies of that one function.
408
- *
409
- * Gated exactly as `sandbox.request`/`sandbox.json` are: these go through the same permission check, so
410
- * an extension still declares `GET /workspace/file` and `POST /workspace/upload` in its manifest and one
411
- * that doesn't is still refused. This removes the encoding, not the grant. */
412
- // The file's text, or undefined when it is not there. Absent is the ordinary FIRST state for most of what
413
- // extensions keep, nothing has been acknowledged because nothing has been seen, so it is a value here,
414
- // not a throw every caller would have to wrap. The daemon reports it the same way (a 200 that says the
415
- // path holds nothing), so a poll over files that do not exist yet is silent rather than a page of failed
416
- // requests in the owner's console.
215
+ // Reads and writes workspace files without the encoding boilerplate. Gated exactly as
216
+ // `sandbox.request`/`sandbox.json`: the manifest grant is unchanged.
217
+ // The file's text, or undefined when it doesn't exist — a valid first state, not a thrown error.
417
218
  file(path: string): Promise<string | undefined>;
418
- /* The file parsed as a JSON object, or undefined when it is absent, truncated, or not an object at all.
419
- *
420
- * One tolerant reader rather than one per caller. These files are written by agents and editable by
421
- * hand, so a half-written or hand-mangled one is a case that WILL happen, and "skip it" is the right
422
- * answer everywhere: one bad file must never blank the surface that reads it. Arrays answer undefined
423
- * too, every caller of this wants a record. */
219
+ // Parsed as a JSON object, or undefined if absent, truncated, or not an object. One tolerant reader:
220
+ // a bad file is skipped, never a thrown error.
424
221
  readJson<T>(path: string): Promise<T | undefined>;
425
- // Create or replace a workspace file. Throws on failure, unlike the reads: a write that silently did
426
- // nothing would lose the thing the caller was told was saved.
222
+ // Creates or replaces a file. Throws on failure, unlike the reads: a silent no-op would lose what the
223
+ // caller was told was saved.
427
224
  write(path: string, body: string): Promise<void>;
428
225
  };
429
- // The extension's OWN declared background processes, names outside the manifest are refused.
226
+ // The extension's own declared background processes; names outside the manifest are refused.
430
227
  readonly processes: {
431
228
  status(name: string): Promise<ProcessStatus>;
432
229
  start(name: string): Promise<void>;
433
230
  stop(name: string): Promise<void>;
434
231
  };
435
- // The shell's ONE global terminal panel, extensions aim it at a tmux session (a capability job, a dev
436
- // server, an agent terminal); the host owns the panel itself.
232
+ // The shell's one global terminal panel; extensions aim it at a tmux session, the host owns the panel.
437
233
  readonly terminal: {
438
- // Open the panel focused on a tmux session (starting/attaching it).
234
+ // Opens the panel focused on a tmux session, starting or attaching it.
439
235
  open(session: string): void;
440
- // Show or hide the panel without focusing a session.
236
+ // Shows or hides the panel without focusing a session.
441
237
  setOpen(open: boolean): void;
442
238
  };
443
- // The shell's chat, the way `terminal` is the shell's one terminal panel: the extension names a transcript,
444
- // the host owns the tab. What this is for is a record that points at agent work, an automation's run
445
- // history, an audit row, where "why did it do that" is only answerable by reading the transcript.
239
+ // The shell's chat, the way `terminal` is the shell's terminal panel: the extension names a
240
+ // transcript, the host owns the tab.
446
241
  readonly chat: {
447
- // Open (or focus) the tab for a stored runtime session id, the same path the History menu and the fleet
448
- // board take. A session the daemon no longer holds opens an empty tab rather than failing.
242
+ // Opens (or focuses) the tab for a stored session id. A session the daemon no longer holds opens an
243
+ // empty tab rather than failing.
449
244
  openSession(sessionId: string): void;
245
+ /* Open (or focus) the docked chat for a fleet agent by its id: the same thing a card press on the
246
+ * agents board does. The agent's conversation appears in the chat panel beside the current view
247
+ * rather than navigating away from it. An agent the roster does not hold yet (archived, or between
248
+ * a start and the first roster frame) is looked up before opening. */
249
+ openAgent(agentId: string): void;
450
250
  /* AIM A NEW CHAT AT A WORKFLOW: the host opens a session exactly as "New agent" does, with the
451
251
  * composer's workflow badge set to this design, so the next message the user types becomes that run's
452
252
  * request instead of a turn on the chat.
@@ -461,68 +261,46 @@ export interface IntenticApi {
461
261
  * nothing here can name a `Workflow` type.
462
262
  */
463
263
  composeWorkflow(workflowId: string): void;
464
- /* AIM A NEW CHAT AT A SAVED LOOP, the same handover as `composeWorkflow` above, for the other kind of
465
- * design: the host opens a session with the composer's loop badge set, so the next message the user
466
- * types becomes the loop's GOAL and Send starts it running.
467
- *
468
- * It is a separate call rather than a flag on that one because the two badges are separate picks that
469
- * cannot both be armed, and a single "compose with this id" would have had to guess which kind an id
470
- * was. A loop id, not a running loop's, nothing has started, and nothing is spent until the send.
471
- */
264
+ // Like `composeWorkflow`, but arms the composer's loop badge: the next message becomes the loop's
265
+ // goal, and Send starts it running.
472
266
  composeLoop(loopId: string): void;
473
267
  };
474
- /* WHICH MODEL A RUN THIS EXTENSION STARTS WILL SPEND, the way `terminal` is the shell's one terminal panel:
475
- * the extension names the choice it is holding, the host owns the picker.
476
- *
477
- * It is an API rather than a kit component because the picker is not a widget, it is a live read of every
478
- * connected provider's catalog, which credentials the sandbox actually holds, and what each model can do.
479
- * An extension that rendered its own control could only ever offer a worse list: the acceptance view's did,
480
- * fetching one provider's models behind a second dropdown for the provider itself, and so it happily
481
- * offered models the sandbox had no credential for, a run that fails on a credential error minutes later.
482
- *
483
- * IT COVERS THE WHOLE CHOICE, provider, account, harness, model, because covering three quarters of it is
484
- * what produced the copy this API exists to prevent. The automations form asked all four questions, found an
485
- * API that answered three, and hand-rolled ROWS OF CHIPS for every one of them to keep its own fields
486
- * consistent with each other: a static provider list that offered providers the sandbox had no credential for
487
- * and could not offer a model endpoint or an ACP agent at all, and a model row eleven chips wide that grew
488
- * with every release. Partial coverage does not buy partial reuse; it buys none. */
268
+ // Which model a run this extension starts will spend; the host owns the picker, a live read of every
269
+ // connected provider's catalog. Covers provider, account, harness and model together.
489
270
  readonly models: {
490
- /* What a run of this KIND opens on when nobody has chosen: the sandbox's model list for that job
491
- * (Sandbox ▸ Agent ▸ Models), falling back to whatever the owner's own chat is set to. Reactive when
492
- * read inside a computed.
493
- *
494
- * `role` is the job, as the sandbox names it — "pipeline-fix", "maintenance-chore", "documentation-run",
495
- * "acceptance-run", "deployment-fix" and the rest. There is a list per job rather than one for
496
- * unattended work as a class, because a documentation sweep and a red production pipeline are not the
497
- * same spend and the owner is the one who gets to say so. Pass the role your surface's turns send as
498
- * `runRole`, or the two will disagree about which model a click costs. An unknown role is not an error:
499
- * it answers with the owner's own chat model, the same floor an unpinned job gets. */
271
+ // The sandbox's default model for a job `role` (e.g. "acceptance-run"), falling back to the owner's
272
+ // chat model; reactive in a computed.
500
273
  agentRun(role: string): PickedModel;
501
- /* NAME A SELECTION THE EXTENSION ALREADY HOLDS, a pin read back from disk, which arrives as bare ids and
502
- * has to be rendered before anyone opens the picker. This is what keeps `label` honest for the surfaces
503
- * that SAVE a choice rather than spend it immediately: without it every one of them would keep a catalog
504
- * to pretty-print its own stored ids, which is the duplication this API exists to end, and it would go
505
- * stale the day a provider renames a model or the owner disconnects an account.
506
- *
507
- * Reactive when read inside a computed, a model that lands in the catalog, or an account that stops being
508
- * connected, changes what a stored pin should say about itself. */
274
+ // Renders a stored pin (bare ids) back into a display-ready `PickedModel`, so a saved choice doesn't
275
+ // need its own catalog. Reactive: reflects a renamed model or disconnected account.
509
276
  describe(selection: {
510
277
  readonly provider: string;
511
278
  readonly model: string;
512
279
  readonly account?: string | undefined;
513
280
  readonly harness?: string | undefined;
514
281
  readonly effort?: string | undefined;
282
+ readonly thinking?: boolean | undefined;
283
+ readonly fast?: boolean | undefined;
515
284
  }): PickedModel;
516
285
  /* Open the picker over `anchor`, a popover on desktop, a sheet on mobile, starting on the selection the
517
286
  * caller is holding. Resolves with the pick, or undefined if it was dismissed. A second call supersedes
518
287
  * the first, resolving it as a dismissal.
519
288
  *
520
- * A MODEL ROW settles it. Account, harness and reasoning-effort rows behave as they do in the composer:
521
- * they update the open selection without closing, and the eventual model pick carries those pins.
522
- * Dismissing after only staging a pin still resolves undefined. Picking a model under a DIFFERENT provider
523
- * clears the account and harness with it, an account id is one provider's store key, so carrying it across
524
- * would pin the run to an account that provider does not have; the EFFORT survives, because a tier is a
525
- * question every model answers for itself. */
289
+ * IT IS A FORM AND IT ENDS IN A PRESS. Every row in the panel — the model list included — updates the
290
+ * open selection and leaves the panel open; the bar at the bottom, carrying your `action` as its label,
291
+ * is what resolves this promise. Nothing else does: Escape, a click outside and the sheet's backdrop are
292
+ * all a plain cancel, resolving undefined and keeping nothing, so a caller never has to guess whether a
293
+ * dismissal meant "as you were" or "yes, but from over there".
294
+ *
295
+ * The panel used to settle on the model row instead, which cost the callers that spend money a second
296
+ * click (leave the panel, then press the thing that starts the run) and made backing out of an effort
297
+ * change impossible. If your surface merely stores the answer, name your verb accordingly ("Use this
298
+ * model" is the default) and the press reads as the save it is.
299
+ *
300
+ * Picking a model under a DIFFERENT provider clears the account and harness with it: an account id is one
301
+ * provider's store key, so carrying it across would pin the work to an account that provider does not
302
+ * have. The RUN SETTINGS survive, because effort, thinking and speed are questions every model answers
303
+ * for itself. */
526
304
  pick(options: {
527
305
  readonly anchor: HTMLElement;
528
306
  readonly provider: string;
@@ -530,41 +308,33 @@ export interface IntenticApi {
530
308
  readonly account?: string | undefined;
531
309
  readonly harness?: string | undefined;
532
310
  readonly effort?: string | undefined;
533
- /* OFFER THE REASONING-EFFORT ROW, for a caller that will carry `effort` onto the turn it starts. Off
534
- * by default, and deliberately: a form that stores a model and no tier (an automation, a workflow
535
- * step) would be showing a control whose answer it drops, which is worse than showing none. Every
536
- * <AgentRunButton> asks for it through `useAgentRunPick`, so a surface using that gets it already. */
537
- readonly chooseEffort?: boolean;
311
+ readonly thinking?: boolean | undefined;
312
+ readonly fast?: boolean | undefined;
313
+ /* THE VERB ON THE PANEL'S OWN BUTTON — "Fix with agent", "Run all 21 stories", "Save this step".
314
+ * Yours, because only you know what the press does, and it is what lets configuring a run and
315
+ * starting it be one act instead of two. Defaults to "Use this model", which is honest for a form
316
+ * that is only storing the answer. */
317
+ readonly action?: string | undefined;
318
+ /* OFFER THE MODEL'S OWN RUN SETTINGS — reasoning effort, extended thinking, speed — for a caller
319
+ * that will carry them onto the turn it starts or the pin it stores. Off by default, and
320
+ * deliberately: a form that keeps a model and none of these (a workflow step) would be showing
321
+ * controls whose answers it drops, which is worse than showing none. Every <AgentRunButton> asks for
322
+ * them through `useAgentRunPick`, so a surface using that gets them already. */
323
+ readonly chooseRun?: boolean;
538
324
  }): Promise<PickedModel | undefined>;
539
325
  };
540
- // Navigate the shell to an app path (e.g. "/capabilities", "/ext/<view>/<key>").
326
+ // Navigates the shell to an app path (e.g. "/capabilities", "/ext/<view>/<key>").
541
327
  readonly navigate: (path: string) => void;
542
- /* THE SAME PATH AS A BROWSER ADDRESS: what a view puts in an `<a href>` so the thing it draws is a real
543
- * link and not a <button> that happens to move the shell.
544
- *
545
- * Every row and card in this app that goes somewhere has a URL behind it, and a view that only calls
546
- * `navigate` throws all of it away: nothing under the pointer in the status bar, nothing in the browser's
547
- * own right-click menu, nothing to copy, and Ctrl/⌘-click navigating the tab the user is reading instead
548
- * of opening a second one. So a navigational row renders as `<a :href="api.href(path)">` and calls
549
- * `navigate` from its click handler: guarded with `browserOwnsClick` (@intentic/extension-ui) so a
550
- * modified click is left to the browser. */
328
+ // The same path as a browser address, for `<a href>` so a navigational row is a real link, not just a
329
+ // click handler. Use with `browserOwnsClick` for modified clicks.
551
330
  readonly href: (path: string) => string;
552
- /* THE URL AS A VIEW'S STATE, so what a reader is looking at can be linked to.
553
- *
554
- * A view's own route space is the QUERY, not extra path segments: `/ext/:ext/:key?` is the whole route, and
555
- * the `:key` segment already means "which activation" (one per repo). A view with internal navigation, a
556
- * document browser, a selected run, an open file, therefore has nowhere in the path to put it, and without
557
- * this it could only hold that state in memory, where a reload loses it and a link cannot carry it.
558
- *
559
- * Reading is reactive: read inside a computed and the view re-renders when the URL moves, which lets a view
560
- * DERIVE its state from the query rather than mirror it in a ref (mirroring needs two watchers that can fight
561
- * each other). Back and forward then work for free, because the URL is the state. */
331
+ // The URL as a view's own state, so what's on screen can be linked. A view's route space is the query
332
+ // only (`:key` already names the activation).
562
333
  readonly route: {
563
- // The current query, flattened, a repeated key takes its first value, since a view's state is scalar.
334
+ // The current query, flattened; a repeated key takes its first value.
564
335
  query(): Readonly<Record<string, string>>;
565
- /* Merge a patch in; a key set to `undefined` is removed. Replaces the history entry by default and pushes
566
- * a new one when asked: a filter or a display toggle should not fill the back stack, while moving to
567
- * another document is exactly what Back ought to undo. Other views' params are left alone. */
336
+ // Merges a patch in; a key set to `undefined` is removed. Replaces the history entry by default, or
337
+ // pushes one when `push` is set.
568
338
  setQuery(patch: Readonly<Record<string, string | undefined>>, options?: { readonly push?: boolean }): void;
569
339
  };
570
340
  readonly theme: {
@@ -579,8 +349,8 @@ export interface ExtensionContext {
579
349
  readonly subscriptions: Disposable[];
580
350
  }
581
351
 
582
- // The shape of the bundle's default export (or its named exports): `activate` runs once after the engines
583
- // check; `deactivate` runs before the host discards the extension.
352
+ // The bundle's default (or named) exports: `activate` runs once after the engines check, `deactivate`
353
+ // runs before the host discards the extension.
584
354
  export interface ExtensionModule {
585
355
  activate(api: IntenticApi, context: ExtensionContext): void | Promise<void>;
586
356
  deactivate?(): void | Promise<void>;