@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.
- package/dist/agents-service.d.ts +103 -6
- package/dist/agents-service.d.ts.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-panel-entry-focus.d.ts +122 -0
- package/dist/use-panel-entry-focus.d.ts.map +1 -0
- package/dist/use-panel-entry-focus.js +186 -0
- package/dist/use-panel-entry-focus.js.map +1 -0
- package/package.json +2 -2
- package/src/agents-service.ts +105 -6
- package/src/index.ts +15 -4
- package/src/types.ts +31 -0
- package/src/use-panel-entry-focus.test.ts +244 -0
- package/src/use-panel-entry-focus.ts +237 -0
|
@@ -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.
|
|
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
|
},
|
package/src/agents-service.ts
CHANGED
|
@@ -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
|
|
755
|
-
* for what is deliberately left out — see
|
|
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`, `
|
|
10
|
-
* `NetworkError`). The SDK peer-depends on React 19; changes
|
|
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
|
-
//
|
|
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
|