@oharness/server 0.10.0 → 0.11.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/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +152 -8
- package/dist/i18n.js.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +140 -4
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +40 -16
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +111 -7
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/dialogs.js +75 -0
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +25 -2
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +4 -4
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +173 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +200 -54
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +7 -2
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +63 -7
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/style.js +183 -22
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +4 -5
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +17 -0
- package/dist/prefs.js +5 -0
- package/dist/prefs.js.map +1 -1
- package/dist/server.js +465 -42
- package/dist/server.js.map +1 -1
- package/dist/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +28 -2
- package/dist/terminal.js +45 -3
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/turn-shots.d.ts +18 -2
- package/dist/turn-shots.js +10 -1
- package/dist/turn-shots.js.map +1 -1
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { userPath } from "@oharness/engine/paths.ts";
|
|
5
|
+
import { parseJson } from "@oharness/engine/util/json.ts";
|
|
6
|
+
import { renameWithRetry } from "@oharness/engine/util/fs.ts";
|
|
7
|
+
import { pruneAssets } from "./theme-assets.js";
|
|
8
|
+
import { resolveTheme, themeAssets, themeCss } from "./user-theme.js";
|
|
9
|
+
const MAX_HISTORY = 10;
|
|
10
|
+
const MAX_UNDO = 20;
|
|
11
|
+
const FILE = () => userPath("theme.json");
|
|
12
|
+
export class ThemeStore {
|
|
13
|
+
file = { version: 1, active: null, history: [], undo: [] };
|
|
14
|
+
resolved;
|
|
15
|
+
static async load() {
|
|
16
|
+
const store = new ThemeStore();
|
|
17
|
+
try {
|
|
18
|
+
const parsed = parseJson(await readFile(FILE(), "utf8"));
|
|
19
|
+
if (parsed.ok)
|
|
20
|
+
store.file = clean(parsed.value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// No file yet, or one that cannot be read: the built-in theme.
|
|
24
|
+
}
|
|
25
|
+
store.resolveActive();
|
|
26
|
+
// Images no theme in the history uses any more. In the background: a slow
|
|
27
|
+
// disk must not hold up the page.
|
|
28
|
+
void pruneAssets(store.file.history.flatMap((h) => themeAssets(h.input)));
|
|
29
|
+
return store;
|
|
30
|
+
}
|
|
31
|
+
state() {
|
|
32
|
+
const entry = this.activeEntry();
|
|
33
|
+
return {
|
|
34
|
+
active: entry && this.resolved ? { id: entry.id, name: entry.name } : null,
|
|
35
|
+
css: this.resolved ? themeCss(this.resolved) : "",
|
|
36
|
+
canUndo: this.file.undo.length > 0,
|
|
37
|
+
history: this.file.history.map((h) => ({ id: h.id, name: h.name, appliedAt: h.appliedAt, active: h.id === this.file.active })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** The active theme as given, for the model to start a revision from. */
|
|
41
|
+
activeInput() {
|
|
42
|
+
return this.resolved ? this.activeEntry()?.input : undefined;
|
|
43
|
+
}
|
|
44
|
+
async apply(input) {
|
|
45
|
+
const entry = { id: randomUUID(), name: input.name, appliedAt: Date.now(), input };
|
|
46
|
+
this.file.history = [entry, ...this.file.history].slice(0, MAX_HISTORY);
|
|
47
|
+
return this.moveTo(entry.id);
|
|
48
|
+
}
|
|
49
|
+
async applyHistory(id) {
|
|
50
|
+
if (!this.file.history.some((h) => h.id === id))
|
|
51
|
+
return undefined;
|
|
52
|
+
return this.moveTo(id);
|
|
53
|
+
}
|
|
54
|
+
async reset() {
|
|
55
|
+
return this.moveTo(null);
|
|
56
|
+
}
|
|
57
|
+
async undo() {
|
|
58
|
+
const previous = this.file.undo.pop();
|
|
59
|
+
if (previous === undefined)
|
|
60
|
+
return this.state();
|
|
61
|
+
// An entry pushed out of history since cannot be gone back to; the
|
|
62
|
+
// built-in theme always can.
|
|
63
|
+
this.file.active = previous && this.file.history.some((h) => h.id === previous) ? previous : null;
|
|
64
|
+
this.resolveActive();
|
|
65
|
+
await this.save();
|
|
66
|
+
return this.state();
|
|
67
|
+
}
|
|
68
|
+
async moveTo(id) {
|
|
69
|
+
if (id === this.file.active)
|
|
70
|
+
return this.state();
|
|
71
|
+
this.file.undo = [...this.file.undo, this.file.active].slice(-MAX_UNDO);
|
|
72
|
+
this.file.active = id;
|
|
73
|
+
this.resolveActive();
|
|
74
|
+
await this.save();
|
|
75
|
+
return this.state();
|
|
76
|
+
}
|
|
77
|
+
activeEntry() {
|
|
78
|
+
return this.file.history.find((h) => h.id === this.file.active);
|
|
79
|
+
}
|
|
80
|
+
resolveActive() {
|
|
81
|
+
const entry = this.activeEntry();
|
|
82
|
+
const result = entry ? resolveTheme(entry.input) : undefined;
|
|
83
|
+
this.resolved = result?.ok ? result.theme : undefined;
|
|
84
|
+
}
|
|
85
|
+
async save() {
|
|
86
|
+
const file = FILE();
|
|
87
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
88
|
+
tempSeq += 1;
|
|
89
|
+
const temp = `${file}.${process.pid}.${tempSeq}.tmp`;
|
|
90
|
+
await writeFile(temp, `${JSON.stringify(this.file, null, 2)}\n`, "utf8");
|
|
91
|
+
await renameWithRetry(temp, file);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let tempSeq = 0;
|
|
95
|
+
/** Only the shape this module wrote; anything else is dropped, not trusted. */
|
|
96
|
+
function clean(raw) {
|
|
97
|
+
const out = { version: 1, active: null, history: [], undo: [] };
|
|
98
|
+
if (typeof raw !== "object" || raw === null)
|
|
99
|
+
return out;
|
|
100
|
+
const body = raw;
|
|
101
|
+
if (Array.isArray(body["history"])) {
|
|
102
|
+
for (const item of body["history"]) {
|
|
103
|
+
if (typeof item !== "object" || item === null)
|
|
104
|
+
continue;
|
|
105
|
+
const h = item;
|
|
106
|
+
if (typeof h["id"] !== "string" || typeof h["input"] !== "object" || h["input"] === null)
|
|
107
|
+
continue;
|
|
108
|
+
out.history.push({
|
|
109
|
+
id: h["id"],
|
|
110
|
+
name: typeof h["name"] === "string" ? h["name"] : "Custom",
|
|
111
|
+
appliedAt: typeof h["appliedAt"] === "number" ? h["appliedAt"] : 0,
|
|
112
|
+
input: h["input"],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const ids = new Set(out.history.map((h) => h.id));
|
|
117
|
+
if (typeof body["active"] === "string" && ids.has(body["active"]))
|
|
118
|
+
out.active = body["active"];
|
|
119
|
+
if (Array.isArray(body["undo"])) {
|
|
120
|
+
out.undo = body["undo"].filter((v) => v === null || (typeof v === "string" && ids.has(v))).slice(-MAX_UNDO);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=theme-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-store.js","sourceRoot":"","sources":["../src/theme-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Tool } from "@oharness/engine/tools/types.ts";
|
|
2
|
+
import type { ResolvedTheme, ThemeInput } from "./user-theme.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The agent's side of theming: learn what can change, and propose a theme.
|
|
5
|
+
*
|
|
6
|
+
* Proposing is all it can do. `set_theme` makes a preview card in the
|
|
7
|
+
* conversation and returns; the theme is applied only when the person clicks
|
|
8
|
+
* Apply on it. That is why the tool is read-only and needs no approval: the
|
|
9
|
+
* card is the approval, and it comes with the result in front of them rather
|
|
10
|
+
* than a description of it.
|
|
11
|
+
*
|
|
12
|
+
* Registered by the web server only. A terminal has no page to theme.
|
|
13
|
+
*/
|
|
14
|
+
export declare const GET_THEME_SCHEMA_TOOL = "get_theme_schema";
|
|
15
|
+
export declare const SET_THEME_TOOL = "set_theme";
|
|
16
|
+
export declare const RESET_THEME_TOOL = "reset_theme";
|
|
17
|
+
export interface ThemeToolHost {
|
|
18
|
+
/** The theme in use as it was given, or undefined for the built-in one. */
|
|
19
|
+
activeInput(): ThemeInput | undefined;
|
|
20
|
+
/** Put a preview card in front of the person; returns its id. */
|
|
21
|
+
propose(sessionId: string, theme: ResolvedTheme): string;
|
|
22
|
+
/** Go back to the built-in theme; returns the name of the one it replaced. */
|
|
23
|
+
reset(): Promise<string | undefined>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createThemeTools(host: ThemeToolHost): Tool[];
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { toolError, toolText } from "@oharness/engine/tools/types.ts";
|
|
3
|
+
import { DARK, LIGHT } from "./theme.js";
|
|
4
|
+
import { assetExists, importAsset } from "./theme-assets.js";
|
|
5
|
+
import { ASSET_ID, IMAGE_SLOTS, SEEDS, TOKENS, describeReport, resolveTheme } from "./user-theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* The agent's side of theming: learn what can change, and propose a theme.
|
|
8
|
+
*
|
|
9
|
+
* Proposing is all it can do. `set_theme` makes a preview card in the
|
|
10
|
+
* conversation and returns; the theme is applied only when the person clicks
|
|
11
|
+
* Apply on it. That is why the tool is read-only and needs no approval: the
|
|
12
|
+
* card is the approval, and it comes with the result in front of them rather
|
|
13
|
+
* than a description of it.
|
|
14
|
+
*
|
|
15
|
+
* Registered by the web server only. A terminal has no page to theme.
|
|
16
|
+
*/
|
|
17
|
+
export const GET_THEME_SCHEMA_TOOL = "get_theme_schema";
|
|
18
|
+
export const SET_THEME_TOOL = "set_theme";
|
|
19
|
+
export const RESET_THEME_TOOL = "reset_theme";
|
|
20
|
+
const paletteSchema = {
|
|
21
|
+
type: "object",
|
|
22
|
+
description: "Token → hex colour (#rrggbb, or #rrggbbaa for translucent non-seed tokens).",
|
|
23
|
+
additionalProperties: { type: "string" },
|
|
24
|
+
};
|
|
25
|
+
const imageRef = {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Path to a PNG, JPEG or WebP file in the workspace (for example one you just generated), or an asset id already used by the current theme.",
|
|
28
|
+
};
|
|
29
|
+
const imagesSchema = {
|
|
30
|
+
type: "object",
|
|
31
|
+
description: "Optional images, by slot. Call get_theme_schema for what each slot is and its options.",
|
|
32
|
+
properties: {
|
|
33
|
+
conversation: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
image: imageRef,
|
|
37
|
+
fit: { type: "string", enum: ["cover", "contain", "tile"] },
|
|
38
|
+
anchor: { type: "string", enum: ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"] },
|
|
39
|
+
scrim: { type: "number", minimum: 0.5, maximum: 0.95 },
|
|
40
|
+
blend: { type: "string", enum: ["normal", "multiply", "screen", "luminosity", "soft-light"] },
|
|
41
|
+
dark: { type: "object", properties: { image: imageRef, scrim: { type: "number", minimum: 0.5, maximum: 0.95 } } },
|
|
42
|
+
},
|
|
43
|
+
required: ["image"],
|
|
44
|
+
},
|
|
45
|
+
empty: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: { image: imageRef, size: { type: "string", enum: ["small", "medium", "large"] }, align: { type: "string", enum: ["left", "center"] } },
|
|
48
|
+
required: ["image"],
|
|
49
|
+
},
|
|
50
|
+
avatar: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: { image: imageRef, shape: { type: "string", enum: ["circle", "rounded", "square"] } },
|
|
53
|
+
required: ["image"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Turn every `image` the model named into a stored asset, in place, as
|
|
60
|
+
* `asset`. A file outside what the agent may read is refused by the same
|
|
61
|
+
* policy that guards its other tools; a file that is not a still image is
|
|
62
|
+
* refused by its bytes. Either way the slot is dropped and the reason is
|
|
63
|
+
* reported, and the rest of the theme still goes through.
|
|
64
|
+
*/
|
|
65
|
+
async function importImages(input, ctx) {
|
|
66
|
+
const problems = [];
|
|
67
|
+
if (typeof input !== "object" || input === null)
|
|
68
|
+
return { input, problems };
|
|
69
|
+
const copy = structuredClone(input);
|
|
70
|
+
const images = copy["images"];
|
|
71
|
+
if (typeof images !== "object" || images === null)
|
|
72
|
+
return { input: copy, problems };
|
|
73
|
+
const toAsset = async (where, slot) => {
|
|
74
|
+
// `asset` too: get_theme_schema shows the theme in use as stored, where
|
|
75
|
+
// an image is an asset id under that name, and a model revising it copies
|
|
76
|
+
// what it was shown.
|
|
77
|
+
const ref = slot["image"] ?? slot["asset"];
|
|
78
|
+
delete slot["image"];
|
|
79
|
+
delete slot["asset"];
|
|
80
|
+
if (typeof ref !== "string" || !ref.trim()) {
|
|
81
|
+
problems.push(`images.${where}: no image given`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (ASSET_ID.test(ref)) {
|
|
85
|
+
if (await assetExists(ref)) {
|
|
86
|
+
slot["asset"] = ref;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
problems.push(`images.${where}: no stored asset ${ref}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
let file;
|
|
93
|
+
try {
|
|
94
|
+
file = ctx.host?.resolvePath ? ctx.host.resolvePath(ref, "read") : path.resolve(ctx.cwd, ref);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
problems.push(`images.${where}: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const imported = await importAsset(file);
|
|
101
|
+
if (!imported.ok) {
|
|
102
|
+
problems.push(`images.${where}: ${imported.error}`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
slot["asset"] = imported.id;
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
for (const [name, value] of Object.entries(images)) {
|
|
109
|
+
if (typeof value !== "object" || value === null)
|
|
110
|
+
continue;
|
|
111
|
+
const slot = value;
|
|
112
|
+
if (!(await toAsset(name, slot))) {
|
|
113
|
+
delete images[name];
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const dark = slot["dark"];
|
|
117
|
+
if (typeof dark === "object" && dark !== null && "image" in dark) {
|
|
118
|
+
if (!(await toAsset(`${name}.dark`, dark)))
|
|
119
|
+
delete dark["asset"];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { input: copy, problems };
|
|
123
|
+
}
|
|
124
|
+
export function createThemeTools(host) {
|
|
125
|
+
const schemaTool = {
|
|
126
|
+
name: GET_THEME_SCHEMA_TOOL,
|
|
127
|
+
description: "Describe what the app's colour theme can change: the tokens, what each colours, the rules a theme must follow, and the theme in use now. Call this before set_theme.",
|
|
128
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
129
|
+
readOnly: true,
|
|
130
|
+
source: "builtin",
|
|
131
|
+
renderTitle: () => "Theme schema",
|
|
132
|
+
async execute() {
|
|
133
|
+
return toolText(schemaText(host.activeInput()));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const setTool = {
|
|
137
|
+
name: SET_THEME_TOOL,
|
|
138
|
+
description: [
|
|
139
|
+
"Propose a theme for this app's interface: colours, and optionally images in fixed slots (conversation background, empty-session art, assistant avatar). It is shown to the user as a preview card and is NOT applied until they click Apply.",
|
|
140
|
+
"Give the four seeds (bg, fg, accent, brand) for each side you change; everything else is derived from them. Override any other token only when the user asked for something specific.",
|
|
141
|
+
"Colours are hex only. Text colours that fail contrast are adjusted, and the result says what moved.",
|
|
142
|
+
"For an image, generate or find a PNG, JPEG or WebP file first and pass its path. Pick seed colours that suit the image.",
|
|
143
|
+
"Use only for the app's own look. Call get_theme_schema first.",
|
|
144
|
+
].join(" "),
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
name: { type: "string", description: "A short name for the theme, e.g. \"Ember\"." },
|
|
149
|
+
mode: {
|
|
150
|
+
type: "string",
|
|
151
|
+
enum: ["both", "light", "dark"],
|
|
152
|
+
description: "both (default): follows the system and the light/dark switch. light or dark: always that side.",
|
|
153
|
+
},
|
|
154
|
+
light: paletteSchema,
|
|
155
|
+
dark: paletteSchema,
|
|
156
|
+
images: imagesSchema,
|
|
157
|
+
},
|
|
158
|
+
required: ["name"],
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
},
|
|
161
|
+
// Changes nothing by itself: see the header.
|
|
162
|
+
readOnly: true,
|
|
163
|
+
concurrencySafe: false,
|
|
164
|
+
source: "builtin",
|
|
165
|
+
renderTitle: (input) => {
|
|
166
|
+
const name = typeof input === "object" && input && typeof input.name === "string"
|
|
167
|
+
? input.name
|
|
168
|
+
: "";
|
|
169
|
+
return name ? `Theme(${name})` : "Theme";
|
|
170
|
+
},
|
|
171
|
+
async execute(input, ctx) {
|
|
172
|
+
const imported = await importImages(input, ctx);
|
|
173
|
+
const result = resolveTheme(imported.input);
|
|
174
|
+
if (!result.ok) {
|
|
175
|
+
const why = [result.error, ...imported.problems].join("; ");
|
|
176
|
+
return toolError(`The theme was not accepted: ${why}. Call get_theme_schema for the format.`);
|
|
177
|
+
}
|
|
178
|
+
result.theme.report.dropped.unshift(...imported.problems);
|
|
179
|
+
const id = host.propose(ctx.sessionId, result.theme);
|
|
180
|
+
return toolText([
|
|
181
|
+
`A preview card for "${result.theme.input.name}" now follows your reply in the conversation. It is not applied: the user previews, applies or discards it from that card. Do not say the theme has changed, and do not say where on screen the card is.`,
|
|
182
|
+
describeReport(result.theme.report),
|
|
183
|
+
].join("\n\n"), { themePreview: id });
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
/*
|
|
187
|
+
* The one theme tool that changes something by itself, and the reason it
|
|
188
|
+
* may: going back to the built-in look cannot make the app unreadable, and
|
|
189
|
+
* it is one click to undo from the account menu. Asking for approval to
|
|
190
|
+
* undo a theme the person wants gone would only stand in their way.
|
|
191
|
+
*/
|
|
192
|
+
const resetTool = {
|
|
193
|
+
name: RESET_THEME_TOOL,
|
|
194
|
+
description: "Put the app back on its built-in theme, removing any custom colours and images. Use when the user asks to reset, restore the default look, or remove a custom theme. It takes effect at once and can be undone from the account menu.",
|
|
195
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
196
|
+
readOnly: true,
|
|
197
|
+
concurrencySafe: false,
|
|
198
|
+
source: "builtin",
|
|
199
|
+
renderTitle: () => "Reset theme",
|
|
200
|
+
async execute() {
|
|
201
|
+
const was = await host.reset();
|
|
202
|
+
return toolText(was
|
|
203
|
+
? `The built-in theme is back; "${was}" is no longer applied. The user can undo this from the account menu.`
|
|
204
|
+
: "The built-in theme was already in use; nothing changed.");
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
return [schemaTool, setTool, resetTool];
|
|
208
|
+
}
|
|
209
|
+
function schemaText(active) {
|
|
210
|
+
const tokens = Object.entries(TOKENS)
|
|
211
|
+
.map(([token, meaning]) => `- ${token}: ${meaning} Built-in: light ${LIGHT[token]}, dark ${DARK[token]}.`)
|
|
212
|
+
.join("\n");
|
|
213
|
+
return [
|
|
214
|
+
"The interface is coloured by a set of tokens with a light and a dark value each. A theme sets some of them; set_theme derives the rest.",
|
|
215
|
+
"",
|
|
216
|
+
`Seeds: ${SEEDS.join(", ")}. Give all four for each side you change. panel, frame, dim, line, hover, sel, field, track, focus, ring, brand-tint and brand-ink are derived from them; the status and syntax colours keep their built-in values unless set.`,
|
|
217
|
+
"",
|
|
218
|
+
"Rules:",
|
|
219
|
+
"- Values are hex colours. Seeds must be opaque.",
|
|
220
|
+
"- light describes the light side and dark the dark side. mode \"both\" keeps the system and the user's light/dark switch working; a side you leave out keeps the built-in palette.",
|
|
221
|
+
"- Text pairs are held to contrast (4.5:1 for text, 3:1 for marks) and moved if they fail; prefer colours that already pass.",
|
|
222
|
+
"- Colours and the image slots below are all that can change: not layout, size, fonts or which elements show.",
|
|
223
|
+
"",
|
|
224
|
+
"Tokens:",
|
|
225
|
+
tokens,
|
|
226
|
+
"",
|
|
227
|
+
"Image slots (optional, under \"images\"; each takes an \"image\": a workspace path to a PNG, JPEG or WebP, or an asset id from the current theme):",
|
|
228
|
+
...Object.entries(IMAGE_SLOTS).map(([slot, about]) => `- ${slot}: ${about}`),
|
|
229
|
+
"Images are copied into the theme when proposed; SVG, GIF and animated images are refused. The page decides where each slot is drawn and keeps text over the conversation background readable.",
|
|
230
|
+
"",
|
|
231
|
+
active ? `Theme in use now:\n${JSON.stringify(active, null, 2)}` : "Theme in use now: the built-in one.",
|
|
232
|
+
"",
|
|
233
|
+
"Example:",
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
name: "Ember",
|
|
236
|
+
mode: "both",
|
|
237
|
+
light: { bg: "#fffaf5", fg: "#1c1410", accent: "#3a1d0e", brand: "#c25300" },
|
|
238
|
+
dark: { bg: "#120c09", fg: "#f3e9e1", accent: "#f3e9e1", brand: "#ff8a3d" },
|
|
239
|
+
images: { conversation: { image: "./ember-wall.png", fit: "cover", scrim: 0.82 }, avatar: { image: "./ember-avatar.png" } },
|
|
240
|
+
}),
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=theme-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-tools.js","sourceRoot":"","sources":["../src/theme-tools.ts"],"names":[],"mappings":"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"}
|
package/dist/turn-shots.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ComputerSnapshotEvent } from "@oharness/engine/computer/session.ts";
|
|
2
1
|
/**
|
|
3
2
|
* The windows a Computer use turn left behind, as the person saw them.
|
|
4
3
|
*
|
|
@@ -14,11 +13,28 @@ import type { ComputerSnapshotEvent } from "@oharness/engine/computer/session.ts
|
|
|
14
13
|
*/
|
|
15
14
|
export interface TurnShot {
|
|
16
15
|
at: number;
|
|
16
|
+
/** The app the window belongs to; for a browser tab, the site's host. */
|
|
17
17
|
app: string;
|
|
18
18
|
title?: string;
|
|
19
|
+
/** Absent for a Computer use window. */
|
|
20
|
+
kind?: "browser";
|
|
19
21
|
file: string;
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
/** One picture to keep: base64 JPEG and what it shows. */
|
|
24
|
+
export interface TurnShotInput {
|
|
25
|
+
app: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
kind?: "browser";
|
|
28
|
+
data: string;
|
|
29
|
+
}
|
|
30
|
+
/** A kept picture as the page draws it. */
|
|
31
|
+
export declare function turnShotView(session: string, shot: TurnShot): {
|
|
32
|
+
app: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
kind?: "browser";
|
|
35
|
+
src: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function saveTurnShots(session: string, at: number, shots: readonly TurnShotInput[]): Promise<TurnShot[]>;
|
|
22
38
|
/** Synchronous: the transcript is flattened synchronously, and this file is a few lines. */
|
|
23
39
|
export declare function listTurnShots(session: string): TurnShot[];
|
|
24
40
|
/** The bytes of one picture, or nothing for a name that is not one of ours. */
|
package/dist/turn-shots.js
CHANGED
|
@@ -2,6 +2,15 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { appendFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { userPath } from "@oharness/engine/paths.ts";
|
|
5
|
+
/** A kept picture as the page draws it. */
|
|
6
|
+
export function turnShotView(session, shot) {
|
|
7
|
+
return {
|
|
8
|
+
app: shot.app,
|
|
9
|
+
...(shot.title ? { title: shot.title } : {}),
|
|
10
|
+
...(shot.kind ? { kind: shot.kind } : {}),
|
|
11
|
+
src: turnShotSrc(session, shot.file),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
5
14
|
function dir(session) {
|
|
6
15
|
// Session ids are the store's own (date_hex), but the id also arrives on a
|
|
7
16
|
// URL: anything that could climb out of the directory is refused.
|
|
@@ -17,7 +26,7 @@ export async function saveTurnShots(session, at, shots) {
|
|
|
17
26
|
for (const [index, shot] of shots.entries()) {
|
|
18
27
|
const file = `${at}-${stamp}-${index}.jpg`;
|
|
19
28
|
await writeFile(path.join(root, file), Buffer.from(shot.data, "base64"));
|
|
20
|
-
saved.push({ at, app: shot.app, ...(shot.title ? { title: shot.title } : {}), file });
|
|
29
|
+
saved.push({ at, app: shot.app, ...(shot.title ? { title: shot.title } : {}), ...(shot.kind ? { kind: shot.kind } : {}), file });
|
|
21
30
|
}
|
|
22
31
|
await appendFile(path.join(root, "index.jsonl"), saved.map((one) => JSON.stringify(one) + "\n").join(""));
|
|
23
32
|
return saved;
|
package/dist/turn-shots.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-shots.js","sourceRoot":"","sources":["../src/turn-shots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"turn-shots.js","sourceRoot":"","sources":["../src/turn-shots.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A theme the agent proposes, and what the page does with it.
|
|
3
|
+
*
|
|
4
|
+
* The agent never writes CSS. It hands over a few colours — the seeds — and
|
|
5
|
+
* optionally a value for any other colour token, and everything here is what
|
|
6
|
+
* turns that into a stylesheet that cannot break the page: the rest of the
|
|
7
|
+
* palette is derived from the seeds the way the built-in palettes relate to
|
|
8
|
+
* theirs, every pair that carries text is held to a contrast ratio, and the
|
|
9
|
+
* printed CSS only ever sets the custom properties `theme.ts` already defines.
|
|
10
|
+
* There is no selector, property or URL in a theme for anything to go wrong
|
|
11
|
+
* with.
|
|
12
|
+
*
|
|
13
|
+
* Images go in slots the page defines — the conversation background, the art
|
|
14
|
+
* on an empty session, the assistant's avatar — each with a few enumerated or
|
|
15
|
+
* clamped options. A theme names a stored asset, never a URL; where the image
|
|
16
|
+
* lands, what sits above it and how readable the text over it stays are the
|
|
17
|
+
* page's decisions, not the theme's.
|
|
18
|
+
*
|
|
19
|
+
* See docs/plans/ai-theming.md. This covers global colour tokens and image
|
|
20
|
+
* slots. Shape tokens, part-scoped variables and layout variants come later.
|
|
21
|
+
*/
|
|
22
|
+
export type ThemeMode = "both" | "light" | "dark";
|
|
23
|
+
/** A stored image: the sha-256 of its bytes and its type. See theme-assets.ts. */
|
|
24
|
+
export declare const ASSET_ID: RegExp;
|
|
25
|
+
export declare const FITS: readonly ["cover", "contain", "tile"];
|
|
26
|
+
export declare const ANCHORS: readonly ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
|
|
27
|
+
export declare const BLENDS: readonly ["normal", "multiply", "screen", "luminosity", "soft-light"];
|
|
28
|
+
/** Behind the conversation. Text sits over it, so it always has a scrim. */
|
|
29
|
+
export interface WallpaperSlot {
|
|
30
|
+
asset: string;
|
|
31
|
+
fit: (typeof FITS)[number];
|
|
32
|
+
anchor: (typeof ANCHORS)[number];
|
|
33
|
+
/** How much of the page colour covers the image, 0.5–0.95. The page raises it further if text would be hard to read. */
|
|
34
|
+
scrim: number;
|
|
35
|
+
blend: (typeof BLENDS)[number];
|
|
36
|
+
/** The dark side's own image or scrim; the rest is shared. */
|
|
37
|
+
dark?: {
|
|
38
|
+
asset?: string;
|
|
39
|
+
scrim?: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Above the title of a new, empty session. Nothing is drawn over it. */
|
|
43
|
+
export interface EmptyArtSlot {
|
|
44
|
+
asset: string;
|
|
45
|
+
size: "small" | "medium" | "large";
|
|
46
|
+
align: "left" | "center";
|
|
47
|
+
}
|
|
48
|
+
/** Beside the "assistant" label of every reply. */
|
|
49
|
+
export interface AvatarSlot {
|
|
50
|
+
asset: string;
|
|
51
|
+
shape: "circle" | "rounded" | "square";
|
|
52
|
+
}
|
|
53
|
+
export interface ThemeImages {
|
|
54
|
+
conversation?: WallpaperSlot;
|
|
55
|
+
empty?: EmptyArtSlot;
|
|
56
|
+
avatar?: AvatarSlot;
|
|
57
|
+
}
|
|
58
|
+
/** What `set_theme` accepts, after validation. */
|
|
59
|
+
export interface ThemeInput {
|
|
60
|
+
version: 1;
|
|
61
|
+
name: string;
|
|
62
|
+
mode: ThemeMode;
|
|
63
|
+
light?: Record<string, string>;
|
|
64
|
+
dark?: Record<string, string>;
|
|
65
|
+
images?: ThemeImages;
|
|
66
|
+
}
|
|
67
|
+
/** The slots, as the model is told about them in `get_theme_schema`. */
|
|
68
|
+
export declare const IMAGE_SLOTS: Record<keyof ThemeImages, string>;
|
|
69
|
+
export interface ThemeReport {
|
|
70
|
+
/** Keys or values thrown away, and why. */
|
|
71
|
+
dropped: string[];
|
|
72
|
+
/** Colours moved to pass a contrast check. */
|
|
73
|
+
fixed: string[];
|
|
74
|
+
/** Things worth knowing that did not change anything. */
|
|
75
|
+
notes: string[];
|
|
76
|
+
}
|
|
77
|
+
export interface ResolvedTheme {
|
|
78
|
+
input: ThemeInput;
|
|
79
|
+
light?: Record<string, string>;
|
|
80
|
+
dark?: Record<string, string>;
|
|
81
|
+
report: ThemeReport;
|
|
82
|
+
}
|
|
83
|
+
/** The four colours the rest of a palette is derived from. */
|
|
84
|
+
export declare const SEEDS: readonly ["bg", "fg", "accent", "brand"];
|
|
85
|
+
/**
|
|
86
|
+
* Every token a theme may set, with what it is for. The meanings are what the
|
|
87
|
+
* model reads in `get_theme_schema`, so they say where the colour shows rather
|
|
88
|
+
* than restating its name.
|
|
89
|
+
*/
|
|
90
|
+
export declare const TOKENS: Record<string, string>;
|
|
91
|
+
interface Rgba {
|
|
92
|
+
r: number;
|
|
93
|
+
g: number;
|
|
94
|
+
b: number;
|
|
95
|
+
a: number;
|
|
96
|
+
}
|
|
97
|
+
/** `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa`; anything else is not a colour here. */
|
|
98
|
+
export declare function parseHex(value: string): Rgba | undefined;
|
|
99
|
+
export declare function toHex(c: Rgba): string;
|
|
100
|
+
/** WCAG contrast ratio, with a translucent colour composited first. */
|
|
101
|
+
export declare function contrast(top: Rgba, under: Rgba): number;
|
|
102
|
+
/**
|
|
103
|
+
* Check what the model sent and turn it into a theme.
|
|
104
|
+
*
|
|
105
|
+
* Lenient where leniency cannot hurt: an unknown key or a bad value is dropped
|
|
106
|
+
* and reported, so one typo does not cost the whole proposal. Strict where it
|
|
107
|
+
* can: nothing that is not a known token with a hex colour reaches the CSS.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readThemeInput(raw: unknown): {
|
|
110
|
+
ok: true;
|
|
111
|
+
input: ThemeInput;
|
|
112
|
+
dropped: string[];
|
|
113
|
+
} | {
|
|
114
|
+
ok: false;
|
|
115
|
+
error: string;
|
|
116
|
+
};
|
|
117
|
+
/** Validate, derive and check: everything between the model's JSON and the CSS. */
|
|
118
|
+
export declare function resolveTheme(raw: unknown): {
|
|
119
|
+
ok: true;
|
|
120
|
+
theme: ResolvedTheme;
|
|
121
|
+
} | {
|
|
122
|
+
ok: false;
|
|
123
|
+
error: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The stylesheet, printed after the built-in one with the same selectors so it
|
|
127
|
+
* wins on order alone. A one-sided theme pins `color-scheme` and prints its
|
|
128
|
+
* palette under every selector, so the system setting and the light/dark
|
|
129
|
+
* switch cannot bring the other side back.
|
|
130
|
+
*/
|
|
131
|
+
export declare function themeCss(theme: ResolvedTheme): string;
|
|
132
|
+
/** Where a stored image is served; see `GET /theme-asset/` in server.ts. */
|
|
133
|
+
export declare const assetUrl: (asset: string) => string;
|
|
134
|
+
/** Every asset a theme refers to, for keeping and for cleaning up. */
|
|
135
|
+
export declare function themeAssets(input: ThemeInput): string[];
|
|
136
|
+
/** A few colours per side for a card to show before anything is applied. */
|
|
137
|
+
export declare function swatches(theme: ResolvedTheme): {
|
|
138
|
+
light?: string[];
|
|
139
|
+
dark?: string[];
|
|
140
|
+
};
|
|
141
|
+
/** What the model reads back after `set_theme`. */
|
|
142
|
+
export declare function describeReport(report: ThemeReport): string;
|
|
143
|
+
export {};
|