ocuclaw 1.3.0 → 1.3.2
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/README.md +3 -1
- package/dist/config/runtime-config-session-title-model.test.js +22 -0
- package/dist/config/runtime-config.js +24 -15
- package/dist/domain/debug-store.js +18 -0
- package/dist/domain/glasses-display-system-prompt.js +52 -0
- package/dist/domain/glasses-display-system-prompt.test.js +44 -0
- package/dist/domain/glasses-ui-system-prompt.js +6 -22
- package/dist/domain/glasses-ui-system-prompt.test.js +13 -0
- package/dist/domain/prompt-channel-fragments.js +32 -0
- package/dist/domain/prompt-channel-fragments.test.js +70 -0
- package/dist/gateway/gateway-timing-ledger.js +15 -3
- package/dist/gateway/openclaw-client.js +80 -3
- package/dist/index.js +22 -0
- package/dist/runtime/channel-two-hook.js +36 -0
- package/dist/runtime/container-env.js +41 -0
- package/dist/runtime/display-toggle-states.js +98 -0
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/register-session-title-distiller.js +100 -0
- package/dist/runtime/relay-core.js +307 -68
- package/dist/runtime/relay-service.js +120 -13
- package/dist/runtime/relay-worker-entry.js +26 -0
- package/dist/runtime/relay-worker-protocol.js +0 -4
- package/dist/runtime/relay-worker-supervisor.js +43 -79
- package/dist/runtime/relay-worker-transport.js +41 -0
- package/dist/runtime/session-service.js +159 -15
- package/dist/runtime/session-title-distiller-budget.js +36 -0
- package/dist/runtime/session-title-distiller-helpers.js +130 -0
- package/dist/runtime/session-title-distiller.js +354 -0
- package/dist/runtime/session-title-record.js +21 -0
- package/dist/runtime/stable-prompt-snapshot.js +119 -0
- package/dist/tools/glasses-ui-cron.js +9 -3
- package/dist/tools/glasses-ui-paint-floor.js +10 -3
- package/dist/tools/glasses-ui-recipes.js +13 -178
- package/dist/tools/glasses-ui-surfaces.js +8 -1
- package/dist/tools/glasses-ui-tool-description.test.js +16 -0
- package/dist/tools/glasses-ui-tool.js +98 -60
- package/dist/tools/session-title-tool.js +14 -76
- package/dist/tools/session-title-tool.test.js +53 -0
- package/dist/version.js +2 -2
- package/openclaw.plugin.json +9 -0
- package/package.json +6 -4
- package/skills/glasses-ui/SKILL.md +163 -0
- package/dist/runtime/downstream-server.js +0 -2057
- package/dist/runtime/plugin-update-service.js +0 -216
|
@@ -51,21 +51,12 @@ function isEvenAiDedicatedKey(sessionKey) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function gateReason(sessionKey, deps) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
) {
|
|
61
|
-
return "feature_disabled";
|
|
62
|
-
}
|
|
63
|
-
// Hard guard against the agent titling a session before the user has sent
|
|
64
|
-
// their first real message. The synthetic session-starter prompt the agent
|
|
65
|
-
// sees on /new + the proactive prompt-hook nudge can otherwise tempt the
|
|
66
|
-
// model into titling from a non-user input (observed: titles like "New
|
|
67
|
-
// session"). Real user sends are recorded via dispatchOcuClawUserSend ->
|
|
68
|
-
// sessionService.recordFirstSentUserMessage; the synthetic starter never is.
|
|
54
|
+
// Explicit-rename-only: the Neural Session Names toggle governs AUTOMATIC
|
|
55
|
+
// titling (the distiller), not user-requested renames, so feature_disabled is
|
|
56
|
+
// gone. session_user_locked is gone too — a user who already named a session
|
|
57
|
+
// must be able to rename it again (the lock only blocks the distiller).
|
|
58
|
+
// The structural no_active_session / EvenAI-renamable guards live in the
|
|
59
|
+
// handler body. Only the no-user-message guard remains here.
|
|
69
60
|
if (
|
|
70
61
|
typeof deps.hasRecordedUserMessage === "function" &&
|
|
71
62
|
!deps.hasRecordedUserMessage(sessionKey)
|
|
@@ -104,7 +95,9 @@ export function createSessionTitleToolHandler(deps) {
|
|
|
104
95
|
err.code = blockedReason;
|
|
105
96
|
throw err;
|
|
106
97
|
}
|
|
107
|
-
const result = await deps.setSessionTitle(sessionKey, validation.spec.title
|
|
98
|
+
const result = await deps.setSessionTitle(sessionKey, validation.spec.title, {
|
|
99
|
+
origin: "user_tool",
|
|
100
|
+
});
|
|
108
101
|
if (result && result.ok === false) {
|
|
109
102
|
const err = new Error(`${result.code}: ${result.message || "set rejected"}`);
|
|
110
103
|
err.code = result.code;
|
|
@@ -115,60 +108,15 @@ export function createSessionTitleToolHandler(deps) {
|
|
|
115
108
|
return { setSessionTitle };
|
|
116
109
|
}
|
|
117
110
|
|
|
118
|
-
const TOOL_DESCRIPTION = [
|
|
119
|
-
"
|
|
111
|
+
export const TOOL_DESCRIPTION = [
|
|
112
|
+
"Rename the current chat session (shown in the user's glasses session list).",
|
|
120
113
|
"",
|
|
121
|
-
"
|
|
114
|
+
"Call ONLY when the user explicitly asks to rename or retitle the session.",
|
|
115
|
+
"Automatic titling is handled elsewhere — do not call this proactively.",
|
|
122
116
|
"",
|
|
123
|
-
"Title: 2-5 word noun phrase, ≤55 chars, no trailing punctuation or
|
|
124
|
-
"",
|
|
125
|
-
"Do not announce the rename unless the user explicitly asked to retitle.",
|
|
117
|
+
"Title: 2-5 word noun phrase, ≤55 chars, no trailing punctuation or quotes.",
|
|
126
118
|
].join("\n");
|
|
127
119
|
|
|
128
|
-
export function createSessionTitlePromptHook(deps) {
|
|
129
|
-
return function sessionTitleBeforePromptBuild(_event, ctx) {
|
|
130
|
-
const sessionKey = ctx && typeof ctx.sessionKey === "string" ? ctx.sessionKey : null;
|
|
131
|
-
if (!sessionKey) return undefined;
|
|
132
|
-
const title = typeof deps.getSessionTitle === "function" ? deps.getSessionTitle(sessionKey) : null;
|
|
133
|
-
const userLocked =
|
|
134
|
-
typeof deps.isSessionUserLocked === "function" && deps.isSessionUserLocked(sessionKey);
|
|
135
|
-
const featureEnabled =
|
|
136
|
-
typeof deps.isNeuralSessionNamesEnabled === "function"
|
|
137
|
-
? deps.isNeuralSessionNamesEnabled(sessionKey)
|
|
138
|
-
: true;
|
|
139
|
-
|
|
140
|
-
const fragments = [];
|
|
141
|
-
if (title) {
|
|
142
|
-
fragments.push(`Current session title: "${title}".`);
|
|
143
|
-
}
|
|
144
|
-
if (userLocked) {
|
|
145
|
-
fragments.push(
|
|
146
|
-
"The user has set a custom title; do not call set_session_title.",
|
|
147
|
-
);
|
|
148
|
-
} else if (!featureEnabled) {
|
|
149
|
-
fragments.push(
|
|
150
|
-
"Neural Topic Distiller is disabled; do not call set_session_title.",
|
|
151
|
-
);
|
|
152
|
-
} else if (title) {
|
|
153
|
-
fragments.push(
|
|
154
|
-
"Call set_session_title only if the topic has clearly shifted.",
|
|
155
|
-
);
|
|
156
|
-
} else {
|
|
157
|
-
const hasUserMessage =
|
|
158
|
-
typeof deps.hasRecordedUserMessage !== "function" ||
|
|
159
|
-
deps.hasRecordedUserMessage(sessionKey);
|
|
160
|
-
if (hasUserMessage) {
|
|
161
|
-
fragments.push(
|
|
162
|
-
"No session title is set yet. Call set_session_title now if the user's latest message names any concrete topic.",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (fragments.length === 0) return undefined;
|
|
168
|
-
return { appendSystemContext: fragments.join(" ") };
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
120
|
export function registerSessionTitleTool(api, service) {
|
|
173
121
|
if (!api || typeof api.registerTool !== "function") {
|
|
174
122
|
throw new Error("registerSessionTitleTool requires api.registerTool");
|
|
@@ -196,14 +144,4 @@ export function registerSessionTitleTool(api, service) {
|
|
|
196
144
|
};
|
|
197
145
|
},
|
|
198
146
|
});
|
|
199
|
-
|
|
200
|
-
if (typeof api.on === "function") {
|
|
201
|
-
const hook = createSessionTitlePromptHook({
|
|
202
|
-
getSessionTitle: (sessionKey) => service.getSessionTitle(sessionKey),
|
|
203
|
-
isSessionUserLocked: (sessionKey) => service.isSessionUserLocked(sessionKey),
|
|
204
|
-
isNeuralSessionNamesEnabled: (sessionKey) => service.isNeuralSessionNamesEnabled(sessionKey),
|
|
205
|
-
hasRecordedUserMessage: (sessionKey) => service.hasRecordedUserMessage(sessionKey),
|
|
206
|
-
});
|
|
207
|
-
api.on("before_prompt_build", hook);
|
|
208
|
-
}
|
|
209
147
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSessionTitleToolHandler, TOOL_DESCRIPTION } from "./session-title-tool.ts";
|
|
4
|
+
|
|
5
|
+
function deps(over = {}) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
return {
|
|
8
|
+
calls,
|
|
9
|
+
peekSessionKey: () => "ocuclaw:123",
|
|
10
|
+
setSessionTitle: (k, t, o) => { calls.push({ k, t, o }); return { ok: true }; },
|
|
11
|
+
...over,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("description is explicit-rename-only", () => {
|
|
16
|
+
assert.match(TOOL_DESCRIPTION, /explicitly asks to rename|user explicitly/i);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("explicit rename passes origin user_tool", async () => {
|
|
20
|
+
const d = deps();
|
|
21
|
+
const h = createSessionTitleToolHandler(d);
|
|
22
|
+
await h.setSessionTitle({ title: "Trip Planning" });
|
|
23
|
+
assert.equal(d.calls[0].o.origin, "user_tool");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a user-locked session can STILL be renamed via the tool", async () => {
|
|
27
|
+
const d = deps({
|
|
28
|
+
setSessionTitle: (k, t, o) => {
|
|
29
|
+
// service-layer would allow user_tool over a lock; tool must not pre-block
|
|
30
|
+
return { ok: true };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const h = createSessionTitleToolHandler(d);
|
|
34
|
+
const r = await h.setSessionTitle({ title: "New Name" });
|
|
35
|
+
assert.deepEqual(r, { ok: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("feature-disabled does NOT block an explicit rename", async () => {
|
|
39
|
+
const d = deps({ isNeuralSessionNamesEnabled: () => false });
|
|
40
|
+
const h = createSessionTitleToolHandler(d);
|
|
41
|
+
const r = await h.setSessionTitle({ title: "Anything" });
|
|
42
|
+
assert.deepEqual(r, { ok: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("still rejects an empty title", async () => {
|
|
46
|
+
const h = createSessionTitleToolHandler(deps());
|
|
47
|
+
await assert.rejects(() => h.setSessionTitle({ title: " " }), /title_empty/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("still rejects when no active session", async () => {
|
|
51
|
+
const h = createSessionTitleToolHandler(deps({ peekSessionKey: () => "" }));
|
|
52
|
+
await assert.rejects(() => h.setSessionTitle({ title: "X" }), /no_active_session/);
|
|
53
|
+
});
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "1.3.
|
|
2
|
-
export const REQUIRES_CLIENT_VERSION = "1.3.
|
|
1
|
+
export const PLUGIN_VERSION = "1.3.2";
|
|
2
|
+
export const REQUIRES_CLIENT_VERSION = "1.3.2";
|
package/openclaw.plugin.json
CHANGED
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"help": "Optional extra system prompt appended to Even AI runs only.",
|
|
44
44
|
"advanced": true
|
|
45
45
|
},
|
|
46
|
+
"sessionTitleModel": {
|
|
47
|
+
"label": "Session-title model",
|
|
48
|
+
"help": "Optional model override (\"provider/model\") for the background session-title distiller. Leave blank to use your normal model.",
|
|
49
|
+
"advanced": true
|
|
50
|
+
},
|
|
46
51
|
"evenAiRoutingMode": {
|
|
47
52
|
"label": "Even AI routing mode",
|
|
48
53
|
"help": "active = current session; background = dedicated background session; background_new = fresh background session per request.",
|
|
@@ -226,6 +231,10 @@
|
|
|
226
231
|
"maximum": 8000,
|
|
227
232
|
"default": 5000,
|
|
228
233
|
"description": "How long (ms) a fresh agent summary stays preferred over a tool label in the glasses activity status. Clamped 3000-8000."
|
|
234
|
+
},
|
|
235
|
+
"sessionTitleModel": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Optional model override (\"provider/model\") for the background session-title distiller. When absent, the user's normal model is used."
|
|
229
238
|
}
|
|
230
239
|
},
|
|
231
240
|
"if": {
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocuclaw",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"requiresClientVersion": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"requiresClientVersion": "1.3.2",
|
|
5
5
|
"description": "OcuClaw for Even Realities G2 smart glasses.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"README.md",
|
|
10
10
|
"dist/",
|
|
11
|
-
"openclaw.plugin.json"
|
|
11
|
+
"openclaw.plugin.json",
|
|
12
|
+
"skills/"
|
|
12
13
|
],
|
|
13
14
|
"keywords": [
|
|
14
15
|
"even",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "node ./scripts/build.mjs",
|
|
42
|
-
"prepare": "node ./scripts/build.mjs"
|
|
43
|
+
"prepare": "node ./scripts/build.mjs",
|
|
44
|
+
"test:unit": "node --import ./scripts/test-loader.mjs --test --test-force-exit \"src/**/*.test.ts\""
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: glasses-ui
|
|
3
|
+
description: Authoring guide for render_glasses_ui surfaces on Even G2 glasses. Load BEFORE building any live/refreshing surface, per-item detail list, or multi-screen flow — covers the capability-tier ladder (system-stats host metrics, http data), the patch/replace/push moves and exit-to-chat policy, recipe recon, per-item {label,body} templates, and worked examples.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Authoring glasses surfaces with `render_glasses_ui`
|
|
8
|
+
|
|
9
|
+
`render_glasses_ui` paints an interactive surface on the user's Even G2 HUD instead of a text reply. The call blocks until the user selects, dismisses, or backs out. This skill is the source of truth for **authoring** surfaces; the tool description is deliberately lean.
|
|
10
|
+
|
|
11
|
+
## Before you author: is the tool loaded?
|
|
12
|
+
|
|
13
|
+
`render_glasses_ui` is a plugin tool, and depending on the host runtime it may not sit in your initial tool list:
|
|
14
|
+
|
|
15
|
+
1. **Not listed but searchable** — some runtimes (e.g. the Codex harness) defer OpenClaw dynamic tools behind tool search. Search your available/deferred tools for `render_glasses_ui` (it surfaces under the `openclaw` namespace), load it, and proceed.
|
|
16
|
+
2. **Not findable at all** — the host's tool policy is filtering plugin tools. Newer OpenClaw versions (2026.6+) default `tools.profile` to `"coding"`, a base allowlist that strips plugin-owned tools; skills are not policy-filtered, which is why you can read this guide for a tool you cannot call. Don't improvise a workaround: tell the user to run `openclaw config set tools.alsoAllow '["ocuclaw"]' --strict-json` (merging `"ocuclaw"` into any existing `alsoAllow` list rather than overwriting) and restart the gateway, then try again.
|
|
17
|
+
|
|
18
|
+
## Surface kinds
|
|
19
|
+
|
|
20
|
+
| kind | use it for | caps |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `text_surface` | one formatted read-only block | body ≤ 1000 chars; optional `title` ≤ 64 |
|
|
23
|
+
| `list_surface` | a short pickable list, label-only | ≤ 20 items × ≤ 64 chars |
|
|
24
|
+
| `list_with_details_surface` | a pickable list where each item carries a detail body shown as the user scrolls | label ≤ 64, body ≤ 200; total of all bodies ≤ 6144 |
|
|
25
|
+
|
|
26
|
+
`list_with_details` items are `{ label, body }` objects (`label` required, `body` optional). A bare string is treated as a label-only item. Use it when each option needs a 1–2 sentence compare-before-choosing detail — all bodies ship in one call, so the user browses with zero round-trips.
|
|
27
|
+
|
|
28
|
+
## Capability tiers — pick the lowest tier that answers the need
|
|
29
|
+
|
|
30
|
+
The refresh recipe runs at a capability tier. **Always pick the lowest tier that answers the need.**
|
|
31
|
+
|
|
32
|
+
- **L0 — `http` (data APIs).** In-process, SSRF-guarded fetch → template → surface. For pure data: scores, status APIs, quotes, weather. **Not currently enableable on this install:** `http` is gated by `glassesUiLive.httpEnabled`, and that opt-in isn't reachable yet (the plugin's config-schema omits the `glassesUiLive` block — tracked as a separate fix). Until that lands, an `http` refresh is **rejected (refresh disabled)** — do not author one expecting it to run. It is documented here so you know the tier exists and is the right pick once enabled.
|
|
33
|
+
- **L0′ — `system-stats` (host metrics).** Built-in, in-process `node:os` reader (RAM/CPU/load). **No operator gate** — governed only by the master `glassesUiLive.enabled` (on by default). **This is the only refresh tier that runs without operator config today.** Reach for it for anything host-metric.
|
|
34
|
+
- **Reasoning / judgment tier (`agent`, L1/L2) — designed, not yet available.** A future tier where a sandboxed subagent interprets a page or local files on a timer. It is design-gated (credential-isolation + containment spike) and **not built** — there is no `agent` recipe kind. For a surface that needs interpretation *right now*, render it **once from your own turn** (compose the content yourself, render a static `text_surface`/list). A *self-refreshing* reasoning surface needs an agent in the loop, which isn't wired yet; a live reasoning tier is coming.
|
|
35
|
+
|
|
36
|
+
### `system-stats` output fields (the `{{path}}` sources)
|
|
37
|
+
|
|
38
|
+
`memTotalMb`, `memUsedMb`, `memFreeMb`, `memUsedPct`, `cpuPct`, `loadAvg1`. Optional recipe param `sampleWindowMs` (50–1000, default 200) sizes the CPU-sample window.
|
|
39
|
+
|
|
40
|
+
## The four moves
|
|
41
|
+
|
|
42
|
+
`render_glasses_ui` takes an optional `update` telling it how this render relates to the current surface. **Default is `replace`.**
|
|
43
|
+
|
|
44
|
+
| move | what it does | when |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `patch` | edit *some fields* of the current screen; the refresh cron **keeps ticking** | a partial in-place edit |
|
|
47
|
+
| `replace` *(default)* | swap the *whole content* of the current screen in place; **no back-target** | new content, no going back |
|
|
48
|
+
| `push` | stack a *new child screen*; the parent is retained and **its cron pauses** (resumes on Back) | new content, the user can go back |
|
|
49
|
+
| exit to chat | *not a param* — just end your turn with a short text reply; the chat screen takes over and the surface disappears | you're done; respond in chat |
|
|
50
|
+
|
|
51
|
+
**One-line rule:** partial edit → `patch`; new content, no going back → `replace`; new content, can go back → `push`; done → exit to chat.
|
|
52
|
+
|
|
53
|
+
**Exit-to-chat policy:** on a *deliberate* exit (the user backs past the root, or you're finished), the default is to **respond in chat and not reflexively re-surface**. You retain the capability to surface again later in the conversation — just don't bounce a new surface up reflexively. On an *in-stack* Back (depth ≥ 2), the client transparently restores the parent and its cron resumes; you don't need to re-render it.
|
|
54
|
+
|
|
55
|
+
> **Don't preempt yourself.** Omitting `update` means `replace` — it swaps your current surface in place. To drill into a detail *without losing the list*, use `push`. (Back restores the parent **surface** and re-fires its cron; it does not restore list scroll position.)
|
|
56
|
+
|
|
57
|
+
## Live refresh: recipe recon (validate-then-commit)
|
|
58
|
+
|
|
59
|
+
Add a `refresh` block to make a surface self-update: `{ recipe, intervalMs, targets, onError?, maxDurationMs?, maxConsecutiveFailures? }`. `intervalMs` ≥ 1000.
|
|
60
|
+
|
|
61
|
+
When you submit a render carrying `refresh`, the plugin runs an **initial smoke-test tick before the surface commits**. If that tick fails, the call resolves with `result: "recipe_failed"` and a `failureReason` string — **read it, fix the recipe, and retry**; the surface and cron only start on success. This is your recon loop: don't guess twice, read the failure.
|
|
62
|
+
|
|
63
|
+
`targets` maps recipe output → display:
|
|
64
|
+
- `targets.body` — a string template for `text_surface`.
|
|
65
|
+
- `targets.items` — an array of templates for list surfaces; each entry is either a string (label-only) or `{ label, body }`. The **whole array** is emitted each tick. **Labels are templated too**, so keep static labels as plain literals (no `{{…}}`) — only put `{{…}}` in the parts that should change.
|
|
66
|
+
|
|
67
|
+
### Template filters
|
|
68
|
+
|
|
69
|
+
`{{path}}` reads a value (`{{output}}` for the raw recipe output). Chain filters left-to-right:
|
|
70
|
+
|
|
71
|
+
`trim` · `upper` · `lower` · `int` · `round:N` · `percent` (×100, append %) · `truncate:N` · `default:"--"` · `prefix:"+"` · `minus:previous.value` · `plus:previous.value`. Use `previous.*` paths for deltas vs the prior tick.
|
|
72
|
+
|
|
73
|
+
## Worked examples
|
|
74
|
+
|
|
75
|
+
### 1. Live host stats — `system-stats` text_surface (runs today)
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
render_glasses_ui({
|
|
79
|
+
kind: "text_surface",
|
|
80
|
+
title: "Host",
|
|
81
|
+
body: "RAM — CPU —",
|
|
82
|
+
refresh: {
|
|
83
|
+
recipe: { kind: "system-stats" },
|
|
84
|
+
intervalMs: 2000,
|
|
85
|
+
targets: { body: "RAM {{memUsedPct | round:0}}% CPU {{cpuPct | round:0}}% Load {{loadAvg1 | round:2}}" }
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Live host stats — `system-stats` list_with_details (the canonical interactive example, runs today)
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
render_glasses_ui({
|
|
94
|
+
kind: "list_with_details_surface",
|
|
95
|
+
items: [
|
|
96
|
+
{ label: "Memory", body: "—" },
|
|
97
|
+
{ label: "CPU", body: "—" },
|
|
98
|
+
{ label: "Load", body: "—" }
|
|
99
|
+
],
|
|
100
|
+
refresh: {
|
|
101
|
+
recipe: { kind: "system-stats" },
|
|
102
|
+
intervalMs: 2000,
|
|
103
|
+
targets: {
|
|
104
|
+
items: [
|
|
105
|
+
{ label: "Memory", body: "{{memUsedMb}} / {{memTotalMb}} MB ({{memUsedPct | round:0}}%)" },
|
|
106
|
+
{ label: "CPU", body: "{{cpuPct | round:1}}% busy" },
|
|
107
|
+
{ label: "Load", body: "{{loadAvg1 | round:2}} (1-min avg)" }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
// Labels are static literals (don't re-render); bodies tick every 2s.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. `push` drill-down (don't preempt the list)
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// User highlights "Memory" and asks for detail → stack a child, keep the live list underneath.
|
|
119
|
+
render_glasses_ui({
|
|
120
|
+
kind: "text_surface",
|
|
121
|
+
title: "Memory detail",
|
|
122
|
+
body: "Used … of … MB …", // compose from what you know, or give the child its own system-stats refresh
|
|
123
|
+
update: "push"
|
|
124
|
+
})
|
|
125
|
+
// The parent list's cron pauses while the child is up; on Back it staleness-resumes.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. `http` data (designed; NOT enableable on this install — shown for completeness)
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
// L0 http — DATA APIs. Requires operator opt-in glassesUiLive.httpEnabled, which is
|
|
132
|
+
// NOT currently reachable (config-schema gap). This will be REJECTED today — prefer system-stats.
|
|
133
|
+
render_glasses_ui({
|
|
134
|
+
kind: "text_surface",
|
|
135
|
+
title: "AAPL",
|
|
136
|
+
body: "—",
|
|
137
|
+
refresh: {
|
|
138
|
+
recipe: { kind: "http", url: "https://api.example.com/quote/AAPL", jsonPath: "$.data" },
|
|
139
|
+
intervalMs: 30000,
|
|
140
|
+
targets: { body: "AAPL {{price}} ({{change | round:1 | prefix:\"+\"}})" }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Outcomes (the `result` you get back)
|
|
146
|
+
|
|
147
|
+
| result | meaning |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `selected` | user picked a list item; `selected_index` + `selected_text` returned |
|
|
150
|
+
| `back` | user double-tapped above the root; they want to revise — re-render the previous step or pivot |
|
|
151
|
+
| `dismissed` | dismissed at root, or no selection made |
|
|
152
|
+
| `timeout` | no interaction within the window (non-refresh default 30 min; refresh ends at `maxDurationMs`) |
|
|
153
|
+
| `recipe_failed` | refresh only — initial smoke tick failed, the consecutive-failure breaker fired, or `onError:stop`; `failureReason` carries the last error |
|
|
154
|
+
| `glasses_disconnected` | refresh only — the glasses client dropped mid-cron |
|
|
155
|
+
|
|
156
|
+
Refresh results also carry: `ticks: { count, succeeded, failed, lastSuccessAt, lastFailureAt? }`, `lastBody`, `lastItems`, and `failureReason` (on `recipe_failed`).
|
|
157
|
+
|
|
158
|
+
## Quick reference
|
|
159
|
+
|
|
160
|
+
- Pick the **lowest tier**: host metrics → `system-stats`; pure data API → `http` (not enabled yet). Needs interpretation → render once from your turn.
|
|
161
|
+
- `update` default is `replace` (in-place). Use `push` to drill in without losing the parent; `patch` to edit fields while the cron keeps ticking.
|
|
162
|
+
- `intervalMs` ≥ 1000. Read `failureReason` on `recipe_failed` and fix the recipe.
|
|
163
|
+
- After a surface resolves, a short text reply exits to chat; another render replaces; silence lets it linger.
|