@joe-broadhead/opencode-gws 0.0.0-stage → 0.0.1
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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/NOTICE +31 -0
- package/README.md +130 -2
- package/dist/index.js +11783 -0
- package/dist/tui.js +589 -0
- package/manifest/generated.json +1 -0
- package/package.json +179 -4
- package/skills/opencode-gws/SKILL.md +126 -0
- package/skills/opencode-gws-admin/SKILL.md +97 -0
- package/skills/opencode-gws-recipes/SKILL.md +261 -0
- package/views/gmail-thread.html +193 -0
- package/views/jelly-view.d.ts +73 -0
- package/views/jelly-view.js +87 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of `jelly-view.js`, for a page written or checked with TypeScript:
|
|
3
|
+
* `/// <reference path="./jelly-view.d.ts" />`. They mirror the bridge envelopes
|
|
4
|
+
* in Jelly's contracts (`protocol/contracts/src/view-bridge.ts`); the page itself
|
|
5
|
+
* carries only the JavaScript.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type JellyViewPlace = "tool-result" | "home-widget" | "settings-section";
|
|
9
|
+
type JellyViewGrant = "data" | "state" | "rpc" | "open-agent";
|
|
10
|
+
|
|
11
|
+
/** The fixed theme token set; the helper sets each as `--jelly-<name>` on the page. */
|
|
12
|
+
interface JellyViewTheme {
|
|
13
|
+
text: string;
|
|
14
|
+
muted: string;
|
|
15
|
+
surface: string;
|
|
16
|
+
raised: string;
|
|
17
|
+
border: string;
|
|
18
|
+
accent: string;
|
|
19
|
+
"on-accent": string;
|
|
20
|
+
danger: string;
|
|
21
|
+
font: string;
|
|
22
|
+
radius: string;
|
|
23
|
+
scheme: "light" | "dark";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JellyViewHello {
|
|
27
|
+
v: 1;
|
|
28
|
+
kind: "hello";
|
|
29
|
+
/** `package` is null for a Home app an agent made. */
|
|
30
|
+
view: { package: string | null; id: string; title: string; place: JellyViewPlace };
|
|
31
|
+
grants: JellyViewGrant[];
|
|
32
|
+
/** The methods `call` may name. */
|
|
33
|
+
rpc: string[];
|
|
34
|
+
theme: JellyViewTheme;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A view's state: JSON of at most 64 KB, and the revision a write must name. */
|
|
38
|
+
interface JellyViewState {
|
|
39
|
+
revision: number;
|
|
40
|
+
data: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type JellyViewPush =
|
|
44
|
+
| { v: 1; kind: "data.changed"; data: unknown }
|
|
45
|
+
| ({ v: 1; kind: "state.changed" } & JellyViewState)
|
|
46
|
+
| { v: 1; kind: "theme.changed"; theme: JellyViewTheme }
|
|
47
|
+
| { v: 1; kind: "visibility"; visible: boolean }
|
|
48
|
+
/** The call with request id `re` waits for the person, at most until `until` (ISO time). */
|
|
49
|
+
| { v: 1; kind: "rpc.waiting"; re: string; until: string };
|
|
50
|
+
|
|
51
|
+
interface JellyView {
|
|
52
|
+
hello: JellyViewHello;
|
|
53
|
+
/** Hear pushes; returns the way to stop. */
|
|
54
|
+
on(listener: (push: JellyViewPush) => void): () => void;
|
|
55
|
+
/** Any request by its op, for one the helper has no name for. */
|
|
56
|
+
request(op: string, fields?: Record<string, unknown>): Promise<unknown>;
|
|
57
|
+
/** The place's data (the `data` grant). */
|
|
58
|
+
data(): Promise<unknown>;
|
|
59
|
+
/** The view's state (the `state` grant). */
|
|
60
|
+
state(): Promise<JellyViewState>;
|
|
61
|
+
/** Replace the state if `revision` is still the stored one; rejects on a conflict. */
|
|
62
|
+
setState(revision: number, data: unknown): Promise<JellyViewState>;
|
|
63
|
+
/** One of the methods the hello lists (the `rpc` grant). */
|
|
64
|
+
call(method: string, input?: unknown): Promise<unknown>;
|
|
65
|
+
/** Start a message for the person to send; it is never sent (the `open-agent` grant). */
|
|
66
|
+
openAgent(prefill?: string): Promise<null>;
|
|
67
|
+
/** The height the page needs, in CSS pixels, up to 1200: a hint the app may draw shorter. */
|
|
68
|
+
resize(height: number): Promise<null>;
|
|
69
|
+
/** Not mine: the app closes the frame and keeps its ordinary look. */
|
|
70
|
+
decline(): Promise<null>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare function jellyView(): Promise<JellyView>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// jelly-view.js: a client of Jelly's view bridge. Paste it into the page's own <script>:
|
|
2
|
+
// a sealed page loads no script by URL and is given no global. Call jellyView() as the
|
|
3
|
+
// script starts; it resolves with the client when the app says hello.
|
|
4
|
+
function jellyView() {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
addEventListener("message", function hello(event) {
|
|
7
|
+
const said = event.data;
|
|
8
|
+
if (event.source !== parent || said?.v !== 1 || said.kind !== "hello" || !event.ports[0])
|
|
9
|
+
return;
|
|
10
|
+
removeEventListener("message", hello);
|
|
11
|
+
const port = event.ports[0];
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const latest = new Map();
|
|
14
|
+
const queue = [];
|
|
15
|
+
let sequence = 0;
|
|
16
|
+
let current;
|
|
17
|
+
const theme = (tokens) => {
|
|
18
|
+
const style = document.documentElement.style;
|
|
19
|
+
for (const [name, value] of Object.entries(tokens))
|
|
20
|
+
if (name === "scheme") style.colorScheme = value;
|
|
21
|
+
else style.setProperty(`--jelly-${name}`, value);
|
|
22
|
+
};
|
|
23
|
+
// The app answers one request at a time, so requests go in turn.
|
|
24
|
+
const next = () => {
|
|
25
|
+
if (current !== undefined || queue.length === 0) return;
|
|
26
|
+
current = queue.shift();
|
|
27
|
+
current.timer = setTimeout(
|
|
28
|
+
() => settle({ ok: false, error: "No answer from the app" }),
|
|
29
|
+
15000,
|
|
30
|
+
);
|
|
31
|
+
port.postMessage(current.message);
|
|
32
|
+
};
|
|
33
|
+
const settle = (answer) => {
|
|
34
|
+
const done = current;
|
|
35
|
+
current = undefined;
|
|
36
|
+
clearTimeout(done.timer);
|
|
37
|
+
if (answer.ok) done.resolve(answer.value);
|
|
38
|
+
else done.reject(new Error(answer.error));
|
|
39
|
+
next();
|
|
40
|
+
};
|
|
41
|
+
port.onmessage = ({ data }) => {
|
|
42
|
+
if (data?.v !== 1) return;
|
|
43
|
+
if (typeof data.kind !== "string") {
|
|
44
|
+
if (current !== undefined && data.re === current.message.id) settle(data);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// A call waiting for the person is answered by `until`, not in 15 s.
|
|
48
|
+
if (data.kind === "rpc.waiting" && data.re === current?.message.id) {
|
|
49
|
+
clearTimeout(current.timer);
|
|
50
|
+
current.timer = setTimeout(
|
|
51
|
+
() => settle({ ok: false, error: "No answer from the app" }),
|
|
52
|
+
Math.max(0, Date.parse(data.until) - Date.now()) + 15000,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (data.kind === "theme.changed") theme(data.theme);
|
|
56
|
+
// A wait is news only while it lasts, so a later listener is not told of it.
|
|
57
|
+
if (data.kind !== "rpc.waiting") latest.set(data.kind, data);
|
|
58
|
+
for (const listener of listeners) listener(data);
|
|
59
|
+
};
|
|
60
|
+
port.start();
|
|
61
|
+
theme(said.theme);
|
|
62
|
+
const request = (op, fields = {}) =>
|
|
63
|
+
new Promise((resolve, reject) => {
|
|
64
|
+
queue.push({ resolve, reject, message: { ...fields, v: 1, id: String(++sequence), op } });
|
|
65
|
+
next();
|
|
66
|
+
});
|
|
67
|
+
resolve({
|
|
68
|
+
hello: said,
|
|
69
|
+
// A listener first hears the latest push of each kind that came before it.
|
|
70
|
+
on(listener) {
|
|
71
|
+
for (const push of latest.values()) listener(push);
|
|
72
|
+
listeners.add(listener);
|
|
73
|
+
return () => listeners.delete(listener);
|
|
74
|
+
},
|
|
75
|
+
request,
|
|
76
|
+
data: () => request("data.get"),
|
|
77
|
+
state: () => request("state.get"),
|
|
78
|
+
setState: (revision, data) => request("state.set", { revision, data }),
|
|
79
|
+
call: (method, input = {}) => request("rpc.call", { method, input }),
|
|
80
|
+
openAgent: (prefill) => request("open-agent", prefill === undefined ? {} : { prefill }),
|
|
81
|
+
resize: (height) =>
|
|
82
|
+
request("resize", { height: Math.min(1200, Math.max(1, Math.ceil(height))) }),
|
|
83
|
+
decline: () => request("decline"),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|