- fall back to the default when VOICE_TIMEOUT_MS is non-numeric or <= 0, so a
bad override can't make the abort fire immediately
- type-check the tts `text` before calling .trim() so a non-string body returns
400 instead of throwing
- abort the in-flight TTS fetch on stop() and on a superseding play, so tapping
read-aloud repeatedly doesn't leave orphaned requests generating audio
drop leftover sidecar/faster-whisper references now that the backend is any
openai-compatible voice api, and add jsdoc to the voice-proxy functions so the
docstring coverage check passes.
Validates the user-supplied backend URL (http/https only, blocks the link-local
metadata range) to prevent SSRF; remaps upstream 401/403 so a bad voice API key
isn't read as the app's own auth failing; adds a client-side AbortController timeout
on the read-aloud request so the button can't sit in loading if a request stalls.
Read-aloud now runs in a single module-level player outside the React tree instead
of per-message component state. Switching chats or re-rendering a message no longer
revokes the blob URL mid-play (the 'Invalid URI' cutoff). Adds content-keyed caching so
re-listening doesn't regenerate, and reuses one audio element (also unlocks iOS once).
Bumps the proxy timeout to 5 minutes (VOICE_TIMEOUT_MS) since local TTS can
synthesize long messages at roughly real-time, and returns a clear timed-out
message (504) instead of failing silently. The read-aloud button now shows
backend errors.
Switches the voice proxy to the OpenAI audio API (/v1/audio/transcriptions and
/v1/audio/speech) so it works with OpenAI, Groq, or a local server. Adds a
Settings -> Voice tab (base URL, API key, models, voice) plus a Quick Settings
toggle, and removes the bundled Python sidecar.
Review fixes: stop mic tracks on unmount, clear the global TTS stop handler and
revoke leaked blob URLs, add fetch timeouts in the proxy, surface mic errors in
the button, trim before appending transcripts, and drop the repo-wide wav ignore.
Adds a push-to-talk mic button in the composer and a read-aloud button on
assistant messages. Both are opt-in and hidden unless a voice backend is
configured via VOICE_SIDECAR_URL.
The auth-gated /api/voice proxy forwards to a configurable backend exposing
/transcribe and /tts (provider-agnostic); the frontend probes /api/voice/health
and hides the controls when disabled. Adds i18n keys and docs/voice.md.
Includes a local, no-API-key reference backend in voice-sidecar/ (faster-whisper
for STT, Kokoro-82M for TTS, both CPU-capable).
Claude stores some tool failures as errored tool_result rows. The UI either
attached those rows to hidden tool output or dropped them when no matching tool
call was rendered, which made validation failures disappear from chat history.
Render unattached errored tool results, unwrap Claude tool_use_error content,
and keep tool errors visible even for tools whose successful output is hidden.
Also remove the permission-grant recovery controls from rendered error history
so denied tool use stays a plain error message.
Users deploying behind a reverse proxy need a config they can adapt.
The template documents each proxy block and centralizes upstream/subpath values.
It also notes that Nginx location matchers still require literal subpath edits.
* perf(file-tree): parallelize directory traversal and widen default ignore list
The project file-tree endpoint walked children sequentially with
`await fsPromises.stat()` inside a for-loop plus a separate
`fsPromises.access()` probe before recursing. On high-latency
filesystems (NFS/SMB) every one of those round-trips was serialized,
so a 120k-file SMB-mounted project took ~2 minutes to load.
This change:
* Runs stat() and recursive getFileTree() calls in parallel via
`Promise.all` — pipelines round-trips and lets subtree traversals
overlap.
* Drops the redundant access() probe; any EACCES now surfaces from
readdir's own try/catch in the recursive call, saving one RTT per
directory.
* Extracts the hardcoded skip list into an IGNORED_DIRS Set and
extends it to cover common Python / Rust / JVM / IDE build
artefacts (.next, __pycache__, .pytest_cache, .tox, .venv,
target, .gradle, .idea, coverage, etc).
No API shape change; existing consumers get the same tree structure,
only much faster on large or remote-mounted projects.
* fix(file-tree): bound filesystem traversal concurrency
Prevent large file-tree scans from launching unbounded stat and readdir work.
Keep the parallel traversal benefit on high-latency mounts with a bounded queue.
Ignore skipped names only for directories so same-named files stay visible.
* fix(file-tree): inspect entries with lstat
Use lstat for file-tree metadata so symlink entries are identified without following targets.
---------
Co-authored-by: leonkong via Claude <leonkong.claude@users.noreply.github.com>
Claude's model catalog changes quickly enough that a shared three-day cache can
leave users selecting stale defaults or missing newly available model aliases.
Route Claude model lookups through the provider every time so the UI and slash
commands reflect the current provider result instead of an old disk snapshot.
Keep the static fallback catalog aligned with the latest Claude defaults so the
provider still has a sensible response when live discovery is unavailable.
Add dir="auto" to chat message content and composer textarea so
Persian and Arabic text automatically renders right-to-left
while English and other LTR text remains unaffected.
Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
The WebSocket gateway never sent ping frames, so any reverse proxy with
an idle timeout (Cloudflare Tunnel ~100s, AWS ALB 60s, nginx 60s, etc.)
would silently tear down /shell, /ws and /plugin-ws/* connections after
the idle window. The UI reconnects automatically but users see a
"Connecting to shell" toast every 1–3 minutes during normal use and any
in-flight PTY/chat traffic can race the reconnect.
Schedule a 30s ws.ping() per connection at the gateway level, cleared on
close/error. ping/pong counts as protocol activity for all proxies that
implement WebSocket correctly, so this single change covers every
deployment topology without per-proxy tuning.
Fixes#769
Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
Plugin WebSocket connections (e.g. the official Terminal plugin) hang
in `npm run dev` because Vite proxies /api, /ws, and /shell but not
/plugin-ws/*. Production is unaffected because the same Express server
serves both the frontend and the WS gateway.
Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
The effect cleanup sets unmountedRef.current = true to prevent reconnects after
the provider unmounts. Without an inverse reset at the start of the effect,
re-running the effect (e.g. when the auth token rotates) leaves the ref true,
and connect() short-circuits at its unmounted guard. The socket then stays
permanently disconnected for the lifetime of the provider.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com>
* fix(security)(components): unsanitized svg content injected via `dangerouslys
The plugin icon renderer fetches SVG text from `/api/plugins/.../assets/...` and injects it directly into the DOM using `dangerouslySetInnerHTML` after only checking that the payload starts with `<svg`. This does not remove malicious attributes/elements (e.g., event handlers, scriptable SVG payloads), enabling DOM-based XSS if a plugin asset is malicious or compromised.
Affected files: PluginIcon.tsx
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
* fix: sanitize plugin svg icons
---------
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
Co-authored-by: tuanaiseo <tuanaiseo@gmail.com>
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
* fix: harden router basename detection
* fix: broaden icon basename detection
* fix: ignore cross-origin basename hints
* fix: keep root deployments from inheriting asset basenames
Router basename detection must support root hosting and path-prefix hosting at runtime.
The icon fallback used /icons/icon-192x192.png as a basename on root deployments.
After login, React Router mounted at /icons while the current URL was /.
That mismatch made authenticated root deployments render a blank page.
Strip known asset directories even when they are the only path segment.
Root icon URLs now keep basename ''. Prefixed /ai/icons/... URLs still resolve to /ai.
---------
Co-authored-by: JohnGenri <myname945@gmail.com>
Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
* fix: preserve WebSocket frame type in plugin proxy
The plugin WebSocket proxy relays all messages as binary frames
regardless of the original frame type. This causes text-based ready
messages to be forwarded as binary, so the browser never processes
them and plugin UIs (like web-terminal) show a spinner indefinitely.
Pass the isBinary flag through in both relay directions so the
original frame type is preserved.
FixesCoderLuii/HolyClaude#11
* fix(plugins): preserve websocket frame type in proxy
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix: refresh Claude auth status after login flow
* fix: rely on refreshed auth status after login
---------
Co-authored-by: HolyCode User <noreply@holycode.local>
PromptInputSubmit already has type="submit" via the parent form, so the
button's click triggers handleSubmit through the form's onSubmit path.
The added onMouseDown/onTouchStart handlers created two extra paths that
both invoked handleSubmit; on iOS Safari a single tap could fire both
touchstart and a synthetic mousedown before isLoading state propagated,
producing two messages and two image-upload roundtrips.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rename input shares a parent div that uses `group-hover:opacity-100`,
so moving the cursor off the row visually hid the input mid-edit.
While editing, force the action panel to `opacity-100` and dismiss it
via an outside-click listener instead of mouseleave. Also hide the
relative-time badge so it does not overlap the input.
The pulsing green dot next to a session row signals that the session
had activity in the last 10 minutes, but the meaning was undocumented.
Hovering it now shows a translated tooltip, and an aria-label exposes
the same text to screen readers.
Uses the existing shared Tooltip component (portal-positioned, so it
is not clipped by the sidebar overflow). Translation key added to all
eight sidebar locale files (en, de, it, ja, ko, ru, tr, zh-CN).
* fix: remove the hide cursor on windows logic
* feat(cursor): update fallback models
* fix(claude): force fallback models and disable supportedModels lookup