diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 985550730671..3b2afcf39aa1 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -240,9 +240,9 @@ The terminal is also available as a full-screen, terminal-only document at `/?vi Talk mode uses a registered realtime voice provider. Configure OpenAI with `talk.realtime.provider: "openai"` plus an `openai` API-key auth profile, `talk.realtime.providers.openai.apiKey`, or `OPENAI_API_KEY`; OpenAI OAuth profiles do not configure Realtime voice. Configure Google with `talk.realtime.provider: "google"` plus `talk.realtime.providers.google.apiKey`. The browser never receives a standard provider API key: OpenAI receives an ephemeral Realtime client secret for WebRTC, and Google Live receives a one-use constrained Live API auth token for a browser WebSocket session, with instructions and tool declarations locked into the token by the Gateway. Providers that only expose a backend realtime bridge run through the Gateway relay transport, so credentials and vendor sockets stay server-side while browser audio moves through authenticated Gateway RPCs. The Realtime session prompt is assembled by the Gateway; `talk.client.create` does not accept caller-provided instruction overrides. - The Chat composer includes a Talk options button next to the Talk start/stop button. Its compact panel keeps only Voice, Model, and Sensitivity for the next Talk session. **More in Settings** opens **Settings → Communications → Talk**, where persistent provider, transport, reasoning effort, exact VAD threshold, silence duration, and prefix padding defaults live; changing those defaults requires `operator.admin` access. Blank composer values fall back to those configured defaults or the provider default. Configuring Gateway relay forces the backend relay path; configuring WebRTC keeps the session client-owned and fails instead of silently falling back to relay if the provider cannot create a browser session. + The Chat composer includes a Talk options caret next to the Talk start/stop button. Its compact panel keeps only Voice, Model, and Sensitivity for the next Talk session. **More in Settings** opens **Settings → Communications → Talk**, where persistent provider, transport, reasoning effort, exact VAD threshold, silence duration, and prefix padding defaults live; changing those defaults requires `operator.admin` access. Blank composer values fall back to those configured defaults or the provider default. Configuring Gateway relay forces the backend relay path; configuring WebRTC keeps the session client-owned and fails instead of silently falling back to relay if the provider cannot create a browser session. - The Talk control itself is the waves button next to the microphone dictation button. When Talk starts, the composer status row shows `Connecting Talk...`, then `Talk live` while audio is connected, or `Asking OpenClaw...` while a realtime tool call is consulting the configured larger model through `talk.client.toolCall`. + The Talk control itself is the microphone button in the composer toolbar, with a small caret beside it that opens Talk options. When Talk starts, the composer status row shows `Connecting Talk...`, then `Talk live` while audio is connected, or `Asking OpenClaw...` while a realtime tool call is consulting the configured larger model through `talk.client.toolCall`. Maintainer live smoke: `OPENAI_API_KEY=... GEMINI_API_KEY=... node --import tsx scripts/dev/realtime-talk-live-smoke.ts` verifies the OpenAI backend WebSocket bridge, OpenAI browser WebRTC SDP exchange, Google Live constrained-token browser WebSocket setup, and the Gateway relay browser adapter with fake microphone media. The command prints provider status only and does not log secrets. diff --git a/ui/src/i18n/.i18n/ar.meta.json b/ui/src/i18n/.i18n/ar.meta.json index 9fb7e1f5eb36..b5d520f4ede4 100644 --- a/ui/src/i18n/.i18n/ar.meta.json +++ b/ui/src/i18n/.i18n/ar.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:01:23.261Z", + "generatedAt": "2026-07-05T22:25:08.320Z", "locale": "ar", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/de.meta.json b/ui/src/i18n/.i18n/de.meta.json index b558c6569bb2..3357ab0f14ed 100644 --- a/ui/src/i18n/.i18n/de.meta.json +++ b/ui/src/i18n/.i18n/de.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T20:59:33.063Z", + "generatedAt": "2026-07-05T22:25:07.310Z", "locale": "de", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/es.meta.json b/ui/src/i18n/.i18n/es.meta.json index bd82819238a2..38e213ed8202 100644 --- a/ui/src/i18n/.i18n/es.meta.json +++ b/ui/src/i18n/.i18n/es.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:00:39.848Z", + "generatedAt": "2026-07-05T22:25:07.454Z", "locale": "es", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/fa.meta.json b/ui/src/i18n/.i18n/fa.meta.json index bac4448bcb22..ec717de08275 100644 --- a/ui/src/i18n/.i18n/fa.meta.json +++ b/ui/src/i18n/.i18n/fa.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:03:19.039Z", + "generatedAt": "2026-07-05T22:25:09.693Z", "locale": "fa", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/fr.meta.json b/ui/src/i18n/.i18n/fr.meta.json index 0da3f731c29f..392e58a482bd 100644 --- a/ui/src/i18n/.i18n/fr.meta.json +++ b/ui/src/i18n/.i18n/fr.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:00:52.467Z", + "generatedAt": "2026-07-05T22:25:07.964Z", "locale": "fr", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/hi.meta.json b/ui/src/i18n/.i18n/hi.meta.json index 8644bd0650ff..dd55808b7082 100644 --- a/ui/src/i18n/.i18n/hi.meta.json +++ b/ui/src/i18n/.i18n/hi.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T19:32:32.620Z", + "generatedAt": "2026-07-05T22:25:08.150Z", "locale": "hi", "model": "", "provider": "", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/id.meta.json b/ui/src/i18n/.i18n/id.meta.json index 03bf61e4769f..916d0a09c534 100644 --- a/ui/src/i18n/.i18n/id.meta.json +++ b/ui/src/i18n/.i18n/id.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:02:12.906Z", + "generatedAt": "2026-07-05T22:25:08.968Z", "locale": "id", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/it.meta.json b/ui/src/i18n/.i18n/it.meta.json index cdce094c550f..df66a78a4329 100644 --- a/ui/src/i18n/.i18n/it.meta.json +++ b/ui/src/i18n/.i18n/it.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:01:24.757Z", + "generatedAt": "2026-07-05T22:25:08.470Z", "locale": "it", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ja-JP.meta.json b/ui/src/i18n/.i18n/ja-JP.meta.json index d2ead6d5d36b..58d0ee8921a4 100644 --- a/ui/src/i18n/.i18n/ja-JP.meta.json +++ b/ui/src/i18n/.i18n/ja-JP.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:00:35.490Z", + "generatedAt": "2026-07-05T22:25:07.625Z", "locale": "ja-JP", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ko.meta.json b/ui/src/i18n/.i18n/ko.meta.json index 4b253f4c0257..e7c5b146cb4d 100644 --- a/ui/src/i18n/.i18n/ko.meta.json +++ b/ui/src/i18n/.i18n/ko.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:00:39.218Z", + "generatedAt": "2026-07-05T22:25:07.784Z", "locale": "ko", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/nl.meta.json b/ui/src/i18n/.i18n/nl.meta.json index 452563ff03ff..dfd49e7d79e2 100644 --- a/ui/src/i18n/.i18n/nl.meta.json +++ b/ui/src/i18n/.i18n/nl.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:03:15.146Z", + "generatedAt": "2026-07-05T22:25:09.554Z", "locale": "nl", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/pl.meta.json b/ui/src/i18n/.i18n/pl.meta.json index c4189b0ddce8..c763d7a16474 100644 --- a/ui/src/i18n/.i18n/pl.meta.json +++ b/ui/src/i18n/.i18n/pl.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:02:29.694Z", + "generatedAt": "2026-07-05T22:25:09.112Z", "locale": "pl", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/pt-BR.meta.json b/ui/src/i18n/.i18n/pt-BR.meta.json index 67b36112d8a5..e4561ee52103 100644 --- a/ui/src/i18n/.i18n/pt-BR.meta.json +++ b/ui/src/i18n/.i18n/pt-BR.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T20:59:44.069Z", + "generatedAt": "2026-07-05T22:25:07.155Z", "locale": "pt-BR", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/ru.meta.json b/ui/src/i18n/.i18n/ru.meta.json index fb84c415c4af..af6a1235bf7f 100644 --- a/ui/src/i18n/.i18n/ru.meta.json +++ b/ui/src/i18n/.i18n/ru.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T19:32:34.753Z", + "generatedAt": "2026-07-05T22:25:09.849Z", "locale": "ru", "model": "", "provider": "", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/th.meta.json b/ui/src/i18n/.i18n/th.meta.json index 108a758f3449..c709d291c3ce 100644 --- a/ui/src/i18n/.i18n/th.meta.json +++ b/ui/src/i18n/.i18n/th.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:02:41.557Z", + "generatedAt": "2026-07-05T22:25:09.261Z", "locale": "th", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/tr.meta.json b/ui/src/i18n/.i18n/tr.meta.json index 8cda57bd12e1..9ee581fcc861 100644 --- a/ui/src/i18n/.i18n/tr.meta.json +++ b/ui/src/i18n/.i18n/tr.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:01:42.084Z", + "generatedAt": "2026-07-05T22:25:08.624Z", "locale": "tr", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/uk.meta.json b/ui/src/i18n/.i18n/uk.meta.json index ea701fe61e2e..180c9a95049a 100644 --- a/ui/src/i18n/.i18n/uk.meta.json +++ b/ui/src/i18n/.i18n/uk.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:01:40.702Z", + "generatedAt": "2026-07-05T22:25:08.790Z", "locale": "uk", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/vi.meta.json b/ui/src/i18n/.i18n/vi.meta.json index ab10df05dfa0..1c4b2b0dff2c 100644 --- a/ui/src/i18n/.i18n/vi.meta.json +++ b/ui/src/i18n/.i18n/vi.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T21:03:06.663Z", + "generatedAt": "2026-07-05T22:25:09.425Z", "locale": "vi", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/zh-CN.meta.json b/ui/src/i18n/.i18n/zh-CN.meta.json index 1f6c18c36ebd..76b5dc1de7c7 100644 --- a/ui/src/i18n/.i18n/zh-CN.meta.json +++ b/ui/src/i18n/.i18n/zh-CN.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T20:59:46.734Z", + "generatedAt": "2026-07-05T22:25:06.843Z", "locale": "zh-CN", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/.i18n/zh-TW.meta.json b/ui/src/i18n/.i18n/zh-TW.meta.json index 9b79cda5c0a2..42461078e5ee 100644 --- a/ui/src/i18n/.i18n/zh-TW.meta.json +++ b/ui/src/i18n/.i18n/zh-TW.meta.json @@ -1,10 +1,10 @@ { "fallbackKeys": [], - "generatedAt": "2026-07-05T20:59:36.968Z", + "generatedAt": "2026-07-05T22:25:07.009Z", "locale": "zh-TW", "model": "claude-opus-4-8", "provider": "anthropic", - "sourceHash": "116e85303569dc538dcd374ee7788ea9664fd8aae5cda4a2f0f2451cfefeaccf", + "sourceHash": "53b87190ae24c60a0c03150797350e7757cb8c3aad65967cf6b64f851774fead", "totalKeys": 1528, "translatedKeys": 1528, "workflow": 1 diff --git a/ui/src/i18n/locales/ar.ts b/ui/src/i18n/locales/ar.ts index e2d1b13231bc..555f03e17913 100644 --- a/ui/src/i18n/locales/ar.ts +++ b/ui/src/i18n/locales/ar.ts @@ -1498,7 +1498,7 @@ export const ar: TranslationMap = { badge: "منتهي الصلاحية", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/de.ts b/ui/src/i18n/locales/de.ts index a6219fa93a3c..45df8c8bdcf0 100644 --- a/ui/src/i18n/locales/de.ts +++ b/ui/src/i18n/locales/de.ts @@ -1525,7 +1525,7 @@ export const de: TranslationMap = { badge: "Abgelaufen", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 18e71c6d1d1e..3a050c717107 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -1504,7 +1504,7 @@ export const en: TranslationMap = { badge: "Expired", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/es.ts b/ui/src/i18n/locales/es.ts index 966d6501e9e2..9cc50f1e8e82 100644 --- a/ui/src/i18n/locales/es.ts +++ b/ui/src/i18n/locales/es.ts @@ -1523,7 +1523,7 @@ export const es: TranslationMap = { badge: "Caducado", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/fa.ts b/ui/src/i18n/locales/fa.ts index a55e7ef00200..49b799e5be33 100644 --- a/ui/src/i18n/locales/fa.ts +++ b/ui/src/i18n/locales/fa.ts @@ -1516,7 +1516,7 @@ export const fa: TranslationMap = { badge: "منقضی‌شده", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/fr.ts b/ui/src/i18n/locales/fr.ts index abed363fe26f..7f3e5fc12cfd 100644 --- a/ui/src/i18n/locales/fr.ts +++ b/ui/src/i18n/locales/fr.ts @@ -1530,7 +1530,7 @@ export const fr: TranslationMap = { badge: "Expiré", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/hi.ts b/ui/src/i18n/locales/hi.ts index 8f65154eb13e..985c6f0d5ef0 100644 --- a/ui/src/i18n/locales/hi.ts +++ b/ui/src/i18n/locales/hi.ts @@ -1500,7 +1500,7 @@ export const hi: TranslationMap = { badge: "समाप्त", }, composer: { - placeholder: "{name} को संदेश भेजें (भेजने के लिए Enter)", + placeholder: "{name} को संदेश भेजें", placeholderWithAttachments: "संदेश जोड़ें या और छवियाँ पेस्ट करें...", placeholderDisconnected: "चैट शुरू करने के लिए gateway से कनेक्ट करें...", attachFile: "फ़ाइल संलग्न करें", diff --git a/ui/src/i18n/locales/id.ts b/ui/src/i18n/locales/id.ts index bbf6c3969449..36fd83bc9576 100644 --- a/ui/src/i18n/locales/id.ts +++ b/ui/src/i18n/locales/id.ts @@ -1514,7 +1514,7 @@ export const id: TranslationMap = { badge: "Kedaluwarsa", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/it.ts b/ui/src/i18n/locales/it.ts index 4160557a3832..4d9005867822 100644 --- a/ui/src/i18n/locales/it.ts +++ b/ui/src/i18n/locales/it.ts @@ -1523,7 +1523,7 @@ export const it: TranslationMap = { badge: "Scaduto", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/ja-JP.ts b/ui/src/i18n/locales/ja-JP.ts index 1a842742306a..1e482c2a5578 100644 --- a/ui/src/i18n/locales/ja-JP.ts +++ b/ui/src/i18n/locales/ja-JP.ts @@ -1520,7 +1520,7 @@ export const ja_JP: TranslationMap = { badge: "期限切れ", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/ko.ts b/ui/src/i18n/locales/ko.ts index 41e53b81d055..ce8fef4b3375 100644 --- a/ui/src/i18n/locales/ko.ts +++ b/ui/src/i18n/locales/ko.ts @@ -1505,7 +1505,7 @@ export const ko: TranslationMap = { badge: "만료됨", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/nl.ts b/ui/src/i18n/locales/nl.ts index da055fd8b887..2458d895e3d3 100644 --- a/ui/src/i18n/locales/nl.ts +++ b/ui/src/i18n/locales/nl.ts @@ -1520,7 +1520,7 @@ export const nl: TranslationMap = { badge: "Verlopen", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/pl.ts b/ui/src/i18n/locales/pl.ts index 432e3c8920f2..dfaf5b089e31 100644 --- a/ui/src/i18n/locales/pl.ts +++ b/ui/src/i18n/locales/pl.ts @@ -1519,7 +1519,7 @@ export const pl: TranslationMap = { badge: "Wygasł", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index ae9da8ba5d12..0a5755c8f720 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -1516,7 +1516,7 @@ export const pt_BR: TranslationMap = { badge: "Expirado", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/ru.ts b/ui/src/i18n/locales/ru.ts index 63dcdec04309..eb0f201f9e94 100644 --- a/ui/src/i18n/locales/ru.ts +++ b/ui/src/i18n/locales/ru.ts @@ -1525,7 +1525,7 @@ export const ru: TranslationMap = { badge: "Истек", }, composer: { - placeholder: "Сообщение {name} (Enter для отправки)", + placeholder: "Сообщение {name}", placeholderWithAttachments: "Добавьте сообщение или вставьте еще изображения...", placeholderDisconnected: "Подключитесь к шлюзу, чтобы начать общение...", attachFile: "Прикрепить файл", diff --git a/ui/src/i18n/locales/th.ts b/ui/src/i18n/locales/th.ts index e33065fc9f05..c8cec230890e 100644 --- a/ui/src/i18n/locales/th.ts +++ b/ui/src/i18n/locales/th.ts @@ -1482,7 +1482,7 @@ export const th: TranslationMap = { badge: "หมดอายุ", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/tr.ts b/ui/src/i18n/locales/tr.ts index 2bc15ceb9a6a..4e44d0e7e820 100644 --- a/ui/src/i18n/locales/tr.ts +++ b/ui/src/i18n/locales/tr.ts @@ -1520,7 +1520,7 @@ export const tr: TranslationMap = { badge: "Süresi doldu", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/uk.ts b/ui/src/i18n/locales/uk.ts index ef877e480925..bf19d9efbcad 100644 --- a/ui/src/i18n/locales/uk.ts +++ b/ui/src/i18n/locales/uk.ts @@ -1518,7 +1518,7 @@ export const uk: TranslationMap = { badge: "Застарів", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/vi.ts b/ui/src/i18n/locales/vi.ts index e085477d7d31..a8a9752d1556 100644 --- a/ui/src/i18n/locales/vi.ts +++ b/ui/src/i18n/locales/vi.ts @@ -1506,7 +1506,7 @@ export const vi: TranslationMap = { badge: "Đã hết hạn", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index 625bf3acfd86..5461a4aa2c54 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -1477,7 +1477,7 @@ export const zh_CN: TranslationMap = { badge: "已过期", }, composer: { - placeholder: "给 {name} 发消息(Enter 发送)", + placeholder: "给 {name} 发消息", placeholderWithAttachments: "添加消息或继续粘贴图片...", placeholderDisconnected: "连接到 Gateway 后开始聊天...", attachFile: "附加文件", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 45599164f4bc..ce56bf6939a7 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -1479,7 +1479,7 @@ export const zh_TW: TranslationMap = { badge: "已過期", }, composer: { - placeholder: "Message {name} (Enter to send)", + placeholder: "Message {name}", placeholderWithAttachments: "Add a message or paste more images...", placeholderDisconnected: "Connect to the gateway to start chatting...", attachFile: "Attach file", diff --git a/ui/src/pages/chat/chat-responsive.browser.test.ts b/ui/src/pages/chat/chat-responsive.browser.test.ts index 6b6b1e512974..15ea42fb86ec 100644 --- a/ui/src/pages/chat/chat-responsive.browser.test.ts +++ b/ui/src/pages/chat/chat-responsive.browser.test.ts @@ -177,7 +177,8 @@ function composerControlsHtml() {
- Default model · Off + Default model + Off ${iconSvg()}
@@ -186,6 +187,7 @@ function composerControlsHtml() {
+
Reasoning
@@ -334,8 +336,10 @@ function chatHtml(
- - +
+ + +
8
${composerControlsHtml()} @@ -804,6 +808,29 @@ describeBrowserLayout("chat responsive browser layout", () => { } }); + it.each([ + [320, 568], + [375, 812], + [667, 375], + [768, 500], + ] as const)( + "keeps the composer model menu inside the mobile viewport at %sx%s", + async (width, height) => { + const page = await openFixture(width, height); + try { + await page.locator('[data-chat-composer-model="true"]').click(); + const menu = await getBoundingBox(page, ".chat-controls__inline-select-menu--combined"); + const reasoning = await getBoundingBox(page, ".chat-controls__reasoning-panel"); + expect(menu.x).toBeGreaterThanOrEqual(0); + expect(menu.x + menu.width).toBeLessThanOrEqual(width + 1); + expect(reasoning.x).toBeGreaterThanOrEqual(0); + expect(reasoning.x + reasoning.width).toBeLessThanOrEqual(width + 1); + } finally { + await closeBrowserPage(page); + } + }, + ); + it.each([ [320, 568], [393, 852], diff --git a/ui/src/pages/chat/components/chat-composer.ts b/ui/src/pages/chat/components/chat-composer.ts index 8be9295b2413..da86f4bd3d63 100644 --- a/ui/src/pages/chat/components/chat-composer.ts +++ b/ui/src/pages/chat/components/chat-composer.ts @@ -2000,49 +2000,55 @@ export function renderChatComposer(props: ChatComposerProps) { - ${props.onToggleRealtimeTalk + ${props.onToggleRealtimeTalk || props.onToggleRealtimeTalkOptions ? html` - - - - ` - : nothing} - ${props.onToggleRealtimeTalkOptions - ? html` - - - +
+ ${props.onToggleRealtimeTalk + ? html` + + + + ` + : nothing} + ${props.onToggleRealtimeTalkOptions + ? html` + + + + ` + : nothing} +
` : nothing} ${tokens ? html`${tokens}` : nothing} diff --git a/ui/src/pages/chat/components/chat-model-controls.ts b/ui/src/pages/chat/components/chat-model-controls.ts index 7280e68d988a..45e2f907b84d 100644 --- a/ui/src/pages/chat/components/chat-model-controls.ts +++ b/ui/src/pages/chat/components/chat-model-controls.ts @@ -153,8 +153,6 @@ function renderChatModelReasoningSelect(params: { const triggerModel = formatCombinedPickerModelLabel(selectedModelLabel); const triggerThinking = formatCombinedPickerThinkingLabel(selectedThinkingLabel); const triggerTitle = `${triggerModel} · ${triggerThinking}`; - const triggerLabel = - selectedThinkingValue === "" ? triggerModel : `${triggerModel} · ${triggerThinking}`; const sliderStops = thinkingOptions.filter((option) => option.value !== ""); const defaultStopIndex = sliderStops.findIndex((option) => option.value === thinkingDefaultValue); const hasThinkingOverride = selectedThinkingValue !== ""; @@ -206,7 +204,16 @@ function renderChatModelReasoningSelect(params: { } }} > - ${triggerLabel} + ${triggerModel} + ${showReasoning || hasThinkingOverride + ? html`` + : ""} diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index e4b49b48d904..281f7f8a1dd3 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -819,13 +819,15 @@ openclaw-chat-page { margin: 0; } +/* Model/effort/settings cluster hugs the send button on the right; the left + toolbar half absorbs all spare width. */ .agent-chat__composer-controls { display: flex; position: relative; align-items: center; - justify-content: flex-start; + justify-content: flex-end; gap: 6px; - flex: 1 1 auto; + flex: 0 1 auto; min-width: 0; padding: 0; } @@ -902,7 +904,7 @@ openclaw-chat-page { .chat-settings-chip:hover, .chat-settings-chip--open { - background: var(--bg-hover); + background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); } @@ -1013,13 +1015,14 @@ openclaw-chat-page { overflow-wrap: anywhere; } +/* Chromeless status row: the composer frame is the only chrome, so the + toolbar sits flush inside it with no divider line. */ .agent-chat__toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; - padding: 6px 10px; - border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + padding: 2px 8px 8px; } .agent-chat__toolbar-left, @@ -1079,14 +1082,44 @@ openclaw-chat-page { cursor: not-allowed; } -.agent-chat__input-btn--active { - color: var(--accent); - background: color-mix(in srgb, var(--accent) 12%, transparent); +/* Selected/open states stay a quiet neutral wash; only live recording gets a + (soft) danger tint so "active" chrome never competes with the send button. */ +.agent-chat__input-btn--active, +.agent-chat__input-btn--open { + color: var(--text); + background: color-mix(in srgb, var(--text) 7%, transparent); } .agent-chat__input-btn--talk { color: var(--danger, #ef4444); - background: color-mix(in srgb, var(--danger, #ef4444) 14%, transparent); + background: color-mix(in srgb, var(--danger, #ef4444) 9%, transparent); +} + +/* Split talk control: mic toggles Talk, the narrow caret opens Talk settings + (mirrors a mic-with-dropdown control; negative margin keeps the pair reading + as one unit without a wrapper border). */ +.agent-chat__talk-group { + display: inline-flex; + align-items: center; +} + +.agent-chat__talk-group .agent-chat__talk-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.agent-chat__talk-group .agent-chat__talk-caret { + min-width: 20px; + width: 20px; + margin-left: -2px; + padding: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.agent-chat__talk-group .agent-chat__talk-caret svg { + width: 13px; + height: 13px; } .agent-chat__talk-status { @@ -1414,9 +1447,11 @@ openclaw-chat-page { text-overflow: ellipsis; } +/* Status chips keep their semantic hue in the text/icon only; borders stay + near-neutral so the row reads as quiet metadata, not alerts. */ .agent-chat__run-status--in-progress { color: var(--info); - border-color: color-mix(in srgb, var(--info) 32%, transparent); + border-color: color-mix(in srgb, var(--info) 18%, transparent); } .agent-chat__run-status--in-progress svg { @@ -1425,12 +1460,12 @@ openclaw-chat-page { .agent-chat__run-status--done { color: var(--ok); - border-color: color-mix(in srgb, var(--ok) 32%, transparent); + border-color: color-mix(in srgb, var(--ok) 18%, transparent); } .agent-chat__run-status--interrupted { color: var(--warn); - border-color: color-mix(in srgb, var(--warn) 36%, transparent); + border-color: color-mix(in srgb, var(--warn) 22%, transparent); } @keyframes chat-run-status-spin { @@ -2260,7 +2295,7 @@ openclaw-chat-page { box-sizing: border-box; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 6px; width: 100%; height: 30px; @@ -2301,10 +2336,37 @@ openclaw-chat-page { white-space: nowrap; } +/* Effort chip inside the model trigger: shows the effective thinking level as + a muted pill (accent-washed when the session overrides the default). It is + decorative (aria-hidden); the trigger aria-label carries the full state. */ +.chat-controls__effort-chip { + display: inline-flex; + align-items: center; + flex: 0 0 auto; + height: 18px; + padding: 0 7px; + border-radius: var(--radius-full); + background: color-mix(in srgb, var(--text) 6%, transparent); + color: var(--muted); + font-size: 11px; + font-weight: 600; + line-height: 1; + white-space: nowrap; +} + +.chat-controls__effort-chip--override { + background: color-mix(in srgb, var(--accent) 10%, transparent); + color: color-mix(in srgb, var(--accent) 72%, var(--text)); +} + +/* Chevron pins right so label+chip stay grouped when the trigger stretches + (mobile grid cell); on desktop the trigger is content-sized so this is a + no-op vs the old space-between layout. */ .chat-controls__inline-select-icon { display: inline-flex; width: 14px; height: 14px; + margin-left: auto; color: var(--muted); flex: 0 0 auto; } @@ -2368,7 +2430,12 @@ openclaw-chat-page { background: color-mix(in srgb, var(--text) 6%, transparent); } +/* The combined model picker anchors to a right-aligned trigger, so the menu + grows leftward and the reasoning panel flies out to the left; anchoring + left like the generic menu would clip both at the viewport edge. */ .chat-controls__inline-select-menu--combined { + left: auto; + right: 0; width: max(100%, min(320px, calc(100vw - 32px))); max-height: none; overflow: visible; @@ -2403,7 +2470,7 @@ openclaw-chat-page { .chat-controls__reasoning-panel { position: absolute; bottom: 0; - left: calc(100% + 8px); + right: calc(100% + 8px); z-index: 90; display: grid; gap: 6px; @@ -2418,7 +2485,7 @@ openclaw-chat-page { overflow-y: auto; opacity: 0; pointer-events: none; - transform: translateX(-4px); + transform: translateX(4px); transition: opacity var(--duration-fast) ease, transform var(--duration-fast) ease; @@ -2800,6 +2867,26 @@ openclaw-chat-page { height: 44px; } + /* Preserve the phone touch target despite the narrower desktop split caret. */ + .agent-chat__talk-group .agent-chat__talk-caret { + width: 44px; + min-width: 44px; + } + + /* The model trigger owns the left mobile grid column; grow its menu rightward. */ + .chat-controls__inline-select-menu--combined { + left: 0; + right: auto; + } + + .chat-controls__reasoning-panel { + top: auto; + bottom: calc(100% + 8px); + left: 0; + right: auto; + width: min(220px, calc(100vw - 48px)); + } + .agent-chat__input { margin: 0 8px calc(14px + var(--safe-area-bottom)); } @@ -2849,13 +2936,6 @@ openclaw-chat-page { overflow: visible; } - .chat-controls__reasoning-panel { - top: auto; - bottom: calc(100% + 8px); - left: 0; - width: min(220px, calc(100vw - 48px)); - } - .chat-controls { flex-wrap: wrap; gap: 8px;