friend-assistant 0.1.4 → 0.1.6
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
CHANGED
|
@@ -127,10 +127,11 @@ Connectors (Gmail, Chrome control, any MCP service) are managed inside the app,
|
|
|
127
127
|
|
|
128
128
|
| Key | What it is |
|
|
129
129
|
|---|---|
|
|
130
|
-
| `provider` | Which model answers: `claude` (API key), `claude-code` (your subscription)
|
|
130
|
+
| `provider` | Which model answers: `claude` (API key), `claude-code` (your subscription), `ollama` (this PC) or `openai` (any OpenAI-compatible server, such as OmniRoute). |
|
|
131
131
|
| `apiKey`, `model` | The Anthropic API key and the Claude model used with it. |
|
|
132
132
|
| `claudeCode.model`, `claudeCode.command`, `claudeCode.fallback` | The subscription route: model (`sonnet`, `haiku`, `opus`), the command that starts Claude Code, and `ollama` or `none` for what to use when Claude is unavailable. |
|
|
133
133
|
| `ollama.url`, `ollama.model`, `ollama.vision`, `ollama.contextTokens` | The local model: server address, model name, whether it can see pictures, and how much it remembers at once. |
|
|
134
|
+
| `openai.url`, `openai.model`, `openai.apiKey`, `openai.vision` | An OpenAI-compatible server: its address (ending in `/v1`), the model name it knows, its API key (or empty), and whether the model can see pictures. For OmniRoute (https://github.com/diegosouzapw/OmniRoute) run `npm install -g omniroute` and `omniroute`, then `friend config set provider openai` (the defaults are OmniRoute's: `http://localhost:20128/v1` and model `auto`). Also works with LM Studio, LiteLLM, vLLM and OpenRouter. |
|
|
134
135
|
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, and the two thresholds for recognising your voice. |
|
|
135
136
|
| `bind`, `port` | Where Friend listens (easier: `friend lan`). |
|
|
136
137
|
| `https` | `true` serves a secure `https://` address with a certificate Friend makes for itself, so a phone may use the microphone (easier: `friend lan https on`). |
|
package/dist/BUILD.json
CHANGED
package/dist/friend.mjs
CHANGED
|
@@ -3263,6 +3263,7 @@ import { join as join12 } from "node:path";
|
|
|
3263
3263
|
import { chmodSync as chmodSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3264
3264
|
import { homedir as homedir2 } from "node:os";
|
|
3265
3265
|
import { join as join8 } from "node:path";
|
|
3266
|
+
var DEFAULT_OPENAI = { url: "http://localhost:20128/v1", model: "auto", apiKey: null, vision: false };
|
|
3266
3267
|
var DEFAULT_CLAUDE_CODE = { model: "sonnet", command: "claude", fallback: "ollama" };
|
|
3267
3268
|
function sanitizeClaudeCode(raw) {
|
|
3268
3269
|
const r = raw && typeof raw === "object" ? raw : {};
|
|
@@ -3351,6 +3352,7 @@ var defaults = (dataDir) => ({
|
|
|
3351
3352
|
apiKey: null,
|
|
3352
3353
|
provider: "claude",
|
|
3353
3354
|
ollama: { ...DEFAULT_OLLAMA },
|
|
3355
|
+
openai: { ...DEFAULT_OPENAI },
|
|
3354
3356
|
claudeCode: { ...DEFAULT_CLAUDE_CODE },
|
|
3355
3357
|
voice: { ...DEFAULT_VOICE },
|
|
3356
3358
|
view: "companion",
|
|
@@ -3386,6 +3388,7 @@ function loadConfig(dataDir = defaultDataDir()) {
|
|
|
3386
3388
|
https: stored.https === true,
|
|
3387
3389
|
destructivePatterns,
|
|
3388
3390
|
ollama: { ...DEFAULT_OLLAMA, ...stored.ollama },
|
|
3391
|
+
openai: { ...DEFAULT_OPENAI, ...stored.openai },
|
|
3389
3392
|
claudeCode: sanitizeClaudeCode(stored.claudeCode),
|
|
3390
3393
|
voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
|
|
3391
3394
|
view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
|
|
@@ -4991,7 +4994,7 @@ import { z as z11 } from "zod";
|
|
|
4991
4994
|
var word = /^[\w.:/\\-]+$/;
|
|
4992
4995
|
var modelName = z11.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][\w.:/-]*$/, "A model name has no spaces");
|
|
4993
4996
|
var SettingsPatch = z11.object({
|
|
4994
|
-
provider: z11.enum(["claude", "claude-code", "ollama"]),
|
|
4997
|
+
provider: z11.enum(["claude", "claude-code", "ollama", "openai"]),
|
|
4995
4998
|
model: modelName,
|
|
4996
4999
|
/** Empty means "leave the saved key alone". */
|
|
4997
5000
|
apiKey: z11.string().trim().max(400),
|
|
@@ -5013,6 +5016,13 @@ var SettingsPatch = z11.object({
|
|
|
5013
5016
|
vision: z11.boolean(),
|
|
5014
5017
|
contextTokens: z11.number().int().min(1024).max(131072)
|
|
5015
5018
|
}).partial().strict(),
|
|
5019
|
+
openai: z11.object({
|
|
5020
|
+
url: z11.string().trim().max(300),
|
|
5021
|
+
model: modelName,
|
|
5022
|
+
/** Empty means "leave the saved key alone". */
|
|
5023
|
+
apiKey: z11.string().trim().max(400),
|
|
5024
|
+
vision: z11.boolean()
|
|
5025
|
+
}).partial().strict(),
|
|
5016
5026
|
claudeCode: z11.object({
|
|
5017
5027
|
model: modelName,
|
|
5018
5028
|
command: z11.string().trim().min(1).max(260).regex(word, "The command is one word or path without spaces"),
|
|
@@ -5036,6 +5046,7 @@ function settingsView(c) {
|
|
|
5036
5046
|
view: c.view,
|
|
5037
5047
|
look: { ...c.look },
|
|
5038
5048
|
ollama: { url: c.ollama.url, model: c.ollama.model, vision: !!c.ollama.vision, contextTokens: c.ollama.contextTokens },
|
|
5049
|
+
openai: { url: c.openai.url, model: c.openai.model, vision: !!c.openai.vision, apiKeySet: !!c.openai.apiKey },
|
|
5039
5050
|
claudeCode: { ...c.claudeCode },
|
|
5040
5051
|
voice: {
|
|
5041
5052
|
enabled: c.voice.enabled,
|
|
@@ -5053,10 +5064,11 @@ var SettingsError = class extends Error {
|
|
|
5053
5064
|
function refuseForAgent(raw) {
|
|
5054
5065
|
const r = raw && typeof raw === "object" ? raw : {};
|
|
5055
5066
|
const cc = r.claudeCode && typeof r.claudeCode === "object" ? r.claudeCode : {};
|
|
5056
|
-
|
|
5067
|
+
const oa = r.openai && typeof r.openai === "object" ? r.openai : {};
|
|
5068
|
+
if ("apiKey" in r || "apiKey" in oa) throw new SettingsError("The API key can only be changed by the owner on the settings screen.");
|
|
5057
5069
|
if ("command" in cc) throw new SettingsError("The Claude Code command can only be changed by the owner on the settings screen.");
|
|
5058
5070
|
}
|
|
5059
|
-
var PROVIDER_KEYS = /* @__PURE__ */ new Set(["provider", "model", "apiKey", "ollama", "claudeCode"]);
|
|
5071
|
+
var PROVIDER_KEYS = /* @__PURE__ */ new Set(["provider", "model", "apiKey", "ollama", "openai", "claudeCode"]);
|
|
5060
5072
|
var RESTART_VOICE = ["enabled", "sttModel"];
|
|
5061
5073
|
function applySettings(config, raw) {
|
|
5062
5074
|
const parsed = SettingsPatch.safeParse(raw);
|
|
@@ -5076,6 +5088,17 @@ function applySettings(config, raw) {
|
|
|
5076
5088
|
if (u.protocol !== "http:" && u.protocol !== "https:") throw new SettingsError("ollama.url: The URL must start with http:// or https://");
|
|
5077
5089
|
p.ollama.url = u.origin;
|
|
5078
5090
|
}
|
|
5091
|
+
const openaiUrl = p.openai?.url;
|
|
5092
|
+
if (openaiUrl !== void 0) {
|
|
5093
|
+
let u;
|
|
5094
|
+
try {
|
|
5095
|
+
u = new URL(openaiUrl);
|
|
5096
|
+
} catch {
|
|
5097
|
+
throw new SettingsError("openai.url: Expected a URL such as http://localhost:20128/v1");
|
|
5098
|
+
}
|
|
5099
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new SettingsError("openai.url: The URL must start with http:// or https://");
|
|
5100
|
+
p.openai.url = (u.origin + u.pathname).replace(/\/+$/, "");
|
|
5101
|
+
}
|
|
5079
5102
|
const next = {
|
|
5080
5103
|
provider: p.provider ?? config.provider,
|
|
5081
5104
|
apiKey: p.apiKey ? p.apiKey : config.apiKey,
|
|
@@ -5098,6 +5121,11 @@ function applySettings(config, raw) {
|
|
|
5098
5121
|
if (p.model) config.model = p.model;
|
|
5099
5122
|
if (p.apiKey) config.apiKey = p.apiKey;
|
|
5100
5123
|
if (p.ollama) Object.assign(config.ollama, p.ollama);
|
|
5124
|
+
if (p.openai) {
|
|
5125
|
+
const { apiKey, ...rest } = p.openai;
|
|
5126
|
+
Object.assign(config.openai, rest);
|
|
5127
|
+
if (apiKey) config.openai.apiKey = apiKey;
|
|
5128
|
+
}
|
|
5101
5129
|
if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
|
|
5102
5130
|
if (p.voice) {
|
|
5103
5131
|
const safe = next.voice;
|
|
@@ -5282,6 +5310,173 @@ var OllamaProvider = class {
|
|
|
5282
5310
|
}
|
|
5283
5311
|
};
|
|
5284
5312
|
|
|
5313
|
+
// ../server/src/provider/openai.ts
|
|
5314
|
+
import { Agent as Agent3, fetch as undiciFetch2 } from "undici";
|
|
5315
|
+
var encodeName3 = (n) => n.replaceAll(".", "__");
|
|
5316
|
+
var decodeName3 = (n) => n.replaceAll("__", ".");
|
|
5317
|
+
function toOpenAiMessages(system, messages, vision) {
|
|
5318
|
+
const out = [{ role: "system", content: system }];
|
|
5319
|
+
for (const m of messages) {
|
|
5320
|
+
if (m.role === "user") {
|
|
5321
|
+
out.push({ role: "user", content: m.content.map((b) => b.type === "text" ? b.text : "").join("\n") });
|
|
5322
|
+
} else if (m.role === "assistant") {
|
|
5323
|
+
const text2 = m.content.map((b) => b.type === "text" ? b.text : "").join("");
|
|
5324
|
+
const calls = m.content.filter((b) => b.type === "tool_use");
|
|
5325
|
+
out.push({
|
|
5326
|
+
role: "assistant",
|
|
5327
|
+
content: text2 || null,
|
|
5328
|
+
...calls.length ? { tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: encodeName3(c.name), arguments: JSON.stringify(c.input ?? {}) } })) } : {}
|
|
5329
|
+
});
|
|
5330
|
+
} else {
|
|
5331
|
+
for (const b of m.content) if (b.type === "tool_result") out.push({ role: "tool", tool_call_id: b.toolUseId, content: b.content });
|
|
5332
|
+
const pictures = vision ? m.content.flatMap((b) => b.type === "image" ? [`data:${b.mime};base64,${b.data}`] : []) : [];
|
|
5333
|
+
if (pictures.length) out.push({ role: "user", content: [{ type: "text", text: "Here is the picture from the camera." }, ...pictures.map((url) => ({ type: "image_url", image_url: { url } }))] });
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
return out;
|
|
5337
|
+
}
|
|
5338
|
+
function parseArgs2(raw) {
|
|
5339
|
+
if (!raw.trim()) return {};
|
|
5340
|
+
try {
|
|
5341
|
+
return JSON.parse(raw);
|
|
5342
|
+
} catch {
|
|
5343
|
+
return {};
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
var OpenAiProvider = class {
|
|
5347
|
+
constructor(opts) {
|
|
5348
|
+
this.opts = opts;
|
|
5349
|
+
const limit = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
|
|
5350
|
+
this.agent = new Agent3({ headersTimeout: limit, bodyTimeout: limit });
|
|
5351
|
+
}
|
|
5352
|
+
opts;
|
|
5353
|
+
id = "openai";
|
|
5354
|
+
agent;
|
|
5355
|
+
get vision() {
|
|
5356
|
+
return this.opts.vision === true;
|
|
5357
|
+
}
|
|
5358
|
+
async *stream(req) {
|
|
5359
|
+
const base = this.opts.url.replace(/\/+$/, "");
|
|
5360
|
+
const body = {
|
|
5361
|
+
model: this.opts.model,
|
|
5362
|
+
stream: true,
|
|
5363
|
+
stream_options: { include_usage: true },
|
|
5364
|
+
messages: [
|
|
5365
|
+
...toOpenAiMessages(req.system, req.messages, this.vision),
|
|
5366
|
+
// last of all, so every message before it stays identical between requests and a gateway can reuse its cache of them
|
|
5367
|
+
...req.context ? [{ role: "user", content: req.context }] : []
|
|
5368
|
+
],
|
|
5369
|
+
...req.tools.length ? { tools: req.tools.map((t) => ({ type: "function", function: { name: encodeName3(t.name), description: t.description, parameters: t.inputSchema } })) } : {}
|
|
5370
|
+
};
|
|
5371
|
+
let res;
|
|
5372
|
+
try {
|
|
5373
|
+
res = await undiciFetch2(`${base}/chat/completions`, {
|
|
5374
|
+
method: "POST",
|
|
5375
|
+
headers: { "content-type": "application/json", ...this.opts.apiKey ? { authorization: `Bearer ${this.opts.apiKey}` } : {} },
|
|
5376
|
+
body: JSON.stringify(body),
|
|
5377
|
+
signal: req.signal,
|
|
5378
|
+
dispatcher: this.agent
|
|
5379
|
+
});
|
|
5380
|
+
} catch (e) {
|
|
5381
|
+
if (req.signal.aborted) return;
|
|
5382
|
+
const code = e.cause?.code ?? "";
|
|
5383
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
|
|
5384
|
+
yield { type: "error", userFacing: true, message: "The model took too long to answer. Try again, or choose a faster model." };
|
|
5385
|
+
return;
|
|
5386
|
+
}
|
|
5387
|
+
yield { type: "error", userFacing: true, message: `I can't reach the model server at ${base}. Is it running? Check the address in Settings.` };
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
if (!res.ok) {
|
|
5391
|
+
let detail = "";
|
|
5392
|
+
try {
|
|
5393
|
+
const j = await res.json();
|
|
5394
|
+
detail = typeof j.error === "string" ? j.error : j.error?.message ?? "";
|
|
5395
|
+
} catch {
|
|
5396
|
+
}
|
|
5397
|
+
if (res.status === 401 || res.status === 403) yield { type: "error", userFacing: true, message: "The model server refused the API key. Check it in Settings." };
|
|
5398
|
+
else if (res.status === 404) yield { type: "error", userFacing: true, message: `The model server has no "${this.opts.model}", or the address is wrong (it should end in /v1). ${detail}`.trim() };
|
|
5399
|
+
else if (res.status === 429) yield { type: "error", userFacing: true, message: "The model server says too many requests or the limit is used up. Try again in a moment." };
|
|
5400
|
+
else yield { type: "error", message: `Model server error (${res.status})${detail ? `: ${detail}` : ""}` };
|
|
5401
|
+
return;
|
|
5402
|
+
}
|
|
5403
|
+
const reader = res.body.getReader();
|
|
5404
|
+
const decoder = new TextDecoder();
|
|
5405
|
+
let buffer = "";
|
|
5406
|
+
let finished = false;
|
|
5407
|
+
const calls = /* @__PURE__ */ new Map();
|
|
5408
|
+
let usage = null;
|
|
5409
|
+
const flush = function* () {
|
|
5410
|
+
for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {
|
|
5411
|
+
if (c.name) yield { type: "tool_call", id: c.id || `openai-${Date.now().toString(36)}-${c.name}`, name: decodeName3(c.name), args: parseArgs2(c.args) };
|
|
5412
|
+
}
|
|
5413
|
+
calls.clear();
|
|
5414
|
+
};
|
|
5415
|
+
const handle = function* (line) {
|
|
5416
|
+
const text2 = line.trim();
|
|
5417
|
+
if (!text2.startsWith("data:")) return;
|
|
5418
|
+
const data = text2.slice(5).trim();
|
|
5419
|
+
if (data === "[DONE]") {
|
|
5420
|
+
yield* flush();
|
|
5421
|
+
if (usage) yield { type: "usage", ...usage };
|
|
5422
|
+
yield { type: "done" };
|
|
5423
|
+
finished = true;
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
let obj;
|
|
5427
|
+
try {
|
|
5428
|
+
obj = JSON.parse(data);
|
|
5429
|
+
} catch {
|
|
5430
|
+
return;
|
|
5431
|
+
}
|
|
5432
|
+
if (obj.error) {
|
|
5433
|
+
yield { type: "error", message: `Model server error: ${typeof obj.error === "string" ? obj.error : obj.error.message ?? "unknown"}` };
|
|
5434
|
+
finished = true;
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
5437
|
+
if (obj.usage) usage = { input: obj.usage.prompt_tokens ?? 0, output: obj.usage.completion_tokens ?? 0 };
|
|
5438
|
+
for (const choice of obj.choices ?? []) {
|
|
5439
|
+
const delta = choice.delta ?? {};
|
|
5440
|
+
if (typeof delta.content === "string" && delta.content) yield { type: "text_delta", text: delta.content };
|
|
5441
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
5442
|
+
const i = typeof tc.index === "number" ? tc.index : 0;
|
|
5443
|
+
const cur = calls.get(i) ?? { id: "", name: "", args: "" };
|
|
5444
|
+
if (tc.id) cur.id = tc.id;
|
|
5445
|
+
if (tc.function?.name) cur.name += tc.function.name;
|
|
5446
|
+
if (typeof tc.function?.arguments === "string") cur.args += tc.function.arguments;
|
|
5447
|
+
calls.set(i, cur);
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
};
|
|
5451
|
+
try {
|
|
5452
|
+
while (!finished) {
|
|
5453
|
+
const { value, done } = await reader.read();
|
|
5454
|
+
if (done) break;
|
|
5455
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5456
|
+
let nl;
|
|
5457
|
+
while (!finished && (nl = buffer.indexOf("\n")) >= 0) {
|
|
5458
|
+
const line = buffer.slice(0, nl);
|
|
5459
|
+
buffer = buffer.slice(nl + 1);
|
|
5460
|
+
yield* handle(line);
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
if (!finished && buffer.trim()) yield* handle(buffer);
|
|
5464
|
+
if (!finished) {
|
|
5465
|
+
yield* flush();
|
|
5466
|
+
const u = usage;
|
|
5467
|
+
if (u) yield { type: "usage", input: u.input, output: u.output };
|
|
5468
|
+
yield { type: "done" };
|
|
5469
|
+
}
|
|
5470
|
+
} catch (e) {
|
|
5471
|
+
if (req.signal.aborted) return;
|
|
5472
|
+
yield { type: "error", message: `Lost the connection to the model server: ${e.message}` };
|
|
5473
|
+
} finally {
|
|
5474
|
+
reader.cancel().catch(() => {
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
};
|
|
5479
|
+
|
|
5285
5480
|
// ../server/src/tls.ts
|
|
5286
5481
|
import { chmodSync as chmodSync5, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5287
5482
|
import { networkInterfaces } from "node:os";
|
|
@@ -5826,6 +6021,7 @@ async function createApp(opts) {
|
|
|
5826
6021
|
return config.claudeCode.fallback === "ollama" ? new FallbackProvider(cc, new OllamaProvider({ ...config.ollama }), (reason) => tellFallback(reason)) : cc;
|
|
5827
6022
|
}
|
|
5828
6023
|
if (config.provider === "ollama") return new OllamaProvider({ ...config.ollama });
|
|
6024
|
+
if (config.provider === "openai") return new OpenAiProvider({ ...config.openai });
|
|
5829
6025
|
return config.apiKey ? new ClaudeProvider({ apiKey: config.apiKey, model: config.model }) : new MissingKeyProvider();
|
|
5830
6026
|
};
|
|
5831
6027
|
const switchable = opts.provider ? null : new SwitchableProvider(makeProvider());
|
|
@@ -6529,7 +6725,7 @@ var PARSERS = {
|
|
|
6529
6725
|
return n;
|
|
6530
6726
|
},
|
|
6531
6727
|
provider: (v) => {
|
|
6532
|
-
if (v !== "claude" && v !== "ollama" && v !== "claude-code") throw new Error("Provider must be claude, ollama
|
|
6728
|
+
if (v !== "claude" && v !== "ollama" && v !== "claude-code" && v !== "openai") throw new Error("Provider must be claude, ollama, claude-code or openai");
|
|
6533
6729
|
return v;
|
|
6534
6730
|
},
|
|
6535
6731
|
roots: list,
|
|
@@ -6558,6 +6754,24 @@ var GROUPS = {
|
|
|
6558
6754
|
return v;
|
|
6559
6755
|
}
|
|
6560
6756
|
},
|
|
6757
|
+
openai: {
|
|
6758
|
+
url: (v) => {
|
|
6759
|
+
let u;
|
|
6760
|
+
try {
|
|
6761
|
+
u = new URL(v.trim());
|
|
6762
|
+
} catch {
|
|
6763
|
+
throw new Error("Expected a URL that ends in /v1, such as http://localhost:20128/v1");
|
|
6764
|
+
}
|
|
6765
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("The URL must start with http:// or https://");
|
|
6766
|
+
return (u.origin + u.pathname).replace(/\/+$/, "");
|
|
6767
|
+
},
|
|
6768
|
+
model: (v) => {
|
|
6769
|
+
if (!/^[A-Za-z0-9][\w.:/-]*$/.test(v.trim())) throw new Error("Expected a model name without spaces, such as auto");
|
|
6770
|
+
return v.trim();
|
|
6771
|
+
},
|
|
6772
|
+
apiKey: (v) => v.trim() === "" ? null : v.trim(),
|
|
6773
|
+
vision: bool
|
|
6774
|
+
},
|
|
6561
6775
|
ollama: {
|
|
6562
6776
|
url: httpUrl,
|
|
6563
6777
|
model: nonEmpty,
|
|
@@ -6572,7 +6786,7 @@ var GROUPS = {
|
|
|
6572
6786
|
var KEY_HELP = {
|
|
6573
6787
|
apiKey: 'Anthropic API key, used when the model is "claude"',
|
|
6574
6788
|
model: "Claude model name, used with the API key",
|
|
6575
|
-
provider: "which model answers: claude (API key), claude-code (your Claude subscription)
|
|
6789
|
+
provider: "which model answers: claude (API key), claude-code (your Claude subscription), ollama (on this PC) or openai (any OpenAI-compatible server, such as OmniRoute)",
|
|
6576
6790
|
bind: "address Friend listens on: 127.0.0.1 is this PC only, 0.0.0.0 is your whole network (easier: friend lan on)",
|
|
6577
6791
|
port: "the web port (easier: friend lan port N)",
|
|
6578
6792
|
https: "true serves a secure https:// address (Friend makes its own certificate), so a phone may use the microphone (easier: friend lan https on)",
|
|
@@ -6592,6 +6806,10 @@ var KEY_HELP = {
|
|
|
6592
6806
|
"ollama.model": "the local model, for example qwen2.5:7b",
|
|
6593
6807
|
"ollama.vision": "true or false: the local model can look at pictures",
|
|
6594
6808
|
"ollama.contextTokens": "how much the local model remembers at once (1024 to 131072)",
|
|
6809
|
+
"openai.url": "address of an OpenAI-compatible server, ending in /v1; OmniRoute is http://localhost:20128/v1",
|
|
6810
|
+
"openai.model": "the model name that server knows; OmniRoute answers to auto",
|
|
6811
|
+
"openai.apiKey": "the server's API key, or empty when it asks for none (stored in your private config file)",
|
|
6812
|
+
"openai.vision": "true or false: that model can look at pictures",
|
|
6595
6813
|
"claudeCode.model": "model for your Claude subscription: sonnet, haiku, opus or a full name",
|
|
6596
6814
|
"claudeCode.command": "the command that starts Claude Code, if it is not on the PATH",
|
|
6597
6815
|
"claudeCode.fallback": "ollama or none: what to use when Claude is unavailable"
|
|
@@ -8348,7 +8566,7 @@ async function model(c) {
|
|
|
8348
8566
|
io2.out("No model is ready yet. Finish one of: `friend claude setup --use`, `friend ollama setup --use`, or `friend config set apiKey <key>`.");
|
|
8349
8567
|
return "later";
|
|
8350
8568
|
}
|
|
8351
|
-
io2.out(`Friend will think with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}) through your subscription${cfg.claudeCode.fallback === "ollama" ? ", with the local model as a safety net" : ""}` : cfg.provider === "claude" ? "Claude with your API key" : `${cfg.ollama.model} on this PC`}.`);
|
|
8569
|
+
io2.out(`Friend will think with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}) through your subscription${cfg.claudeCode.fallback === "ollama" ? ", with the local model as a safety net" : ""}` : cfg.provider === "claude" ? "Claude with your API key" : cfg.provider === "openai" ? `${cfg.openai.model} through ${cfg.openai.url}` : `${cfg.ollama.model} on this PC`}.`);
|
|
8352
8570
|
return "done";
|
|
8353
8571
|
}
|
|
8354
8572
|
async function voice(c) {
|
|
@@ -8463,7 +8681,7 @@ function summary(cfg, c) {
|
|
|
8463
8681
|
}
|
|
8464
8682
|
return [
|
|
8465
8683
|
` Assistant: ${owner}`,
|
|
8466
|
-
` Thinks with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}), your subscription` : cfg.provider === "claude" ? cfg.apiKey ? "Claude, API key" : "Claude, API key missing" : `${cfg.ollama.model} on this PC`}`,
|
|
8684
|
+
` Thinks with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}), your subscription` : cfg.provider === "claude" ? cfg.apiKey ? "Claude, API key" : "Claude, API key missing" : cfg.provider === "openai" ? `${cfg.openai.model} through ${cfg.openai.url}` : `${cfg.ollama.model} on this PC`}`,
|
|
8467
8685
|
` Voice: ${has(c, "voice") ? voiceLabel(cfg.voice.ttsVoice) : "off"}`,
|
|
8468
8686
|
` Workspace: ${cfg.roots[0] ?? "none (every file action asks)"}`,
|
|
8469
8687
|
` Address: ${u.local}${u.network.length ? ` and ${u.network.join(", ")}` : ""}`,
|
|
@@ -8916,7 +9134,7 @@ async function run(argv, io2) {
|
|
|
8916
9134
|
case "version":
|
|
8917
9135
|
case "--version":
|
|
8918
9136
|
case "-v":
|
|
8919
|
-
io2.out(true ? "0.1.
|
|
9137
|
+
io2.out(true ? "0.1.6" : "development");
|
|
8920
9138
|
return 0;
|
|
8921
9139
|
case "lan":
|
|
8922
9140
|
return await lanCommand(rest, io2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--accent:#3fd8ff;--warn:#ffb454;--bad:#ff6b7a;--ok:#5ef0b0;--ink:#d4ecf7;--muted:#7fa3b6;--glass:#06141c9e;--edge:color-mix(in srgb, var(--accent) 48%, transparent);--ui:"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;--mono:"Cascadia Mono", "SF Mono", Consolas, ui-monospace, monospace;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{color:var(--ink);font:16px/1.5 var(--ui);-webkit-tap-highlight-color:transparent;background:#000;overflow:hidden}button,input{font:inherit;color:inherit}button{cursor:pointer}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.stage{touch-action:manipulation;background:#000;position:fixed;inset:0;overflow:hidden}.ambient{pointer-events:none;background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%), linear-gradient(#3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px, linear-gradient(90deg, #3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px;position:absolute;inset:0;-webkit-mask-image:radial-gradient(#000 30%,#0000 78%);mask-image:radial-gradient(#000 30%,#0000 78%)}.presence{z-index:1;transition:inset .7s cubic-bezier(.2,.7,.2,1),width .7s,height .7s,opacity .5s;position:absolute}.presence-full{inset:0}.presence-corner{left:max(10px, env(safe-area-inset-left));bottom:max(52px, calc(env(safe-area-inset-bottom) + 42px));width:clamp(96px,16vw,190px);height:clamp(96px,16vw,190px)}.objects{pointer-events:none;position:absolute;inset:0}.objects-stack{pointer-events:auto;padding:max(56px, env(safe-area-inset-top)) 14px 140px;flex-direction:column;gap:12px;display:flex;overflow-y:auto}.auto-grid{pointer-events:none;padding:max(24px, env(safe-area-inset-top)) 24px 24px clamp(120px, 20vw, 230px);grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));align-content:safe center;align-items:start;gap:12px;display:grid;position:absolute;inset:0;overflow-y:auto}.auto-grid>.obj{pointer-events:auto}.objects-stack>.auto-grid{flex-direction:column;padding:0;display:flex;position:static;overflow:visible}.obj.leaving{pointer-events:none;animation:.35s forwards leave}@keyframes leave{to{opacity:0;transform:scale(.96)translateY(-6px)}}@media (prefers-reduced-motion:reduce){.obj.leaving{animation:none}}.glass{background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--edge);box-shadow:0 0 22px color-mix(in srgb, var(--accent) 16%, transparent), inset 0 0 28px #3fd8ff0d;clip-path:polygon(0 0,calc(100% - 14px) 0,100% 14px,100% 100%,14px 100%,0 calc(100% - 14px))}.obj{pointer-events:auto;min-width:0;padding:16px 18px;animation:.45s both appear;overflow:auto}.obj.alert{border-color:var(--warn)}@keyframes appear{0%{opacity:0;transform:translateY(8px)scale(.985)}}.obj-close{width:44px;height:44px;color:var(--muted);background:0 0;border:0;font-size:1.5rem;line-height:1;position:absolute;top:2px;right:6px}.obj-close:hover{color:var(--accent)}.obj-title{letter-spacing:.16em;text-transform:uppercase;color:var(--accent);margin:0 36px 8px 0;font-size:.78rem;font-weight:600}.obj-md>:first-child{margin-top:0}.obj-md>:last-child{margin-bottom:0}.obj-md a{color:var(--accent)}.obj-md code{font-family:var(--mono);background:#3fd8ff1a;padding:0 4px;font-size:.9em}.obj-stat{gap:2px;display:grid}.obj-label{letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-size:.78rem}.obj-value{font-variant-numeric:tabular-nums;font-size:clamp(2rem,6vw,3.6rem);font-weight:300;line-height:1.05}.obj-value small{color:var(--muted);margin-left:4px;font-size:.4em}.obj-delta{color:var(--ok);font-size:.9rem}.obj-list{gap:4px;margin:0;padding-left:1.1rem;display:grid}.obj-list .done{color:var(--muted);text-decoration:line-through}.obj-table-wrap{overflow-x:auto}.obj-table{border-collapse:collapse;width:100%;font-size:.92rem}.obj-table th{text-align:left;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);font-size:.72rem;font-weight:600}.obj-table th,.obj-table td{font-variant-numeric:tabular-nums;border-bottom:1px solid #3fd8ff2e;padding:6px 10px}.obj-code{font:.86rem/1.5 var(--mono);margin:0;overflow:auto}.obj-empty{color:var(--muted);margin:0}.obj-chart{width:100%;height:auto;display:block}.obj-chart .grid{stroke:#3fd8ff24;stroke-width:1px}.obj-chart .tick{fill:var(--muted);font:10px var(--mono);font-variant-numeric:tabular-nums}.legend{color:var(--muted);flex-wrap:wrap;gap:6px 14px;margin:6px 0 0;padding:0;font-size:.8rem;list-style:none;display:flex}.legend i{width:10px;height:10px;margin-right:6px;display:inline-block}.obj-gauge{justify-items:center;display:grid}.obj-gauge svg{width:min(100%,260px);height:auto}.obj-gauge .track{stroke:#3fd8ff26}.obj-gauge .value{stroke:var(--accent);filter:drop-shadow(0 0 6px var(--accent))}.obj-gauge.gauge-warn .value{stroke:var(--warn);filter:drop-shadow(0 0 6px var(--warn))}.obj-gauge.gauge-crit .value{stroke:var(--bad);filter:drop-shadow(0 0 6px var(--bad))}.gauge-read{align-items:baseline;gap:4px;margin-top:-62px;display:flex}.gauge-value{font-variant-numeric:tabular-nums;font-size:clamp(1.8rem,5vw,2.8rem);font-weight:300}.gauge-unit{color:var(--muted)}.obj-gauge .obj-label{margin-top:18px}.obj-timeline{border-left:1px solid var(--edge);gap:14px;margin:8px 0 0;padding:0 0 0 20px;list-style:none;display:grid}.obj-timeline li{gap:2px;display:grid;position:relative}.obj-timeline li:before{content:"";background:var(--accent);width:9px;height:9px;box-shadow:0 0 8px var(--accent);border-radius:50%;position:absolute;top:.5em;left:-25px}.obj-timeline time{font:.75rem/1.3 var(--mono);color:var(--accent)}.obj-timeline span{color:var(--muted);font-size:.9rem}.obj-graph{width:100%;height:auto;max-height:70vh;margin:0 auto;display:block}.obj[style*=" height:"] .obj-graph{max-height:calc(100% - 32px)}.obj-graph .edge{stroke:color-mix(in srgb, var(--accent) 55%, transparent);stroke-width:1.4px}.obj-graph .arrow{fill:var(--accent)}.obj-graph .node{fill:#3fd8ff26;stroke:var(--accent);stroke-width:1.5px}.obj-graph .node-label{fill:var(--ink);font:10px var(--ui)}.obj-graph .edge-label{fill:var(--muted);font:9px var(--ui)}.captions{z-index:20;left:50%;bottom:max(7%, env(safe-area-inset-bottom));text-align:center;pointer-events:none;text-shadow:0 0 18px #000,0 0 4px #000;text-wrap:balance;width:min(780px,100% - 32px);margin:0;font-size:clamp(1.1rem,2.6vw,1.6rem);font-weight:300;position:absolute;transform:translate(-50%)}.captions.done{animation:1.4s 8s forwards fadeOut}@keyframes fadeOut{to{opacity:0}}.command-bar{z-index:30;left:50%;bottom:max(14px, env(safe-area-inset-bottom));background:var(--glass);border:1px solid var(--edge);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);gap:8px;width:min(640px,100% - 24px);padding:8px;display:flex;position:absolute;transform:translate(-50%)}.command-bar input{background:0 0;border:0;outline:none;flex:1;min-height:44px;padding:0 10px;font-size:1.05rem}.command-bar button{border:1px solid var(--edge);background:0 0;min-width:44px;min-height:44px}.conn-badge{top:max(10px, env(safe-area-inset-top));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--warn);border:1px solid var(--warn);background:#0009;padding:6px 12px;position:absolute;right:12px}.mic-indicator{left:50%;top:max(12px, env(safe-area-inset-top));font:500 .82rem var(--mono);letter-spacing:.1em;text-transform:uppercase;color:#ffd9a0;border:1px solid var(--warn);background:#000000c7;align-items:center;gap:10px;padding:8px 16px;display:flex;position:absolute;transform:translate(-50%)}.mic-indicator i{background:var(--bad);width:10px;height:10px;box-shadow:0 0 10px var(--bad);border-radius:50%;animation:1s ease-in-out infinite mic-pulse}.mic-transcribing i{background:var(--warn);box-shadow:0 0 10px var(--warn)}@keyframes mic-pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.mic-indicator i{animation:none}}.toasts{top:max(48px, env(safe-area-inset-top));gap:6px;width:min(420px,100% - 24px);display:grid;position:absolute;left:50%;transform:translate(-50%)}.toast{border:1px solid var(--edge);background:#000000d1;padding:10px 14px;font-size:.92rem}.toast-warn{border-color:var(--warn);color:#ffd9a0}.toast-error{border-color:var(--bad);color:#ffc2c9}.approval{border-color:var(--warn);background:#020c12eb;width:min(460px,100% - 24px);max-height:calc(100% - 24px);padding:20px;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #ffb45440}.plugins{width:min(560px,100% - 24px)}.plugins-heading{letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px;font-size:.78rem}.plugin-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.plugin-list li{background:#ffffff0a;justify-content:space-between;align-items:center;gap:10px;padding:8px 10px;display:flex}.plugin-info{min-width:0}.plugin-info .auth-note{font-size:.82rem}.plugin-actions{flex-shrink:0;gap:6px;display:flex}.plugin-actions button{border:1px solid var(--edge);background:0 0;padding:0 12px}.obj-sandbox{flex-direction:column;gap:6px;width:100%;min-width:min(560px,80vw);height:min(420px,60vh);display:flex}.obj-sandbox iframe{border:1px solid var(--edge);background:#050b10;flex:1;width:100%;min-height:200px}.camera-indicator{border-color:var(--bad);color:#ffc2c9;letter-spacing:.12em;text-transform:uppercase;background:#020c12e6;align-items:center;gap:10px;padding:6px 12px 6px 6px;font-size:.8rem;display:flex;position:absolute;bottom:12px;right:12px}.camera-indicator video{object-fit:cover;background:#000;width:72px;height:54px}.camera-indicator span:before{content:"";background:var(--bad);border-radius:50%;width:8px;height:8px;margin-right:8px;display:inline-block}.challenge,.enroll{border-color:var(--accent);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #3fd8ff40}.challenge .approval-title,.enroll .approval-title{color:var(--accent)}.approval-head{align-items:center;gap:10px;display:flex}.approval-title{letter-spacing:.18em;text-transform:uppercase;color:var(--warn);font-size:.8rem}.tier{font:600 .74rem var(--mono);border:1px solid;padding:2px 8px}.tier-T2{color:var(--warn)}.tier-T3{color:var(--bad)}.approval-summary{font:.95rem/1.5 var(--mono);word-break:break-all;background:#ffffff0a;max-height:40vh;margin:14px 0;padding:10px 12px;overflow:auto}.approval-actions{flex-wrap:wrap;gap:8px;margin-top:14px;display:flex}.approval-actions button,.owner-menu button,.auth button{border:1px solid var(--edge);letter-spacing:.04em;background:0 0;padding:0 16px}button.primary{background:color-mix(in srgb, var(--accent) 22%, transparent);border-color:var(--accent)}button.danger{border-color:var(--bad);color:#ffc2c9}button:disabled{opacity:.4;cursor:not-allowed}.pinpad{justify-items:center;gap:8px;display:grid}.pin-dots{letter-spacing:.4em;min-height:1.6em;color:var(--accent);font-size:1.6rem}.pin-keys{grid-template-columns:repeat(3,64px);gap:8px;display:grid}.pin-keys button{border:1px solid var(--edge);background:#3fd8ff0d;font-size:1.2rem}.owner-menu{top:max(60px, env(safe-area-inset-top));background:#020c12f0;gap:8px;width:220px;padding:12px;display:grid;left:12px}.boot{background:#000;height:100%}.auth{gap:8px;width:min(380px,100% - 28px);padding:24px;display:grid;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.auth h1{letter-spacing:.3em;text-transform:uppercase;color:var(--accent);margin:0 0 6px;font-size:1.1rem;font-weight:300}.auth label{letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:6px;font-size:.78rem}.auth input{border:1px solid var(--edge);background:#ffffff0a;padding:0 12px}.auth button{margin-top:12px}.auth-note{color:var(--muted);margin:0;font-size:.9rem}.auth-error{color:#ffc2c9;margin:4px 0 0;font-size:.9rem}@media (width<=599px){.presence-full{inset:0}.captions{bottom:max(11%, env(safe-area-inset-bottom))}}@media (prefers-reduced-motion:reduce){.obj,.presence{transition:none;animation:none}}.status-badge{left:12px;bottom:max(12px, env(safe-area-inset-bottom));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--ink);border:1px solid var(--edge);background:#0000008c;align-items:center;gap:9px;max-width:min(60vw,360px);padding:6px 12px;display:flex;position:absolute}.status-badge span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.status-badge i{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.status-idle i{background:var(--ok);box-shadow:0 0 8px var(--ok)}.status-working i,.status-speaking i{background:var(--accent);box-shadow:0 0 10px var(--accent);animation:1s ease-in-out infinite mic-pulse}.status-listening i{background:var(--bad);box-shadow:0 0 10px var(--bad);animation:1s ease-in-out infinite mic-pulse}.status-waiting{border-color:var(--warn);color:#ffd9a0}.status-waiting i{background:var(--warn);box-shadow:0 0 10px var(--warn);animation:1s ease-in-out infinite mic-pulse}.status-offline{border-color:var(--warn);color:var(--warn)}.status-offline i{background:var(--warn)}@media (prefers-reduced-motion:reduce){.status-badge i{animation:none!important}}.clock{right:12px;bottom:max(12px, env(safe-area-inset-bottom));text-align:right;color:var(--muted);line-height:1.15;font-family:var(--mono);pointer-events:none;position:absolute}.clock b{color:var(--ink);letter-spacing:.06em;font-size:1.15rem;font-weight:500;display:block}.clock span{letter-spacing:.1em;text-transform:uppercase;font-size:.7rem}.settings{flex-direction:column;width:min(520px,100% - 24px);max-height:calc(100% - 24px);display:flex}.settings-body{gap:6px;min-height:0;padding-right:4px;display:grid;overflow-y:auto}.settings-field{gap:3px;display:grid}.settings-field>span{letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-size:.74rem}.settings-field input:not([type=range]):not([type=checkbox]),.settings-field select{border:1px solid var(--edge);color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;padding:0 10px}.settings-field select option{background:#06141c}.settings-check{align-items:center;gap:10px;min-height:40px;display:flex}.slash-list{border:1px solid var(--edge);background:#020c12f5;max-height:40vh;margin:0;padding:4px;list-style:none;position:absolute;bottom:calc(100% + 6px);left:0;right:0;overflow-y:auto}.slash-list li{cursor:pointer;align-items:baseline;gap:10px;padding:7px 10px;display:flex}.slash-list li.on,.slash-list li:hover{background:color-mix(in srgb, var(--accent) 18%, transparent)}.slash-list b{font:600 .9rem var(--mono);color:var(--accent)}.slash-list i{font:.78rem var(--mono);color:var(--warn);font-style:normal}.slash-list span{color:var(--muted);font-size:.82rem}.movable{position:absolute}.movable[data-panel=status],.movable[data-panel=clock]{cursor:grab;touch-action:none;pointer-events:auto}.movable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.panel{resize:both;flex-direction:column;width:clamp(260px,28vw,420px);min-width:220px;min-height:140px;max-height:80vh;padding:0;display:flex;overflow:hidden}.panel-chat{height:62vh}.panel-tasks{width:clamp(240px,26vw,360px);height:40vh}.panel-head{cursor:grab;touch-action:none;border-bottom:1px solid var(--edge);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;display:flex}.panel-head .obj-close{position:static}.chat-lines{flex:1;align-content:start;gap:8px;min-height:0;padding:10px 12px;display:grid;overflow-y:auto}.chat-line{white-space:pre-wrap;overflow-wrap:anywhere;max-width:92%;margin:0;padding:7px 10px;font-size:.9rem}.chat-user{background:color-mix(in srgb, var(--accent) 18%, transparent);border:1px solid var(--edge);justify-self:end}.chat-assistant{background:#ffffff0d;justify-self:start}.chat-form{border-top:1px solid var(--edge);gap:6px;padding:8px;display:flex}.chat-form input{border:1px solid var(--edge);min-width:0;color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;flex:1;padding:0 10px}.chat-form button{border:1px solid var(--edge);color:var(--ink);background:0 0;min-height:40px;padding:0 14px}.task-body{flex:1;min-height:0;padding:10px 12px;overflow-y:auto}.task-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.task-list li{border-left:3px solid var(--accent);background:#ffffff0a;gap:2px;padding:8px 10px;display:grid}.task-list li.overdue{border-left-color:var(--bad)}.task-title{overflow-wrap:anywhere;font-size:.95rem}.task-due{font:.75rem var(--mono);color:var(--muted)}.task-list li.overdue .task-due{color:var(--bad)}@media (width<=600px){.panel{resize:none;width:calc(100% - 24px);left:12px!important}.panel-chat{height:52vh;bottom:56px;top:auto!important}.panel-tasks{height:30vh;top:56px!important}}.help{flex-direction:column;width:min(560px,100% - 24px);max-height:calc(100% - 24px);display:flex}.help-list{gap:4px;min-height:0;margin:8px 0;display:grid;overflow-y:auto}.help-row{border-bottom:1px solid #ffffff0f;grid-template-columns:minmax(130px,38%) 1fr;gap:12px;padding:6px 4px;display:grid}.help-row dt{font:600 .9rem var(--mono);color:var(--accent);overflow-wrap:anywhere}.help-row dt i{color:var(--warn);font-style:normal;font-weight:400}.help-row dd{color:var(--ink);margin:0;font-size:.9rem}@media (width<=600px){.help-row{grid-template-columns:1fr;gap:2px}}.connectors{width:min(600px,100% - 24px);max-height:calc(100% - 24px);overflow-y:auto}.connector-form{gap:8px;display:grid}.connector-form textarea,.settings-field textarea{border:1px solid var(--edge);color:var(--ink);font:.85rem var(--mono);resize:vertical;background:#ffffff0a;padding:8px 10px}.connector-steps{color:var(--muted);gap:4px;margin:0;padding-left:20px;font-size:.88rem;display:grid}.connector-warning{border-left:3px solid var(--warn);color:#ffd9a0;padding-left:10px}.conn-status{font:.74rem var(--mono)}.conn-ready{color:var(--ok)}.conn-error{color:var(--bad)}.conn-connecting{color:var(--warn)}.conn-off{color:var(--muted)}.settings-wide{width:min(980px,100% - 24px);height:min(720px,100% - 24px)}.settings-layout{flex:1;grid-template-columns:190px 1fr;gap:16px;min-height:0;margin-top:8px;display:grid}.settings-tabs{align-content:start;gap:6px;display:grid}.settings-tab{text-align:left;color:var(--ink);background:0 0;border:1px solid #0000;gap:2px;padding:10px 12px;display:grid}.settings-tab small{color:var(--muted);font-size:.74rem}.settings-tab:hover{background:#ffffff0d}.settings-tab.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 16%, transparent)}.settings-wide .settings-body{align-content:start;padding-right:8px}@media (width<=700px){.settings-wide{height:calc(100% - 24px)}.settings-layout{grid-template-columns:1fr;gap:8px}.settings-tabs{grid-auto-columns:1fr;grid-auto-flow:column;overflow-x:auto}.settings-tab small{display:none}}.look-tab{grid-template-columns:minmax(220px,300px) 1fr;align-items:start;gap:18px;display:grid}.look-side{gap:10px;display:grid;position:sticky;top:0}.look-controls{gap:6px;min-width:0;display:grid}@media (width<=860px){.look-tab{grid-template-columns:1fr}.look-side{position:static}}.look-preview{border:1px solid var(--edge);background:#000;height:230px;position:relative;overflow:hidden}.look-preview-glow{background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 65%);position:absolute;inset:0}.look-preview-stage{position:absolute;inset:0}.look-preview-caption{text-align:center;text-shadow:0 0 10px #000;margin:0;position:absolute;bottom:8px;left:8px;right:8px}.look-preview .presence-full{inset:0}.theme-grid{grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:8px;display:grid}.theme-card{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;justify-items:start;gap:1px;padding:10px;display:grid}.theme-card i{background:var(--swatch);width:22px;height:22px;box-shadow:0 0 12px var(--swatch);border-radius:50%;margin-bottom:4px}.theme-card small{color:var(--muted);font-size:.72rem}.theme-card.on,.style-choice.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 18%, transparent)}.style-row{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.style-choice{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;gap:2px;padding:10px;display:grid}.style-choice small{color:var(--muted);font-size:.72rem}.swatch-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.swatch{border:2px solid #0000;border-radius:50%;width:32px;height:32px;min-height:32px;padding:0}.swatch.on{box-shadow:0 0 0 2px var(--accent);border-color:#fff}.swatch-custom{color:var(--muted);align-items:center;gap:6px;font-size:.8rem;display:flex}.swatch-custom input[type=color]{border:1px solid var(--edge);background:0 0;width:40px;height:32px;padding:0}.captions-small{font-size:clamp(.9rem,2vw,1.15rem)}.captions-medium{font-size:clamp(1.1rem,2.6vw,1.6rem)}.captions-large{font-size:clamp(1.4rem,3.6vw,2.3rem)}.voice-hear{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.connector-command{border:1px solid var(--edge);font:.85rem var(--mono);-webkit-user-select:all;user-select:all;background:#ffffff0d;margin:0;padding:8px 10px;overflow-x:auto}html,body{overscroll-behavior:none}@media (pointer:coarse){.stage{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}input,textarea,select,.connector-command{-webkit-user-select:text;user-select:text}.owner-menu{max-height:calc(100dvh - 80px);overflow-y:auto}.touch-controls{z-index:25;left:0;right:0;bottom:max(14px, env(safe-area-inset-bottom));pointer-events:none;justify-content:center;align-items:flex-end;gap:26px;display:flex;position:absolute}.touch-controls>*{pointer-events:auto}.touch-talk{justify-items:center;gap:6px;display:grid}.touch-hint{max-width:70vw;font:500 .74rem var(--mono);letter-spacing:.06em;color:var(--muted);text-align:center;text-shadow:0 0 6px #000,0 0 2px #000;padding:2px 10px}.touch-mic,.touch-side{border:1px solid var(--edge);color:var(--ink);background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);touch-action:none;-webkit-user-select:none;user-select:none;border-radius:50%;place-items:center;padding:0;display:grid}.touch-mic{border-color:var(--accent);width:76px;height:76px;box-shadow:0 0 22px color-mix(in srgb, var(--accent) 30%, transparent)}.touch-mic.on{background:color-mix(in srgb, var(--bad) 40%, transparent);border-color:var(--bad);box-shadow:0 0 28px var(--bad);animation:1s ease-in-out infinite mic-pulse}.touch-side{touch-action:manipulation;width:52px;height:52px;margin-bottom:12px}@media (prefers-reduced-motion:reduce){.touch-mic.on{animation:none}}@media (pointer:coarse){.captions{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px)}.status-badge,.clock{bottom:max(8px, env(safe-area-inset-bottom))}.presence-corner{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 70px)}}@media (width<=600px) and (pointer:coarse){.panel-chat{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px);height:min(48vh,100dvh - 260px)}.objects-stack{padding-bottom:190px}}
|
|
1
|
+
:root{--accent:#3fd8ff;--warn:#ffb454;--bad:#ff6b7a;--ok:#5ef0b0;--ink:#d4ecf7;--muted:#7fa3b6;--glass:#06141c9e;--edge:color-mix(in srgb, var(--accent) 48%, transparent);--ui:"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;--mono:"Cascadia Mono", "SF Mono", Consolas, ui-monospace, monospace;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{color:var(--ink);font:16px/1.5 var(--ui);-webkit-tap-highlight-color:transparent;background:#000;overflow:hidden}button,input{font:inherit;color:inherit}button{cursor:pointer}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.stage{touch-action:manipulation;background:#000;position:fixed;inset:0;overflow:hidden}.ambient{pointer-events:none;background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%), linear-gradient(#3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px, linear-gradient(90deg, #3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px;position:absolute;inset:0;-webkit-mask-image:radial-gradient(#000 30%,#0000 78%);mask-image:radial-gradient(#000 30%,#0000 78%)}.presence{z-index:1;transition:inset .7s cubic-bezier(.2,.7,.2,1),width .7s,height .7s,opacity .5s;position:absolute}.presence-full{inset:0}.presence-corner{left:max(10px, env(safe-area-inset-left));bottom:max(52px, calc(env(safe-area-inset-bottom) + 42px));width:clamp(96px,16vw,190px);height:clamp(96px,16vw,190px)}.objects{pointer-events:none;position:absolute;inset:0}.objects-stack{pointer-events:auto;padding:max(56px, env(safe-area-inset-top)) 14px 140px;flex-direction:column;gap:12px;display:flex;overflow-y:auto}.auto-grid{pointer-events:none;padding:max(24px, env(safe-area-inset-top)) 24px 24px clamp(120px, 20vw, 230px);grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));align-content:safe center;align-items:start;gap:12px;display:grid;position:absolute;inset:0;overflow-y:auto}.auto-grid>.obj{pointer-events:auto}.objects-stack>.auto-grid{flex-direction:column;padding:0;display:flex;position:static;overflow:visible}.obj.leaving{pointer-events:none;animation:.35s forwards leave}@keyframes leave{to{opacity:0;transform:scale(.96)translateY(-6px)}}@media (prefers-reduced-motion:reduce){.obj.leaving{animation:none}}.glass{background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--edge);box-shadow:0 0 22px color-mix(in srgb, var(--accent) 16%, transparent), inset 0 0 28px #3fd8ff0d;clip-path:polygon(0 0,calc(100% - 14px) 0,100% 14px,100% 100%,14px 100%,0 calc(100% - 14px))}.obj{pointer-events:auto;min-width:0;padding:16px 18px;animation:.45s both appear;overflow:auto}.obj.alert{border-color:var(--warn)}@keyframes appear{0%{opacity:0;transform:translateY(8px)scale(.985)}}.obj-close{width:44px;height:44px;color:var(--muted);background:0 0;border:0;font-size:1.5rem;line-height:1;position:absolute;top:2px;right:6px}.obj-close:hover{color:var(--accent)}.obj-title{letter-spacing:.16em;text-transform:uppercase;color:var(--accent);margin:0 36px 8px 0;font-size:.78rem;font-weight:600}.obj-md>:first-child{margin-top:0}.obj-md>:last-child{margin-bottom:0}.obj-md a{color:var(--accent)}.obj-md code{font-family:var(--mono);background:#3fd8ff1a;padding:0 4px;font-size:.9em}.obj-stat{gap:2px;display:grid}.obj-label{letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-size:.78rem}.obj-value{font-variant-numeric:tabular-nums;font-size:clamp(2rem,6vw,3.6rem);font-weight:300;line-height:1.05}.obj-value small{color:var(--muted);margin-left:4px;font-size:.4em}.obj-delta{color:var(--ok);font-size:.9rem}.obj-list{gap:4px;margin:0;padding-left:1.1rem;display:grid}.obj-list .done{color:var(--muted);text-decoration:line-through}.obj-table-wrap{overflow-x:auto}.obj-table{border-collapse:collapse;width:100%;font-size:.92rem}.obj-table th{text-align:left;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);font-size:.72rem;font-weight:600}.obj-table th,.obj-table td{font-variant-numeric:tabular-nums;border-bottom:1px solid #3fd8ff2e;padding:6px 10px}.obj-code{font:.86rem/1.5 var(--mono);margin:0;overflow:auto}.obj-empty{color:var(--muted);margin:0}.obj-chart{width:100%;height:auto;display:block}.obj-chart .grid{stroke:#3fd8ff24;stroke-width:1px}.obj-chart .tick{fill:var(--muted);font:10px var(--mono);font-variant-numeric:tabular-nums}.legend{color:var(--muted);flex-wrap:wrap;gap:6px 14px;margin:6px 0 0;padding:0;font-size:.8rem;list-style:none;display:flex}.legend i{width:10px;height:10px;margin-right:6px;display:inline-block}.obj-gauge{justify-items:center;display:grid}.obj-gauge svg{width:min(100%,260px);height:auto}.obj-gauge .track{stroke:#3fd8ff26}.obj-gauge .value{stroke:var(--accent);filter:drop-shadow(0 0 6px var(--accent))}.obj-gauge.gauge-warn .value{stroke:var(--warn);filter:drop-shadow(0 0 6px var(--warn))}.obj-gauge.gauge-crit .value{stroke:var(--bad);filter:drop-shadow(0 0 6px var(--bad))}.gauge-read{align-items:baseline;gap:4px;margin-top:-62px;display:flex}.gauge-value{font-variant-numeric:tabular-nums;font-size:clamp(1.8rem,5vw,2.8rem);font-weight:300}.gauge-unit{color:var(--muted)}.obj-gauge .obj-label{margin-top:18px}.obj-timeline{border-left:1px solid var(--edge);gap:14px;margin:8px 0 0;padding:0 0 0 20px;list-style:none;display:grid}.obj-timeline li{gap:2px;display:grid;position:relative}.obj-timeline li:before{content:"";background:var(--accent);width:9px;height:9px;box-shadow:0 0 8px var(--accent);border-radius:50%;position:absolute;top:.5em;left:-25px}.obj-timeline time{font:.75rem/1.3 var(--mono);color:var(--accent)}.obj-timeline span{color:var(--muted);font-size:.9rem}.obj-graph{width:100%;height:auto;max-height:70vh;margin:0 auto;display:block}.obj[style*=" height:"] .obj-graph{max-height:calc(100% - 32px)}.obj-graph .edge{stroke:color-mix(in srgb, var(--accent) 55%, transparent);stroke-width:1.4px}.obj-graph .arrow{fill:var(--accent)}.obj-graph .node{fill:#3fd8ff26;stroke:var(--accent);stroke-width:1.5px}.obj-graph .node-label{fill:var(--ink);font:10px var(--ui)}.obj-graph .edge-label{fill:var(--muted);font:9px var(--ui)}.captions{z-index:20;left:50%;bottom:max(7%, env(safe-area-inset-bottom));text-align:center;pointer-events:none;text-shadow:0 0 18px #000,0 0 4px #000;text-wrap:balance;width:min(780px,100% - 32px);margin:0;font-size:clamp(1.1rem,2.6vw,1.6rem);font-weight:300;position:absolute;transform:translate(-50%)}.captions.done{animation:1.4s 8s forwards fadeOut}@keyframes fadeOut{to{opacity:0}}.command-bar{z-index:30;left:50%;bottom:max(14px, env(safe-area-inset-bottom));background:var(--glass);border:1px solid var(--edge);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);gap:8px;width:min(640px,100% - 24px);padding:8px;display:flex;position:absolute;transform:translate(-50%)}.command-bar input{background:0 0;border:0;outline:none;flex:1;min-height:44px;padding:0 10px;font-size:1.05rem}.command-bar button{border:1px solid var(--edge);background:0 0;min-width:44px;min-height:44px}.conn-badge{top:max(10px, env(safe-area-inset-top));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--warn);border:1px solid var(--warn);background:#0009;padding:6px 12px;position:absolute;right:12px}.mic-indicator{left:50%;top:max(12px, env(safe-area-inset-top));font:500 .82rem var(--mono);letter-spacing:.1em;text-transform:uppercase;color:#ffd9a0;border:1px solid var(--warn);background:#000000c7;align-items:center;gap:10px;padding:8px 16px;display:flex;position:absolute;transform:translate(-50%)}.mic-indicator i{background:var(--bad);width:10px;height:10px;box-shadow:0 0 10px var(--bad);border-radius:50%;animation:1s ease-in-out infinite mic-pulse}.mic-transcribing i{background:var(--warn);box-shadow:0 0 10px var(--warn)}@keyframes mic-pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.mic-indicator i{animation:none}}.toasts{top:max(48px, env(safe-area-inset-top));gap:6px;width:min(420px,100% - 24px);display:grid;position:absolute;left:50%;transform:translate(-50%)}.toast{border:1px solid var(--edge);background:#000000d1;padding:10px 14px;font-size:.92rem}.toast-warn{border-color:var(--warn);color:#ffd9a0}.toast-error{border-color:var(--bad);color:#ffc2c9}.approval{border-color:var(--warn);background:#020c12eb;width:min(460px,100% - 24px);max-height:calc(100% - 24px);padding:20px;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #ffb45440}.plugins{width:min(560px,100% - 24px)}.plugins-heading{letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px;font-size:.78rem}.plugin-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.plugin-list li{background:#ffffff0a;justify-content:space-between;align-items:center;gap:10px;padding:8px 10px;display:flex}.plugin-info{min-width:0}.plugin-info .auth-note{font-size:.82rem}.plugin-actions{flex-shrink:0;gap:6px;display:flex}.plugin-actions button{border:1px solid var(--edge);background:0 0;padding:0 12px}.obj-sandbox{flex-direction:column;gap:6px;width:100%;min-width:min(560px,80vw);height:min(420px,60vh);display:flex}.obj-sandbox iframe{border:1px solid var(--edge);background:#050b10;flex:1;width:100%;min-height:200px}.camera-indicator{border-color:var(--bad);color:#ffc2c9;letter-spacing:.12em;text-transform:uppercase;background:#020c12e6;align-items:center;gap:10px;padding:6px 12px 6px 6px;font-size:.8rem;display:flex;position:absolute;bottom:12px;right:12px}.camera-indicator video{object-fit:cover;background:#000;width:72px;height:54px}.camera-indicator span:before{content:"";background:var(--bad);border-radius:50%;width:8px;height:8px;margin-right:8px;display:inline-block}.challenge,.enroll{border-color:var(--accent);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #3fd8ff40}.challenge .approval-title,.enroll .approval-title{color:var(--accent)}.approval-head{align-items:center;gap:10px;display:flex}.approval-title{letter-spacing:.18em;text-transform:uppercase;color:var(--warn);font-size:.8rem}.tier{font:600 .74rem var(--mono);border:1px solid;padding:2px 8px}.tier-T2{color:var(--warn)}.tier-T3{color:var(--bad)}.approval-summary{font:.95rem/1.5 var(--mono);word-break:break-all;background:#ffffff0a;max-height:40vh;margin:14px 0;padding:10px 12px;overflow:auto}.approval-actions{flex-wrap:wrap;gap:8px;margin-top:14px;display:flex}.approval-actions button,.owner-menu button,.auth button{border:1px solid var(--edge);letter-spacing:.04em;background:0 0;padding:0 16px}button.primary{background:color-mix(in srgb, var(--accent) 22%, transparent);border-color:var(--accent)}button.danger{border-color:var(--bad);color:#ffc2c9}button:disabled{opacity:.4;cursor:not-allowed}.pinpad{justify-items:center;gap:8px;display:grid}.pin-dots{letter-spacing:.4em;min-height:1.6em;color:var(--accent);font-size:1.6rem}.pin-keys{grid-template-columns:repeat(3,64px);gap:8px;display:grid}.pin-keys button{border:1px solid var(--edge);background:#3fd8ff0d;font-size:1.2rem}.owner-menu{top:max(60px, env(safe-area-inset-top));background:#020c12f0;gap:8px;width:220px;padding:12px;display:grid;left:12px}.boot{background:#000;height:100%}.auth{gap:8px;width:min(380px,100% - 28px);padding:24px;display:grid;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.auth h1{letter-spacing:.3em;text-transform:uppercase;color:var(--accent);margin:0 0 6px;font-size:1.1rem;font-weight:300}.auth label{letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:6px;font-size:.78rem}.auth input{border:1px solid var(--edge);background:#ffffff0a;padding:0 12px}.auth button{margin-top:12px}.auth-note{color:var(--muted);margin:0;font-size:.9rem}.auth-error{color:#ffc2c9;margin:4px 0 0;font-size:.9rem}@media (width<=599px){.presence-full{inset:0}.captions{bottom:max(11%, env(safe-area-inset-bottom))}}@media (prefers-reduced-motion:reduce){.obj,.presence{transition:none;animation:none}}.status-badge{left:12px;bottom:max(12px, env(safe-area-inset-bottom));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--ink);border:1px solid var(--edge);background:#0000008c;align-items:center;gap:9px;max-width:min(60vw,360px);padding:6px 12px;display:flex;position:absolute}.status-badge span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.status-badge i{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.status-idle i{background:var(--ok);box-shadow:0 0 8px var(--ok)}.status-working i,.status-speaking i{background:var(--accent);box-shadow:0 0 10px var(--accent);animation:1s ease-in-out infinite mic-pulse}.status-listening i{background:var(--bad);box-shadow:0 0 10px var(--bad);animation:1s ease-in-out infinite mic-pulse}.status-waiting{border-color:var(--warn);color:#ffd9a0}.status-waiting i{background:var(--warn);box-shadow:0 0 10px var(--warn);animation:1s ease-in-out infinite mic-pulse}.status-offline{border-color:var(--warn);color:var(--warn)}.status-offline i{background:var(--warn)}@media (prefers-reduced-motion:reduce){.status-badge i{animation:none!important}}.clock{right:12px;bottom:max(12px, env(safe-area-inset-bottom));text-align:right;color:var(--muted);line-height:1.15;font-family:var(--mono);pointer-events:none;position:absolute}.clock b{color:var(--ink);letter-spacing:.06em;font-size:1.15rem;font-weight:500;display:block}.clock span{letter-spacing:.1em;text-transform:uppercase;font-size:.7rem}.settings{flex-direction:column;width:min(520px,100% - 24px);max-height:calc(100% - 24px);display:flex}.settings-body{gap:6px;min-height:0;padding-right:4px;display:grid;overflow-y:auto}.settings-field{gap:3px;display:grid}.settings-field>span{letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-size:.74rem}.settings-field input:not([type=range]):not([type=checkbox]),.settings-field select{border:1px solid var(--edge);color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;padding:0 10px}.settings-field select option{background:#06141c}.settings-check{align-items:center;gap:10px;min-height:40px;display:flex}.slash-list{border:1px solid var(--edge);background:#020c12f5;max-height:40vh;margin:0;padding:4px;list-style:none;position:absolute;bottom:calc(100% + 6px);left:0;right:0;overflow-y:auto}.slash-list li{cursor:pointer;align-items:baseline;gap:10px;padding:7px 10px;display:flex}.slash-list li.on,.slash-list li:hover{background:color-mix(in srgb, var(--accent) 18%, transparent)}.slash-list b{font:600 .9rem var(--mono);color:var(--accent)}.slash-list i{font:.78rem var(--mono);color:var(--warn);font-style:normal}.slash-list span{color:var(--muted);font-size:.82rem}.movable{position:absolute}.movable[data-panel=status],.movable[data-panel=clock]{cursor:grab;touch-action:none;pointer-events:auto}.movable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.panel{resize:both;flex-direction:column;width:clamp(260px,28vw,420px);min-width:220px;min-height:140px;max-height:80vh;padding:0;display:flex;overflow:hidden}.panel-chat{height:62vh}.panel-tasks{width:clamp(240px,26vw,360px);height:40vh}.panel-head{cursor:grab;touch-action:none;border-bottom:1px solid var(--edge);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;display:flex}.panel-head .obj-close{position:static}.chat-lines{flex:1;align-content:start;gap:8px;min-height:0;padding:10px 12px;display:grid;overflow-y:auto}.chat-line{white-space:pre-wrap;overflow-wrap:anywhere;max-width:92%;margin:0;padding:7px 10px;font-size:.9rem}.chat-user{background:color-mix(in srgb, var(--accent) 18%, transparent);border:1px solid var(--edge);justify-self:end}.chat-assistant{background:#ffffff0d;justify-self:start}.chat-form{border-top:1px solid var(--edge);gap:6px;padding:8px;display:flex}.chat-form input{border:1px solid var(--edge);min-width:0;color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;flex:1;padding:0 10px}.chat-form button{border:1px solid var(--edge);color:var(--ink);background:0 0;min-height:40px;padding:0 14px}.task-body{flex:1;min-height:0;padding:10px 12px;overflow-y:auto}.task-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.task-list li{border-left:3px solid var(--accent);background:#ffffff0a;gap:2px;padding:8px 10px;display:grid}.task-list li.overdue{border-left-color:var(--bad)}.task-title{overflow-wrap:anywhere;font-size:.95rem}.task-due{font:.75rem var(--mono);color:var(--muted)}.task-list li.overdue .task-due{color:var(--bad)}@media (width<=600px){.panel{resize:none;width:calc(100% - 24px);left:12px!important}.panel-chat{height:52vh;bottom:56px;top:auto!important}.panel-tasks{height:30vh;top:56px!important}}.help{flex-direction:column;width:min(560px,100% - 24px);max-height:calc(100% - 24px);display:flex}.help-list{gap:4px;min-height:0;margin:8px 0;display:grid;overflow-y:auto}.help-row{border-bottom:1px solid #ffffff0f;grid-template-columns:minmax(130px,38%) 1fr;gap:12px;padding:6px 4px;display:grid}.help-row dt{font:600 .9rem var(--mono);color:var(--accent);overflow-wrap:anywhere}.help-row dt i{color:var(--warn);font-style:normal;font-weight:400}.help-row dd{color:var(--ink);margin:0;font-size:.9rem}@media (width<=600px){.help-row{grid-template-columns:1fr;gap:2px}}.connectors{width:min(600px,100% - 24px);max-height:calc(100% - 24px);overflow-y:auto}.connector-form{gap:8px;display:grid}.connector-form textarea,.settings-field textarea{border:1px solid var(--edge);color:var(--ink);font:.85rem var(--mono);resize:vertical;background:#ffffff0a;padding:8px 10px}.connector-steps{color:var(--muted);gap:4px;margin:0;padding-left:20px;font-size:.88rem;display:grid}.connector-warning{border-left:3px solid var(--warn);color:#ffd9a0;padding-left:10px}.conn-status{font:.74rem var(--mono)}.conn-ready{color:var(--ok)}.conn-error{color:var(--bad)}.conn-connecting{color:var(--warn)}.conn-off{color:var(--muted)}.settings-wide{width:min(980px,100% - 24px);height:min(720px,100% - 24px)}.settings-layout{flex:1;grid-template-columns:190px 1fr;gap:16px;min-height:0;margin-top:8px;display:grid}.settings-tabs{align-content:start;gap:6px;display:grid}.settings-tab{text-align:left;color:var(--ink);background:0 0;border:1px solid #0000;gap:2px;padding:10px 12px;display:grid}.settings-tab small{color:var(--muted);font-size:.74rem}.settings-tab:hover{background:#ffffff0d}.settings-tab.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 16%, transparent)}.settings-wide .settings-body{align-content:start;padding-right:8px}@media (width<=700px){.settings-wide{height:calc(100% - 24px)}.settings-layout{grid-template-columns:1fr;gap:8px}.settings-tabs{grid-auto-columns:1fr;grid-auto-flow:column;overflow-x:auto}.settings-tab small{display:none}}.look-tab{grid-template-columns:minmax(220px,300px) 1fr;align-items:start;gap:18px;display:grid}.look-side{gap:10px;display:grid;position:sticky;top:0}.look-controls{gap:6px;min-width:0;display:grid}@media (width<=860px){.look-tab{grid-template-columns:1fr}.look-side{position:static}}.look-preview{border:1px solid var(--edge);background:#000;height:230px;position:relative;overflow:hidden}.look-preview-glow{background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 65%);position:absolute;inset:0}.look-preview-stage{position:absolute;inset:0}.look-preview-caption{text-align:center;text-shadow:0 0 10px #000;margin:0;position:absolute;bottom:8px;left:8px;right:8px}.look-preview .presence-full{inset:0}.theme-grid{grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:8px;display:grid}.theme-card{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;justify-items:start;gap:1px;padding:10px;display:grid}.theme-card i{background:var(--swatch);width:22px;height:22px;box-shadow:0 0 12px var(--swatch);border-radius:50%;margin-bottom:4px}.theme-card small{color:var(--muted);font-size:.72rem}.theme-card.on,.style-choice.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 18%, transparent)}.style-row{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.style-choice{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;gap:2px;padding:10px;display:grid}.style-choice small{color:var(--muted);font-size:.72rem}.swatch-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.swatch{border:2px solid #0000;border-radius:50%;width:32px;height:32px;min-height:32px;padding:0}.swatch.on{box-shadow:0 0 0 2px var(--accent);border-color:#fff}.swatch-custom{color:var(--muted);align-items:center;gap:6px;font-size:.8rem;display:flex}.swatch-custom input[type=color]{border:1px solid var(--edge);background:0 0;width:40px;height:32px;padding:0}.captions-small{font-size:clamp(.9rem,2vw,1.15rem)}.captions-medium{font-size:clamp(1.1rem,2.6vw,1.6rem)}.captions-large{font-size:clamp(1.4rem,3.6vw,2.3rem)}.voice-hear{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.connector-command{border:1px solid var(--edge);font:.85rem var(--mono);-webkit-user-select:all;user-select:all;background:#ffffff0d;margin:0;padding:8px 10px;overflow-x:auto}html,body{overscroll-behavior:none}@media (pointer:coarse){.stage{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}input,textarea,select,.connector-command{-webkit-user-select:text;user-select:text}.owner-menu{max-height:calc(100dvh - 80px);overflow-y:auto}.touch-controls{z-index:25;left:0;right:0;bottom:max(14px, env(safe-area-inset-bottom));pointer-events:none;justify-content:center;align-items:flex-end;gap:26px;display:flex;position:absolute}.touch-controls>*{pointer-events:auto}.touch-talk{justify-items:center;gap:6px;display:grid}.touch-hint{max-width:70vw;font:500 .74rem var(--mono);letter-spacing:.06em;color:var(--muted);text-align:center;text-shadow:0 0 6px #000,0 0 2px #000;padding:2px 10px}.touch-mic,.touch-side{border:1px solid var(--edge);color:var(--ink);background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;border-radius:50%;place-items:center;padding:0;display:grid}.touch-mic{border-color:var(--accent);width:76px;height:76px;box-shadow:0 0 22px color-mix(in srgb, var(--accent) 30%, transparent)}.touch-mic.on{background:color-mix(in srgb, var(--bad) 40%, transparent);border-color:var(--bad);box-shadow:0 0 28px var(--bad);animation:1s ease-in-out infinite mic-pulse}.touch-side{touch-action:manipulation;width:52px;height:52px;margin-bottom:12px}@media (prefers-reduced-motion:reduce){.touch-mic.on{animation:none}}@media (pointer:coarse){.captions{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px)}.status-badge,.clock{bottom:max(8px, env(safe-area-inset-bottom))}.presence-corner{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 70px)}}@media (width<=600px) and (pointer:coarse){.panel-chat{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px);height:min(48vh,100dvh - 260px)}.objects-stack{padding-bottom:190px}}.elsewhere{z-index:3000;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#000000c7;place-items:center;padding:16px;display:grid;position:fixed;inset:0}.elsewhere-card{border:1px solid var(--edge);background:var(--glass);max-width:380px;color:var(--ink);text-align:center;border-radius:16px;padding:24px}.elsewhere-card h2{margin:0 0 10px;font-size:1.15rem}.elsewhere-card p{color:var(--muted);margin:0 0 18px;line-height:1.45}.elsewhere-btn{border:1px solid var(--accent);background:color-mix(in srgb, var(--accent) 25%, transparent);min-height:48px;color:var(--ink);font:600 1rem var(--mono);cursor:pointer;border-radius:24px;padding:0 28px}
|