Skip to content

Dev#21

Merged
tmlxrd merged 134 commits into
mainfrom
dev
Jun 8, 2026
Merged

Dev#21
tmlxrd merged 134 commits into
mainfrom
dev

Conversation

@tmlxrd

@tmlxrd tmlxrd commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Changes

Screenshots / GIFs

How to test

Checklist

  • PR targets dev
  • npm run typecheck passes
  • npm run build passes
  • Updated CHANGELOG.md under [Unreleased] if user-visible
  • Updated ROADMAP.md if a planned item is now done
  • No new heavyweight dependencies (or motivated in the description)

tmlxrd and others added 30 commits June 1, 2026 11:30
Drop the @deprecated isE2ePlaintextExemptPath wrapper — zero callers in
src/ or test/; its replacement isE2ePlaintextTunnelExempt is the only path
used. Pure dead-code removal, no behaviour change.

Bump 0.2.1 → 0.2.2.

typecheck clean; 366 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
highlightCodeBlocks() called hljs.highlightElement() unconditionally, so
fences without a language tag fell back to highlight.js auto-detection.
That mis-classified prose as Ruby — an apostrophe (e.g. "пам'ятає") opens
a string literal and the github-dark theme dims the rest of the block,
making plain text hard to read.

Now highlight only when the fence declares a language hljs recognizes;
bare/unknown/text fences render as plain monospace (no auto-detect).
Explicit-language blocks still highlight normally, so Cyrillic in code
comments is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Config-driven modes in services/chatModes.ts (ask, execute + disabled
  research/image/safe_run placeholders); single source of truth, extensible.
- Persist mode per message (messages.mode) and per queued row
  (queued_messages.mode); idempotent ensureColumn migrations, plain TEXT.
- Thread mode frontend -> WS 'send' -> chatRunner -> queue flush; missing/
  unknown -> 'execute' so legacy chats and older clients keep working.
- Ask prepends a lightweight 'answer, don't execute' directive to the gateway
  message; Execute is byte-for-byte unchanged. Seams left for routing Ask to a
  no-tools OpenClaw profile or OpenRouter later (see chatModes.ts + README).
- Minimal composer pill + dropdown selector, choice persisted in localStorage.
Ask no longer relies only on a prompt instruction. When a tools-restricted
agent exists on the gateway (ICLAW_ASK_AGENT, default 'ask'), each Ask turn runs
on a throwaway session bound to it, so OpenClaw enforces its tool policy and the
model physically cannot use shell/file/browser tools. The fresh session is
seeded with a compact snapshot of the recent thread so Ask keeps context; the
reply lands in the same chat thread.

- config: loadAskAgentId() (ICLAW_ASK_AGENT; empty/off disables hard Ask)
- openclawWs: cached agentExists() via agents.list
- chatRunner: resolveTurnTarget() routes ask->ephemeral restricted session
  (seeded context) or falls back to the soft prompt-only preamble; Execute
  unchanged. activeRunSessionKeys lets abort target the ephemeral ask session.
- docs: README hard-vs-soft Ask + config example + v1 limits; .env.example.

v1 limitation: Execute's native session memory doesn't include Ask turns
(Ask->Execute bridge via chat.inject is a planned follow-up).
After a hard-Ask turn (which runs on a throwaway tools-restricted session), the
chat's main Execute session has no native memory of it. Append a compact [Ask]
Q&A note to the main session via chat.inject — no model run, zero cost — so a
later Execute turn sees the exchange. Best-effort; stored content keeps secret
placeholders so nothing sensitive is injected.

- openclawWs.injectMessage({sessionKey,message,label})
- chatRunner: buildAskBridgeNote + inject after successful hard-Ask turn
- README: context now bridged both ways
Previously Ask silently fell back to a prompt-only 'please don't use tools'
turn on the full-tools session when the ask agent wasn't configured — so the
model could (and did) still create files. That fail-open is removed.

Now: a lightweight (Ask) turn runs only on the tools-restricted ask agent. If
that agent isn't configured/present (or ICLAW_ASK_AGENT is empty), chatRunner
refuses the turn with a system note instead of running with tools. No
prompt-only path remains.

- chatRunner: fail-closed gate before turn start; resolveTurnTarget no longer
  has a soft branch.
- chatModes: drop ASK_PREAMBLE + applyModeToGatewayMessage (dead); update notes.
- README/.env.example: document fail-closed behavior (no prompt fallback).
- New openRouter.ts: streaming/non-streaming chat completions + audio
  transcription (transcribeAudio) via /chat/completions. No extra deps —
  Node 25 global fetch.
- New kv.ts: tiny iclaw_kv accessor (key/value over existing DB table)
  so config can read settings without pulling store.ts (cycle-free).
- config.ts: loadOpenRouterConfig reads API key from KV (set in Settings,
  not .env); adds setOpenRouterApiKey / clearOpenRouterApiKey / maskOpenRouterApiKey.
- chatTitle.ts: prefer cheap OpenRouter call for titles; fall back to
  OpenClaw session when key absent or call fails.
- chatModes.ts: Ask gated on openRouterEnabled() — hidden from composer
  without key, posted 'ask' coerced to 'execute'.
- chatRunner.ts: Ask branch replaced — direct OpenRouter stream (gemini-
  2.0-flash default), AbortController for Stop, chat.inject bridge into
  main OpenClaw session preserved. Removed ask-agent machinery (ICLAW_ASK_AGENT,
  resolveTurnTarget, ASK_PREAMBLE). No prompt-only fallback.
- index.ts route: POST /api/stt endpoint (express.raw, 25 MB limit) →
  transcribeAudio → {text}; sttEnabled local for views.
- chats.ts: pass sttEnabled to chat view.
- .env.example / README: key is now set in Settings UI, not env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add mic button next to the send button (rendered only when
OPENROUTER_API_KEY is set via Settings).

- composer.ejs: mic button with idle/recording/busy SVG states; gated on
  sttEnabled EJS local.
- iclaw.js: mic JS — MediaRecorder recording, pickMicMime(), transcribeBlob()
  POST to /api/stt, insertTranscript() into composer textarea on success.
  Stop mic on second click; spinner during transcription; alert on error.
- style.css: .composer-mic — mirrors .composer-attach tokens (32px circle,
  --muted, color-mix hover). Recording state: --danger color + 12% tint bg
  + pulse animation. Busy state: spinner replaces mic icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings is now sectioned by URL (/settings/voice-ask,
/settings/remote-access) with pill-tab navigation. /settings redirects
to the first tab.

Voice & Ask page:
- Minimal CTA: one-liner description + monospace key input + Connect
  button + openrouter.ai/keys link. No perks list (removed as noise).
- Connected state: green dot, masked key (sk-or-v••••••••xxxx),
  Disconnect button, live spend readout ($0.42 spent / $9.58 remaining
  + thin progress bar loaded async from /api/openrouter/usage).
- Endpoints: POST /api/openrouter/key (save+validate), DELETE
  /api/openrouter/key (clear), GET /api/openrouter/usage (fetchUsage).
- Key validated for sk-or- prefix; 400 on bad format.
- Page reloads on connect/disconnect so composer picks up new key state.

settings.ts: sidebarLocals() helper; per-tab routes with only the
locals each tab needs. RA script/modal/dialog gated in EJS so they
don't load on voice-ask tab.

sidebar.ejs: settings gear links directly to /settings/voice-ask.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extend ChatMode type to include 'work'
- Add Work Mode entry in CHAT_MODES catalog (requires OpenRouter key)
- Wire chatRunner to route 'work' turns to iclaw-runtime service
- Add workRuntime.ts HTTP client (port 7430, SSE events)
- Add packages/iclaw-runtime/ — NanoClaw fork stripped to essentials:
  - No OneCLI dependency (removed)
  - No Telegram/WhatsApp/Slack/Discord channels
  - OpenRouter via ANTHROPIC_BASE_URL env passthrough
  - New iclaw-http channel: HTTP/SSE API for iClaw browser UI
  - Approval flow simplified for browser-first approval
  - Mount security module intact (path traversal prevention)
- Convert repo to npm workspaces monorepo

Attribution: packages/iclaw-runtime/ forked from NanoClaw (MIT)
https://github.com/nanocoai/nanoclaw

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spawns packages/iclaw-runtime/src/index.ts via tsx when iClaw starts,
shuts it down on SIGINT/SIGTERM. Auto-restarts on crash (max 5 times
per minute). No-op when iclaw-runtime is not installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Voice & Ask section now shows a bullet list of what the key unlocks:
voice messages, Ask mode, Work mode (with description), auto-named chats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-2.0-flash not available on this account; gemini-2.5-flash is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini 2.5 Flash has thinking enabled by default on OpenRouter which
causes very long waits. Explicitly disable it for Ask mode and titles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MODE_STORAGE_KEY is now iclaw:composer-mode:<chatId> so switching
between chats doesn't reset each other's mode selection.
Draft chats migrate their mode to the per-chat key on id adoption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- iclaw-session: seed agent group with model + write "user" destination
  in inbound.db so agent can use <message to="user">
- iclaw-http: fix session wiring (sessionId as platformId), isMention:true
- container-runner: pass ANTHROPIC_API_KEY for OpenRouter auth
- openrouter-proxy: strip unsupported beta headers (claude-code-20250219,
  advisor-tool-2026-03-01) that cause 400 on OpenRouter Anthropic endpoint
- session-manager: write "user" destination row on writeSessionRouting
- config: default model claude-3-5-haiku-20241022 (available on OpenRouter)

Work Mode full flow now works:
  iClaw UI → chatRunner → workRuntime HTTP → iclaw-runtime →
  Docker container → OpenRouter (via proxy) → SSE stream back to UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ing loop

Remove Claude Code CLI dependency entirely. New agent:
- src/agent/loop.ts    — OpenRouter tool-calling cycle (any model)
- src/agent/tools.ts  — list_files, read_file, search_files, write_file, run_command
- src/agent/security.ts — path validation, blocked patterns (.env, .ssh, *.pem)
- src/sessions.ts     — in-memory session manager (no SQLite)
- src/index.ts        — minimal HTTP server (200 lines, replaces NanoClaw pipeline)

Works with any OpenRouter model that supports tool calling.
No Docker for Work Mode, no NanoClaw routing, no delivery pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reuse session across turns so conversation history is preserved
- Pass HOME as default allowedFolder (TODO: use project settings)
- Ignore 'aborted' SSE errors — they mean the stream closed normally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Folder icon button appears in composer when Work Mode is selected
- Click opens modal: add paths, remove with ×, stored in localStorage
- workFolders passed via WS payload → chatRunner → createWorkSession
- Session reuses folders from first turn (per chat)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/pick-folder opens osascript dialog on macOS (zenity/kdialog on Linux).
Browse button in folders modal — click selects folder natively, no typing required.
Manual path input kept as fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ect chats)

Storage key: iclaw:work-folders:project:<id> or iclaw:work-folders:no-project.
All chats in the same project share the same folder set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches modern chat UI style (Claude.ai-like):
- Textarea full width at top with 'Ask anything' placeholder
- Toolbar row at bottom: [+ mode folders] ... [mic send]
- Mode selector as flat text label in toolbar (no pill border)
- All toolbar buttons share .composer-toolbar-btn base style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Secure Mode:
- ChatMode extended: 'ask' | 'execute' | 'work' | 'secure'
- chatModes.ts: 'secure' mode enabled (requires OpenRouter)
- secure-runner.ts: runs agent loop inside Docker container
  (--network none, --memory 512m, disposable temp workspace)
- sessions.ts: routes secure:true sessions to secure-runner
- chatRunner.ts: mode==='secure' goes through runWorkModeTurn(secure:true)

Composer placeholders:
- Ask: "Ask safely…"
- Work: "Work inside selected folders…"
- Secure: "Run risky tasks in isolation…"
- Execute: "Use full iClaw power…"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tmlxrd and others added 29 commits June 6, 2026 12:09
Document the iclaw-relay tunnel feature under [Unreleased]: OPAQUE login,
end-to-end encrypted HTTP/WS transport, trusted device sessions, the relay
access-token gate, and the adversarial smoke + frame-capture coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Composer send-mode was tracked only in per-browser localStorage (keyed per chat,
with a fragile draft->chat migration) plus the last sent message's mode. Moving
page to page (full reloads) restored from the wrong/empty key and snapped the
mode back to the default.

Add a sticky `chats.mode` column, mirroring `chats.model_override` /
`reasoning_mode`:
- persist the moment the user picks a mode, even before sending a message
- render it as the authoritative `data-chat-mode`; client is now server-first
  (falls back to localStorage only for legacy chats, then the UI default)
- broadcast `chat-updated{mode}` so other tabs/devices stay in sync
- incognito stays transient (never persisted)

Fixes the composer mode resetting when navigating between pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a no-Homebrew fallback so the Colima engine lands on a clean Mac with
nothing preinstalled — no brew, no Xcode Command Line Tools, no sudo/password.

scripts/install-docker.sh (macOS): if Homebrew is present, `brew install
colima docker` as before; otherwise download PINNED colima + lima + docker
binaries straight from the projects' official releases into ~/.iclaw/engine,
verifying SHA-256 (hard fail on mismatch). Per-arch (arm64 + x86_64). The
download is via curl, so the binaries aren't Gatekeeper-quarantined and run
with their adhoc signatures.

colima.ts (app + runtime): ensureColimaRouting() → ensureColimaEnv(), which
now ALSO sets PATH — puts ~/.iclaw/engine/bin FIRST (so a no-brew install is
found) and ensures Homebrew's bin dirs are present (a GUI-launched .app starts
with a minimal PATH that omits them). Still pins docker calls to the iclaw
context. New ENGINE_BIN constant (honours ICLAW_ENGINE_DIR).

docker.ts: installDocker() re-runs ensureColimaEnv() after the install so the
freshly downloaded binaries are on PATH before launch. build-secure.sh: same
engine-dir PATH so Secure-image builds find colima too.

Pinned: colima 0.10.3, lima 2.1.2 (brew's known-good pair), docker 29.5.3.

Verified live on macOS arm64: with brew hidden, the installer downloaded and
checksum-verified all three; `colima start iclaw` + a container ran using ONLY
the downloaded toolchain; ensureColimaEnv() puts the engine dir first on PATH.
typecheck passes (app + runtime).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings in: persist composer mode server-side so it survives navigation (3c6950d).
No overlap with the macOS/Colima engine work on dev — clean merge.
Port the voice-recording feel from the Flutter app onto the existing
speech-to-text mic, keeping the /api/stt pipeline untouched:

- Hold the mic to record; slide left to cancel; slide up to lock
  hands-free (tap to send when locked). Pointer Events drive the gesture.
- Live waveform + elapsed timer from a Web Audio meter; amplitude halo
  on the mic. Min 0.8s; quick taps show a "hold to record" hint.
- Transcript is inserted at the caret (dictate-into-text), not appended;
  caret is captured when recording starts.
- While recording the field collapses to just the waveform + mic; other
  controls are hidden via visibility (not display:none) so the mic keeps
  its position instead of jumping left under space-between.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Safe work runs entirely inside the secure sandbox container, but
secure-runner hard-coded `iclaw-secure:latest` with no fallback — so a
fresh `npx @iclawapp/iclaw` install (which ships neither the Dockerfile
nor a pullable image) hard-failed with "Secure sandbox failed to start"
the instant a Safe-work turn began. Work mode already degrades to a
public base here; secure now matches it (AGENTS.md already documents
node:22 as the emergency fallback for both).

resolveSecureImage() prefers the curated image when built (or an explicit
ICLAW_SECURE_IMAGE override), else node:22. The curated image bakes in
`USER node` + a /workspace/.tools PATH for rootless self-installs; a bare
node:22 runs as root and loses that, so the fallback path re-applies
`--user 1000:1000` + HOME/NPM_CONFIG_PREFIX/PATH env to preserve the same
non-root + rootless-install guarantees. The curated path is unchanged
(fallbackArgs empty).

Verified: runtime+host typecheck clean, 413/413 tests pass, docker smoke
of both paths (non-root uid 1000, writable workspace, self-install on
PATH, curl/git present, --network none egress gate intact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er by mode

The new-chat draft page (index.ejs) is a separate, stripped template from
chat.ejs, and adoptDraftChat() promotes a draft to a real chat client-side
without a reload. Share / Reasoning / Suggest-facts / Delete (plus the share
modal + share.js) lived only in chat.ejs, so they were missing on a just-
started chat until a manual reload.

- Extract the header tool cluster into partials/header-chat-tools.ejs, shared by
  both views (draftHeader: true renders them hidden + action-less on the draft).
- promoteDraftHeaderTools() reveals them and fills in /chats/:id actions on
  adopt (Suggest-facts only when the draft picked a project).
- Load the share modal + share.js on the draft too (share.js reads the chat id
  live from #messages, so it works the moment adopt sets it).
- Add [hidden]{display:none!important} for the tools — they're inline-flex, so
  the attribute alone wouldn't hide them.
- Bind the Reasoning toggle unconditionally and resolve activeChatId live.

Also hide the Agent picker outside Full Power (syncAgentVisibility): the agent
selects an OpenClaw session, which Work / Safe work / Incognito ignore (they
route to iclaw-runtime), so showing it there implied a no-op choice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sandboxed tool that searches social platforms by keyword over FREE keyless
sources — Reddit (RSS discovery + shreddit comment scrape) and HackerNews
(Algolia). Runs inside the session container via node, honouring the same
network gate as analyze_link; no API keys, no Python.

- social-fetch.mjs: self-contained Node fetcher (built-ins only), shipped as
  an asset and base64-injected into the container.
- social.ts: SOCIAL_SEARCH_TOOL definition + host-side handler/formatter.
- Wired into Secure Mode (secure-runner) and the host loop (loop.ts +
  executeTool), gated on a sandbox backend.
- build now copies the .mjs asset into dist.

Reddit technique adapted from mvanhorn/last30days-skill (MIT). Reddit post
upvotes are not recoverable keyless; comment counts/scores are.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Work Mode runs the sandbox as the HOST uid with folders mounted at /work/<n>
and no writable /workspace (the image owns it as uid 1000), so the fetcher's
`mkdir /workspace/.tools` failed with EACCES. Secure Mode runs as `node` and
owns /workspace, so it worked there. Write to $HOME instead — set in both
sandboxes (Work: /tmp, Secure: /home/node) and writable by any uid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
social_search (and analyze_link) base64-inject a ~10KB tool script into a
single shell command, which the dev `run_command` logger dumped verbatim on
every call — flooding the log. Clamp the logged `cmd` to head + tail (the
env/arg tail stays visible, the base64 middle is elided) in both the Work and
Secure runners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rd, dead-round breaker)

A Work-Mode research turn could spin for dozens of rounds re-fetching the
same handful of URLs with reworded `focus`/fake `#anchors` (observed: one
turn ran 38 rounds / 434k tokens over ~10 distinct URLs). Since every round
resends the growing context, cutting redundant rounds saves tokens ~quadratically.

Three levers:

- Semantic loop-guard (loop.ts `toolRepeatSignature`): web_fetch now keys on
  the normalized URL alone (strips `#fragment`, lowercases host), web_search on
  the normalized query (case/punct/operator/word-order-insensitive, unique
  sorted terms). The model can no longer dodge the repeat-guard by rewording.
  Non-web tools keep exact-args matching. Tailored nudges per tool.

- Within-turn fetch cache (tools.ts `ToolContext.fetchCache`, fresh per turn):
  web_fetch serves a repeat pull of the same URL from memory and re-summarizes
  for a new `focus` with no network round-trip. Misses hit the network as
  before; failures are not cached so transient errors can still retry.

- Dead-round circuit-breaker (loop.ts `isLowValueResult` + `DEAD_ROUND_LIMIT`,
  env ICLAW_DEAD_ROUND_LIMIT=5): after N consecutive rounds whose tool results
  were all empty/failed/timed-out/guard-blocked, force a conclusion — next
  round sends tool_choice:'none' and nudges the model to answer with what it
  has and not invent numbers, instead of grinding to MAX_ROUNDS.

Verified: runtime + root typecheck, 413 host tests, 29-case smoke test of the
pure helpers (normalizers, guard dedup, low-value classification).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dges

When chats.mode is unset, the composer's "current mode" falls back to the
most recent message's mode. System rows (e.g. the "saved X% tokens" savings
badge) carry the back-compat default 'execute', so once such a row was the
newest a Work chat reopened as Full Power. Skip non-user rows in the fallback
so the last user-chosen mode wins. Fixes existing chats on reload (no migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… default

GitHub repo/file pages are ~6x heavier than their raw content (1.4MB HTML vs
~220KB README) and htmlToText keeps the chrome then truncates at 20k, burying
the actual content (observed: a turn hit the rendered repo page 3x before
discovering the raw README). canonicalizeFetchUrl() now redirects before
fetch+cache:

  github.com/owner/repo                  -> raw .../HEAD/README.md
  github.com/owner/repo/blob/<ref>/<p>   -> raw .../<ref>/<p>

Non-content routes (issues/pull/tree/wiki/profiles/reserved owners) and
non-GitHub URLs pass through untouched. Cache + repeat-guard key on the
rewritten URL, so the repo page and the raw README collapse to one entry.

web_fetch now summarizes by DEFAULT (summarize !== false) so results stay small
in history; the model sets summarize:false when it needs exact text (full lists,
numbers/dates, source code, verbatim quotes). This also fixes the prior silent
no-op where `focus` without `summarize:true` was ignored. Tool schema updated to
teach the opt-out.

Verified: typecheck, 413 host tests, 15-case canonicalizer smoke test; raw
README/blob targets confirmed live (HTTP 200, clean markdown, 222KB vs 1.4MB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap rendered tables in `.md-table-wrap` so wide tables scroll
horizontally instead of breaking the thread layout, with a rounded
frame, zebra rows and row hover. In table cells, collapse long bare-URL
link text to a compact form (e.g. github.com/owner/repo) while keeping
the full URL on href + title. Scoped to cells; prose URLs untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Work/Secure turns only ever showed "Thinking…" / "Generating…" — the agent's
tool activity was dropped. Two causes:
  - chatRunner's runWorkModeTurn consumed the runtime SSE stream but had no
    branch for tool events (only text/note/image/done/error), so they fell
    through silently;
  - the WorkEvent type declared a `tool` variant that never matched what the
    runtime actually emits (`tool_start` / `tool_result`) — dead code.

Now Work/Secure surface tool activity through the SAME turn-tool pipeline the
gateway path already uses (status label + expandable detail in the composer):
  - WorkEvent fixed to `tool_start` / `tool_result`;
  - runWorkModeTurn maps tool_start → tool-start (label+detail), tool_result →
    tool-end; incognito consumer updated to the real event name.

toolLabels: specific labels for iClaw's own tools, checked BEFORE the generic
regex so social_search no longer collapses to "Searching…":
  social_search → "Searching social media…", web_search → "Searching the web…",
  web_fetch → "Reading the web…", analyze_link → "Analyzing the link…", etc.
New toolActivityDetail() pulls a short, safe detail (query / URL host+path /
command / path, capped at 70 chars) for the expandable status line.

Verified: typecheck, 418 tests (updated existing label tests + added coverage
for the specific labels and toolActivityDetail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Searching the web…" / "Thinking…" status line sat ABOVE the streamed
assistant text. Move it below so the agent's words come first and the live
status reads as a footer beneath them.

- reorder stream-body before stream-status in both client stream builders
  (main chat + task-ask) and the server reload-placeholder (chat.ejs);
- flip .stream-status margin-bottom → margin-top so the 4px gap stays between
  the text and the status now under it.

Frontend-only (no build step) — refresh to pick up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… as .json

Follow-up to the research-loop token work, from observing a deep turn
(124k/9 rounds): the model reworded social_search ~7x (the semantic repeat-guard
only covered web_search/web_fetch), and fetched both the heavy HTML reddit thread
page AND its .json.

- toolRepeatSignature: social_search now keys on the normalized query like
  web_search, so reworded social queries collapse and trip the repeat-guard
  after the limit; shares the "rewording returns the same results" nudge.
- canonicalizeFetchUrl: a reddit.com/.../comments/... URL is rewritten to the
  .json endpoint (clean post + comments, no HTML chrome). Idempotent + strips
  trailing slash / query / hash so the HTML URL and the .json URL collapse to
  one fetch + cache entry (kills the double-fetch). GitHub rules unchanged.

Verified: runtime + root typecheck, 418 host tests, 17-case smoke (reddit
variants, cache-key collapse, github regression, social_search guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The .json rewrite from 5a39855 backfired: Reddit's JSON API returns HTTP 403
(IP-blocked — confirmed with a browser UA too), and new-reddit (www) serves a
JS-only shell (~8KB, no content). old.reddit.com is server-rendered: the real
post + comments are in the HTML (verified — a thread returns 200, ~507KB, 139
comment blocks) and it's exactly the fallback the agent kept discovering on its
own (www → old).

canonicalizeFetchUrl now rewrites every reddit host → old.reddit.com, stripping
a trailing `.json` (it 403s) and slash + the hash so the www / old / .json /
slash variants collapse to one fetch + cache entry. This is the answer to "why
does reddit block fetch": the JSON API and new-reddit are blocked; old.reddit
is not. GitHub rules unchanged.

Verified: runtime + root typecheck, 418 host tests, 13-case smoke including a
live fetch of the canonical URL (HTTP 200, 507KB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dges

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar gateway banner appeared whenever OpenClaw was offline, even in Work / Safe work / Incognito modes that never touch the gateway. Gate it on the same getComposerMode() === 'execute' check the composer overlay uses, and refresh it on both gateway-status and mode changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ser/sidebar UX

- Default mode after onboarding keys off whether the OpenClaw CLI is on the
  device: Full Power if installed, otherwise Work. New cached probe
  (openclawInstall.ts), primed at startup.
- Mode selector always renders; runtime modes show locked with a 'needs
  OpenRouter' hint when no key, and clicking one opens the connect chooser.
  New listComposerModes() feeds per-mode availability to the composer.
- Connect-a-model chooser (connectModal.ejs) replaces the post-Skip 'OpenClaw
  off' dead-end: composer stays usable and the chooser fires on first send (or
  on clicking a locked mode) when no backend is reachable.
- Sidebar 'Start OpenClaw' banner gated on Full Power being the selected mode.
- Sidebar declutter: Search after 15 chats, Projects after 5 (or once a project
  exists); the draft skips the project picker when there are no projects.
- Empty-state copy reworded; welcome card (scenarios + about iClaw) extracted to
  a partial and shown on empty chats while no project exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new chat now defaults to whatever's usable right now: Full Power when the
OpenClaw gateway is reachable; Work when it's off but an OpenRouter key is set
(don't drop the user into a dead Full Power); Full Power when off+no key but
OpenClaw is installed (startable on demand); otherwise Work.

Fixes two linked symptoms when OpenClaw is installed-but-off with a key: new
chats defaulted to Full Power, and the sidebar 'Start OpenClaw' banner showed
(it's gated on mode === 'execute'). Both now resolve to Work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quick skip past project selection: pressing Space on the "Choose a project"
stage commits the draft with No project, with a hint under the title. Guarded to
the active picking stage and ignored while a field is focused; Enter still
activates a focused project card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ebar on mode switch

Two linked regressions:

- New-chat default no longer prefers Full Power when the gateway is up. It's
  key-based again: OpenRouter key set -> Work; no key + OpenClaw installed ->
  Full Power; otherwise Work. (Also keeps the 'OpenClaw is off' banner hidden
  for key users, since it's gated on mode === 'execute'.)
- POST /chats/:id/mode no longer broadcasts updatedAt for draft chats, so
  switching mode on a freshly-created draft stops leaking the empty chat into
  the sidebar (the client upserts a row on any updatedAt). mode still syncs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The toggle only controlled visibility of the model's chain-of-thought (the
actual effort knob, thinkingLevel, was never wired up), and behaved the same
on/off in practice. Removed across the stack:

- UI: header toggle + icon (header-chat-tools.ejs, header-tool-icon.ejs).
- Client: change handler, cross-tab sync, turn-reasoning case, render helpers,
  finalize calls, draft reveal, dead selectors (iclaw.js); CSS rules.
- Route: POST /chats/:id/reasoning.
- Gateway: the 'reasoning' event type and the whole patchSession (its only
  caller was that route); analysis/chain-of-thought items are now dropped.
- Store/types/DB: setReasoningMode, reasoning_mode field + column,
  reasoningMode + turn-reasoning protocol types.
- Tests: dropped the reasoning-specific cases.

Existing DBs keep a harmless unused reasoning_mode column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tmlxrd
tmlxrd merged commit f6f6738 into main Jun 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant