Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5df19d9
feat(learning): surface facet provenance (cue_families, evidence_refs…
mysma-9403 Jul 18, 2026
f991500
fix(observability): downgrade inference/ops provider errors from erro…
senamakel Jul 23, 2026
0663e9a
fix(observability): downgrade composio, MCP, vision, HTTP, voice, oll…
senamakel Jul 23, 2026
899c162
fix(observability): downgrade voice/STT errors from error! to warn! (…
senamakel Jul 23, 2026
b10dcec
fix(ipc): guard window.ipc.postMessage in vendored ipc-protocol.js to…
senamakel Jul 23, 2026
ba285d5
fix(meshGradient): guard .classList access against null element refs …
senamakel Jul 23, 2026
82a1745
fix(auth): retry + graceful degradation for auth_me_timeout during de…
senamakel Jul 23, 2026
3702b92
fix(chat): handle unhandled rejection on threads_create_new timeout (…
senamakel Jul 23, 2026
e9f25f8
fix(dispatch): suppress Sentry noise from old client calling harness_…
senamakel Jul 23, 2026
71747de
feat(observability): add before_send filters for user-config, connect…
senamakel Jul 23, 2026
7ceadec
chore(tinycortex): update submodule to pin a935bca (PII auto-sanitize…
senamakel Jul 23, 2026
3fd13ad
fix(mcp): suppress Sentry noise when all MCP registries fail to respond
senamakel Jul 23, 2026
408275a
fix(analytics): guard controlState element.checked with try-catch for…
senamakel Jul 23, 2026
346eafb
fix(memory): downgrade ingest deserialization errors to warn level (#…
senamakel Jul 23, 2026
18f3542
fix(observability): rate-limit Windows ERROR_FILE_SYSTEM_LIMITATION (…
senamakel Jul 23, 2026
f9d062a
fix(chat): prevent infinite re-render loops — isolate setState to eff…
senamakel Jul 23, 2026
33e8f5f
fix(voice): classify whisper-cli STATUS_DLL_NOT_FOUND with actionable…
senamakel Jul 23, 2026
93ff178
chore: remove unused imports left after error-to-warn downgrades
senamakel Jul 23, 2026
1f480d0
fix(managed-backend): wire requires_streaming on the crate-native Ope…
senamakel Jul 23, 2026
6723a9b
fix(learning): publish SessionExpired from crate-native backend model…
senamakel Jul 23, 2026
5dc9f30
fix(config): handle non-UTF-8 config.toml with recovery path + rate-l…
senamakel Jul 23, 2026
85d505f
chore: apply prettier formatting
senamakel Jul 23, 2026
98bfa20
fix: update tauri-cef-expected-sha to match bumped submodule pointer …
senamakel Jul 23, 2026
663c396
fix: push submodule commits upstream and update pointers
senamakel Jul 23, 2026
f8eb3a1
fix: add missing fields to ModelResponse construction sites
senamakel Jul 23, 2026
8b91982
fix: address review feedback — DLL exit code, mutex poison, dead-code…
senamakel Jul 23, 2026
1b5a800
fix: increase auth-store RPC timeout (25s) and scan logentry in FS-li…
senamakel Jul 23, 2026
ea7bcb2
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/5171
senamakel Jul 24, 2026
e0f7a3f
fix(sentry): extend suppress-reauth window to cover full auth-store r…
senamakel Jul 24, 2026
14a22e9
fix(sentry): remove with_requires_streaming after tinyagents refactor…
senamakel Jul 24, 2026
9886cd7
fix: resolve clippy warnings — collapsible_if in observability, unnec…
senamakel Jul 24, 2026
7f0aa1f
style: apply prettier formatting to 36 files across app/src and app/test
senamakel Jul 24, 2026
c4f71d4
fix: address CodeRabbit review comments — README strikethrough, PII-s…
senamakel Jul 24, 2026
11a8aa3
style: prettier fix in desktopDeepLinkListener test
senamakel Jul 24, 2026
851bd0c
fix: move LoopHarness setCount into useEffect to avoid React native r…
senamakel Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/tauri-cef-expected-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11ef51edbeadcca4517b18a037fff858a5dfae0f
f09d7e746a44a5187d2abc116e8a4cd7a9f94a67
18 changes: 18 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2564,6 +2564,24 @@ pub fn run() {
);
return None;
}
// Defense-in-depth: drop Windows `ERROR_FILE_SYSTEM_LIMITATION`
// (os error 665) — a persistent host-filesystem condition with
// zero local lever and no Sentry remediation path. The Tauri
// shell is a separate crate from the core, so the core's emit-site
// classifier (`expected_error_kind`) can only catch events that
// originate inside the core binary. Any filesystem-error event
// that starts in the shell (e.g. file_logging, window_state,
// CEF profile I/O) bypasses the core classifier and lands here;
// this filter is the only net for those events (TAURI-RUST-QT0:
// 6,050 events / 1 user).
if openhuman_core::core::observability::is_windows_file_system_limitation_event(&event)
{
log::debug!(
"[sentry-fs-limitation-filter] dropping Windows file-system-limitation event (os error 665) event_id={:?}",
event.event_id
);
return None;
}
// Strip server_name (hostname) to avoid leaking machine identity.
event.server_name = None;
// Attach the cached account uid so Sentry can count unique users
Expand Down
2 changes: 1 addition & 1 deletion app/src-tauri/vendor/tauri-cef
2 changes: 1 addition & 1 deletion app/src/assets/audio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Short UI chimes for the push-to-talk feature (`docs/superpowers/specs/2026-06-02
| `ptt-close.wav` | Mic closed (PTT key released). | Generated locally with Python `wave` + sine generator (1200–800 Hz sweep). | CC0 / Public Domain. |
| `ptt-error.wav` | Session aborted (empty audio, mic permission denied, etc.). | Generated locally with Python `wave` + sine generator (250 Hz tone). | CC0 / Public Domain. |

All clips are ~80–120ms, LUFS-normalized to roughly match the in-app notification sound (~ -16 LUFS). Replace freely with better-sounding equivalents — just keep them under 200ms and CC0/MIT-equivalent.
All clips are ~~80–120ms, LUFS-normalized to roughly match the in-app notification sound (~ -16 LUFS)~~. Replace freely with better-sounding equivalents — just keep them under 200ms and CC0/MIT-equivalent.
3 changes: 2 additions & 1 deletion app/src/chat/chatSendError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type ChatSendErrorCode =
| 'usage_limit_reached'
| 'prompt_blocked'
| 'prompt_review'
| 'attachment_invalid';
| 'attachment_invalid'
| 'create_thread_failed';

export interface ChatSendError {
code: ChatSendErrorCode;
Expand Down
29 changes: 28 additions & 1 deletion app/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

import type { ChatSendError } from '../../chat/chatSendError';
import type { Attachment } from '../../lib/attachments';
Expand All @@ -8,6 +8,14 @@ import AttachmentPreview from './AttachmentPreview';
/** Max composer height ≈ 8 lines of text-sm + padding. */
const COMPOSER_MAX_HEIGHT = 192;

/**
* Render-loop guard threshold: if the component re-renders this many times
* within a single microtick, we log a warning to help diagnose "Maximum update
* depth exceeded" issues (TAURI-REACT-2G). React's own limit is 50; we warn at
* a lower threshold so developers catch it before the crash.
*/
const RENDER_LOOP_WARN_THRESHOLD = 30;

export interface ChatComposerProps {
inputValue: string;
setInputValue: (value: string | ((prev: string) => string)) => void;
Expand Down Expand Up @@ -101,6 +109,25 @@ export default function ChatComposer({
const { t } = useT();
const [isDragging, setIsDragging] = useState(false);

// Render-loop detection guard: tracks re-renders within a single microtick
// and warns when the count exceeds a safe threshold, helping diagnose
// "Maximum update depth exceeded" issues (TAURI-REACT-2G).
const renderCountRef = useRef(0);
const renderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
renderCountRef.current += 1;
if (renderCountRef.current >= RENDER_LOOP_WARN_THRESHOLD) {
console.warn(
`[ChatComposer] Render-loop detected: ${renderCountRef.current} renders in a single tick. ` +
'Check whether a parent onChange/setState cycle is causing cascading re-renders.'
);
}
if (renderTimerRef.current === null) {
renderTimerRef.current = setTimeout(() => {
renderCountRef.current = 0;
renderTimerRef.current = null;
}, 0);
}

// While a turn streams (`allowParallelSend`) the composer stays usable for a
// queued follow-up / parallel branch, so the in-flight `isSending` spinner
// and lock no longer apply — only real typed content gates the send button.
Expand Down
131 changes: 130 additions & 1 deletion app/src/components/chat/__tests__/ChatComposer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { createRef } from 'react';
import { createRef, useEffect, useState } from 'react';
import { describe, expect, it, vi } from 'vitest';

import type { Attachment } from '../../../lib/attachments';
Expand Down Expand Up @@ -242,6 +242,135 @@ describe('ChatComposer', () => {
});
});

describe('render-loop detection guard', () => {
it('does not warn under normal rendering', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
renderComposer();
expect(screen.getByRole('textbox')).toBeInTheDocument();
// Normal rendering should not trigger the render-loop guard.
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Render-loop detected'));
warnSpy.mockRestore();
});

it('warns when render-loop threshold is exceeded', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const textInputRef = createRef<HTMLTextAreaElement | null>();
const fileInputRef = createRef<HTMLInputElement | null>();
const isComposingTextRef = { current: false };
// This test harness simulates a render loop by calling setState
// from a useEffect, exceeding the guard's 30-render threshold
// without triggering React's native "too many re-renders" error.
function LoopHarness() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');

useEffect(() => {
if (count < 35) {
setCount(count + 1);
}
}, [count, setCount]);

return (
<ChatComposer
inputValue={text}
setInputValue={v => setText(typeof v === 'function' ? v('') : v)}
onSend={vi.fn().mockResolvedValue(undefined)}
textInputRef={textInputRef}
fileInputRef={fileInputRef}
composerInteractionBlocked={false}
isSending={false}
attachments={[]}
onAttachFiles={vi.fn().mockResolvedValue(undefined)}
onRemoveAttachment={vi.fn()}
attachError={null}
onSwitchToMicCloud={vi.fn()}
handleInputKeyDown={vi.fn()}
inlineCompletionSuffix=""
isComposingTextRef={isComposingTextRef}
maxAttachments={5}
allowedMimeTypes={[]}
attachmentsEnabled={false}
/>
);
}

render(<LoopHarness />);

const loopCalls = warnSpy.mock.calls.filter(
args => typeof args[0] === 'string' && args[0].includes('Render-loop detected')
);
expect(loopCalls.length).toBeGreaterThan(0);
expect(loopCalls[0][0]).toContain('ChatComposer');
warnSpy.mockRestore();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('recovers from a temporary loop (counter resets between ticks)', async () => {
// After a render loop ends, the setTimeout(0) in the guard should reset
// the counter, so a subsequent normal render doesn't trigger a warning.
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const textInputRef = createRef<HTMLTextAreaElement | null>();
const fileInputRef = createRef<HTMLInputElement | null>();
const isComposingTextRef = { current: false };

// First, render normally.
const { rerender } = render(
<ChatComposer
inputValue=""
setInputValue={vi.fn()}
onSend={vi.fn().mockResolvedValue(undefined)}
textInputRef={textInputRef}
fileInputRef={fileInputRef}
composerInteractionBlocked={false}
isSending={false}
attachments={[]}
onAttachFiles={vi.fn().mockResolvedValue(undefined)}
onRemoveAttachment={vi.fn()}
attachError={null}
onSwitchToMicCloud={vi.fn()}
handleInputKeyDown={vi.fn()}
inlineCompletionSuffix=""
isComposingTextRef={isComposingTextRef}
maxAttachments={5}
allowedMimeTypes={[]}
attachmentsEnabled={false}
/>
);

// Wait for the setTimeout(0) reset to fire.
await new Promise(r => setTimeout(r, 50));

// Then render again — should not warn because the counter was reset.
rerender(
<ChatComposer
inputValue="hello"
setInputValue={vi.fn()}
onSend={vi.fn().mockResolvedValue(undefined)}
textInputRef={textInputRef}
fileInputRef={fileInputRef}
composerInteractionBlocked={false}
isSending={false}
attachments={[]}
onAttachFiles={vi.fn().mockResolvedValue(undefined)}
onRemoveAttachment={vi.fn()}
attachError={null}
onSwitchToMicCloud={vi.fn()}
handleInputKeyDown={vi.fn()}
inlineCompletionSuffix=""
isComposingTextRef={isComposingTextRef}
maxAttachments={5}
allowedMimeTypes={[]}
attachmentsEnabled={false}
/>
);

const loopCalls = warnSpy.mock.calls.filter(
args => typeof args[0] === 'string' && args[0].includes('Render-loop detected')
);
expect(loopCalls).toHaveLength(0);
warnSpy.mockRestore();
});
});

describe('follow-up / parallel mode (allowParallelSend during a streaming turn)', () => {
it('keeps the textarea editable even while an in-flight turn is sending', () => {
renderComposer({
Expand Down
38 changes: 27 additions & 11 deletions app/src/features/conversations/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ const Conversations = ({
const [sendError, setSendError] = useState<ChatSendError | null>(null);
const [attachError, setAttachError] = useState<ChatSendError | null>(null);
const [sendAdvisory, setSendAdvisory] = useState<string | null>(null);
// Refs mirroring error/advisory state for effects that read them without
// depending on them, preventing the classic "effect→setState→re-fire" cascade
// that contributes to "Maximum update depth exceeded" (TAURI-REACT-2G).
const sendErrorRef = useRef(sendError);
sendErrorRef.current = sendError;
const sendAdvisoryRef = useRef(sendAdvisory);
sendAdvisoryRef.current = sendAdvisory;
const [openRouterStatus, setOpenRouterStatus] = useState<'idle' | 'saving' | 'error'>('idle');
// Threads whose send is mid-flight (dispatched locally, backend not yet
// accepted). A Set so concurrent sends to different threads each track their
Expand Down Expand Up @@ -477,14 +484,19 @@ const Conversations = ({
typeof navigator.mediaDevices.getUserMedia === 'function';

const handleCreateNewThread = async () => {
const thread = await dispatch(createNewThread()).unwrap();
dispatch(setSelectedThread(thread.id));
void dispatch(loadThreadMessages(thread.id));
if (shouldSyncChatRoute) {
debug('[chat][route] created thread thread=%s navigate=true', thread.id);
navigate(chatThreadPath(thread.id));
} else {
debug('[chat][route] created thread thread=%s navigate=false', thread.id);
try {
const thread = await dispatch(createNewThread()).unwrap();
dispatch(setSelectedThread(thread.id));
void dispatch(loadThreadMessages(thread.id));
if (shouldSyncChatRoute) {
debug('[chat][route] created thread thread=%s navigate=true', thread.id);
navigate(chatThreadPath(thread.id));
} else {
debug('[chat][route] created thread thread=%s navigate=false', thread.id);
}
} catch (error) {
debug('[chat] create thread failed: %O', error);
setSendError(chatSendError('create_thread_failed', t('chat.createThreadFailed')));
}
};

Expand Down Expand Up @@ -716,13 +728,17 @@ const Conversations = ({
}, []);

useEffect(() => {
if (sendError && inputValue.length > 0) {
if (sendErrorRef.current && inputValue.length > 0) {
setSendError(null);
}
if (sendAdvisory && inputValue.length > 0) {
if (sendAdvisoryRef.current && inputValue.length > 0) {
setSendAdvisory(null);
}
}, [inputValue, sendAdvisory, sendError]);
// Reads sendError/sendAdvisory through refs to avoid re-firing when they
// are cleared — which would cascade into extra render cycles and contribute
// to "Maximum update depth exceeded" (TAURI-REACT-2G).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue]);

const clearSilenceTimer = useCallback((threadId: string) => {
const existing = sendingTimeoutsRef.current.get(threadId);
Expand Down
21 changes: 13 additions & 8 deletions app/src/features/conversations/components/ThreadGoalChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,19 @@ export function useThreadGoal(
}
}, [api, threadId]);

// Reset the editor + cached goal when the thread changes. Done during render
// (React's sanctioned "reset state on prop change" pattern) rather than in an
// effect, so it's synchronous and lint-clean.
if (activeThread.current !== threadId) {
activeThread.current = threadId;
setExpanded(false);
setGoal(null);
}
// Reset the editor + cached goal when the thread changes. Done in a useEffect
// rather than during render to avoid React's nested-update detection, which
// can cascade with concurrent state changes into a "Maximum update depth
// exceeded" error (TAURI-REACT-2G).
const prevThreadRef = useRef(threadId);
useEffect(() => {
if (prevThreadRef.current !== threadId) {
prevThreadRef.current = threadId;
activeThread.current = threadId; // keep the race guard in sync
setExpanded(false);
setGoal(null);
Comment on lines +101 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the active-thread guard in sync

When the selected chat thread changes, this effect clears the cached goal but never updates activeThread.current; refresh() still checks if (activeThread.current === threadId) before storing the fetched goal. After navigating from one thread to another, every fetch for the new thread is therefore discarded, so the goal chip/editor stays empty even when that thread has a saved goal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1b5a800: updates activeThread.current = threadId in the thread-change useEffect so the race guard stays in sync with the current thread.

}
}, [threadId]);

// Fetch on mount/thread-change and poll lightly. `refresh` is async, so its
// setState lands in a later microtask (not a synchronous effect write).
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'متابعات في قائمة الانتظار',
'chat.queuedFollowups.clear': 'مسح',
'chat.queuedFollowups.clearFailed': 'تعذّر مسح القائمة: حاول مرة أخرى.',
'chat.createThreadFailed': 'تعذّر إنشاء محادثة جديدة: حاول مرة أخرى.',
'chat.parallelBranchLabel': 'فرع متوازٍ',
'chat.thinking': 'جارٍ التفكير...',
'chat.noMessages': 'لا توجد رسائل بعد',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'সারিবদ্ধ ফলো-আপ',
'chat.queuedFollowups.clear': 'সাফ করুন',
'chat.queuedFollowups.clearFailed': 'সারি সাফ করা যায়নি: আবার চেষ্টা করুন।',
'chat.createThreadFailed': 'নতুন থ্রেড তৈরি করা যায়নি: আবার চেষ্টা করুন।',
'chat.parallelBranchLabel': 'সমান্তরাল শাখা',
'chat.thinking': 'ভাবছে...',
'chat.noMessages': 'এখনো কোনো বার্তা নেই',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.clear': 'Löschen',
'chat.queuedFollowups.clearFailed':
'Warteschlange konnte nicht geleert werden – bitte erneut versuchen.',
'chat.createThreadFailed': 'Neuer Thread konnte nicht erstellt werden – bitte erneut versuchen.',
'chat.parallelBranchLabel': 'Paralleler Zweig',
'chat.thinking': 'Denken...',
'chat.noMessages': 'Noch keine Nachrichten',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ const en: TranslationMap = {
'chat.queuedFollowups.label': 'Queued follow-ups',
'chat.queuedFollowups.clear': 'Clear',
'chat.queuedFollowups.clearFailed': "Couldn't clear the queue. Try again.",
'chat.createThreadFailed': "Couldn't create a new thread. Please try again.",
'chat.parallelBranchLabel': 'Parallel branch',
'chat.thinking': 'Thinking...',
'chat.noMessages': 'No messages yet',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'Seguimientos en cola',
'chat.queuedFollowups.clear': 'Borrar',
'chat.queuedFollowups.clearFailed': 'No se pudo vaciar la cola: inténtalo de nuevo.',
'chat.createThreadFailed': 'No se pudo crear un nuevo hilo. Inténtalo de nuevo.',
'chat.parallelBranchLabel': 'Rama paralela',
'chat.thinking': 'Pensando...',
'chat.noMessages': 'Sin mensajes aún',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'Suivis en file',
'chat.queuedFollowups.clear': 'Effacer',
'chat.queuedFollowups.clearFailed': 'Impossible de vider la file: réessayez.',
'chat.createThreadFailed': 'Impossible de créer un nouveau fil. Veuillez réessayer.',
'chat.parallelBranchLabel': 'Branche parallèle',
'chat.thinking': 'En train de réfléchir…',
'chat.noMessages': "Aucun message pour l'instant",
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'कतारबद्ध फ़ॉलो-अप',
'chat.queuedFollowups.clear': 'साफ़ करें',
'chat.queuedFollowups.clearFailed': 'कतार साफ़ नहीं हो सकी: फिर से प्रयास करें।',
'chat.createThreadFailed': 'नया थ्रेड नहीं बनाया जा सका: कृपया पुनः प्रयास करें।',
'chat.parallelBranchLabel': 'समानांतर शाखा',
'chat.thinking': 'सोच रहा है...',
'chat.noMessages': 'अभी कोई मैसेज नहीं',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'Tindak lanjut dalam antrean',
'chat.queuedFollowups.clear': 'Hapus',
'chat.queuedFollowups.clearFailed': 'Gagal mengosongkan antrean: coba lagi.',
'chat.createThreadFailed': 'Gagal membuat thread baru: coba lagi.',
'chat.parallelBranchLabel': 'Cabang paralel',
'chat.thinking': 'Berpikir...',
'chat.noMessages': 'Belum ada pesan',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,7 @@ const messages: TranslationMap = {
'chat.queuedFollowups.label': 'Follow-up in coda',
'chat.queuedFollowups.clear': 'Cancella',
'chat.queuedFollowups.clearFailed': 'Impossibile svuotare la coda: riprova.',
'chat.createThreadFailed': 'Impossibile creare una nuova conversazione. Riprova.',
'chat.parallelBranchLabel': 'Ramo parallelo',
'chat.thinking': 'Sto pensando...',
'chat.noMessages': 'Nessun messaggio',
Expand Down
Loading
Loading