diff --git a/README.md b/README.md
index 3765e7e..632fabb 100644
--- a/README.md
+++ b/README.md
@@ -61,9 +61,9 @@ See [team setup and behavior](docs/TEAM.md) for commands, budgets, privacy bound
| Explicit local files | `external_list_dir`, `external_read_file`, `external_edit_file`, `external_write_file`, `external_grep` (regex), `external_glob`, `external_search_files` (literal) |
| Web | `web_fetch` (direct anonymous HTTP GET, HTML → text, short cache), `web_search` (remote search service); no browser login; Docker browser for login-gated pages |
| Background | `spawn_subagent` (returns immediately; `kind=computerUse` for desktop GUI), `check_subagent`, `message_subagent`, `stop_subagent`. Subagents and external command completion revive the turn; observe detached box commands with `await_shell`. |
-| Desktop GUI | Parent: `screenshot` (read-only). Clicks: `spawn_subagent kind=computerUse`, which gets `computer` (screenshot/click/move/drag/type/key/scroll/wait via xdotool on `DISPLAY=:1`). One computerUse per agent computer. Passwords still `request_box_help`. |
+| Desktop GUI | Parent: `screenshot` (read-only). Clicks: `spawn_subagent kind=computerUse`, which gets `computer` (screenshot/snapshot/click/move/drag/type/key/scroll/wait via xdotool on `DISPLAY=:1`; `snapshot` lists on-screen elements with centre coordinates from the AT-SPI accessibility tree). One computerUse per agent computer. Passwords still `request_box_help`. |
| MCP | `get_mcp_tools`, `call_mcp_tool`, `get_mcp_server_status`, `add_mcp_server`, `remove_mcp_server`. Config: `~/.lazyboy/mcp.json` (or `LAZYBOY_MCP_CONFIG`). Prefer a connector over the browser for that service. |
-| My computer (box) | Docker Linux desktop (XFCE + **Chromium** + 終端機 + xdotool). Named agents each get their own container; opening an agent starts it and the UI confirms ready/error. Session CLI keeps `lazyboy-box`. Default tools: `shell`, `read`, `await_shell`, `screenshot`. Profile: `/home/box/chrome-profile` on that seat. Viewer:`lazyboy computer` or the agent overlay. |
+| My computer (box) | Docker Linux desktop (TigerVNC + minimal XFCE in lazyBoy's Arc-Dark/粉圓 look, zh-TW/Asia/Taipei, **Chromium** + 終端機 + xdotool + AT-SPI). Named agents each get their own container; opening an agent starts it and the UI confirms ready/error. Session CLI keeps `lazyboy-box`. Default tools: `shell`, `read`, `await_shell`, `screenshot`. Profile: `/home/box/chrome-profile` on that seat. Viewer:`lazyboy computer` or the agent overlay. |
| Browser observation | `browser_navigate`, `browser_snapshot`, `browser_read_page`, `browser_tabs` |
| Browser actions | `browser_click`, `browser_type`, `browser_press`, `browser_select`, `browser_scroll`, `browser_wait` |
| Browser file exchange | `browser_upload`, `browser_download` |
diff --git a/box/Dockerfile b/box/Dockerfile
index 4be1608..cbaf7f7 100644
--- a/box/Dockerfile
+++ b/box/Dockerfile
@@ -1,26 +1,81 @@
+# LazyBoy's computer: a real Linux desktop the human (noVNC) and the agent
+# (xdotool + AT-SPI + CDP) share. Looks like lazyBoy's computer image:
+# Arc-Dark + Papirus + jf open 粉圓, Traditional Chinese, Taipei time.
+# One TigerVNC X server replaces Xvfb + x11vnc; no XFCE session manager.
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && apt-get install -y --no-install-recommends \
- xvfb x11-utils x11-xserver-utils x11vnc novnc websockify autocutsel xclip imagemagick xdotool \
- dbus-x11 xfce4-session xfce4-panel xfwm4 xfdesktop4 xfce4-settings \
- xfce4-terminal thunar \
- fonts-noto-core fonts-noto-cjk \
+# dpkg never unpacks docs/man/foreign locales; zh_TW catalogs are kept so the
+# XFCE menus are in Traditional Chinese.
+RUN printf '%s\n' \
+ 'path-exclude=/usr/share/doc/*' \
+ 'path-include=/usr/share/doc/*/copyright' \
+ 'path-exclude=/usr/share/man/*' \
+ 'path-exclude=/usr/share/info/*' \
+ 'path-exclude=/usr/share/gtk-doc/*' \
+ 'path-exclude=/usr/share/backgrounds/*' \
+ 'path-exclude=/usr/share/locale/*' \
+ 'path-include=/usr/share/locale/locale.alias' \
+ 'path-include=/usr/share/locale/zh_TW/*' \
+ 'path-include=/usr/share/locale/zh/*' \
+ > /etc/dpkg/dpkg.cfg.d/zz-lazyboy-slim \
+ && apt-get update && apt-get install -y --no-install-recommends \
+ tigervnc-standalone-server tigervnc-tools x11-utils x11-xserver-utils \
+ novnc websockify xclip imagemagick xdotool \
+ dbus-x11 xfconf xfce4-panel xfwm4 xfdesktop4 xfce4-settings xfce4-terminal \
+ arc-theme papirus-icon-theme adwaita-icon-theme gnome-themes-extra librsvg2-common \
+ at-spi2-core libatk-adaptor python3-gi gir1.2-atspi-2.0 gir1.2-gtk-3.0 \
+ dconf-gsettings-backend gsettings-desktop-schemas libglib2.0-bin \
+ fonts-dejavu-core fonts-liberation fonts-noto-cjk fonts-noto-color-emoji \
+ locales tzdata \
python3 curl ca-certificates git bash tini procps \
chromium \
+ && echo "zh_TW.UTF-8 UTF-8" >> /etc/locale.gen \
+ && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \
+ && locale-gen \
+ && ln -sf /usr/share/zoneinfo/Asia/Taipei /etc/localtime \
+ && echo "Asia/Taipei" > /etc/timezone \
+ && rm -rf /usr/share/icons/ePapirus /usr/share/icons/ePapirus-Dark /usr/share/icons/Papirus-Light \
+ && test -e /usr/share/icons/Papirus-Dark/48x48/apps/web-browser.svg \
+ && test -d /usr/share/themes/Arc-Dark/xfwm4 \
&& rm -rf /var/lib/apt/lists/*
+# jf open 粉圓 (OFL) as the desktop UI font, pinned like lazyBoy.
+RUN mkdir -p /usr/share/fonts/truetype/huninn \
+ && curl -fsSL https://github.com/justfont/open-huninn-font/releases/download/v2.1/jf-openhuninn-2.1.ttf \
+ -o /usr/share/fonts/truetype/huninn/jf-openhuninn-2.1.ttf \
+ && echo '9d5bf4932d31fe94c18cd8cfddc98bc1b14ce10f4e354c682179db290a99c825 /usr/share/fonts/truetype/huninn/jf-openhuninn-2.1.ttf' | sha256sum -c \
+ && fc-cache -f
COPY --from=node:22-bookworm-slim /usr/local/ /usr/local/
COPY playwright/ /opt/lazyboy/playwright/
RUN cd /opt/lazyboy/playwright && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --omit=dev
-RUN mkdir -p /workspace /home/box /usr/share/lazyboy \
+RUN mkdir -p /workspace /home/box /usr/share/lazyboy/xfce-skel /etc/xdg/xfce4 /usr/share/xfce4/helpers /etc/gtk-3.0 /etc/fonts/conf.d /usr/share/licenses/huninn \
&& chmod 1777 /workspace
-COPY start-desktop.sh stop-browser.py stop-browser.mjs box-chrome /usr/local/bin/
-COPY welcome.html /usr/share/lazyboy/welcome.html
-COPY autostart/ /etc/xdg/autostart/
-COPY desktop/ /usr/share/lazyboy/desktop/
-RUN chmod +x /usr/local/bin/start-desktop.sh /usr/local/bin/box-chrome
-ENV DISPLAY=:1
+COPY --chmod=755 start-desktop.sh stop-browser.py stop-browser.mjs box-chrome box-terminal box-a11y /usr/local/bin/
+COPY --chmod=644 welcome.html /usr/share/lazyboy/welcome.html
+COPY --chmod=644 xfce/wallpaper.svg /usr/share/lazyboy/wallpaper.svg
+COPY --chmod=644 xfce/xfce4-panel.xml xfce/xfwm4.xml xfce/xfce4-desktop.xml /usr/share/lazyboy/xfce-skel/
+COPY --chmod=644 xfce/helpers.rc /etc/xdg/xfce4/helpers.rc
+COPY --chmod=644 xfce/helper-browser.desktop /usr/share/xfce4/helpers/lazyboy-browser.desktop
+COPY --chmod=644 xfce/helper-terminal.desktop /usr/share/xfce4/helpers/lazyboy-terminal.desktop
+COPY --chmod=644 xfce/browser.desktop /usr/share/applications/lazyboy-browser.desktop
+COPY --chmod=644 xfce/terminal.desktop /usr/share/applications/lazyboy-terminal.desktop
+COPY --chmod=644 fonts.conf /etc/fonts/conf.d/99-lazyboy-cjk.conf
+COPY --chmod=644 gtk3-settings.ini /etc/gtk-3.0/settings.ini
+COPY --chmod=644 licenses/huninn-OFL.txt /usr/share/licenses/huninn/OFL.txt
+# Hide the stock launchers so the menu shows only 瀏覽器 / 終端機 (box-chrome
+# is the only supported way to start Chromium: same profile + CDP port).
+RUN for f in chromium xfce4-terminal debian-xterm xterm uxterm; do \
+ if [ -f "/usr/share/applications/${f}.desktop" ]; then \
+ printf '\nNoDisplay=true\nHidden=true\n' >> "/usr/share/applications/${f}.desktop"; \
+ fi; \
+ done \
+ && test -f /usr/share/locale/zh_TW/LC_MESSAGES/xfce4-panel.mo
+# Traditional Chinese locale, Taipei time and AT-SPI on for every process in
+# the box, including `docker exec` launches.
+ENV DISPLAY=:1 \
+ LANG=zh_TW.UTF-8 LC_ALL=zh_TW.UTF-8 LANGUAGE=zh_TW:zh:en TZ=Asia/Taipei \
+ GTK_MODULES=atk-bridge GTK_A11Y=atspi GNOME_ACCESSIBILITY=1 NO_AT_BRIDGE=0
WORKDIR /workspace
EXPOSE 6080
-LABEL lazyboy.box.revision=computer-use-5
+LABEL lazyboy.box.revision=computer-use-7
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/local/bin/start-desktop.sh"]
diff --git a/box/autostart/lazyboy-browser.desktop b/box/autostart/lazyboy-browser.desktop
deleted file mode 100644
index 0250922..0000000
--- a/box/autostart/lazyboy-browser.desktop
+++ /dev/null
@@ -1,5 +0,0 @@
-[Desktop Entry]
-Type=Application
-Name=Browser
-Exec=box-chrome /usr/share/lazyboy/welcome.html
-X-XFCE-Autostart-Enabled=true
diff --git a/box/autostart/lazyboy-terminal.desktop b/box/autostart/lazyboy-terminal.desktop
deleted file mode 100644
index ff42d76..0000000
--- a/box/autostart/lazyboy-terminal.desktop
+++ /dev/null
@@ -1,5 +0,0 @@
-[Desktop Entry]
-Type=Application
-Name=Terminal
-Exec=xfce4-terminal --title=終端機 --working-directory=/workspace
-X-XFCE-Autostart-Enabled=true
diff --git a/box/box-a11y b/box/box-a11y
new file mode 100755
index 0000000..3866b9f
--- /dev/null
+++ b/box/box-a11y
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+"""Accessibility snapshot of the box desktop (AT-SPI).
+
+Prints one line per interactive/visible element with its role, name and the
+screen-pixel centre the computer tool can click:
+
+ [12] button "登入" @ (640,412) 96x36 win="蝦皮購物 - Chromium"
+
+Usage: box-a11y [--json] [--limit N] [--budget SECONDS]
+Exit 0 with an empty list when nothing is exposed; exit 2 when AT-SPI itself is
+unreachable (the desktop or registry is not up).
+"""
+import argparse
+import json
+import os
+import sys
+import time
+import warnings
+
+warnings.filterwarnings("ignore", category=DeprecationWarning)
+
+# Always listed when on screen, even without a name (the agent can still
+# target them by position/value).
+INTERACTIVE = {
+ "push button", "toggle button", "radio button", "check box", "link",
+ "menu item", "check menu item", "radio menu item", "tab", "page tab",
+ "list item", "tree item", "combo box", "spin button", "slider",
+ "entry", "password text",
+}
+# Listed only when they carry a name or text (headings, labels, cells...).
+NAMED = {
+ "heading", "label", "static", "text", "image", "table cell",
+ "column header", "row header", "icon", "menu", "tool tip", "alert",
+ "dialog", "frame",
+}
+ROLE_SHORT = {
+ "push button": "button", "toggle button": "toggle", "radio button": "radio",
+ "check box": "checkbox", "menu item": "menuitem", "check menu item": "menuitem",
+ "radio menu item": "menuitem", "page tab": "tab", "list item": "listitem",
+ "tree item": "treeitem", "combo box": "combobox", "spin button": "spinner",
+ "password text": "password", "entry": "textbox", "text": "text",
+ "static": "text", "document frame": "document", "table cell": "cell",
+ "column header": "header", "row header": "header", "internal frame": "frame",
+}
+
+
+def load_dbus_env():
+ if os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
+ return
+ try:
+ with open("/tmp/lazyboy/dbus.env", encoding="utf-8") as fh:
+ for line in fh:
+ key, _, value = line.strip().partition("=")
+ if key == "DBUS_SESSION_BUS_ADDRESS" and value:
+ os.environ[key] = value
+ except OSError:
+ pass
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--json", action="store_true")
+ ap.add_argument("--limit", type=int, default=300)
+ ap.add_argument("--budget", type=float, default=6.0)
+ ap.add_argument("--max-depth", type=int, default=40)
+ args = ap.parse_args()
+
+ load_dbus_env()
+ os.environ.setdefault("DISPLAY", ":1")
+ try:
+ import gi
+ gi.require_version("Atspi", "2.0")
+ from gi.repository import Atspi
+ except Exception as exc: # noqa: BLE001
+ print(f"AT-SPI bindings unavailable: {exc}", file=sys.stderr)
+ return 2
+
+ try:
+ Atspi.init()
+ desktop = Atspi.get_desktop(0)
+ app_count = desktop.get_child_count()
+ except Exception as exc: # noqa: BLE001
+ print(f"AT-SPI registry unreachable: {exc}", file=sys.stderr)
+ return 2
+
+ screen_w = int(os.environ.get("LAZYBOY_SCREEN_W", "0") or 0)
+ screen_h = int(os.environ.get("LAZYBOY_SCREEN_H", "0") or 0)
+ deadline = time.monotonic() + args.budget
+ out = []
+ truncated = False
+ seen_apps = []
+
+ def showing(node):
+ try:
+ st = node.get_state_set()
+ return st.contains(Atspi.StateType.SHOWING) and st.contains(Atspi.StateType.VISIBLE)
+ except Exception: # noqa: BLE001
+ return False
+
+ def extents(node):
+ try:
+ comp = node.get_component() if hasattr(node, "get_component") else node.get_component_iface()
+ if comp is None:
+ return None
+ r = comp.get_extents(Atspi.CoordType.SCREEN)
+ return int(r.x), int(r.y), int(r.width), int(r.height)
+ except Exception: # noqa: BLE001
+ return None
+
+ def onscreen(box):
+ x, y, w, h = box
+ if w <= 0 or h <= 0:
+ return False
+ if screen_w and (x >= screen_w or x + w <= 0):
+ return False
+ if screen_h and (y >= screen_h or y + h <= 0):
+ return False
+ return True
+
+ def walk(node, depth, win_title):
+ nonlocal truncated
+ if truncated or time.monotonic() > deadline:
+ truncated = True
+ return
+ if depth > args.max_depth or len(out) >= args.limit:
+ truncated = truncated or len(out) >= args.limit
+ return
+ try:
+ role = node.get_role_name()
+ except Exception: # noqa: BLE001
+ return
+ if depth > 1 and not showing(node):
+ return
+ box = extents(node)
+ if depth > 1 and (box is None or not onscreen(box)):
+ return
+ name = ""
+ try:
+ name = (node.get_name() or "").strip()
+ except Exception: # noqa: BLE001
+ pass
+ if role in ("frame", "dialog", "window") and name:
+ win_title = name
+ try:
+ st = node.get_state_set()
+ focused = st.contains(Atspi.StateType.FOCUSED)
+ editable = st.contains(Atspi.StateType.EDITABLE)
+ checked = st.contains(Atspi.StateType.CHECKED)
+ selected = st.contains(Atspi.StateType.SELECTED)
+ disabled = not st.contains(Atspi.StateType.ENABLED)
+ except Exception: # noqa: BLE001
+ focused = editable = checked = selected = disabled = False
+ value = ""
+ if role in ("entry", "password text", "text", "spin button", "slider") or editable:
+ try:
+ txt = node.get_text() if hasattr(node, "get_text") else node.get_text_iface()
+ if txt is not None and role != "password text":
+ value = (txt.get_text(0, min(txt.get_character_count(), 80)) or "").strip()
+ except Exception: # noqa: BLE001
+ pass
+ printable = depth > 1 and box is not None and (
+ role in INTERACTIVE
+ or (role in NAMED and (name or value))
+ or (editable and role == "text")
+ )
+ if printable:
+ x, y, w, h = box
+ out.append({
+ "role": ROLE_SHORT.get(role, role.replace(" ", "")),
+ "name": name[:120],
+ "value": value[:80],
+ "x": x, "y": y, "w": w, "h": h,
+ "cx": x + w // 2, "cy": y + h // 2,
+ "focused": focused, "checked": checked, "selected": selected,
+ "disabled": disabled, "editable": editable,
+ "window": win_title,
+ })
+ try:
+ count = node.get_child_count()
+ except Exception: # noqa: BLE001
+ return
+ for i in range(min(count, 2000)):
+ if truncated:
+ return
+ try:
+ child = node.get_child_at_index(i)
+ except Exception: # noqa: BLE001
+ continue
+ if child is None:
+ continue
+ walk(child, depth + 1, win_title)
+
+ for i in range(app_count):
+ try:
+ app = desktop.get_child_at_index(i)
+ if app is None:
+ continue
+ app_name = app.get_name() or ""
+ except Exception: # noqa: BLE001
+ continue
+ if app_name in ("xfce4-panel", "wrapper-2.0", "xfdesktop", "xfwm4", "box-a11y", "python3"):
+ # The desktop shell is visible in the screenshot already; keep the
+ # list for the windows the agent actually works in.
+ continue
+ seen_apps.append(app_name)
+ walk(app, 0, app_name)
+
+ # Chromium mirrors some nodes (omnibox rows, nested list items); one entry
+ # per role/name/box is enough for targeting. Then top-to-bottom,
+ # left-to-right reading order.
+ unique = {}
+ for e in out:
+ unique.setdefault((e["role"], e["name"], e["cx"], e["cy"], e["w"], e["h"]), e)
+ window_order = {}
+ for e in unique.values():
+ window_order.setdefault(e["window"], len(window_order))
+ out = sorted(unique.values(), key=lambda e: (window_order[e["window"]], e["cy"] // 12, e["cx"]))
+ for i, e in enumerate(out):
+ e["id"] = i
+ if args.json:
+ json.dump({"apps": seen_apps, "elements": out, "truncated": truncated}, sys.stdout, ensure_ascii=False)
+ print()
+ return 0
+ lines = []
+ current_window = None
+ for e in out:
+ if len(window_order) > 1 and e["window"] != current_window:
+ current_window = e["window"]
+ lines.append(f"## {current_window}")
+ flags = []
+ if e["focused"]:
+ flags.append("focused")
+ if e["checked"]:
+ flags.append("checked")
+ if e["selected"]:
+ flags.append("selected")
+ if e["disabled"]:
+ flags.append("disabled")
+ label = f'"{e["name"]}"' if e["name"] else ""
+ if e["value"]:
+ label += f' value="{e["value"]}"'
+ parts = [f"[{e['id']}]", e["role"]]
+ if label:
+ parts.append(label)
+ parts.append(f"@ ({e['cx']},{e['cy']}) {e['w']}x{e['h']}")
+ if flags:
+ parts.append("(" + ", ".join(flags) + ")")
+ lines.append(" ".join(parts))
+ if not lines:
+ lines.append("(no accessible elements exposed; apps: " + ", ".join(seen_apps) + ")")
+ if truncated:
+ lines.append(f"... truncated at {len(out)} elements; scroll or focus a window and snapshot again")
+ print("\n".join(lines))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/box/box-chrome b/box/box-chrome
old mode 100644
new mode 100755
index 99855e7..892427c
--- a/box/box-chrome
+++ b/box/box-chrome
@@ -3,6 +3,25 @@
# attach to the browser the human sees (Firefox profiles cannot).
set -euo pipefail
export DISPLAY="${DISPLAY:-:1}"
+# Chromium reads HOME for its config/crash-report dirs; keep them on the
+# persisted /home/box volume rather than in /root.
+export HOME=/home/box
+export XDG_CONFIG_HOME=/home/box/.config
+export LANG="${LANG:-zh_TW.UTF-8}"
+export LC_ALL="${LC_ALL:-zh_TW.UTF-8}"
+export LANGUAGE="${LANGUAGE:-zh_TW:zh:en}"
+export TZ="${TZ:-Asia/Taipei}"
+export GTK_MODULES="${GTK_MODULES:-atk-bridge}"
+export GTK_A11Y="${GTK_A11Y:-atspi}"
+export GNOME_ACCESSIBILITY="${GNOME_ACCESSIBILITY:-1}"
+export NO_AT_BRIDGE=0
+# Join the desktop's session bus so Chromium registers with the same AT-SPI
+# registry that box-a11y reads (launches over `docker exec` start without it).
+if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" && -r /tmp/lazyboy/dbus.env ]]; then
+ # shellcheck disable=SC1091
+ source /tmp/lazyboy/dbus.env
+ export DBUS_SESSION_BUS_ADDRESS
+fi
PROFILE=/home/box/chrome-profile
CDP_PORT=9222
LAUNCH_LOCK=/tmp/.box-chrome-launch.lock
@@ -31,7 +50,11 @@ flock -w 30 9 || true
if profile_in_use; then
flock -u 9
exec 9>&-
- exec chromium --no-sandbox --user-data-dir="$PROFILE" "$@"
+ # This invocation only forwards its URLs to the running instance, but
+ # Chromium treats *this* command line as the startup command line when it
+ # decides whether to show the "unsupported flag: --no-sandbox" infobar in
+ # the new tab, so --test-type must be present here too.
+ exec chromium --no-sandbox --test-type --user-data-dir="$PROFILE" "$@"
fi
# The profile lives on a volume that outlives the container. Chromium's
@@ -42,16 +65,33 @@ fi
# stale by definition.
rm -f "$PROFILE/SingletonLock" "$PROFILE/SingletonSocket" "$PROFILE/SingletonCookie"
+# Flags mirror lazyBoy's lazyboy-browser (the desktop that passes Shopee's
+# checks): Traditional Chinese UI + Accept-Language, maximized normal window,
+# no translate/crash bubbles. Never add --kiosk or --app.
chromium \
--no-sandbox \
- --disable-dev-shm-usage \
- --disable-gpu \
- --no-first-run --restore-last-session \
- --no-default-browser-check \
- --password-store=basic \
- --user-data-dir="$PROFILE" \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port="$CDP_PORT" \
+ --test-type \
+ --disable-gpu \
+ --disable-dev-shm-usage \
+ --disable-features=TranslateUI \
+ --disable-blink-features=AutomationControlled \
+ --no-first-run \
+ --no-default-browser-check \
+ --disable-session-crashed-bubble \
+ --hide-crash-restore-bubble \
+ --disable-infobars \
+ --force-renderer-accessibility \
+ --disable-background-networking \
+ --disable-component-update \
+ --renderer-process-limit=4 \
+ --password-store=basic \
+ --lang=zh-TW \
+ --accept-lang=zh-TW,zh,en-US,en \
+ --start-maximized \
+ --restore-last-session \
+ --user-data-dir="$PROFILE" \
"$@" 9>&- &
pid=$!
trap 'kill -TERM "$pid" 2>/dev/null' TERM INT
diff --git a/box/box-terminal b/box/box-terminal
new file mode 100755
index 0000000..54b81db
--- /dev/null
+++ b/box/box-terminal
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Terminal on my computer: xfce4-terminal in /workspace. Plain bash keeps the
+# image small; --disable-server so each window is its own process.
+export LANG="${LANG:-zh_TW.UTF-8}"
+export LC_ALL="${LC_ALL:-zh_TW.UTF-8}"
+export LANGUAGE="${LANGUAGE:-zh_TW:zh:en}"
+export TERM="${TERM:-xterm-256color}"
+export COLORTERM="${COLORTERM:-truecolor}"
+cd /workspace 2>/dev/null || cd /
+exec xfce4-terminal \
+ --disable-server \
+ --geometry=100x28+40+40 \
+ --title=終端機 \
+ --working-directory=/workspace \
+ "$@"
diff --git a/box/desktop/browser.desktop b/box/desktop/browser.desktop
deleted file mode 100644
index 59bf6aa..0000000
--- a/box/desktop/browser.desktop
+++ /dev/null
@@ -1,9 +0,0 @@
-[Desktop Entry]
-Version=1.0
-Type=Application
-Name=瀏覽器
-Comment=Chrome on my computer
-Exec=box-chrome
-Icon=web-browser
-Terminal=false
-Categories=Network;
diff --git a/box/fonts.conf b/box/fonts.conf
new file mode 100644
index 0000000..b1faef7
--- /dev/null
+++ b/box/fonts.conf
@@ -0,0 +1,41 @@
+
+
+
登入、驗證碼、付款請在這個畫面完成。帳密只在這裡輸入,對話裡看不到。
-桌面上有「瀏覽器」和「終端機」。做完後回到聊天回覆「好了」。
+下方工作列有「瀏覽器」和「終端機」。做完後回到聊天回覆「好了」。