diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a72bfd01..3cab32893 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -248,6 +248,21 @@ jobs: sudo dpkg --add-architecture arm64 sudo apt-get update + # libgraphite2-3 / libharfbuzz0b / libfreetype6 / libssl3 are + # Multi-Arch: same — their :arm64 and :amd64 copies must be the + # EXACT same version. The runner ships stale :amd64 copies, and a + # jammy security bump (e.g. graphite2 1.3.14-1ubuntu0.1) made the + # :arm64 webkit dev chain demand a newer version than the preinstalled + # :amd64. apt then refuses the :arm64 copy ("not going to be + # installed" / held broken packages). Upgrading the preinstalled + # :amd64 copies first lets both arches land on the same version. + # --only-upgrade never installs new packages, so any lib that is + # absent is simply skipped. + sudo apt-get install -y --only-upgrade \ + libgraphite2-3 \ + libharfbuzz0b \ + libfreetype6 \ + libssl3 sudo apt-get install -y \ gcc-aarch64-linux-gnu \ g++-aarch64-linux-gnu \ @@ -455,6 +470,11 @@ jobs: sudo dpkg --add-architecture arm64 sudo apt-get update + # libssl3 is Multi-Arch: same; align the preinstalled :amd64 copy + # with the version libssl-dev:arm64 pins so the cross install isn't + # blocked by a held-back amd64 copy (same failure mode as the desktop + # arm64 build — see the build-tauri step for the full rationale). + sudo apt-get install -y --only-upgrade libssl3 sudo apt-get install -y \ gcc-aarch64-linux-gnu \ g++-aarch64-linux-gnu \ diff --git a/install.sh b/install.sh index 09fd37836..a225fcc48 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,7 @@ set -euo pipefail REPO="xintaofei/codeg" INSTALL_DIR="${CODEG_INSTALL_DIR:-/usr/local/bin}" +WEB_DIR="${CODEG_WEB_DIR:-/usr/local/share/codeg/web}" VERSION="" # Stale codeg-server / codeg-mcp binaries elsewhere in PATH are removed by # default so the user's `codeg-server` command always runs the freshly @@ -117,6 +118,66 @@ read_bin_version() { rm -f "$tmp" } +# ── Privilege model ── +# +# Root can write anywhere and must NEVER call `sudo`: minimal root environments +# (containers, slim images) frequently don't ship sudo, and a blind `sudo mkdir` +# there aborts the whole script under `set -e` AFTER the binaries already landed +# — leaving a half-installed tree the version short-circuit then refuses to +# repair. A non-root user needs sudo only when the destination's nearest +# existing ancestor isn't writable. + +PRIV="" +IS_ROOT=0 +# Conservative default: if `id -u` somehow fails, assume NON-root (echo 1) so we +# fall back to writability-probing + sudo rather than wrongly skipping elevation. +# This is still correct for a real root whose `id` broke: `[ -w ]` on existing +# system dirs is true for root, so resolve_priv runs directly anyway. +if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then + IS_ROOT=1 +fi + +HAVE_SUDO=0 +if command -v sudo >/dev/null 2>&1; then + HAVE_SUDO=1 +fi + +# Walk up from $1 to the first ancestor that already exists, so writability can +# be tested for a not-yet-created path (e.g. /usr/local/share/codeg/web, whose +# parent /usr/local/share/codeg also doesn't exist on a fresh install). +nearest_existing_ancestor() { + local p="$1" + while [ -n "$p" ] && [ "$p" != "/" ] && [ ! -e "$p" ]; do + p="$(dirname "$p")" + done + echo "$p" +} + +# Decide how to create/write into directory $1. Sets global PRIV to "" (run +# directly) or "sudo". Returns non-zero — without aborting under `set -e`, since +# callers invoke it via `if` — when elevation is required but sudo is absent. +resolve_priv() { + PRIV="" + [ "$IS_ROOT" -eq 1 ] && return 0 + local anchor + anchor="$(nearest_existing_ancestor "$1")" + [ -w "$anchor" ] && return 0 + if [ "$HAVE_SUDO" -eq 1 ]; then + PRIV="sudo" + return 0 + fi + return 1 +} + +# Run "$@", elevating with sudo only when the last resolve_priv call decided so. +priv_run() { + if [ -n "$PRIV" ]; then + sudo "$@" + else + "$@" + fi +} + # ── Scan PATH for codeg-server binaries that shadow the target install ── # # A binary "shadows" the install only if it appears in PATH BEFORE the @@ -188,16 +249,21 @@ fi TARGET_VER="${VERSION#v}" # Only short-circuit when the active binary is up to date AND the destination -# itself has it AND no other PATH entries shadow it. Otherwise we still need to -# install / clean up so the user's `codeg-server` command runs the new version. +# has it AND no other PATH entries shadow it AND the web assets are present. +# The web-asset check makes the installer self-healing: a prior run that placed +# the binary but failed before copying web/ (the classic root-without-sudo +# case) is repaired on re-run instead of exiting "nothing to do" forever. if [ -n "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" = "$TARGET_VER" ] \ && [ "${#PATH_CONFLICTS[@]}" -eq 0 ] \ - && [ -x "$DEST_BIN" ]; then - echo "codeg-server is already at version ${TARGET_VER}, nothing to do." + && [ -x "$DEST_BIN" ] \ + && [ -f "${WEB_DIR}/index.html" ]; then + echo "codeg-server is already at version ${TARGET_VER} with web assets in place, nothing to do." exit 0 fi -if [ -n "$CURRENT_VERSION" ]; then +if [ -n "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" = "$TARGET_VER" ]; then + echo "codeg-server is already at ${TARGET_VER}; reinstalling to repair the existing install..." +elif [ -n "$CURRENT_VERSION" ]; then echo "Upgrading codeg-server: ${CURRENT_VERSION} -> ${TARGET_VER}..." else echo "Installing codeg-server ${VERSION} (${PLATFORM}/${ARCH_SUFFIX})..." @@ -303,23 +369,28 @@ for _name in "${MANAGED_BINS[@]}"; do fi done -mkdir -p "$INSTALL_DIR" +# Resolve how to write into INSTALL_DIR, then create it and drop the binaries. +# Root writes directly; a non-root user uses sudo only when the prefix isn't +# already writable. Bail out clearly if elevation is needed but sudo is absent, +# instead of crashing mid-install under `set -e`. +if ! resolve_priv "$INSTALL_DIR"; then + echo "Error: need elevated privileges to install to ${INSTALL_DIR}, but 'sudo' is not installed." + echo " Re-run as root, install sudo, or set CODEG_INSTALL_DIR/CODEG_WEB_DIR to writable" + echo " paths (e.g. \$HOME/.local/bin and \$HOME/.local/share/codeg/web)." + exit 1 +fi +if [ -n "$PRIV" ]; then + echo "Need sudo to install to ${INSTALL_DIR}" +fi + +priv_run mkdir -p "$INSTALL_DIR" _install_one() { local name="$1" local src="${TMP_DIR}/${ARTIFACT}/${name}" local dst="${INSTALL_DIR}/${name}" - if [ -w "$INSTALL_DIR" ]; then - cp "$src" "$dst" - chmod +x "$dst" - else - sudo cp "$src" "$dst" - sudo chmod +x "$dst" - fi + priv_run cp "$src" "$dst" + priv_run chmod +x "$dst" } - -if [ ! -w "$INSTALL_DIR" ]; then - echo "Need sudo to install to ${INSTALL_DIR}" -fi for _name in "${MANAGED_BINS[@]}"; do _install_one "$_name" done @@ -332,17 +403,16 @@ DEST_BIN_REAL="$(canon_path "$DEST_BIN")" # ── Install web assets ── WEB_SRC="${TMP_DIR}/${ARTIFACT}/web" -WEB_DIR="${CODEG_WEB_DIR:-/usr/local/share/codeg/web}" if [ -d "$WEB_SRC" ]; then echo "Installing web assets to ${WEB_DIR}..." - if [ -w "$(dirname "$WEB_DIR")" ] 2>/dev/null; then - mkdir -p "$WEB_DIR" - cp -r "$WEB_SRC"/* "$WEB_DIR"/ - else - sudo mkdir -p "$WEB_DIR" - sudo cp -r "$WEB_SRC"/* "$WEB_DIR"/ + if ! resolve_priv "$WEB_DIR"; then + echo "Error: need elevated privileges to write ${WEB_DIR}, but 'sudo' is not installed." + echo " Re-run as root, install sudo, or set CODEG_WEB_DIR to a writable path." + exit 1 fi + priv_run mkdir -p "$WEB_DIR" + priv_run cp -r "$WEB_SRC"/* "$WEB_DIR"/ fi # ── Remove shadowing binaries from earlier PATH entries ── diff --git a/package.json b/package.json index 8f69ccd20..9639907fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeg", "private": true, - "version": "0.15.12", + "version": "0.15.13", "scripts": { "dev": "next dev --turbopack", "build": "next build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f54026498..e36d48374 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -982,7 +982,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "codeg" -version = "0.15.12" +version = "0.15.13" dependencies = [ "aes-gcm", "agent-client-protocol-schema", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6afa125dc..4ab4da99f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codeg" -version = "0.15.12" +version = "0.15.13" description = "Agent Code Generation App" authors = ["feitao"] edition = "2021" diff --git a/src-tauri/experts/skills/brainstorming/SKILL.md b/src-tauri/experts/skills/brainstorming/SKILL.md index 06cd0a21e..b0d52b258 100644 --- a/src-tauri/experts/skills/brainstorming/SKILL.md +++ b/src-tauri/experts/skills/brainstorming/SKILL.md @@ -22,7 +22,7 @@ Every project goes through this process. A todo list, a single-function utility, You MUST create a task for each of these items and complete them in order: 1. **Explore project context** — check files, docs, recent commits -2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below. +2. **Offer the visual companion just-in-time** — NOT upfront. The first time a question would genuinely be clearer shown than described, offer it then (its own message); on approval its browser tab opens for you. If no visual question ever arises, never offer it. See the Visual Companion section below. 3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria 4. **Propose 2-3 approaches** — with trade-offs and your recommendation 5. **Present design** — in sections scaled to their complexity, get user approval after each section @@ -36,8 +36,6 @@ You MUST create a task for each of these items and complete them in order: ```dot digraph brainstorming { "Explore project context" [shape=box]; - "Visual questions ahead?" [shape=diamond]; - "Offer Visual Companion\n(own message, no other content)" [shape=box]; "Ask clarifying questions" [shape=box]; "Propose 2-3 approaches" [shape=box]; "Present design sections" [shape=box]; @@ -47,10 +45,7 @@ digraph brainstorming { "User reviews spec?" [shape=diamond]; "Invoke writing-plans skill" [shape=doublecircle]; - "Explore project context" -> "Visual questions ahead?"; - "Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"]; - "Visual questions ahead?" -> "Ask clarifying questions" [label="no"]; - "Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions"; + "Explore project context" -> "Ask clarifying questions"; "Ask clarifying questions" -> "Propose 2-3 approaches"; "Propose 2-3 approaches" -> "Present design sections"; "Present design sections" -> "User approves design?"; @@ -148,10 +143,10 @@ Wait for the user's response. If they request changes, make them and re-run the A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser. -**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent: -> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)" +**Offering the companion (just-in-time):** Do NOT offer it upfront. Wait until a question would genuinely be clearer shown than told — a real mockup / layout / diagram question, not merely a UI *topic*. The first time that happens, offer it then, as its own message: +> "This next part might be easier if I show you — I can put together mockups, diagrams, and comparisons in a browser tab as we go. It's still new and can be token-intensive. Want me to? I'll open it for you." -**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming. +**This offer MUST be its own message.** Only the offer — no clarifying question, summary, or other content. Wait for the user's response. If they accept, start the server with `--open` so their browser opens to the first screen automatically. If they decline, continue text-only and don't offer again unless they raise it. **Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?** diff --git a/src-tauri/experts/skills/brainstorming/scripts/frame-template.html b/src-tauri/experts/skills/brainstorming/scripts/frame-template.html index dcfe01817..f540bb8a4 100644 --- a/src-tauri/experts/skills/brainstorming/scripts/frame-template.html +++ b/src-tauri/experts/skills/brainstorming/scripts/frame-template.html @@ -9,11 +9,11 @@ * * This template provides a consistent frame with: * - OS-aware light/dark theming - * - Fixed header and selection indicator bar + * - Header branding and connection status * - Scrollable main content area * - CSS helpers for common UI patterns * - * Content is injected via placeholder comment in #claude-content. + * Content is injected via placeholder comment in #frame-content. */ * { box-sizing: border-box; margin: 0; padding: 0; } @@ -63,34 +63,37 @@ } /* ===== FRAME STRUCTURE ===== */ - .header { - background: var(--bg-secondary); - padding: 0.5rem 1.5rem; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid var(--border); - flex-shrink: 0; + .brand { display: flex; align-items: center; min-width: 0; overflow: hidden; color: var(--text-secondary); line-height: 1; } + .brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; } + .brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); } + .brand-logo { display: block; height: 1em; width: auto; max-width: 180px; flex-shrink: 0; filter: invert(1); } + @media (prefers-color-scheme: dark) { + .brand-logo { filter: none; } } - .header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); } - .header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; } - .header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; } + .status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; justify-self: end; white-space: nowrap; line-height: 1; } + .status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; } .main { flex: 1; overflow-y: auto; } - #claude-content { padding: 2rem; min-height: 100%; } + #frame-content { padding: 2rem; min-height: 100%; } - .indicator-bar { + .header { background: var(--bg-secondary); - border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); padding: 0.5rem 1.5rem; flex-shrink: 0; - text-align: center; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 1rem; + min-height: 42px; } - .indicator-bar span { + .header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; } + .header .status { grid-column: 2; line-height: 1; } + .header span { font-size: 0.75rem; color: var(--text-secondary); } - .indicator-bar .selected-text { + .header .selected-text { color: var(--accent); font-weight: 500; } @@ -196,19 +199,15 @@