wings-plugin-sdk 0.0.0-stage → 0.2.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/LICENSE +18 -0
- package/README.md +221 -2
- package/bin.mjs +27 -0
- package/build.d.ts +20 -0
- package/build.mjs +177 -0
- package/package.json +70 -4
- package/react.d.ts +6 -0
- package/react.js +11 -0
- package/shell.d.ts +14 -0
- package/shell.js +38 -0
- package/ui-css.d.ts +4 -0
- package/ui-css.js +547 -0
- package/ui.d.ts +144 -0
- package/ui.js +290 -0
- package/wings.d.ts +225 -0
package/ui.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// wings-plugin-sdk/ui: a page says what to show and where, and Wings decides how it looks. Every component renders
|
|
2
|
+
// plain elements with `w-*` class names and data attributes that Wings' plugin-ui.css styles, never a style of its
|
|
3
|
+
// own, so a Wings update restyles every plugin. Your own components and CSS work alongside them.
|
|
4
|
+
import { createContext, createElement as h, useContext, useEffect, useId, useState } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
|
|
7
|
+
import { mountShell } from "./shell.js";
|
|
8
|
+
|
|
9
|
+
export { mountShell } from "./shell.js";
|
|
10
|
+
export { useBroadcast } from "./react.js";
|
|
11
|
+
|
|
12
|
+
// ---------- places ----------
|
|
13
|
+
|
|
14
|
+
let root = null;
|
|
15
|
+
|
|
16
|
+
function show(place, element, options) {
|
|
17
|
+
const shell = mountShell(place, options);
|
|
18
|
+
root ??= createRoot(shell);
|
|
19
|
+
root.render(element);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Shows `element` as the page of a sidebar. One call per page; calling again renders the new element. */
|
|
23
|
+
export const showSidebar = (element, options) => show("sidebar", element, options);
|
|
24
|
+
/** Shows `element` as a view's page, in a modal or a window of its own. */
|
|
25
|
+
export const showView = (element, options) => show("view", element, options);
|
|
26
|
+
/** Shows `element` as a view's page in a tab next to the terminals. */
|
|
27
|
+
export const showTab = (element, options) => show("tab", element, options);
|
|
28
|
+
/** Shows `element` as a sidebar's popover. */
|
|
29
|
+
export const showPopover = (element, options) => show("popover", element, options);
|
|
30
|
+
/** Shows `element` as the plugin's page in Wings Settings. */
|
|
31
|
+
export const showSettings = (element, options) => show("settings", element, options);
|
|
32
|
+
|
|
33
|
+
// ---------- talking to main ----------
|
|
34
|
+
|
|
35
|
+
const HELLO = { type: "hello" };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The newest message of `type` the plugin's other pages broadcast, or null until one comes. Sends `hello` once
|
|
39
|
+
* when the component mounts, so main answers with its state; pass null to send nothing.
|
|
40
|
+
*/
|
|
41
|
+
export function useMainState(type, hello = HELLO) {
|
|
42
|
+
const [state, setState] = useState(null);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const off = wings.onBroadcast((message) => {
|
|
45
|
+
if (message?.type === type) setState(message);
|
|
46
|
+
});
|
|
47
|
+
if (hello != null) void wings.broadcast(hello);
|
|
48
|
+
return off;
|
|
49
|
+
// `hello` is only sent when the component mounts.
|
|
50
|
+
}, [type]);
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------- helpers ----------
|
|
55
|
+
|
|
56
|
+
const cx = (base, extra) => (extra ? `${base} ${extra}` : base);
|
|
57
|
+
/** A data attribute that's there or not. */
|
|
58
|
+
const flag = (on) => (on ? "" : undefined);
|
|
59
|
+
|
|
60
|
+
// ---------- layout and text ----------
|
|
61
|
+
|
|
62
|
+
/** Children side by side, centred. `gap` is none, xs, s, m (default) or l. */
|
|
63
|
+
export function Row({ as = "div", gap, align, justify, wrap, grow, className, ...rest }) {
|
|
64
|
+
return h(as, { ...rest, className: cx("w-row", className), "data-gap": gap, "data-align": align, "data-justify": justify, "data-wrap": flag(wrap), "data-grow": flag(grow) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Children one under the other. */
|
|
68
|
+
export function Stack({ as = "div", gap, grow, className, ...rest }) {
|
|
69
|
+
return h(as, { ...rest, className: cx("w-stack", className), "data-gap": gap, "data-grow": flag(grow) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Text with a tone and size, that can grow to fill a row or truncate with an ellipsis. */
|
|
73
|
+
export function Text({ as = "span", tone, size, weight, mono, numeric, truncate, clamp, grow, className, ...rest }) {
|
|
74
|
+
return h(as, {
|
|
75
|
+
...rest,
|
|
76
|
+
className: cx("w-text", className),
|
|
77
|
+
"data-tone": tone,
|
|
78
|
+
"data-size": size,
|
|
79
|
+
"data-weight": weight,
|
|
80
|
+
"data-mono": flag(mono),
|
|
81
|
+
"data-numeric": flag(numeric),
|
|
82
|
+
"data-truncate": flag(truncate),
|
|
83
|
+
"data-clamp": flag(clamp),
|
|
84
|
+
"data-grow": flag(grow),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A section heading, with `actions` like chips or a button on its right. */
|
|
89
|
+
export function Heading({ actions, className, ...rest }) {
|
|
90
|
+
const heading = h("h2", { ...rest, className: cx("w-heading", className) });
|
|
91
|
+
return actions ? h("div", { className: "w-heading-row" }, heading, actions) : heading;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A card. `compact` for a summary line. */
|
|
95
|
+
export function Card({ as = "div", compact, className, ...rest }) {
|
|
96
|
+
return h(as, { ...rest, className: cx("w-card", className), "data-size": compact ? "compact" : undefined });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------- figures ----------
|
|
100
|
+
|
|
101
|
+
const levelTone = (percent, levels) => (!levels ? undefined : percent >= levels[1] ? "danger" : percent >= levels[0] ? "warn" : undefined);
|
|
102
|
+
|
|
103
|
+
/** A bar from 0 to 100. `tone` is warn or danger. */
|
|
104
|
+
export function Meter({ percent, label, tone, className, ...rest }) {
|
|
105
|
+
return h(
|
|
106
|
+
"div",
|
|
107
|
+
{ ...rest, className: cx("w-meter", className), role: "meter", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": percent, "data-tone": tone },
|
|
108
|
+
// How full it is is data, not a style: plugin-ui.css sizes the bar from it.
|
|
109
|
+
h("span", { className: "w-meter-fill", style: { "--w-value": `${Math.max(0, Math.min(100, percent))}%` } }),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A labelled bar with its percentage and a note under it. `levels` are where it turns warn and danger. */
|
|
114
|
+
export function Gauge({ label, percent, note, tone, levels, className, ...rest }) {
|
|
115
|
+
return h(
|
|
116
|
+
"div",
|
|
117
|
+
{ ...rest, className: cx("w-gauge", className) },
|
|
118
|
+
h("span", { className: "w-gauge-label" }, label),
|
|
119
|
+
h("span", { className: "w-gauge-value" }, `${percent}%`),
|
|
120
|
+
h(Meter, { percent, label, tone: tone ?? levelTone(percent, levels) }),
|
|
121
|
+
note ? h("span", { className: "w-gauge-note" }, note) : null,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Small figures side by side, label over value. */
|
|
126
|
+
export function Stats({ items, className, ...rest }) {
|
|
127
|
+
return h(
|
|
128
|
+
"dl",
|
|
129
|
+
{ ...rest, className: cx("w-stats", className) },
|
|
130
|
+
items.map((item) => h("div", { key: item.label }, h("dt", { className: "w-stat-label" }, item.label), h("dd", { className: "w-stat-value", "data-tone": item.tone, title: item.title }, item.value))),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Label and value pairs, one per line. */
|
|
135
|
+
export function Properties({ items, className, ...rest }) {
|
|
136
|
+
return h(
|
|
137
|
+
"dl",
|
|
138
|
+
{ ...rest, className: cx("w-props", className) },
|
|
139
|
+
items.map((item) => h("div", { key: item.label, className: "w-prop" }, h("dt", { className: "w-prop-label" }, item.label), h("dd", { className: "w-prop-value" }, item.value))),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------- lists ----------
|
|
144
|
+
|
|
145
|
+
/** A list of rows. `ordered` for an `ol`, `compact` for rows with no space between them. */
|
|
146
|
+
export function List({ ordered, compact, className, ...rest }) {
|
|
147
|
+
return h(ordered ? "ol" : "ul", { ...rest, className: cx("w-list", className), "data-density": compact ? "compact" : undefined });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A row in a `List`. Holding a `ListButton`, the button is the row. */
|
|
151
|
+
export function ListItem({ align, className, ...rest }) {
|
|
152
|
+
return h("li", { ...rest, className: cx("w-list-item", className), "data-align": align });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A row that's a button, inside a `ListItem`. */
|
|
156
|
+
export function ListButton({ align, type = "button", className, ...rest }) {
|
|
157
|
+
return h("button", { ...rest, type, className: cx("w-list-button", className), "data-align": align });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------- status ----------
|
|
161
|
+
|
|
162
|
+
/** A status dot. With a `label` it's announced; otherwise it's decoration next to text that says the same. */
|
|
163
|
+
export function Dot({ tone, label, className, ...rest }) {
|
|
164
|
+
return h("span", { ...rest, className: cx("w-dot", className), "data-tone": tone, ...(label ? { role: "img", "aria-label": label } : { "aria-hidden": true }) });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A spinner. With a `label` it's announced. */
|
|
168
|
+
export function Spinner({ size, label, className, ...rest }) {
|
|
169
|
+
return h("span", { ...rest, className: cx("w-spinner", className), "data-size": size, ...(label ? { role: "img", "aria-label": label } : { "aria-hidden": true }) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A filled pill, like a label on a command. */
|
|
173
|
+
export function Pill({ tone, className, ...rest }) {
|
|
174
|
+
return h("span", { ...rest, className: cx("w-pill", className), "data-tone": tone });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** An outlined tag, like a branch name. */
|
|
178
|
+
export function Tag({ tone, className, ...rest }) {
|
|
179
|
+
return h("span", { ...rest, className: cx("w-tag", className), "data-tone": tone });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** What shows before the first data comes. */
|
|
183
|
+
export function Loading({ children = "Loading…", className, ...rest }) {
|
|
184
|
+
return h("p", { ...rest, className: cx("w-loading", className) }, children);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Nothing to show: what's missing, and a `detail` line on what to do about it. */
|
|
188
|
+
export function Empty({ title, detail, className, ...rest }) {
|
|
189
|
+
return h("div", { ...rest, className: cx("w-empty", className) }, h("p", null, title), detail === undefined ? null : h("p", { className: "w-empty-detail" }, detail));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------- buttons ----------
|
|
193
|
+
|
|
194
|
+
/** A button. `variant` is primary, ghost or danger; `size` small. */
|
|
195
|
+
export function Button({ variant, size, type = "button", className, ...rest }) {
|
|
196
|
+
return h("button", { ...rest, type, className: cx("w-button", className), "data-variant": variant, "data-size": size });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A small quiet button. With `pressed` it's a toggle. */
|
|
200
|
+
export function Chip({ pressed, type = "button", className, ...rest }) {
|
|
201
|
+
return h("button", { ...rest, type, className: cx("w-chip", className), "aria-pressed": pressed });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Chips that pick one of `options`, like a filter. `onChange` gets each click, the chip that's on too. */
|
|
205
|
+
export function Chips({ label, options, value, onChange, className, ...rest }) {
|
|
206
|
+
return h(
|
|
207
|
+
"div",
|
|
208
|
+
{ ...rest, className: cx("w-chips", className), role: "group", "aria-label": label },
|
|
209
|
+
options.map((o) => h(Chip, { key: o.value, pressed: o.value === value, title: o.title, onClick: () => onChange(o.value) }, o.label)),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Tabs over the panel they show, `children`. Arrow keys, Home and End move between tabs. */
|
|
214
|
+
export function Tabs({ label, items, value, onChange, children, className, ...rest }) {
|
|
215
|
+
const id = useId();
|
|
216
|
+
const tabId = (v) => `${id}-tab-${v}`;
|
|
217
|
+
const onKeyDown = (e) => {
|
|
218
|
+
const at = items.findIndex((t) => t.value === value);
|
|
219
|
+
const next = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: items.length - 1 }[e.key];
|
|
220
|
+
if (next === undefined) return;
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
const tab = items[(next + items.length) % items.length];
|
|
223
|
+
onChange(tab.value);
|
|
224
|
+
document.getElementById(tabId(tab.value))?.focus();
|
|
225
|
+
};
|
|
226
|
+
return h(
|
|
227
|
+
"div",
|
|
228
|
+
{ ...rest, className },
|
|
229
|
+
h(
|
|
230
|
+
"div",
|
|
231
|
+
{ className: "w-tabs", role: "tablist", "aria-label": label, onKeyDown },
|
|
232
|
+
items.map((t) =>
|
|
233
|
+
h("button", { key: t.value, type: "button", id: tabId(t.value), className: "w-tab", role: "tab", "aria-selected": t.value === value, "aria-controls": `${id}-panel`, tabIndex: t.value === value ? 0 : -1, onClick: () => onChange(t.value) }, t.label),
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
h("div", { id: `${id}-panel`, role: "tabpanel", "aria-labelledby": tabId(value) }, children),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------- fields ----------
|
|
241
|
+
|
|
242
|
+
const FieldContext = createContext(null);
|
|
243
|
+
|
|
244
|
+
/** Gives an input in a `Field` its label and hint. */
|
|
245
|
+
function useFieldProps(rest) {
|
|
246
|
+
const field = useContext(FieldContext);
|
|
247
|
+
return field ? { id: field.id, "aria-describedby": field.hint, "aria-invalid": field.invalid || undefined, ...rest } : rest;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A labelled field with a `hint` or an `error` under it, for the input in `children`. */
|
|
251
|
+
export function Field({ label, hint, error, children, className, ...rest }) {
|
|
252
|
+
const id = useId();
|
|
253
|
+
const note = error ?? hint;
|
|
254
|
+
return h(
|
|
255
|
+
"div",
|
|
256
|
+
{ ...rest, className: cx("w-field", className) },
|
|
257
|
+
h("label", { className: "w-field-label", htmlFor: id }, label),
|
|
258
|
+
h(FieldContext.Provider, { value: { id, hint: note ? `${id}-hint` : undefined, invalid: Boolean(error) } }, children),
|
|
259
|
+
note ? h("p", { id: `${id}-hint`, className: "w-field-hint", "data-tone": error ? "danger" : undefined }, note) : null,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A text input. Outside a `Field`, give it a `label`. */
|
|
264
|
+
export function TextInput({ label, type = "text", className, ...rest }) {
|
|
265
|
+
return h("input", { "aria-label": label, ...useFieldProps(rest), type, className: cx("w-input", className) });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A search box. `label` names it for screen readers. */
|
|
269
|
+
export function SearchField({ label, className, ...rest }) {
|
|
270
|
+
return h("input", { "aria-label": label, ...useFieldProps(rest), type: "search", className: cx("w-search", className) });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A select of `options`. `onChange` gets the value picked. */
|
|
274
|
+
export function Select({ label, options, onChange, className, ...rest }) {
|
|
275
|
+
return h(
|
|
276
|
+
"select",
|
|
277
|
+
{ "aria-label": label, ...useFieldProps(rest), className: cx("w-select", className), onChange: (e) => onChange?.(e.currentTarget.value) },
|
|
278
|
+
options.map((o) => h("option", { key: o.value, value: o.value, disabled: o.disabled }, o.label)),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A checkbox with its label, `children`. `onChange` gets whether it's checked. */
|
|
283
|
+
export function Checkbox({ onChange, children, className, ...rest }) {
|
|
284
|
+
return h("label", { className: cx("w-check", className) }, h("input", { ...rest, type: "checkbox", onChange: (e) => onChange?.(e.currentTarget.checked) }), children);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** More behind a `summary` line that opens it. */
|
|
288
|
+
export function Disclosure({ summary, children, className, ...rest }) {
|
|
289
|
+
return h("details", { ...rest, className: cx("w-disclosure", className) }, h("summary", null, summary), children);
|
|
290
|
+
}
|
package/wings.d.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Types for the Wings plugin SDK, API 1. `window.wings` exists in every plugin; this file only helps your editor.
|
|
2
|
+
// In TypeScript, add "wings-plugin-sdk" to `types` in tsconfig.json. In JavaScript, start a file with
|
|
3
|
+
// `// @ts-check` and `/// <reference types="wings-plugin-sdk" />`.
|
|
4
|
+
|
|
5
|
+
type Tone = "neutral" | "info" | "success" | "warning" | "danger" | "merged";
|
|
6
|
+
|
|
7
|
+
interface WingsPane {
|
|
8
|
+
paneId: string;
|
|
9
|
+
/** Follows `cd` in the pane. */
|
|
10
|
+
cwd: string;
|
|
11
|
+
/** What runs in the pane right now, like `zsh` or `claude`. */
|
|
12
|
+
command: string;
|
|
13
|
+
project: string;
|
|
14
|
+
/** Set while Claude Code runs in the pane. `startedAt` is when that `claude` started, in ms; a resumed session's transcript also has what came before. */
|
|
15
|
+
session: { sessionId: string; name: string | null; state: string; startedAt: number } | null;
|
|
16
|
+
/** The focused pane of the tab on screen. At most one pane has it, and none while no tab is open. */
|
|
17
|
+
focused: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WingsTranscriptEvent {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
/** The pane the session runs in. */
|
|
23
|
+
paneId: string;
|
|
24
|
+
/** The entry as Claude Code wrote it. Its `type` is one of `permissions.transcript`. */
|
|
25
|
+
entry: { type: string } & Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface WingsBadge {
|
|
29
|
+
label: string;
|
|
30
|
+
tone: Tone;
|
|
31
|
+
icon?: "pr-open" | "pr-merged" | "pr-closed" | "pr-draft";
|
|
32
|
+
/** Small icon-and-number chips after the label. Leave out zeros. */
|
|
33
|
+
counts?: { icon: "changed" | "push" | "pull"; value: number }[];
|
|
34
|
+
/** Shows a spinner in the pill. */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
title?: string;
|
|
37
|
+
subtitle?: string;
|
|
38
|
+
rows?: { label: string; value: string; tone?: Tone }[];
|
|
39
|
+
actions?: { id: string; label: string; primary?: boolean }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface WingsComment {
|
|
43
|
+
id: number;
|
|
44
|
+
replyTo: number | null;
|
|
45
|
+
/** `null` for a comment on the whole change. */
|
|
46
|
+
path: string | null;
|
|
47
|
+
/** `null` when the comment no longer matches the diff. */
|
|
48
|
+
line: number | null;
|
|
49
|
+
side: "additions" | "deletions";
|
|
50
|
+
author: string;
|
|
51
|
+
body: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
url: string;
|
|
54
|
+
/** On the first comment of a thread: the viewer offers a reply box, and sends replies to `onReply`. */
|
|
55
|
+
canReply?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A reply typed in the diff viewer. `replyTo` is the id of the thread's first comment. */
|
|
59
|
+
interface WingsReply {
|
|
60
|
+
diffId: string;
|
|
61
|
+
replyTo: number;
|
|
62
|
+
body: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A usage limit window. `resetsAt` is in Unix epoch seconds. */
|
|
66
|
+
interface WingsLimitWindow {
|
|
67
|
+
usedPercentage: number;
|
|
68
|
+
resetsAt: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What Claude Code last told `wings statusline`. `at` is when Wings got it, in ms. */
|
|
72
|
+
interface WingsStatusline {
|
|
73
|
+
/** Your account's 5-hour and weekly limits, from the newest session that reported them. `null` until one has. */
|
|
74
|
+
rateLimits: { fiveHour: WingsLimitWindow | null; sevenDay: WingsLimitWindow | null; at: number } | null;
|
|
75
|
+
/** By session id. Token counts are from the session's last API response. */
|
|
76
|
+
sessions: Record<
|
|
77
|
+
string,
|
|
78
|
+
{
|
|
79
|
+
/** Like `Opus 5.5 (1M context)`. */
|
|
80
|
+
model: string | null;
|
|
81
|
+
contextWindowSize: number | null;
|
|
82
|
+
usedPercentage: number | null;
|
|
83
|
+
totalInputTokens: number | null;
|
|
84
|
+
totalOutputTokens: number | null;
|
|
85
|
+
currentUsage: { inputTokens: number; outputTokens: number; cacheCreationInputTokens: number; cacheReadInputTokens: number } | null;
|
|
86
|
+
/** `expiresAt` is in Unix epoch seconds. */
|
|
87
|
+
promptCache: { warm: boolean; ttl: "5m" | "1h" | null; expiresAt: number | null } | null;
|
|
88
|
+
at: number;
|
|
89
|
+
}
|
|
90
|
+
>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface WingsExecOptions {
|
|
94
|
+
cwd?: string;
|
|
95
|
+
/** 30 s by default, 5 min at most. */
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
/** Gets each stdout and stderr line while the program runs. */
|
|
98
|
+
onOutput?: (line: string) => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface Wings {
|
|
102
|
+
// Every `on*` call returns a function that removes the listener, for cleaning up in a React effect.
|
|
103
|
+
/** Every terminal pane, on every change. */
|
|
104
|
+
onPanes(listener: (panes: WingsPane[]) => void): () => void;
|
|
105
|
+
/**
|
|
106
|
+
* A badge action was clicked. Return a promise: the button spins until it settles (up to 5 min), and a thrown
|
|
107
|
+
* error shows in the card. What it resolves to is ignored.
|
|
108
|
+
*/
|
|
109
|
+
onAction(listener: (action: { paneId: string; actionId: string }) => unknown): () => void;
|
|
110
|
+
/**
|
|
111
|
+
* Handles Claude's calls to a tool in `contributes.mcpTools`. Return a string or a JSON value; a thrown error
|
|
112
|
+
* is sent back as a failed call. `paneId` is the Wings pane Claude runs in, or `null`. Register it in `main`.
|
|
113
|
+
*/
|
|
114
|
+
onTool(name: string, handler: (input: Record<string, unknown>, context: { paneId: string | null }) => unknown): void;
|
|
115
|
+
/** Runs a command from `permissions.exec`, like `git branch --show-current`. */
|
|
116
|
+
exec(program: string, args?: string[], options?: WingsExecOptions): Promise<{ code: number | null; stdout: string; stderr: string }>;
|
|
117
|
+
/**
|
|
118
|
+
* Needs `permissions.statusline`. What Claude Code last told its status line, once `wings statusline` is your
|
|
119
|
+
* `statusLine` command: usage limits and each session's context and cache, as Claude Code reports them.
|
|
120
|
+
*/
|
|
121
|
+
statusline(): Promise<WingsStatusline>;
|
|
122
|
+
/** Claude Code transcript entries of types in `permissions.transcript`, oldest first. */
|
|
123
|
+
transcript(sessionId: string, types: string[], options?: { last?: number }): Promise<Record<string, unknown>[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Each transcript entry Claude Code writes while it runs in a pane, for the types in `permissions.transcript`.
|
|
126
|
+
* Only entries written after the plugin started, or after the session started in the pane; `transcript` reads
|
|
127
|
+
* earlier ones. Entries over 256 KB are skipped.
|
|
128
|
+
*/
|
|
129
|
+
onTranscript(listener: (event: WingsTranscriptEvent) => void): () => void;
|
|
130
|
+
/** Needs `"badges"` in `contributes.ui`. `null` removes it. */
|
|
131
|
+
setBadge(paneId: string, badge: WingsBadge | null): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Needs `permissions.ports`. The local TCP ports the programs in your panes listen on, that `localhost`
|
|
134
|
+
* reaches. About 20 ms a call, so poll slowly.
|
|
135
|
+
*/
|
|
136
|
+
ports(): Promise<{ paneId: string; pid: number; name: string; port: number; startedAt: number }[]>;
|
|
137
|
+
/** Opens an https URL, or an http one to localhost, 127.0.0.1 or [::1], that starts with a prefix in `permissions.openUrl`. */
|
|
138
|
+
openUrl(url: string): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Needs `permissions.panes`. Opens a terminal in a new tab, or a split beside the focused pane, and resolves
|
|
141
|
+
* once the pane is in `onPanes`. `command`, like `"npm run dev"`, is typed into its shell and must start with
|
|
142
|
+
* an entry of `permissions.panes`. `cwd` must be inside one of your projects; without it the pane opens at the
|
|
143
|
+
* root of the project on screen.
|
|
144
|
+
*/
|
|
145
|
+
openPane(options?: { command?: string; cwd?: string; placement?: "tab" | "right" | "down" }): Promise<{ paneId: string }>;
|
|
146
|
+
/** Needs `permissions.panes`. Shows an open pane and moves the keyboard to it. */
|
|
147
|
+
focusPane(paneId: string): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* After `exec` rejected with "<program> is not installed or not on PATH": Wings asks you, installs it in a new tab
|
|
150
|
+
* and restarts the plugin once it's there. Only `az`, `gh` and `glab`, and only ones `permissions.exec` names.
|
|
151
|
+
* Resolves true once the install started (or it's already installed), false if you cancelled.
|
|
152
|
+
*/
|
|
153
|
+
installTool(program: "az" | "gh" | "glab"): Promise<boolean>;
|
|
154
|
+
/** Needs `permissions.notify`. A desktop notification, up to 3 a minute. The title is cut at 64 characters, the body at 256. */
|
|
155
|
+
notify(notification: { title: string; body?: string }): Promise<void>;
|
|
156
|
+
/** Needs `"diff"` in `contributes.ui`. Leave out `patch` to open it loading, then call `updateDiff`. */
|
|
157
|
+
openDiff(diff: { title: string; subtitle?: string; patch?: string; comments?: WingsComment[] }): Promise<{ id: string }>;
|
|
158
|
+
/** Updates a viewer opened with `openDiff`; ignored once it's closed. `{ comments }` alone keeps the patch, like after a reply. */
|
|
159
|
+
updateDiff(id: string, update: { patch: string; comments?: WingsComment[] } | { comments: WingsComment[] } | { error: string }): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Needs `"diff"` in `contributes.ui`. A reply typed under a comment with `canReply` in the diff viewer. Post it,
|
|
162
|
+
* then add it with `updateDiff`. The reply box waits for the returned promise, and a thrown error shows under it
|
|
163
|
+
* with the text kept. One handler: a second call replaces the first.
|
|
164
|
+
*/
|
|
165
|
+
onReply(handler: (reply: WingsReply) => unknown): void;
|
|
166
|
+
/**
|
|
167
|
+
* POSTs `fields` (string or whole-number values, up to 64 KB) as JSON to a `permissions.post` path, as you:
|
|
168
|
+
* `gh api`, `glab api` against `host`, or `az rest` for Azure DevOps. Wings builds the command itself.
|
|
169
|
+
*/
|
|
170
|
+
post(
|
|
171
|
+
program: "gh" | "glab" | "az",
|
|
172
|
+
url: string,
|
|
173
|
+
fields: Record<string, string | number>,
|
|
174
|
+
options?: { host?: string },
|
|
175
|
+
): Promise<{ code: number | null; stdout: string; stderr: string }>;
|
|
176
|
+
/**
|
|
177
|
+
* An HTTP request to a URL under `permissions.fetch`, made by Wings. `bearer` names a secret that Wings sends
|
|
178
|
+
* as `Authorization: Bearer <secret>`. Resolves for any status; redirects aren't followed.
|
|
179
|
+
*/
|
|
180
|
+
fetch(
|
|
181
|
+
url: string,
|
|
182
|
+
init?: { method?: string; headers?: Record<string, string>; body?: string; bearer?: string },
|
|
183
|
+
): Promise<{ status: number; contentType: string | null; body: string }>;
|
|
184
|
+
/** Secrets, like API tokens, in the system keychain. `fetch` can send them, but they can't be read back. */
|
|
185
|
+
secrets: {
|
|
186
|
+
set(name: string, value: string): Promise<void>;
|
|
187
|
+
delete(name: string): Promise<void>;
|
|
188
|
+
has(name: string): Promise<boolean>;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The plugin's own storage for settings and state, since `localStorage` throws in plugin pages. Values are
|
|
192
|
+
* JSON. Keys are 1 to 128 letters, digits and `- _ . : /`, and it holds 1 MB in all. Only this plugin sees it.
|
|
193
|
+
*/
|
|
194
|
+
storage: {
|
|
195
|
+
/** The value, or `null` when the key isn't set. */
|
|
196
|
+
get(key: string): Promise<unknown>;
|
|
197
|
+
/** Refused if it would take the plugin's storage over 1 MB. */
|
|
198
|
+
set(key: string, value: unknown): Promise<void>;
|
|
199
|
+
delete(key: string): Promise<void>;
|
|
200
|
+
/** Every key, sorted. */
|
|
201
|
+
keys(): Promise<string[]>;
|
|
202
|
+
};
|
|
203
|
+
/** Short text next to a sidebar's or view's title bar button, like a running timer. `null` clears it. */
|
|
204
|
+
setSidebarLabel(sidebarId: string, label: string | null, options?: { rows?: { label: string; value: string }[]; tone?: "warning" | "danger" }): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Opens one of `contributes.sidebars` or `contributes.views` by id: in the right sidebar, in a modal over Wings,
|
|
207
|
+
* in a window of its own or as a tab in the project on screen (`tab` needs Wings 0.1.15). `as` must be in the
|
|
208
|
+
* view's `opens`, and defaults to the first. A window or tab that's already open comes to the front. The page
|
|
209
|
+
* gets the same `wings` object wherever it opens.
|
|
210
|
+
*/
|
|
211
|
+
openView(id: string, options?: { as?: "sidebar" | "modal" | "window" | "tab" }): Promise<void>;
|
|
212
|
+
/** Closes a sidebar or view wherever it's open. */
|
|
213
|
+
closeView(id: string): Promise<void>;
|
|
214
|
+
/** Opens Wings Settings at the plugin's settings page, `contributes.settings`. Wings 0.1.15 and newer. */
|
|
215
|
+
openSettings(): Promise<void>;
|
|
216
|
+
/** Sends a JSON value, up to 64 KB, to the plugin's other pages: its main script and open sidebars, modals and windows. */
|
|
217
|
+
broadcast(message: unknown): Promise<void>;
|
|
218
|
+
/** Gets what the plugin's other pages send with `broadcast`. */
|
|
219
|
+
onBroadcast(listener: (message: unknown) => void): () => void;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
declare const wings: Wings;
|
|
223
|
+
|
|
224
|
+
/** A page's own CSS, `import "./sidebar.css"`: wings-plugin build bundles it into the page's `<name>.css`. */
|
|
225
|
+
declare module "*.css";
|