create-openclaw-bot 5.14.0 → 5.15.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/README.md +17 -2
- package/README.vi.md +17 -2
- package/dist/server/local-server.js +1653 -96
- package/dist/setup/shared/bot-config-gen.js +32 -0
- package/dist/setup/shared/docker-gen.js +82 -1
- package/dist/setup/shared/workspace-gen.js +9 -3
- package/dist/web/app.js +133 -6
- package/dist/web/styles.css +20 -1
- package/package.json +1 -1
|
@@ -183,6 +183,26 @@
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// ── messages (ack reaction) ───────────────────────────────────────────────
|
|
187
|
+
// Drop a reaction on inbound messages so people can see the bot registered theirs
|
|
188
|
+
// before the reply lands. This happens in the channel transport — the model is never
|
|
189
|
+
// involved, so it costs no tokens.
|
|
190
|
+
//
|
|
191
|
+
// Zalo-only, and deliberately so: zalo-connect reads this GLOBAL key (it has no
|
|
192
|
+
// per-channel ackReaction of its own), but the same key also feeds Telegram, Discord,
|
|
193
|
+
// Slack and WhatsApp, which each accept only their own fixed reaction set. Seeding an
|
|
194
|
+
// arbitrary emoji globally would make those channels reject every ack, so other
|
|
195
|
+
// channels keep their existing behaviour until someone picks a value valid there.
|
|
196
|
+
//
|
|
197
|
+
// 🦞 is sent as a Zalo *custom* reaction (emoji as rIcon + a hash as rType), which
|
|
198
|
+
// needs zalo-connect ≥3.0.15 — earlier versions only knew the 55 built-ins and would
|
|
199
|
+
// drop it silently. Scope "all" covers DMs and groups; in a mention-gated group the
|
|
200
|
+
// plugin still only reacts to messages that address the bot, since non-addressed ones
|
|
201
|
+
// are buffered as passive context before the reaction step.
|
|
202
|
+
if (isZaloPersonal(channelKey)) {
|
|
203
|
+
cfg.messages = { ackReaction: '🦞', ackReactionScope: 'all', removeAckAfterReply: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
// ── commands ──────────────────────────────────────────────────────────────
|
|
187
207
|
cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
|
|
188
208
|
if (selectedSkills.includes('scheduler')) {
|
|
@@ -228,6 +248,18 @@
|
|
|
228
248
|
if (alsoAllow.length > 0) {
|
|
229
249
|
cfg.tools.alsoAllow = alsoAllow;
|
|
230
250
|
}
|
|
251
|
+
// Hide OpenClaw's native `browser` tool: browsing goes through the browser-automation
|
|
252
|
+
// plugin's own CLI (skills/browser-automation/browser-tool.js), which adds the page-
|
|
253
|
+
// reading commands the native tool lacks (get_text, get_links) — that is the whole
|
|
254
|
+
// point of shipping the plugin. Left visible, the model reaches for the native tool
|
|
255
|
+
// instead and asks it for the container-local "openclaw" profile, which has no browser
|
|
256
|
+
// behind it, then reports "no browser available" while the real one sits unused.
|
|
257
|
+
// The bundled `browser` plugin stays allowed (it provides the browser-control service);
|
|
258
|
+
// only the tool is denied, and browser-tool.js drives Chrome directly over CDP.
|
|
259
|
+
if (hasBrowserDesktop || hasBrowserServer
|
|
260
|
+
|| selectedSkills.includes('browser') || selectedSkills.includes('browser-automation')) {
|
|
261
|
+
cfg.tools.deny = ['browser'];
|
|
262
|
+
}
|
|
231
263
|
// DuckDuckGo is the bundled, credential-free web_search provider. Auto-detect only
|
|
232
264
|
// picks providers that have credentials, so a free provider must be selected
|
|
233
265
|
// explicitly, otherwise web_search reports "no provider is available".
|
|
@@ -266,11 +266,14 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
266
266
|
// created/regenerated — so a plain rebuild would never pick them up):
|
|
267
267
|
// • skills.workshop.approvalPolicy:'auto' → the assistant can author a workspace
|
|
268
268
|
// skill end-to-end on request instead of stopping at "proposal awaiting approval".
|
|
269
|
+
// • tools.deny gains `browser` on projects that have browsing enabled, so the model
|
|
270
|
+
// stops reaching for the native tool instead of the plugin's browser-tool.js.
|
|
271
|
+
// (messages.ackReaction is handled separately, below, because it is Zalo-only.)
|
|
269
272
|
// • imageMaxDimensionPx / imageQuality / contextLimits.toolResultMaxChars → keep one
|
|
270
273
|
// heavy turn (deep research, 4K chart read-back) from overflowing the context
|
|
271
274
|
// window mid tool-loop, which cannot be compacted and poisons the session.
|
|
272
275
|
// Each key is only filled in when absent, so an operator's own tuning is never clobbered.
|
|
273
|
-
const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
276
|
+
const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}if(c.browser&&c.browser.enabled!==false){c.tools=c.tools||{};const dn=Array.isArray(c.tools.deny)?c.tools.deny:[];if(!dn.includes('browser')){dn.push('browser');c.tools.deny=dn;ch=true;}}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
274
277
|
// Companion backfill for the same older projects: their TOOLS.md was generated before the
|
|
275
278
|
// skill-authoring / long-turn guidance existed, and workspace files are only written when a
|
|
276
279
|
// bot is created — so a rebuild alone leaves the assistant stopping at "proposal awaiting
|
|
@@ -344,6 +347,13 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
344
347
|
// native ZaloConnect actions.
|
|
345
348
|
const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'clawhub:openclaw-zalo-connect';
|
|
346
349
|
runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
|
|
350
|
+
// Backfill the inbound-message ack reaction for projects created before it was
|
|
351
|
+
// seeded. Kept out of the shared migration because zalo-connect reads the GLOBAL
|
|
352
|
+
// messages.ackReaction, and that same key also feeds Telegram/Discord/Slack/
|
|
353
|
+
// WhatsApp — which accept only their own fixed reaction sets and would reject an
|
|
354
|
+
// arbitrary emoji. Absent-only, so an operator's own choice is never overwritten.
|
|
355
|
+
const ackReactionScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.messages=c.messages||{};if(c.messages.ackReaction===undefined){c.messages.ackReaction='🦞';ch=true;}if(c.messages.ackReactionScope===undefined){c.messages.ackReactionScope='all';ch=true;}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
356
|
+
runtimeParts.push(`node - <<'NODE'\n${ackReactionScript}\nNODE`);
|
|
347
357
|
}
|
|
348
358
|
// Always-on memory context engine for every bot (see bot-config-gen: plugins.slots
|
|
349
359
|
// .contextEngine = "learning-memory"). ensure_plugin skips if already installed.
|
|
@@ -355,6 +365,77 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
355
365
|
// Backfill skill-authoring + context defaults for configs from an older setup (see above).
|
|
356
366
|
runtimeParts.push(`node - <<'NODE'\n${contextDefaultsScript}\nNODE`);
|
|
357
367
|
runtimeParts.push(`node - <<'NODE'\n${agentsGuidanceScript}\nNODE`);
|
|
368
|
+
// browser-tool.js is a CDP client only — it has no code to launch a browser, so it
|
|
369
|
+
// needs something listening on a debug port. On a desktop that is the operator's own
|
|
370
|
+
// Chrome (started by start-chrome), reached through the host gateway. On a server there
|
|
371
|
+
// is no such Chrome, and browsing simply failed. Start a headless Chromium on loopback
|
|
372
|
+
// 9222 — the second entry in browser-tool.js's candidate list — so the same tool works
|
|
373
|
+
// on every OS.
|
|
374
|
+
//
|
|
375
|
+
// Emitted unconditionally and gated at RUNTIME on the plugin being installed, not on
|
|
376
|
+
// hasBrowser: the dashboard creates projects without that flag, so an operator who turns
|
|
377
|
+
// browser-automation on later (the common path) would otherwise never get this block.
|
|
378
|
+
//
|
|
379
|
+
// Chromium is baked into the image only when hasBrowser was known at build time. When it
|
|
380
|
+
// is missing — every project whose image predates the plugin — download it once instead
|
|
381
|
+
// of telling the bot to give up; that message is what makes it answer "there is no
|
|
382
|
+
// browser in my environment". The download goes to a path under $OPENCLAW_HOME, which is
|
|
383
|
+
// a bind mount, so recreating the container does not pay for it again. It runs in the
|
|
384
|
+
// background: the gateway must not wait ~150MB before answering messages.
|
|
385
|
+
//
|
|
386
|
+
// Skipped when a host Chrome is already reachable (it is tried first anyway) or when
|
|
387
|
+
// something already holds 9222, so a desktop does not pay for an idle browser.
|
|
388
|
+
runtimeParts.push([
|
|
389
|
+
// Not exported: the plugin (and anything else in the image) resolves Playwright's own
|
|
390
|
+
// cache, and pointing that at an empty directory would break a Chromium that IS baked in.
|
|
391
|
+
'openclaw_browsers_dir="$OPENCLAW_HOME/browsers"',
|
|
392
|
+
// The installed plugin folder, not the config: browsing needs browser-tool.js, which
|
|
393
|
+
// ships with the plugin, so "enabled in config but never installed" has nothing to serve.
|
|
394
|
+
'browser_automation_enabled() {',
|
|
395
|
+
' [ -d "$OPENCLAW_HOME/extensions/browser-automation" ]',
|
|
396
|
+
'}',
|
|
397
|
+
'find_chrome_bin() {',
|
|
398
|
+
' for candidate in /usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do',
|
|
399
|
+
' [ -x "$candidate" ] && echo "$candidate" && return 0',
|
|
400
|
+
' done',
|
|
401
|
+
' ls -d "$openclaw_browsers_dir"/chromium-*/chrome-linux*/chrome "$HOME"/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome 2>/dev/null | head -n 1',
|
|
402
|
+
'}',
|
|
403
|
+
'launch_headless_chrome() {',
|
|
404
|
+
' echo "[entrypoint] starting local headless Chromium on 127.0.0.1:9222"',
|
|
405
|
+
' "$1" --headless=new --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 \\',
|
|
406
|
+
' --no-sandbox --disable-dev-shm-usage --disable-gpu --no-first-run --no-default-browser-check \\',
|
|
407
|
+
' --user-data-dir=/tmp/openclaw-headless-chrome >/tmp/openclaw-headless-chrome.log 2>&1 &',
|
|
408
|
+
'}',
|
|
409
|
+
'start_local_headless_chrome() {',
|
|
410
|
+
' browser_automation_enabled || return 0',
|
|
411
|
+
' if curl -s -m 2 http://127.0.0.1:9222/json/version >/dev/null 2>&1; then return 0; fi',
|
|
412
|
+
' host_ip="$(getent hosts host.docker.internal 2>/dev/null | awk \'{print $1}\' | head -n 1)"',
|
|
413
|
+
' if [ -n "$host_ip" ] && curl -s -m 2 "http://$host_ip:9222/json/version" >/dev/null 2>&1; then',
|
|
414
|
+
' echo "[entrypoint] host Chrome reachable at $host_ip:9222; not starting a local one"',
|
|
415
|
+
' return 0',
|
|
416
|
+
' fi',
|
|
417
|
+
' chrome_bin="$(find_chrome_bin || true)"',
|
|
418
|
+
' if [ -n "$chrome_bin" ] && [ -x "$chrome_bin" ]; then',
|
|
419
|
+
' launch_headless_chrome "$chrome_bin"',
|
|
420
|
+
' return 0',
|
|
421
|
+
' fi',
|
|
422
|
+
' echo "[entrypoint] browser-automation is on but this image has no Chromium; downloading it once (~150MB) in the background"',
|
|
423
|
+
' (',
|
|
424
|
+
' mkdir -p "$openclaw_browsers_dir"',
|
|
425
|
+
' PLAYWRIGHT_BROWSERS_PATH="$openclaw_browsers_dir" npx --yes playwright install --with-deps chromium >/tmp/openclaw-chromium-install.log 2>&1 \\',
|
|
426
|
+
' || PLAYWRIGHT_BROWSERS_PATH="$openclaw_browsers_dir" npx --yes playwright install chromium >>/tmp/openclaw-chromium-install.log 2>&1',
|
|
427
|
+
' installed_bin="$(find_chrome_bin || true)"',
|
|
428
|
+
' if [ -n "$installed_bin" ] && [ -x "$installed_bin" ]; then',
|
|
429
|
+
' echo "[entrypoint] Chromium ready at $installed_bin"',
|
|
430
|
+
' launch_headless_chrome "$installed_bin"',
|
|
431
|
+
' else',
|
|
432
|
+
' echo "[entrypoint] Chromium download failed; see /tmp/openclaw-chromium-install.log — browsing still works if the operator runs start-chrome on the host"',
|
|
433
|
+
' fi',
|
|
434
|
+
' ) &',
|
|
435
|
+
'}',
|
|
436
|
+
// `set -e` is on: a non-zero return here must never stop the gateway from starting.
|
|
437
|
+
'start_local_headless_chrome || true',
|
|
438
|
+
].join('\n'));
|
|
358
439
|
runtimeParts.push('openclaw gateway run');
|
|
359
440
|
const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
|
|
360
441
|
let browserInstall = '';
|
|
@@ -1180,9 +1180,15 @@ description: Guide for utilizing tools and skills
|
|
|
1180
1180
|
: (isVi
|
|
1181
1181
|
? `Reaction phải nhắm vào **tin nhắn mới nhất của user** (inbound hiện tại), KHÔNG phải tin cũ hơn hay tin của chính mình.\n- Cách đúng: tool \`message\` với \`action: "react"\`, ${reactValue}, **KHÔNG truyền \`messageId\`** — tool sẽ tự reaction vào inbound message mới nhất. Chỉ truyền \`messageId\` khi cần reaction vào một tin CỤ THỂ không phải tin mới nhất.`
|
|
1182
1182
|
: `The reaction MUST target the **user's latest (current inbound) message**, not an older one or your own message.\n- Correct: \`message\` tool with \`action: "react"\`, ${reactValue}, **do NOT pass \`messageId\`** — the tool auto-reacts to the latest inbound message. Only pass \`messageId\` to react to a specific OLDER message.`);
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1183
|
+
// Zalo bots no longer carry this: the channel now drops an ack reaction on every
|
|
1184
|
+
// inbound message by itself (messages.ackReaction), so telling the model to react
|
|
1185
|
+
// as well only spends tokens and risks a second reaction on the same message.
|
|
1186
|
+
// Channels without that automatic ack keep the instruction.
|
|
1187
|
+
const dmOverride = isZalo
|
|
1188
|
+
? ''
|
|
1189
|
+
: isVi
|
|
1190
|
+
? `\n\n## ⚡ Reaction khi nhắn riêng (DM) — BẮT BUỘC\n- Khi DM với user, PHẢI luôn dùng tool/action reaction native để thả reaction CÙNG LÚC khi trả lời.\n- KHÔNG thả reaction trong group chat.\n- Chọn theo cảm xúc/ngữ cảnh tin của user. ${reactList}\n- ${reactHow}\n- ${reactExamples}`
|
|
1191
|
+
: `\n\n## ⚡ Reactions in DMs — MANDATORY\n- In DMs with the user, you MUST always use the native reaction tool/action to react WHILE replying.\n- Do NOT react in group chats.\n- Pick by the user's emotion/context. ${reactList}\n- ${reactHow}\n- ${reactExamples}`;
|
|
1186
1192
|
|
|
1187
1193
|
// Doc structure mirrors OpenClaw's default TOOLS.md (local environment notes + why it is a
|
|
1188
1194
|
// separate file), followed by the repo's tool-usage rules and the mandatory DM reaction guide.
|
package/dist/web/app.js
CHANGED
|
@@ -7,9 +7,13 @@ const OS_OPTIONS = [
|
|
|
7
7
|
{ id: 'linux-desktop', title: 'Linux Desktop', subtitle: 'Ubuntu / Debian / Fedora', icon: `${SVG_CDN}/linux/default.svg`, badge: 'Desktop' },
|
|
8
8
|
{ id: 'vps', title: 'Linux VPS', subtitle: 'Server install with public bind', icon: `${SVG_CDN}/ubuntu/default.svg`, badge: 'Server' },
|
|
9
9
|
];
|
|
10
|
-
// Docker
|
|
10
|
+
// Two deploy modes. Docker isolates the bot in a container; native runs openclaw + 9router
|
|
11
|
+
// straight on this machine as a managed service (launchd/systemd/schtasks). Native needs no
|
|
12
|
+
// Docker install, sees the host filesystem directly, and is the only mode where the bot can
|
|
13
|
+
// drive apps on the desktop — at the cost of the container's isolation.
|
|
11
14
|
const MODE_OPTIONS = [
|
|
12
15
|
{ id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
|
|
16
|
+
{ id: 'native', title: 'Native', subtitle: 'Runs on this machine, controls apps', icon: `${SVG_CDN}/gnubash/default.svg`, badge: 'Desktop' },
|
|
13
17
|
];
|
|
14
18
|
const BOT_CHANNELS = [
|
|
15
19
|
{ id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
|
|
@@ -105,7 +109,8 @@ function actionIcon(name) {
|
|
|
105
109
|
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
|
106
110
|
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
|
|
107
111
|
edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
|
|
108
|
-
spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>'
|
|
112
|
+
spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>',
|
|
113
|
+
key: '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>'
|
|
109
114
|
}[name];
|
|
110
115
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
|
111
116
|
}
|
|
@@ -212,6 +217,7 @@ function installModal() {
|
|
|
212
217
|
const osChoices = OS_OPTIONS.map(o => [o.id, t(o.title, o.title), trChoice(o).subtitle]);
|
|
213
218
|
const modeChoices = [
|
|
214
219
|
['docker', 'Docker', t('\u0043ontainer c\u00f4 l\u1eadp, an to\u00e0n nh\u1ea5t', 'Isolated containers, safest default')],
|
|
220
|
+
['native', 'Native', t('Ch\u1ea1y th\u1eb3ng tr\u00ean m\u00e1y n\u00e0y \u2014 kh\u00f4ng c\u1ea7n Docker, \u0111i\u1ec1u khi\u1ec3n \u0111\u01b0\u1ee3c app', 'Runs on this machine \u2014 no Docker, can drive desktop apps')],
|
|
215
221
|
];
|
|
216
222
|
return `<div class="modal-backdrop install-backdrop" data-install-modal="close">
|
|
217
223
|
<section class="donate-modal install-modal" role="dialog" aria-modal="true" aria-label="${t('T\u1ea1o Project','Create Project')}" onclick="event.stopPropagation()">
|
|
@@ -308,6 +314,52 @@ function openPathModal({ title, message, value = '', placeholder = '', field2 =
|
|
|
308
314
|
render();
|
|
309
315
|
setTimeout(() => document.getElementById('path-modal-input')?.focus(), 0);
|
|
310
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Shown right after PC control is granted on a native install: what the bot can now do, and the
|
|
319
|
+
* one thing only the operator can do — grant the OS screen permissions from the system settings.
|
|
320
|
+
*/
|
|
321
|
+
function openComputerUseModal(r = {}) {
|
|
322
|
+
const codex = r.codex || {};
|
|
323
|
+
const appOk = codex.app && codex.app.present;
|
|
324
|
+
const cli = (r.commands || []).includes('codex');
|
|
325
|
+
const line = (icon, text) => `<li><span aria-hidden="true">${icon}</span><div>${text}</div></li>`;
|
|
326
|
+
const statusItems = [
|
|
327
|
+
cli
|
|
328
|
+
? line('✅', t('Bot giao được việc cho <code>codex</code> — Codex tự nhìn màn hình, click, gõ phím rồi trả kết quả về.', 'The bot can hand jobs to <code>codex</code> — Codex looks at the screen, clicks and types, then reports back.'))
|
|
329
|
+
: line('⚠️', t('Không tìm thấy CLI <code>codex</code> — cài app ChatGPT/Codex rồi bật lại quyền này.', 'No <code>codex</code> CLI found — install the ChatGPT/Codex app, then re-enable this.')),
|
|
330
|
+
codex.pluginInstalled
|
|
331
|
+
? line('✅', t(`Computer Use trong app Codex đã ${codex.installedNow ? 'được cài' : 'sẵn sàng'}${codex.mcpRepaired ? ' (đã sửa khai báo MCP cũ)' : ''}.`, `Computer Use in the Codex app is ${codex.installedNow ? 'installed' : 'ready'}${codex.mcpRepaired ? ' (stale MCP entry repaired)' : ''}.`))
|
|
332
|
+
: line('⚠️', t(`Chưa bật được Computer Use trong app Codex: ${escapeHtml(codex.error || 'không rõ lý do')}`, `Could not enable Computer Use in the Codex app: ${escapeHtml(codex.error || 'unknown reason')}`)),
|
|
333
|
+
appOk
|
|
334
|
+
? line('✅', t('Đã thấy ứng dụng ChatGPT/Codex — <b>nhớ để app đang chạy</b>.', 'Found the ChatGPT/Codex desktop app — <b>keep it running</b>.'))
|
|
335
|
+
: line('⚠️', t('CHƯA thấy ứng dụng ChatGPT/Codex — cài rồi mở lên, không có nó thì không điều khiển GUI được.', 'No ChatGPT/Codex desktop app found — install and open it, GUI control needs it.')),
|
|
336
|
+
(r.granted && r.granted.length)
|
|
337
|
+
? line('✅', t(`Đã cấp quyền chạy: ${escapeHtml(r.granted.join(', '))}.`, `Granted: ${escapeHtml(r.granted.join(', '))}.`))
|
|
338
|
+
: '',
|
|
339
|
+
].filter(Boolean).join('');
|
|
340
|
+
state.confirmModal = {
|
|
341
|
+
icon: '🖥️',
|
|
342
|
+
eyebrow: t('Điều khiển máy','PC control'),
|
|
343
|
+
title: t('Xong — còn 1 bước bạn tự làm','Done — one step left for you'),
|
|
344
|
+
message: t('Model chính của bot vẫn là smart-route, không đổi.','Your bot keeps smart-route as its primary model.'),
|
|
345
|
+
bodyHtml: `
|
|
346
|
+
<ul class="cu-status">${statusItems}</ul>
|
|
347
|
+
<h4>${t('Cấp quyền màn hình cho máy','Grant the OS screen permissions')}</h4>
|
|
348
|
+
<p>${t('macOS chỉ cấp <b>Screen Recording</b> / <b>Accessibility</b> từ System Settings — bấm nút dưới, rồi bật cho <code>node</code> và app Codex.','macOS only grants <b>Screen Recording</b> / <b>Accessibility</b> from System Settings — click below, then tick <code>node</code> and the Codex app.')}</p>
|
|
349
|
+
<p class="cu-perm-row">
|
|
350
|
+
<button class="secondary cu-btn" type="button" data-host-perm="screen">${t('Chụp/quay màn hình','Screen recording')}</button>
|
|
351
|
+
<button class="secondary cu-btn" type="button" data-host-perm="accessibility">${t('Accessibility (chuột/bàn phím)','Accessibility (mouse/keys)')}</button>
|
|
352
|
+
</p>
|
|
353
|
+
<p class="cu-note">${t('Cách dùng: nhắn bot bình thường, ví dụ “mở TeamViewer và đọc giúp mật khẩu trên màn hình” — bot tự giao cho Codex rồi báo kết quả về.','How to use it: just ask your bot normally, e.g. “open TeamViewer and read the password on screen” — it hands the job to Codex and reports back.')}</p>
|
|
354
|
+
<p class="cu-note">${t('Việc giao cho Codex chạy bằng gói ChatGPT đã đăng nhập (tốn quota gói đó); chat thường vẫn đi qua các model free của <code>smart-route</code>. Điều khiển chuột/bàn phím hiện chỉ có trên macOS.','Jobs handed to Codex run on the signed-in ChatGPT plan (they spend that quota); ordinary chat still uses the free <code>smart-route</code> models. Mouse/keyboard control is macOS-only for now.')}</p>
|
|
355
|
+
`,
|
|
356
|
+
okText: t('Đã hiểu','Got it'),
|
|
357
|
+
okDanger: false,
|
|
358
|
+
hideCancel: true,
|
|
359
|
+
onConfirm: () => { state.confirmModal = null; render(); },
|
|
360
|
+
};
|
|
361
|
+
render();
|
|
362
|
+
}
|
|
311
363
|
async function pickFolderPathShared() {
|
|
312
364
|
try {
|
|
313
365
|
const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
|
|
@@ -816,6 +868,9 @@ function setupView() {
|
|
|
816
868
|
function botView() {
|
|
817
869
|
const s = state.install || {};
|
|
818
870
|
const sys = state.system || {};
|
|
871
|
+
// Native projects have no container: Rebuild (no image) and Grant disk (host FS is already
|
|
872
|
+
// reachable) do not apply, so those buttons are hidden rather than failing when pressed.
|
|
873
|
+
const isNativeMode = s.deployMode === 'native';
|
|
819
874
|
const bots = s.bots || [];
|
|
820
875
|
const ch = state.botChannel || 'telegram';
|
|
821
876
|
const channelBots = bots.filter(b => b.channel === ch);
|
|
@@ -870,10 +925,12 @@ function botView() {
|
|
|
870
925
|
<div class="runtime-status-card"><div class="runtime-status-head"><span>9Router</span>${statusBadge(s.routerStatus)}</div><div class="runtime-card-actions"><a class="runtime-open-btn secondary icon-btn2" href="${sameHostUrl(s.routerUrl, 20128)}" target="_blank" rel="noopener" style="justify-content:center; flex:1; font-size:12px; height:36px; border-width:1px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>${t('Mở web','Open')}</a><button class="runtime-open-btn icon-btn2" data-update-router type="button" style="justify-content:center; flex:1; font-size:12px; height:36px; border:none; background:rgba(255,36,54,.15); color:#ff4b5d;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>${t('Update','Update')}</button></div></div>
|
|
871
926
|
</div>
|
|
872
927
|
|
|
928
|
+
${''/* native: no container → no image to rebuild, and the host FS is already visible */}
|
|
873
929
|
<div class="bot-docker-actions" style="display:flex; gap:8px; margin-top:14px;">
|
|
874
930
|
<button class="secondary icon-btn2" data-bot-restart type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Khởi động lại container bot','Restart bot container')}">🔄 ${t('Restart','Restart')}</button>
|
|
875
|
-
|
|
876
|
-
|
|
931
|
+
${isNativeMode ? '' : `<button class="secondary icon-btn2" data-bot-rebuild type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="docker compose up -d --build">🔨 ${t('Rebuild','Rebuild')}</button>`}
|
|
932
|
+
${isNativeMode ? '' : `<button class="secondary icon-btn2" data-bot-add-mount type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Cấp quyền ổ đĩa/thư mục cho bot','Grant the bot a disk/folder')}">💽 ${t('Cấp quyền ổ đĩa','Grant disk')}</button>`}
|
|
933
|
+
<button class="secondary icon-btn2" data-bot-host-control type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Cho bot mở Chrome/ứng dụng trên máy này','Let the bot open Chrome/apps on this machine')}">🖥️ ${t('Điều khiển máy','Control PC')}</button>
|
|
877
934
|
</div>
|
|
878
935
|
|
|
879
936
|
<div class="dash-version-list" style="margin-top: 18px;">
|
|
@@ -1097,7 +1154,13 @@ function botSkillsPanel() {
|
|
|
1097
1154
|
if (isInstalled) {
|
|
1098
1155
|
toggleHtml = `<label class="feature-switch" title="${locked ? t('Bắt buộc cho bot Zalo — không thể tắt','Required for Zalo bots — cannot be disabled') : ''}"${locked ? ' style="opacity:.4;pointer-events:none;cursor:not-allowed;"' : ''}><input type="checkbox" data-feature-toggle="${key}" ${(flags[key] || locked) ? 'checked' : ''} ${(loading || locked) ? 'disabled' : ''}/><span></span></label>`;
|
|
1099
1156
|
if (item.openWebPort) {
|
|
1100
|
-
|
|
1157
|
+
// zalo-mod's dashboard is always gateway port + 1 (docker 18790, native 18890, or whatever
|
|
1158
|
+
// the SELECTED project's gateway runs on) — never the hardcoded default. Derive it from the
|
|
1159
|
+
// active project's gatewayUrl so every project opens its own dashboard.
|
|
1160
|
+
let gwPort = 0;
|
|
1161
|
+
try { gwPort = parseInt(new URL(state.install?.gatewayUrl).port, 10) || 0; } catch {}
|
|
1162
|
+
const openPort = (gwPort ? gwPort + 1 : item.openWebPort);
|
|
1163
|
+
secs.push(`<a class="secondary icon-btn2" href="${sameHostUrl('', openPort)}${item.openWebPath || ''}" target="_blank" rel="noopener" title="${t('Mở dashboard của plugin','Open the plugin dashboard')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px;">${actionIcon('link')}<span>${t('Mở web','Open')}</span></a>`);
|
|
1101
1164
|
}
|
|
1102
1165
|
if (requiresInstall) {
|
|
1103
1166
|
secs.push(`<button class="secondary icon-btn2 update-plugin-btn" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''} title="${t('Cập nhật lên bản mới nhất','Update to latest version')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#ffb020; border-color: rgba(255,176,32,0.25); background: rgba(255,176,32,0.05);">${actionIcon('refresh')}<span>${t('Cập nhật','Update')}</span></button>`);
|
|
@@ -1176,6 +1239,21 @@ function wireTab() {
|
|
|
1176
1239
|
if (action === 'cancel' || !m) { state.confirmModal = null; render(); return; }
|
|
1177
1240
|
if (action === 'ok' && typeof m.onConfirm === 'function') await m.onConfirm();
|
|
1178
1241
|
}));
|
|
1242
|
+
// Screen-recording / accessibility grants: only the OS can give these, so the button opens the
|
|
1243
|
+
// right settings pane (and on macOS pokes the capture API so the system prompt shows up).
|
|
1244
|
+
document.querySelectorAll('[data-host-perm]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
|
|
1245
|
+
const kind = btn.dataset.hostPerm;
|
|
1246
|
+
try {
|
|
1247
|
+
const r = await api('/api/host/permissions', { method: 'POST', body: { kind, projectDir: activeProjectDir() } });
|
|
1248
|
+
if (!r.opened) return showToast(t('Không mở được','Could not open'), t('Hệ điều hành này không có bảng cài đặt đó — cấp quyền thủ công.','This OS has no such settings pane — grant it manually.'), 'error');
|
|
1249
|
+
const granted = r.screen && r.screen.supported ? r.screen.granted : null;
|
|
1250
|
+
showToast(t('Đã mở cài đặt quyền','Opened settings'),
|
|
1251
|
+
granted === true ? t('Quyền chụp/quay màn hình: đã có. Bật thêm Accessibility nếu cần gõ/click.','Screen recording: already granted. Also enable Accessibility for typing/clicking.')
|
|
1252
|
+
: granted === false ? t('Chưa có quyền — bật cho "node" (và app Codex) trong danh sách vừa mở, rồi restart bot.','Not granted yet — tick "node" (and the Codex app) in the list that just opened, then restart the bot.')
|
|
1253
|
+
: t('Bật quyền cho "node" và app Codex trong danh sách vừa mở.','Tick "node" and the Codex app in the list that just opened.'),
|
|
1254
|
+
granted === false ? 'error' : 'success', 8000);
|
|
1255
|
+
} catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1256
|
+
}));
|
|
1179
1257
|
document.querySelectorAll('[data-pref]').forEach(btn => btn.onclick = () => {
|
|
1180
1258
|
state[btn.dataset.pref] = btn.dataset.value;
|
|
1181
1259
|
localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
|
|
@@ -1320,6 +1398,55 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1320
1398
|
} catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1321
1399
|
},
|
|
1322
1400
|
}));
|
|
1401
|
+
document.querySelectorAll('[data-bot-host-control]').forEach(btn => btn.onclick = async () => {
|
|
1402
|
+
let cur;
|
|
1403
|
+
try { cur = await api('/api/host/control' + projectQuery({})); }
|
|
1404
|
+
catch (err) { return showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1405
|
+
const on = !!cur.enabled;
|
|
1406
|
+
const apps = cur.apps || [];
|
|
1407
|
+
const grants = cur.grants || [];
|
|
1408
|
+
const chips = (items) => items.map((i) => `<code>${escapeHtml(i)}</code>`).join(' ');
|
|
1409
|
+
const scripts = grants.filter((g) => ['node', 'npx', 'codex'].includes(g));
|
|
1410
|
+
// One row per capability instead of a wall of prose — the operator is granting real access to
|
|
1411
|
+
// their machine and should be able to see, at a glance, exactly what each row means.
|
|
1412
|
+
const rows = [
|
|
1413
|
+
{ icon: '🗂', title: t('Mở ứng dụng','Open apps'), desc: apps.length ? chips(apps) : t('chưa dò được app nào','no apps detected') },
|
|
1414
|
+
...(cur.native ? [
|
|
1415
|
+
{ icon: '📸', title: t('Chụp & quay màn hình','Screen capture & recording'), desc: t('bot nhìn được màn hình khi bạn nhờ','the bot can see your screen when you ask') },
|
|
1416
|
+
{ icon: '🖱', title: t('Điều khiển chuột/bàn phím','Mouse & keyboard control'), desc: t('bot giao việc cho Codex CLI (dùng gói ChatGPT đã đăng nhập); macOS','the bot hands the job to the Codex CLI on your ChatGPT plan; macOS only') },
|
|
1417
|
+
{ icon: '⚙️', title: t('Chạy script','Run scripts'), desc: `${chips(scripts.length ? scripts : ['node'])} — <b>${t('chạy được mã tuỳ ý trên máy này','arbitrary code on this machine')}</b>` },
|
|
1418
|
+
] : []),
|
|
1419
|
+
];
|
|
1420
|
+
const bodyHtml = on ? '' : `
|
|
1421
|
+
<ul class="cu-status grant-list">${rows.map((r) => `<li><span aria-hidden="true">${r.icon}</span><div><b>${r.title}</b><br>${r.desc}</div></li>`).join('')}</ul>
|
|
1422
|
+
<p class="cu-note">${t('Danh sách sửa trong <code>.openclaw/host-control.json</code>. Chỉ dùng trên máy có màn hình (không áp dụng VPS headless).','Edit the list in <code>.openclaw/host-control.json</code>. Desktop only (not a headless VPS).')}</p>`;
|
|
1423
|
+
state.confirmModal = {
|
|
1424
|
+
icon: '🖥️',
|
|
1425
|
+
eyebrow: t('Điều khiển máy','PC control'),
|
|
1426
|
+
title: on ? t('Tắt điều khiển máy?','Turn off PC control?') : t('Bật điều khiển máy?','Turn on PC control?'),
|
|
1427
|
+
message: on
|
|
1428
|
+
? t('Bot sẽ không còn mở được Chrome/ứng dụng trên máy này nữa.','The bot will no longer be able to open Chrome/apps on this machine.')
|
|
1429
|
+
: t('Bạn sắp cho bot những quyền sau trên MÁY NÀY:','You are about to grant the bot the following on THIS machine:'),
|
|
1430
|
+
bodyHtml,
|
|
1431
|
+
okText: on ? t('Tắt','Turn off') : t('Bật','Turn on'),
|
|
1432
|
+
okDanger: on,
|
|
1433
|
+
onConfirm: async () => {
|
|
1434
|
+
state.confirmModal = null; render();
|
|
1435
|
+
if (!on) showToast(t('Đang bật','Enabling'), t('Đang cấp quyền & cài Computer Use (có thể mất ~30s)…','Granting access & installing Computer Use (may take ~30s)…'), 'success');
|
|
1436
|
+
try {
|
|
1437
|
+
const r = await api('/api/host/control', { method: 'POST', body: { enabled: !on, projectDir: activeProjectDir() } });
|
|
1438
|
+
if (!on && r.started && r.started.ok === false && r.started.reason) {
|
|
1439
|
+
showToast(t('Đã bật (lưu ý)','Enabled (note)'), t('Không khởi động được service: ','Service did not start: ') + r.started.reason, 'error');
|
|
1440
|
+
} else {
|
|
1441
|
+
showToast(r.enabled ? t('Đã bật điều khiển máy','PC control on') : t('Đã tắt','Turned off'),
|
|
1442
|
+
r.enabled ? t('Bot có thể mở Chrome/app trong danh sách.','The bot can open allow-listed Chrome/apps.') : t('Đã thu hồi quyền.','Access revoked.'), 'success');
|
|
1443
|
+
}
|
|
1444
|
+
if (r.enabled && r.native) openComputerUseModal(r);
|
|
1445
|
+
} catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
render();
|
|
1449
|
+
});
|
|
1323
1450
|
document.querySelectorAll('[data-project-remove]').forEach(btn => btn.onclick = (ev) => {
|
|
1324
1451
|
ev.stopPropagation();
|
|
1325
1452
|
const projectDir = btn.dataset.projectRemove;
|
|
@@ -1593,7 +1720,7 @@ function zaloToolbar(channelBots = []) {
|
|
|
1593
1720
|
const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
|
|
1594
1721
|
const health = zaloAccountHealth(active);
|
|
1595
1722
|
const loginLabel = health?.sessionSaved ? t('Đăng nhập lại','Log in again') : t('Đăng nhập Zalo','Zalo Login');
|
|
1596
|
-
return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button"
|
|
1723
|
+
return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button">${actionIcon('key')}<span>${loginLabel}</span></button></div>`;
|
|
1597
1724
|
}
|
|
1598
1725
|
async function loadZaloHealth(silent=false){
|
|
1599
1726
|
if (!activeProjectDir()) { state.zaloHealth = null; return; }
|
package/dist/web/styles.css
CHANGED
|
@@ -1812,7 +1812,7 @@ html[data-theme="light"] .bottom .nav.active {
|
|
|
1812
1812
|
}
|
|
1813
1813
|
|
|
1814
1814
|
/* Compact per-account Zalo health — shared versions stay in the right status column. */
|
|
1815
|
-
.zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px}.zalo-toolbar svg{width:14px;height:14px}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
|
|
1815
|
+
.zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px;gap:7px}.zalo-toolbar svg{width:14px;height:14px;flex:0 0 auto}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
|
|
1816
1816
|
|
|
1817
1817
|
/* Ensure bottom navigation bar is visible on tablet and mobile (under 1180px) */
|
|
1818
1818
|
@media (max-width: 1180px) {
|
|
@@ -1960,6 +1960,25 @@ body:has(.modal-backdrop) .bottom{display:none!important}
|
|
|
1960
1960
|
.confirm-body .cmd{margin-top:0;user-select:all;word-break:break-all;white-space:pre-wrap;font-size:11.5px;line-height:1.5}
|
|
1961
1961
|
.guide-step{margin:4px 0 0;color:var(--body);font-size:13px;font-weight:700}
|
|
1962
1962
|
|
|
1963
|
+
/* [Computer Use modal] Post-grant checklist: what got provisioned + the two manual steps
|
|
1964
|
+
(9router combo, OS screen permissions). Scrolls on short screens instead of clipping. */
|
|
1965
|
+
.confirm-body{max-height:min(58vh,520px);overflow-y:auto}
|
|
1966
|
+
.confirm-body h4{margin:10px 0 2px;font-size:13px;font-weight:800;color:var(--body)}
|
|
1967
|
+
.confirm-body p{margin:0;font-size:12.5px;line-height:1.6;color:var(--muted)}
|
|
1968
|
+
.confirm-body code{padding:1px 5px;border-radius:5px;background:rgba(127,127,127,.16);font-size:11.5px}
|
|
1969
|
+
.cu-status{margin:0;padding:0;list-style:none;display:grid;gap:6px}
|
|
1970
|
+
.cu-status li{display:flex;gap:8px;align-items:flex-start;font-size:12.5px;line-height:1.55;color:var(--body)}
|
|
1971
|
+
.cu-status li>span{flex:0 0 auto;line-height:1.5}
|
|
1972
|
+
.cu-status li>div{min-width:0}
|
|
1973
|
+
/* Grant checklist in the confirm step: boxed rows so each capability reads as its own item. */
|
|
1974
|
+
.grant-list li{gap:10px;padding:9px 11px;border-radius:10px;background:rgba(127,127,127,.10)}
|
|
1975
|
+
.grant-list b{font-weight:800}
|
|
1976
|
+
.grant-list li>div{color:var(--muted)}
|
|
1977
|
+
.cu-models{display:flex;flex-wrap:wrap;gap:6px}
|
|
1978
|
+
.cu-perm-row{display:flex;flex-wrap:wrap;gap:8px}
|
|
1979
|
+
.cu-btn{display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;border-radius:9px;font-size:12px;font-weight:700;text-decoration:none;cursor:pointer}
|
|
1980
|
+
.cu-note{margin-top:6px!important;padding:8px 10px;border-radius:9px;background:rgba(127,127,127,.12);font-size:11.5px!important}
|
|
1981
|
+
|
|
1963
1982
|
@media(max-width:760px){
|
|
1964
1983
|
/* [Topbar] Keep search + theme + language on ONE row; theme becomes icon-only and the
|
|
1965
1984
|
language globe icon is dropped so the whole bar stays compact. */
|