@silo-code/sdk 0.51.0 → 0.53.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.
@@ -0,0 +1,186 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ // Entry focus for a dock panel — landing the caret in the panel's content when
3
+ // the user enters it.
4
+ //
5
+ // A freshly-mounted or newly-activated dock panel does NOT reliably inherit DOM
6
+ // focus: dockview runs its own focus shuffle when panels mount/activate, and
7
+ // rich content (Monaco, xterm) only accepts focus once its internal textarea
8
+ // has been laid out with non-zero dimensions. A single `focus()` call loses
9
+ // these races, which is why a panel would otherwise need a second click before
10
+ // you can type. We retry across animation frames until the target actually
11
+ // reports focus, the panel is no longer the one we wanted to focus, or we hit a
12
+ // frame cap (~330ms) so we never loop forever when focus legitimately can't be
13
+ // grabbed (panel hidden, content disabled, etc.).
14
+ const DEFAULT_CAP = 20;
15
+ // Every menu implementation whose open state must suppress the retry: Silo's
16
+ // own menus (`ctx.ui.showMenu`, which renders `[data-silo-menu]` with
17
+ // `role="menu"`), a third-party menu that follows the same ARIA role, and
18
+ // Monaco's context menu (`.context-view` / `.monaco-menu`).
19
+ const MENU_SELECTOR = '[data-silo-menu], [role="menu"], .context-view, .monaco-menu';
20
+ /**
21
+ * Whether focus currently sits inside an open floating menu. Monaco renders its
22
+ * context menu into a shadow root, where `document.activeElement` resolves only
23
+ * to the open `shadow-root-host` and the menu nodes hang off its `shadowRoot` —
24
+ * hence the shadow peek, not just a light-DOM `closest`.
25
+ */
26
+ function focusInMenu(active) {
27
+ if (!active)
28
+ return false;
29
+ if (active.closest(MENU_SELECTOR))
30
+ return true;
31
+ return active.shadowRoot?.querySelector(MENU_SELECTOR) != null;
32
+ }
33
+ /**
34
+ * Repeatedly call `focus()` on successive animation frames until `isFocused()`
35
+ * returns true, `stillWanted()` returns false, or `cap` frames have elapsed.
36
+ *
37
+ * `stillWanted` is **required**, and must express "this panel is still the one
38
+ * that should hold focus" — normally `() => api.isActive`, or the center
39
+ * dock's focus-intent token (`focusGen`). It is deliberately not optional: a
40
+ * retry loop without it keeps grabbing focus for its entire frame budget no
41
+ * matter what else happened meanwhile. That matters most on *mount*, because
42
+ * re-entering a workspace re-mounts its panels — so an unguarded mount-time
43
+ * grab in a background tab yanks focus away from the tab the user actually
44
+ * left active. And because DOM focus landing inside a dockview panel makes
45
+ * dockview mark that panel's group active
46
+ * (`contentContainer.onDidFocus → doSetGroupActive`), stealing focus silently
47
+ * changes the visible active tab too. That was symptom 1 in ADR 0034.
48
+ *
49
+ * Panel content should reach for {@link usePanelEntryFocus} instead, which
50
+ * supplies the guard and both entry subscriptions for you. This raw form is
51
+ * host/`core.*` plumbing for the non-React callers that can't.
52
+ *
53
+ * @internal
54
+ */
55
+ export function retryFocus(focus, isFocused, stillWanted, cap = DEFAULT_CAP) {
56
+ let attempts = 0;
57
+ let landed = false;
58
+ const tick = () => {
59
+ // Bail before touching focus if we're no longer wanted — a newer focus
60
+ // intent superseded us (e.g. the user kept pressing the region-cycle chord).
61
+ // Checked here, not just after the grab below, so a stale retry can't yank
62
+ // focus back from the area the user actually moved to.
63
+ if (!stillWanted())
64
+ return;
65
+ attempts += 1;
66
+ if (isFocused()) {
67
+ landed = true;
68
+ }
69
+ else {
70
+ // Before focus first lands, grab aggressively each frame — Monaco/xterm
71
+ // only accept focus once their textarea is laid out, so early calls lose.
72
+ // After it has landed, only re-grab if focus fell to NOTHING (<body>/null):
73
+ // dockview's show/hide shuffle can bounce focus off the just-focused
74
+ // content a frame or two later (the "keystrokes dropped after a switch"
75
+ // symptom). We watch the full frame budget to catch that — but never yank
76
+ // focus away from another real element the user may have moved to.
77
+ const active = document.activeElement;
78
+ // Focus legitimately moved into an open context menu — don't yank it
79
+ // back, or the menu's focus-out dismiss fires and it flashes shut.
80
+ // Right-clicking an *inactive* panel activates it, which starts this
81
+ // retry; without the guard the per-frame `focus()` steals focus from the
82
+ // just-opened menu (the focused case never starts the retry, which is why
83
+ // the bug only shows when the panel was unfocused). End the loop: the
84
+ // menu's owner restores content focus itself once the menu closes.
85
+ if (focusInMenu(active))
86
+ return;
87
+ const lostToNothing = active === null || active === document.body;
88
+ if (!landed || lostToNothing)
89
+ focus();
90
+ }
91
+ if (attempts >= cap || !stillWanted())
92
+ return;
93
+ requestAnimationFrame(tick);
94
+ };
95
+ requestAnimationFrame(tick);
96
+ }
97
+ /**
98
+ * Land keyboard focus in a dock panel's content whenever the user enters the
99
+ * panel, winning dockview's focus shuffle — and, symmetrically, release it when
100
+ * the panel's tab is deactivated.
101
+ *
102
+ * A panel that puts the cursor somewhere on entry (a chat composer, a search
103
+ * box, an editor) needs three things that are easy to get individually wrong,
104
+ * and this hook is all three in one call:
105
+ *
106
+ * 1. **Both entry signals.** {@link DockPanelApi.onDidActiveChange} covers real
107
+ * activations. {@link DockPanelApi.onDidRequestFocus} covers a click on a
108
+ * tab that was *already* active — dockview treats that as a complete no-op
109
+ * and fires no active-change event, even though it is the commonest way a
110
+ * user asks for the cursor back (they were typing in a side panel, clicked
111
+ * the status bar, then clicked your tab).
112
+ * 2. **A guard.** Focus is only ever taken while the panel is the active one.
113
+ * Mount is not "the user just created this panel" — re-entering a workspace
114
+ * re-mounts every panel in its dock — and DOM focus landing in a panel makes
115
+ * dockview activate its group, so an unguarded grab silently changes the
116
+ * visible tab (ADR 0034).
117
+ * 3. **A retry.** A single `focus()` loses dockview's shuffle, and rich content
118
+ * only accepts focus once its internal textarea is laid out. The hook
119
+ * retries across frames until focus lands, standing down if the panel stops
120
+ * being active or focus moves into an open context menu.
121
+ *
122
+ * ```tsx
123
+ * function AcmeChatPanel({ api }: DockPanelProps) {
124
+ * const input = useRef<HTMLTextAreaElement | null>(null);
125
+ *
126
+ * usePanelEntryFocus(api, {
127
+ * focus: () => input.current?.focus(),
128
+ * isFocused: () => document.activeElement === input.current,
129
+ * });
130
+ *
131
+ * return <textarea ref={input} />;
132
+ * }
133
+ * ```
134
+ *
135
+ * The returned function runs the same guarded retry on demand, for content that
136
+ * becomes focusable at a moment that is neither a mount nor an activation — a
137
+ * session finishing its connect, an editor finishing its own mount. It is
138
+ * referentially stable, so it is safe in a dependency array:
139
+ *
140
+ * ```tsx
141
+ * const focusComposer = usePanelEntryFocus(api, { focus, isFocused });
142
+ *
143
+ * useEffect(() => {
144
+ * if (ready) focusComposer();
145
+ * }, [ready, focusComposer]);
146
+ * ```
147
+ *
148
+ * @param api - The surrounding panel's api ({@link DockPanelProps.api}).
149
+ * @param target - How to focus, test, and release the panel's content.
150
+ * @returns A stable callback that runs the guarded focus retry on demand.
151
+ *
152
+ * @category Consumer Services
153
+ * @public
154
+ */
155
+ export function usePanelEntryFocus(api, target) {
156
+ // Read the callbacks through a ref rather than closing over them: the
157
+ // subscription effect depends on `api` alone (re-subscribing every render
158
+ // would be worse), so a captured `target` would freeze at its first render
159
+ // and an author's inline `() => { if (enabled) … }` would go stale.
160
+ const latest = useRef(target);
161
+ useEffect(() => {
162
+ latest.current = target;
163
+ });
164
+ const focusNow = useCallback(() => {
165
+ if (!api.isActive)
166
+ return;
167
+ retryFocus(() => latest.current.focus(), () => latest.current.isFocused(), () => api.isActive);
168
+ }, [api]);
169
+ useEffect(() => {
170
+ const subActive = api.onDidActiveChange(() => {
171
+ if (api.isActive)
172
+ focusNow();
173
+ else
174
+ latest.current.blur?.();
175
+ });
176
+ // `focusNow` guards on `isActive` itself: a Focus Request says only that
177
+ // the user asked, never that this panel became active.
178
+ const subRequest = api.onDidRequestFocus(focusNow);
179
+ return () => {
180
+ subActive.dispose();
181
+ subRequest.dispose();
182
+ };
183
+ }, [api, focusNow]);
184
+ return focusNow;
185
+ }
186
+ //# sourceMappingURL=use-panel-entry-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-panel-entry-focus.js","sourceRoot":"","sources":["../src/use-panel-entry-focus.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.51.0",
3
+ "version": "0.53.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",
@@ -36,10 +36,10 @@
36
36
  "react": "^19.1.0",
37
37
  "react-dom": "^19.1.0"
38
38
  },
39
- "dependencies": {},
40
39
  "devDependencies": {
41
40
  "@types/react": "^19.1.8",
42
41
  "@types/react-dom": "^19.1.0",
42
+ "jsdom": "^29.1.1",
43
43
  "typescript": "~5.8.3",
44
44
  "vitest": "^4.1.7"
45
45
  },
@@ -96,6 +96,25 @@ export interface AgentInfo {
96
96
  readonly terminalId?: string;
97
97
  /** The workspace this session belongs to. */
98
98
  readonly workspaceId: string;
99
+ /**
100
+ * Where this session is currently working — an absolute path, or `undefined`
101
+ * while it is not yet known.
102
+ *
103
+ * Host-computed for both kinds, which is the point: a Terminal session fills
104
+ * it from the working directory of its foreground process, and a Chat session
105
+ * from the **working checkout** the host derives for it. A consumer that
106
+ * wants to show "where is this agent" reads this one field and does not care
107
+ * which kind it got.
108
+ *
109
+ * For a Chat session this is *not* simply the `cwd` the session was started
110
+ * with. That value is fixed at `session/new` — the Agent Client Protocol has
111
+ * no working-directory-change notification — but agents relocate anyway, most
112
+ * commonly by creating a git worktree and moving into it. The host reports
113
+ * the session's start directory until it has evidence the agent has moved,
114
+ * and every moved-to directory is confirmed to be a real checkout before it
115
+ * is reported here.
116
+ */
117
+ readonly cwd?: string;
99
118
  /**
100
119
  * The session's display label — host-computed, the same string for either
101
120
  * kind, and the one a consumer should render. A status row, a navigator row
@@ -192,9 +211,6 @@ export interface AgentInfo {
192
211
  * release — the user runs `resumeCommand` themselves; the flag is the
193
212
  * forward-looking capability signal RFC 0038 replaces the shell-string
194
213
  * `resumeCommand` contract with.
195
- * - Chat session: `true` when the agent advertises `session/load`, so a
196
- * fresh process can reload the transcript after the old one died.
197
- *
198
214
  * - Chat session: `true` when the agent advertises **either**
199
215
  * `sessionCapabilities.resume` or `agentCapabilities.loadSession` (RFC
200
216
  * 0042) — the two are distinct capabilities (see {@link ChatResumeState})
@@ -357,6 +373,16 @@ export interface AgentProfileSummary {
357
373
  * be offered a profile that cannot work in the surface they are looking at.
358
374
  */
359
375
  readonly interface: AgentSessionKind;
376
+ /**
377
+ * The catalog agent this profile resolves to, if any — the same id
378
+ * {@link AgentsService.catalog} keys on, and the same resolution
379
+ * {@link AgentSessionHandle.agentId} reports once a Chat session is live
380
+ * (the profile's explicit assertion, or an auto-detect match from its
381
+ * command line). Look up {@link CatalogAgentSummary.icon} with it to show
382
+ * a profile's brand mark in a picker *before* connecting — `undefined`
383
+ * when the profile resolves to no known agent.
384
+ */
385
+ readonly agentId?: string;
360
386
  }
361
387
 
362
388
  /**
@@ -739,6 +765,30 @@ export interface AgentPlanEntry {
739
765
  readonly priority?: string;
740
766
  }
741
767
 
768
+ /**
769
+ * Session context/cost state from a `usage_update` — the protocol's own
770
+ * `used` / `size` naming. **Sender-optional**: many agents never emit this,
771
+ * and those that do report `used` and `size` differently (recon), so a
772
+ * missing {@link AgentSessionUpdate.usage} on a given update is normal, not
773
+ * an error — never synthesize a reading when the agent hasn't sent one.
774
+ *
775
+ * @category Consumer Services
776
+ * @public
777
+ * @beta
778
+ */
779
+ export interface AgentSessionUsage {
780
+ /** Tokens currently consumed in the session context. */
781
+ readonly used: number;
782
+ /** Total token capacity for the session. */
783
+ readonly size: number;
784
+ /** Cumulative session cost, when the agent reports one. */
785
+ readonly cost?: {
786
+ readonly amount: number;
787
+ /** ISO 4217 currency code, e.g. `"USD"`. */
788
+ readonly currency: string;
789
+ };
790
+ }
791
+
742
792
  /**
743
793
  * One `session/update` notification from a **Chat session**, lightly
744
794
  * normalized. The Agent Client Protocol streams a turn as a sequence of these:
@@ -751,8 +801,9 @@ export interface AgentPlanEntry {
751
801
  *
752
802
  * Everything a Chat UI must draw to render a transcript is a modelled field:
753
803
  * {@link text} / {@link content} for the message kinds, {@link toolCall} for
754
- * `tool_call` and `tool_call_update`, {@link plan} for `plan`. {@link raw} is
755
- * for what is deliberately left out — see its own note.
804
+ * `tool_call` and `tool_call_update`, {@link plan} for `plan`, {@link usage}
805
+ * for `usage_update`. {@link raw} is for what is deliberately left out — see
806
+ * its own note.
756
807
  *
757
808
  * @category Consumer Services
758
809
  * @public
@@ -798,13 +849,18 @@ export interface AgentSessionUpdate {
798
849
  * showing rather than appending to it. May be empty.
799
850
  */
800
851
  readonly plan?: readonly AgentPlanEntry[];
852
+ /**
853
+ * Session context/cost state, for `kind` `"usage_update"`; `undefined`
854
+ * otherwise, and also whenever the connected agent doesn't send this
855
+ * notification at all — see {@link AgentSessionUsage}.
856
+ */
857
+ readonly usage?: AgentSessionUsage;
801
858
  /**
802
859
  * The raw Agent Client Protocol `update` object — the **escape hatch**, for
803
860
  * the kinds and fields this surface does not model.
804
861
  *
805
862
  * What is deliberately not modelled, and why: `available_commands_update`
806
863
  * (an agent's slash commands — a menu, not a transcript row),
807
- * `usage_update` (token counts, which no agent reports the same way),
808
864
  * `current_mode_update` (already surfaced as
809
865
  * {@link AgentSessionConfigOption.currentValue}), `session_info_update`
810
866
  * (already surfaced as {@link AgentInfo.title}), and vendor extensions such
@@ -1055,6 +1111,11 @@ export interface AgentSessionHandle {
1055
1111
  * after the process dies. Mirrors {@link AgentInfo.canResume}.
1056
1112
  */
1057
1113
  readonly canResume: boolean;
1114
+ /**
1115
+ * Whether the agent advertises `session/list` — whether {@link
1116
+ * listSessions} can be called at all (Session Discovery, RFC 0051).
1117
+ */
1118
+ readonly canList: boolean;
1058
1119
  /**
1059
1120
  * How this handle came to be connected (RFC 0042) — see
1060
1121
  * {@link AgentSessionRestore}:
@@ -1170,6 +1231,20 @@ export interface AgentSessionHandle {
1170
1231
  * changes, so there is no change event for it.
1171
1232
  */
1172
1233
  readonly promptCapabilities: AgentPromptCapabilities;
1234
+ /**
1235
+ * Ask the agent what other sessions it knows about (ACP `session/list`, v1
1236
+ * stable) — **Session Discovery** (RFC 0051). Metadata only, not a live
1237
+ * handle: pass a picked {@link AgentSessionSummary.sessionId} back as
1238
+ * {@link AgentSessionRestore.sessionId} on a fresh {@link
1239
+ * AgentSessionsService.connect} to open it — the same restore flow as any
1240
+ * other resume.
1241
+ *
1242
+ * Rejects when {@link canList} is `false` — check it before calling, the
1243
+ * same way a caller checks {@link canResume} before offering resume. Also
1244
+ * rejects when {@link resumeOutcome} is `"journal-only"` — there is no live
1245
+ * connection to ask.
1246
+ */
1247
+ listSessions(): Promise<readonly AgentSessionSummary[]>;
1173
1248
  /**
1174
1249
  * Tear the session down: kill the agent process and drop it from
1175
1250
  * {@link AgentsService.getState}. Idempotent. The process is a piped child of
@@ -1179,6 +1254,30 @@ export interface AgentSessionHandle {
1179
1254
  dispose(): void;
1180
1255
  }
1181
1256
 
1257
+ /**
1258
+ * One session an agent reports via `session/list` (ACP v1 stable) —
1259
+ * **Session Discovery** (RFC 0051). Metadata only, not a live handle.
1260
+ *
1261
+ * @category Consumer Services
1262
+ * @public
1263
+ * @beta
1264
+ */
1265
+ export interface AgentSessionSummary {
1266
+ /** What to pass as {@link AgentSessionRestore.sessionId} to resume this
1267
+ * one. */
1268
+ readonly sessionId: string;
1269
+ /** The working directory this session ran in, when the agent reports one —
1270
+ * may differ from the current session's own cwd. */
1271
+ readonly cwd?: string;
1272
+ /** A display title, when the agent reports one. */
1273
+ readonly title?: string;
1274
+ /** Last-activity timestamp (ISO 8601), when the agent reports one. */
1275
+ readonly updatedAt?: string;
1276
+ /** The agent's own `SessionInfo` object, untouched — the escape hatch for
1277
+ * a vendor field this type does not model. */
1278
+ readonly raw: Record<string, unknown>;
1279
+ }
1280
+
1182
1281
  /**
1183
1282
  * Options for {@link AgentSessionsService.connect}.
1184
1283
  *
package/src/index.ts CHANGED
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * **What's here:** the types-first extension contract (see `types.ts`) plus a
8
8
  * small set of blessed runtime helpers (`Tooltip`, the modal design-system
9
- * kit, `useFocusGroup`, `useServiceState`, `DND_MIME`, `PathDeniedError`,
10
- * `NetworkError`). The SDK peer-depends on React 19; changes to the runtime
11
- * helpers can be breaking even when the types are unchanged.
9
+ * kit, `useFocusGroup`, `usePanelEntryFocus`, `useServiceState`, `DND_MIME`,
10
+ * `PathDeniedError`, `NetworkError`). The SDK peer-depends on React 19; changes
11
+ * to the runtime helpers can be breaking even when the types are unchanged.
12
12
  *
13
13
  * This is also the entry point the API-reference generator (TypeDoc) reads, so
14
14
  * the published reference is exactly this surface — no more, no less.
@@ -122,11 +122,13 @@ export type {
122
122
  AgentSessionHandle,
123
123
  AgentSessionConnectOptions,
124
124
  AgentSessionRestore,
125
+ AgentSessionSummary,
125
126
  ChatResumeState,
126
127
  AgentPromptBlock,
127
128
  AgentPromptResult,
128
129
  AgentStopReason,
129
130
  AgentSessionUpdate,
131
+ AgentSessionUsage,
130
132
  AgentContentBlock,
131
133
  AgentToolCall,
132
134
  AgentToolCallContent,
@@ -356,10 +358,19 @@ export type {
356
358
  FocusGroupNavQuery,
357
359
  } from "./use-focus-group";
358
360
 
359
- // Host/InlineEdit coordination for InlineEdit's two-stage Escape (RFC 0016).
361
+ // Entry focus for a dock panel (RFC 0049): both entry signals + the
362
+ // `isActive`-guarded frame retry that wins dockview's focus shuffle, in one
363
+ // call. The one sanctioned way for panel content to take focus on entry.
364
+ export { usePanelEntryFocus } from "./use-panel-entry-focus";
365
+ export type { PanelEntryFocusTarget } from "./use-panel-entry-focus";
366
+
367
+ // Host/InlineEdit coordination for InlineEdit's two-stage Escape (RFC 0016),
368
+ // and the raw focus retry behind `usePanelEntryFocus` for the non-React
369
+ // callers that can't use the hook (RFC 0049).
360
370
  // @internal — host-only plumbing, not part of the documented reference;
361
371
  // extension authors never call these directly.
362
372
  export {
363
373
  setActiveInlineEditCancel,
364
374
  yieldEscapeToInlineEdit,
365
375
  } from "./inline-edit-controller";
376
+ export { retryFocus } from "./use-panel-entry-focus";
package/src/types.ts CHANGED
@@ -90,6 +90,37 @@ export interface DockPanelApi {
90
90
  onDidActiveChange(
91
91
  listener: (event: { readonly isActive: boolean }) => void,
92
92
  ): Disposable;
93
+ /**
94
+ * Subscribe to the user asking, by gesture, for keyboard focus in this
95
+ * panel — fired on every click of its tab, including one that leaves
96
+ * {@link DockPanelApi.isActive | isActive} unchanged because the tab was
97
+ * already active.
98
+ *
99
+ * That already-active case is why the event exists: it has no other signal.
100
+ * {@link DockPanelApi.onDidActiveChange | onDidActiveChange} only fires on a
101
+ * real transition, so clicking back into an already-active tab from
102
+ * elsewhere (a side panel, the status bar) is otherwise silent even though
103
+ * the user's intent — "put keyboard focus here" — is identical to a genuine
104
+ * activation. A panel that drives its own entry focus from
105
+ * `onDidActiveChange` should drive it from this event too, guarded on
106
+ * `isActive`, so both paths land focus the same way.
107
+ *
108
+ * The name is the intent, but today exactly one gesture carries it:
109
+ *
110
+ * - **Fires** on a primary click of this panel's tab, whether or not the
111
+ * panel was already the active one.
112
+ * - **Does not fire** for activation from anywhere else —
113
+ * {@link DockPanelApi.setActive | setActive}, a keybinding, a command or
114
+ * command-palette jump, or the host restoring a saved layout. Those change
115
+ * which panel is active, so they surface as `onDidActiveChange` instead;
116
+ * none of them reaches this event.
117
+ * - **Does not fire** for a click inside the panel's own content (focus
118
+ * there is yours to manage), the tab's close button, or a middle-click
119
+ * close.
120
+ *
121
+ * Returns a {@link Disposable} that cancels the subscription.
122
+ */
123
+ onDidRequestFocus(listener: () => void): Disposable;
93
124
  /**
94
125
  * `true` while this panel is visible — its tab is the selected one in its
95
126
  * group. Distinct from {@link DockPanelApi.isActive | isActive}: with split