@silo-code/sdk 0.43.0 → 0.45.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.
@@ -21,6 +21,51 @@ export interface OscEvent {
21
21
  /** The raw payload string after the code and its separating semicolon. */
22
22
  payload: string;
23
23
  }
24
+ /**
25
+ * Where a chunk of terminal output came from in time.
26
+ *
27
+ * A terminal session outlives the app, so re-attaching to one replays the
28
+ * session host's recent scrollback — bytes that are byte-for-byte identical to
29
+ * live output but describe things that already happened, sometimes long ago.
30
+ * Treating them as live is what makes a reattached agent terminal announce a
31
+ * turn that finished before the app started.
32
+ *
33
+ * @category Consumer Services
34
+ * @public
35
+ */
36
+ export interface OutputOrigin {
37
+ /**
38
+ * `true` when the chunk is scrollback the session host is replaying on
39
+ * attach, `false` when the session produced it just now.
40
+ *
41
+ * Only ever `true` for a subscription that opted in with
42
+ * {@link SubscribeOutputOptions.includeReplay}.
43
+ */
44
+ replay: boolean;
45
+ }
46
+ /**
47
+ * Options for {@link TerminalService.subscribeOutput} and
48
+ * {@link TerminalService.subscribeOsc}.
49
+ *
50
+ * @category Consumer Services
51
+ * @public
52
+ */
53
+ export interface SubscribeOutputOptions {
54
+ /**
55
+ * Deliver the scrollback the session host replays when re-attaching to a
56
+ * terminal, in addition to live output. Defaults to `false`.
57
+ *
58
+ * Leave it off for anything that reads output as *activity* — "the agent is
59
+ * working", "something just happened", a notification — because replayed
60
+ * bytes would fire all of it for events that are already over.
61
+ *
62
+ * Turn it on for anything that needs the terminal's *history*: rendering the
63
+ * scrollback, or identifying which program is running in a terminal you have
64
+ * only just attached to. Each chunk then arrives with an
65
+ * {@link OutputOrigin} saying which kind it is.
66
+ */
67
+ includeReplay?: boolean;
68
+ }
24
69
  export type { TerminalKind, TerminalRecord } from "./domain-types.js";
25
70
  /**
26
71
  * @deprecated Prefer {@link TerminalService.setIndicator} /
@@ -214,8 +259,16 @@ export interface TerminalService extends TabAdornmentMethods {
214
259
  * });
215
260
  * ctx.subscriptions.push(sub);
216
261
  * ```
262
+ *
263
+ * @remarks
264
+ * OSC sequences ride the raw output stream, so the same replay rule applies
265
+ * as for {@link TerminalService.subscribeOutput}: by default only sequences
266
+ * from **live** output are delivered, and `{ includeReplay: true }` adds
267
+ * those found in the scrollback replayed on attach. Status titles are the
268
+ * usual reason to leave it off — a replayed "busy" title describes a turn
269
+ * that is already over.
217
270
  */
218
- subscribeOsc(terminalId: string, handler: (event: OscEvent) => void): Disposable;
271
+ subscribeOsc(terminalId: string, handler: (event: OscEvent, origin: OutputOrigin) => void, options?: SubscribeOutputOptions): Disposable;
219
272
  /**
220
273
  * Subscribe to the raw PTY output stream of the terminal identified by
221
274
  * `terminalId`. The `handler` is called with every chunk of bytes the PTY
@@ -232,19 +285,42 @@ export interface TerminalService extends TabAdornmentMethods {
232
285
  * not the underlying PTY session id, so it survives terminal recreation within
233
286
  * the same record.
234
287
  *
288
+ * By default only **live** output is delivered. A terminal session outlives
289
+ * the app, so attaching to one replays its recent scrollback; without this
290
+ * default every reattach would look like a burst of activity happening right
291
+ * now. Pass `{ includeReplay: true }` to receive that history too — each
292
+ * chunk then arrives with an {@link OutputOrigin} saying which kind it is.
293
+ *
235
294
  * Returns a {@link Disposable} that cancels the subscription.
236
295
  *
237
296
  * @example
238
297
  * ```ts
239
298
  * // Track the last time any output arrived to confirm agent activity.
299
+ * // Replayed scrollback is excluded by default, so re-attaching to a
300
+ * // long-idle terminal doesn't read as fresh activity.
240
301
  * let lastOutputAt = 0;
241
302
  * const sub = ctx.terminals.subscribeOutput(terminalId, () => {
242
303
  * lastOutputAt = Date.now();
243
304
  * });
244
305
  * ctx.subscriptions.push(sub);
245
306
  * ```
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * // Opt in to the replayed scrollback — needed to work out what is running
311
+ * // in a terminal you just attached to — and keep the two apart.
312
+ * const sub = ctx.terminals.subscribeOutput(
313
+ * terminalId,
314
+ * (chunk, { replay }) => {
315
+ * identifyProgram(chunk); // history is evidence
316
+ * if (!replay) noteActivity(); // …but only live output is activity
317
+ * },
318
+ * { includeReplay: true },
319
+ * );
320
+ * ctx.subscriptions.push(sub);
321
+ * ```
246
322
  */
247
- subscribeOutput(terminalId: string, handler: (data: string) => void): Disposable;
323
+ subscribeOutput(terminalId: string, handler: (data: string, origin: OutputOrigin) => void, options?: SubscribeOutputOptions): Disposable;
248
324
  /**
249
325
  * The record id of the terminal tab that is currently active in the active
250
326
  * workspace's center dock, or `null` when an editor tab (or nothing) is
@@ -1 +1 @@
1
- {"version":3,"file":"terminal-service.d.ts","sourceRoot":"","sources":["../src/terminal-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"terminal-service.d.ts","sourceRoot":"","sources":["../src/terminal-service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silo-code/sdk",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "The public, types-first SDK for building Silo extensions — the only surface third-party extensions import.",
5
5
  "license": "MIT",
6
6
  "author": "Dave Weaver",
@@ -1,6 +1,7 @@
1
- import type { HTMLAttributes } from "react";
1
+ import type { CSSProperties, HTMLAttributes } from "react";
2
2
  import {
3
3
  activityClass,
4
+ activityJitterStyle,
4
5
  type Activity as ActivityKind,
5
6
  type ActivitySize,
6
7
  } from "./activity";
@@ -13,10 +14,14 @@ import {
13
14
  * Styled purely via host-provided `.silo-activity*` classes — no stylesheet
14
15
  * import is needed in the extension.
15
16
  *
17
+ * Pass `jitterKey` whenever several animated glyphs render together (a list of
18
+ * agent or workspace rows) so their pulses don't run in lockstep — and so each
19
+ * one starts mid-cycle instead of freezing on first paint.
20
+ *
16
21
  * @example
17
22
  * ```tsx
18
23
  * <ActivityGlyph activity="working" size="md" />
19
- * <ActivityGlyph activity="ready" />
24
+ * <ActivityGlyph activity="ready" jitterKey={row.id} />
20
25
  * <ActivityGlyph size="sm" /> // workspace neutral (omit)
21
26
  * ```
22
27
  *
@@ -27,15 +32,31 @@ export function ActivityGlyph({
27
32
  activity,
28
33
  size = "sm",
29
34
  className,
35
+ jitterKey,
36
+ style,
30
37
  ...rest
31
38
  }: {
32
39
  activity?: ActivityKind;
33
40
  size?: ActivitySize;
41
+ /**
42
+ * Stable id (a row or terminal id — never an array index) that gives this
43
+ * glyph a deterministic per-instance animation offset. Desynchronizes its
44
+ * pulse from other glyphs rendered alongside it, and makes the animation
45
+ * mount already in progress rather than pausing on first paint (a WebKit
46
+ * quirk when the glyph appears inside a just-shown container). Inert on
47
+ * non-animated glyphs.
48
+ */
49
+ jitterKey?: string;
34
50
  } & Omit<HTMLAttributes<HTMLSpanElement>, "children">) {
35
51
  const classes = activityClass(activity, size);
52
+ const mergedStyle =
53
+ jitterKey != null
54
+ ? ({ ...activityJitterStyle(jitterKey), ...style } as CSSProperties)
55
+ : style;
36
56
  return (
37
57
  <span
38
58
  className={className ? `${classes} ${className}` : classes}
59
+ style={mergedStyle}
39
60
  aria-hidden={rest["aria-label"] == null ? true : undefined}
40
61
  {...rest}
41
62
  />
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { activityClass, activityFromAgent } from "./activity";
2
+ import {
3
+ activityClass,
4
+ activityFromAgent,
5
+ activityJitterStyle,
6
+ } from "./activity";
3
7
 
4
8
  describe("activityClass", () => {
5
9
  it("maps kinds and sizes", () => {
@@ -33,3 +37,32 @@ describe("activityFromAgent", () => {
33
37
  expect(activityFromAgent("dead")).toBeNull();
34
38
  });
35
39
  });
40
+
41
+ describe("activityJitterStyle", () => {
42
+ const delay = (key: string) =>
43
+ Number.parseFloat(activityJitterStyle(key)["--silo-activity-jitter"]);
44
+
45
+ it("is deterministic for a given key", () => {
46
+ expect(activityJitterStyle("term-42")).toEqual(
47
+ activityJitterStyle("term-42"),
48
+ );
49
+ });
50
+
51
+ it("spreads different keys across the cycle", () => {
52
+ const keys = ["a", "b", "c", "d", "e", "term-1", "ws:build", "term-2"];
53
+ const delays = keys.map(delay);
54
+ // Not all landing on the same offset (the lockstep bug).
55
+ expect(new Set(delays).size).toBeGreaterThan(1);
56
+ });
57
+
58
+ it("emits a negative delay within one animation period", () => {
59
+ for (const key of ["", "x", "a-very-long-stable-row-identifier", "ws:1"]) {
60
+ const d = delay(key);
61
+ expect(d).toBeLessThanOrEqual(0);
62
+ expect(d).toBeGreaterThan(-1.8);
63
+ expect(activityJitterStyle(key)["--silo-activity-jitter"]).toMatch(
64
+ /^-?\d+\.\d{3}s$/,
65
+ );
66
+ }
67
+ });
68
+ });
package/src/activity.ts CHANGED
@@ -11,8 +11,11 @@ import type { AgentActivity } from "./agents-service";
11
11
  export type Activity = "working" | "ready" | "warn" | "error";
12
12
 
13
13
  /**
14
- * Glyph size for {@link Activity}. `"sm"` matches workspace status rows (~6px);
15
- * `"md"` is slightly larger for CenterDock / SideDock tabs.
14
+ * Glyph size for {@link Activity}. `"sm"` (8px at the default `uiFontSize`) is
15
+ * the workbench size — workspace status rows and CenterDock / SideDock tabs;
16
+ * `"md"` (10px) is the roomier one, for a list that leads with the dot rather
17
+ * than tucking it into chrome. Both scale with `uiFontSize` rather than being
18
+ * fixed pixels.
16
19
  *
17
20
  * @category Core Types
18
21
  * @public
@@ -55,3 +58,35 @@ export function activityClass(
55
58
  const kind = activity ?? "none";
56
59
  return `silo-activity silo-activity-${kind} silo-activity-${size}`;
57
60
  }
61
+
62
+ /**
63
+ * Deterministic per-instance `animation-delay` (via the `--silo-activity-jitter`
64
+ * custom property) for an animated {@link Activity} glyph, keyed off a stable id
65
+ * — a row or terminal id, not an array index. Two purposes:
66
+ *
67
+ * - **Desync.** Several `working` / `ready` glyphs rendered side by side would
68
+ * otherwise pulse in perfect lockstep; a per-id offset staggers them.
69
+ * - **Start mid-cycle.** The value is a *negative* delay, so the animation
70
+ * mounts already in progress rather than pausing on first paint — a WebKit
71
+ * quirk that bites when the glyph is created inside a container that was
72
+ * `display: none` moments earlier (e.g. a Navigator view on its first
73
+ * activation), where a zero delay can leave the pulse frozen until the next
74
+ * show/hide toggle.
75
+ *
76
+ * Hashed rather than random so the offset is stable across re-renders. Pinned
77
+ * by unit tests.
78
+ *
79
+ * @internal
80
+ */
81
+ export function activityJitterStyle(
82
+ key: string,
83
+ ): Record<"--silo-activity-jitter", string> {
84
+ let hash = 0;
85
+ for (let i = 0; i < key.length; i++) {
86
+ hash = (hash * 31 + key.charCodeAt(i)) | 0;
87
+ }
88
+ // 1.8s is the longest activity animation period (`silo-activity-wave`), so the
89
+ // offset spans one full cycle.
90
+ const delaySeconds = -(((hash >>> 0) % 1000) / 1000) * 1.8;
91
+ return { "--silo-activity-jitter": `${delaySeconds.toFixed(3)}s` };
92
+ }
@@ -172,6 +172,187 @@ export interface CatalogAgentSummary {
172
172
  readonly icon?: AgentIcon;
173
173
  }
174
174
 
175
+ /**
176
+ * One **Agent Profile** as an extension may read it through
177
+ * {@link AgentProfilesService.list} — a named recipe for starting a coding
178
+ * agent in a terminal, defined by the user on Settings → Agents → Profiles.
179
+ * Deliberately a summary, never the host's own profile record: the command
180
+ * line, its config directory, and every other launch detail stay host-owned
181
+ * (RFC 0033).
182
+ *
183
+ * @category Consumer Services
184
+ * @public
185
+ * @beta
186
+ */
187
+ export interface AgentProfileSummary {
188
+ /** Stable id — pass it to {@link AgentProfilesService.launch}. */
189
+ readonly id: string;
190
+ /** The user's own name for this profile, e.g. `"Claude (work)"`. Show this;
191
+ * never show or parse the id. */
192
+ readonly label: string;
193
+ /** True for the single profile marked default, which is what `launch()`
194
+ * starts when no `profileId` is given. False for every profile when the
195
+ * user has not chosen one. */
196
+ readonly isDefault: boolean;
197
+ /**
198
+ * Whether this profile's agent can be given an **opening prompt**. A static
199
+ * fact about the agent, not about any particular launch — so a picker can
200
+ * grey out or annotate a profile up front instead of discovering
201
+ * `"agent-takes-none"` after the user has already typed one.
202
+ */
203
+ readonly acceptsPrompt: boolean;
204
+ }
205
+
206
+ /**
207
+ * Why an opening prompt could not be delivered. Silo refuses rather than
208
+ * approximating: a prompt it cannot quote exactly is never typed, and no agent
209
+ * is started without the prompt the caller asked for.
210
+ *
211
+ * @category Consumer Services
212
+ * @public
213
+ * @beta
214
+ */
215
+ export type PromptRefusal =
216
+ /** The profile matches no agent Silo knows, so there is no way to tell how
217
+ * its CLI takes a prompt. The user can fix this by setting the agent on the
218
+ * profile. */
219
+ | "no-agent"
220
+ /** The profile's agent has no way to accept an opening prompt while staying
221
+ * interactive. Check {@link AgentProfileSummary.acceptsPrompt} first to
222
+ * avoid offering a prompt for such a profile at all. */
223
+ | "agent-takes-none"
224
+ /** Silo has no exact quoting rule for the shell this terminal would run and
225
+ * will not guess one. bash, zsh, and fish are supported. */
226
+ | "unsupported-shell"
227
+ /**
228
+ * The prompt exceeds Silo's 2 KiB limit — roughly a page of prose.
229
+ *
230
+ * The ceiling is low because the prompt is *typed* into the user's shell,
231
+ * and a shell with syntax highlighting or autosuggestions cannot reliably
232
+ * consume more than a few KiB in one go. Silo refuses well short of where
233
+ * delivery starts failing, because a truncated prompt would reach the agent
234
+ * looking complete. Trim it and retry.
235
+ */
236
+ | "too-large";
237
+
238
+ /**
239
+ * Options for {@link AgentProfilesService.launch}. Every field is optional —
240
+ * a bare `launch()` starts the default profile in the active workspace.
241
+ *
242
+ * @category Consumer Services
243
+ * @public
244
+ * @beta
245
+ */
246
+ export interface LaunchAgentProfileOptions {
247
+ /** Which profile to start. Defaults to the one marked default, else the
248
+ * first — the same profile the built-in "New Agent" command uses. */
249
+ profileId?: string;
250
+ /** Which workspace to start it in. Defaults to the active one. A
251
+ * background workspace works: the session is spawned eagerly, since no
252
+ * panel will mount to do it. */
253
+ workspaceId?: string;
254
+ /** Working directory for the new terminal. Defaults to the workspace folder. */
255
+ cwd?: string;
256
+ /**
257
+ * An opening prompt to hand the agent on its launch line.
258
+ *
259
+ * The text is delivered as a literal — it is never interpreted by the
260
+ * shell, so `$HOME`, backticks, quotes, and newlines are all safe. If Silo
261
+ * cannot deliver it exactly, the launch is **refused** rather than mangled
262
+ * or silently dropped: nothing is typed, no terminal is created, and
263
+ * `launch()` returns the reason.
264
+ *
265
+ * Keep it to an opening instruction. The limit is **2 KiB** — about a page —
266
+ * and anything longer is refused with `"too-large"`; see that member for
267
+ * why the ceiling is where it is.
268
+ *
269
+ * The composed line is typed into the user's own interactive shell, so it
270
+ * appears in scrollback and in shell history exactly as if they had typed
271
+ * it. Don't put a secret in one.
272
+ */
273
+ prompt?: string;
274
+ /**
275
+ * Activate the target workspace and focus the new terminal. Defaults to
276
+ * `true`. Pass `false` to start an agent without stealing the user's place.
277
+ */
278
+ activate?: boolean;
279
+ }
280
+
281
+ /**
282
+ * What {@link AgentProfilesService.launch} did. A **result**, never a throw:
283
+ * every foreseeable reason a launch cannot happen is a value you can branch
284
+ * on and report to your own user.
285
+ *
286
+ * @category Consumer Services
287
+ * @public
288
+ * @beta
289
+ */
290
+ export type LaunchAgentProfileResult =
291
+ | {
292
+ readonly ok: true;
293
+ /** The created terminal's record id — the same id
294
+ * {@link AgentsService.getByTerminalId} and `ctx.terminals` take. */
295
+ readonly terminalId: string;
296
+ }
297
+ | {
298
+ readonly ok: false;
299
+ /** Why nothing was launched. `"no-profile"` — the named profile does
300
+ * not exist, or there are no profiles at all. `"no-workspace"` — the
301
+ * named workspace does not exist, or none is open. Otherwise one of the
302
+ * {@link PromptRefusal} reasons. */
303
+ readonly refusal: PromptRefusal | "no-profile" | "no-workspace";
304
+ };
305
+
306
+ /**
307
+ * Read the user's **Agent Profiles** and start one, optionally with an opening
308
+ * prompt — exposed as `ctx.agents.profiles` (RFC 0033).
309
+ *
310
+ * A profile is a way to *start* a terminal, not a way to talk to an agent:
311
+ * what comes up is a PTY running a real agent CLI, exactly as if the user had
312
+ * typed the command themselves. There is no agent-agnostic messaging layer
313
+ * here and there is not meant to be one.
314
+ *
315
+ * There is deliberately no `pick()` — build one from `list()` and
316
+ * `ctx.ui.showMenu`, which is the shared chrome — and no `get()`, which is
317
+ * `list().find()`.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * // Let the user choose a profile, then start it on a task.
322
+ * const profiles = ctx.agents.profiles.list();
323
+ * const chosen = await ctx.ui.showMenu(
324
+ * profiles.map((p) => ({ id: p.id, label: p.label })),
325
+ * );
326
+ * if (chosen) {
327
+ * const result = ctx.agents.profiles.launch({
328
+ * profileId: chosen,
329
+ * prompt: "Fix the failing test in src/foo.test.ts",
330
+ * });
331
+ * if (!result.ok) ctx.ui.notify("warn", `Couldn't start it: ${result.refusal}`);
332
+ * }
333
+ * ```
334
+ *
335
+ * @category Consumer Services
336
+ * @public
337
+ * @beta
338
+ */
339
+ export interface AgentProfilesService {
340
+ /**
341
+ * Every Agent Profile the user has defined, in the order they appear in
342
+ * Settings. The returned array is **read-only and deeply frozen**; it is
343
+ * recomputed when the profile list changes, not on every call.
344
+ */
345
+ list(): readonly AgentProfileSummary[];
346
+ /**
347
+ * Start a profile in a terminal. Returns the created terminal's id, or a
348
+ * typed refusal — see {@link LaunchAgentProfileResult}.
349
+ *
350
+ * A refused prompt creates nothing at all: no terminal record, no workspace
351
+ * activation, no focus change.
352
+ */
353
+ launch(options?: LaunchAgentProfileOptions): LaunchAgentProfileResult;
354
+ }
355
+
175
356
  /**
176
357
  * Host-computed coding-agent observability — exposed as
177
358
  * {@link ExtensionContext.agents}. Detection (what OSC/output signals mean
@@ -250,4 +431,13 @@ export interface AgentsService {
250
431
  * @public
251
432
  */
252
433
  catalog(): readonly CatalogAgentSummary[];
434
+ /**
435
+ * The user's **Agent Profiles** — read them, and start one, optionally with
436
+ * an opening prompt. See {@link AgentProfilesService}.
437
+ *
438
+ * @category Consumer Services
439
+ * @public
440
+ * @beta
441
+ */
442
+ readonly profiles: AgentProfilesService;
253
443
  }
@@ -35,6 +35,25 @@ export interface ExtensionStorage {
35
35
  subscribe(listener: () => void): Disposable;
36
36
  }
37
37
 
38
+ /**
39
+ * One workspace's per-extension storage directory, as returned by
40
+ * {@link ExtensionStorageScopes.workspaceDirs}: the workspace's id paired with
41
+ * the absolute path to that workspace's directory for the calling extension.
42
+ *
43
+ * @category Core Types
44
+ * @public
45
+ */
46
+ export interface WorkspaceStorageDir {
47
+ /** The workspace this directory belongs to. */
48
+ readonly workspaceId: string;
49
+ /**
50
+ * Absolute path to `~/.config/silo/extension-storage/<your-id>/workspaces/<workspaceId>`.
51
+ * Readable and writable through {@link ExtensionContext.files} with no
52
+ * `fs:*` permission, like every other own-storage path.
53
+ */
54
+ readonly dir: string;
55
+ }
56
+
38
57
  /**
39
58
  * The persisted-storage scopes available to an extension, exposed as
40
59
  * {@link ExtensionContext.storage}: two key/value bags for settings-sized
@@ -87,22 +106,53 @@ export interface ExtensionStorageScopes {
87
106
  */
88
107
  globalDir(): Promise<string>;
89
108
  /**
90
- * An absolute path to a directory of your own scoped to the **active
91
- * workspace** — the filesystem counterpart to
92
- * {@link ExtensionStorageScopes.workspace}. Created on first call, at
109
+ * An absolute path to a directory of your own scoped to a **workspace** — the
110
+ * filesystem counterpart to {@link ExtensionStorageScopes.workspace}. Lives at
93
111
  * `~/.config/silo/extension-storage/<your-id>/workspaces/<workspaceId>`.
94
112
  *
95
113
  * Everything {@link ExtensionStorageScopes.globalDir} says about permissions
96
- * and relative paths applies here too. Call it again after the active
97
- * workspace changes — the path changes with it, so don't cache it across a
98
- * workspace switch.
114
+ * and relative paths applies here too.
115
+ *
116
+ * @param workspaceId - Which workspace's directory to resolve. Omit for the
117
+ * **active** workspace — the path then changes as the active workspace
118
+ * switches, so don't cache it across a switch, and the call rejects with
119
+ * `NoWorkspaceError` when no workspace is open. Pass an id (from
120
+ * {@link ExtensionContext.workspaces}) to resolve **any** workspace's
121
+ * directory, active or not — the aggregating case (a view over every
122
+ * workspace's data).
123
+ * @param options - `create` (default `true`) creates the directory if it is
124
+ * missing, matching the historical behaviour — every caller that then
125
+ * **writes** into the path needs this, because {@link FileService.writeText}
126
+ * does not create parent directories. Pass `create: false` when you only
127
+ * need the path to **read** from (a missing file already reads as absent);
128
+ * nothing is written to disk for a workspace that has no data yet.
99
129
  *
100
130
  * The directory follows the workspace's **identity**, not its folder path:
101
131
  * deleting a workspace and re-adding the same folder gives you a new, empty
102
132
  * directory (the same rule {@link ExtensionStorageScopes.workspace} follows).
103
133
  * Deleting a workspace leaves the directory on disk.
134
+ */
135
+ workspaceDir(
136
+ workspaceId?: string,
137
+ options?: { create?: boolean },
138
+ ): Promise<string>;
139
+ /**
140
+ * Resolve a storage directory for **every open workspace** in one call — the
141
+ * building block for a surface that aggregates per-workspace data (a
142
+ * cross-workspace list, say) without making a separate
143
+ * {@link ExtensionStorageScopes.workspaceDir} call per workspace.
144
+ *
145
+ * Returns one entry per workspace in
146
+ * {@link WorkspaceService.getState}`().open`, each `{ workspaceId, dir }`.
147
+ * There is no ordering guarantee — match entries to workspaces by
148
+ * `workspaceId`.
104
149
  *
105
- * Rejects with `NoWorkspaceError` when no workspace is open.
150
+ * @param options - `create` (default `true`) behaves exactly as it does on
151
+ * {@link ExtensionStorageScopes.workspaceDir}. The aggregating case passes
152
+ * `create: false`: it only reads each workspace's data, so no directory is
153
+ * created for a workspace that has none.
106
154
  */
107
- workspaceDir(): Promise<string>;
155
+ workspaceDirs(options?: {
156
+ create?: boolean;
157
+ }): Promise<readonly WorkspaceStorageDir[]>;
108
158
  }
package/src/index.ts CHANGED
@@ -110,6 +110,11 @@ export type {
110
110
  AgentIcon,
111
111
  AgentIconMode,
112
112
  CatalogAgentSummary,
113
+ AgentProfilesService,
114
+ AgentProfileSummary,
115
+ LaunchAgentProfileOptions,
116
+ LaunchAgentProfileResult,
117
+ PromptRefusal,
113
118
  } from "./agents-service";
114
119
  export type {
115
120
  TerminalService,
@@ -119,6 +124,8 @@ export type {
119
124
  TerminalTabDecoration,
120
125
  TerminalTabDecorationProvider,
121
126
  OscEvent,
127
+ OutputOrigin,
128
+ SubscribeOutputOptions,
122
129
  } from "./terminal-service";
123
130
  export type {
124
131
  TabAdornmentColor,
@@ -190,6 +197,7 @@ export type {
190
197
  export type {
191
198
  ExtensionStorage,
192
199
  ExtensionStorageScopes,
200
+ WorkspaceStorageDir,
193
201
  } from "./extension-storage";
194
202
  // The drag-and-drop domain: the consumer service + its payload/handler shapes.
195
203
  // DND_MIME is a value (the well-known MIME vocabulary), so it's a runtime export.
@@ -1,4 +1,5 @@
1
1
  import type { Disposable } from "./types";
2
+ import type { OutputOrigin, SubscribeOutputOptions } from "./terminal-service";
2
3
 
3
4
  // `ctx.process` — persistent process / PTY sessions that survive app restarts.
4
5
  // The core primitive under the terminal (and future task runners / REPLs).
@@ -59,8 +60,20 @@ export interface ProcessSession {
59
60
  getBuffer(): Promise<string>;
60
61
  /** Persist an output buffer for later restore. */
61
62
  saveBuffer(data: string): Promise<void>;
62
- /** Subscribe to output data. Dispose to stop listening. */
63
- onData(listener: (data: string) => void): Disposable;
63
+ /**
64
+ * Subscribe to output data. Dispose to stop listening.
65
+ *
66
+ * By default only **live** output is delivered. A session survives app
67
+ * restarts, so re-attaching to one replays its recent scrollback; delivering
68
+ * that by default would make every reattach look like a burst of output
69
+ * arriving right now. Pass `{ includeReplay: true }` to receive the replayed
70
+ * history too — for instance to paint scrollback into a fresh view — and read
71
+ * `origin.replay` to tell the two apart.
72
+ */
73
+ onData(
74
+ listener: (data: string, origin: OutputOrigin) => void,
75
+ options?: SubscribeOutputOptions,
76
+ ): Disposable;
64
77
  /** Subscribe to session exit. Dispose to stop listening. */
65
78
  onExit(listener: (exitCode: number) => void): Disposable;
66
79
  }