fix(acp): return authe error for acp provider from goose acp server (#11202)

This commit is contained in:
Lifei Zhou
2026-08-13 09:28:59 +00:00
committed by GitHub
parent 11deb564d0
commit 849b6f2ae8
34 changed files with 380 additions and 58 deletions
@@ -262,6 +262,7 @@ pub struct SystemNotificationContent {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MessageErrorKind {
Authentication,
ContextLengthExceeded,
CreditsExhausted,
#[serde(other)]
@@ -272,6 +273,7 @@ impl From<&crate::errors::ProviderError> for MessageErrorKind {
fn from(err: &crate::errors::ProviderError) -> Self {
use crate::errors::ProviderError;
match err {
ProviderError::Authentication(_) => MessageErrorKind::Authentication,
ProviderError::ContextLengthExceeded(_) => MessageErrorKind::ContextLengthExceeded,
ProviderError::CreditsExhausted { .. } => MessageErrorKind::CreditsExhausted,
_ => MessageErrorKind::Other,
@@ -1320,9 +1322,10 @@ pub struct TokenState {
#[cfg(test)]
mod tests {
use crate::conversation::message::{
ActionRequiredData, Message, MessageContentBlock, MessageMetadata, ProviderMetadata,
ToolResponse,
ActionRequiredData, Message, MessageContentBlock, MessageErrorKind, MessageMetadata,
ProviderMetadata, ToolResponse,
};
use crate::errors::ProviderError;
use base64::Engine;
use rmcp::model::{
Annotations, CallToolResult, ElicitationAction, ErrorCode, ErrorData, ImageContent,
@@ -1332,6 +1335,17 @@ mod tests {
use rmcp::object;
use serde_json::Value;
#[test]
fn provider_authentication_error_has_authentication_kind() {
let message = Message::from_provider_error(&ProviderError::Authentication(
"Authentication required".to_string(),
));
assert_eq!(message.error_kind(), Some(MessageErrorKind::Authentication));
assert!(message.is_user_visible());
assert!(!message.is_agent_visible());
}
#[test]
fn test_sanitize_with_text() {
let malicious = "Hello\u{E0041}\u{E0042}\u{E0043}world"; // Invisible "ABC"
+29
View File
@@ -14,3 +14,32 @@ pub use goose_sdk_types::{custom_notifications, custom_requests};
pub use provider::{
resolve_extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
};
pub(crate) fn is_auth_required(error: &anyhow::Error) -> bool {
error.chain().any(|source| {
source
.downcast_ref::<agent_client_protocol::Error>()
.is_some_and(|error| {
error.code == agent_client_protocol::schema::v1::ErrorCode::AuthRequired
})
})
}
#[cfg(test)]
mod tests {
use super::is_auth_required;
#[test]
fn identifies_typed_auth_required_errors() {
let error = anyhow::Error::new(agent_client_protocol::Error::auth_required());
assert!(is_auth_required(&error));
}
#[test]
fn does_not_classify_other_acp_errors_as_authentication() {
let error = anyhow::Error::new(agent_client_protocol::Error::internal_error());
assert!(!is_auth_required(&error));
}
}
+80 -8
View File
@@ -126,7 +126,15 @@ enum AcpUpdate {
response_tx: oneshot::Sender<RequestPermissionResponse>,
},
Complete(StopReason, Option<AcpUsage>),
Error(String),
Error(agent_client_protocol::Error),
}
fn provider_error_from_acp(error: agent_client_protocol::Error) -> ProviderError {
if error.code == agent_client_protocol::schema::v1::ErrorCode::AuthRequired {
ProviderError::Authentication(error.to_string())
} else {
ProviderError::RequestFailed(error.to_string())
}
}
/// Per-tool-call buffer for accumulating ACP ToolCallUpdate fields across
@@ -723,7 +731,7 @@ impl Provider for AcpProvider {
break;
}
AcpUpdate::Error(e) => {
Err(ProviderError::RequestFailed(e))?;
Err(provider_error_from_acp(e))?;
}
}
}
@@ -1108,6 +1116,11 @@ fn log_undelivered<E: std::fmt::Debug>(result: Result<(), E>, method: &str) {
}
}
fn acp_method_error(method: &str, error: agent_client_protocol::Error) -> anyhow::Error {
let message = format!("ACP {method} failed: {error}");
anyhow::Error::new(error).context(message)
}
async fn handle_requests(
config: AcpProviderConfig,
goose_mode: Arc<Mutex<GooseMode>>,
@@ -1164,10 +1177,7 @@ async fn handle_requests(
.await?;
apply_session_mode(&config, &goose_mode, &cx, session).await
}
Err(err) => Err(anyhow::anyhow!(
"ACP {} failed: {err}",
AGENT_METHOD_NAMES.session_new
)),
Err(error) => Err(acp_method_error(AGENT_METHOD_NAMES.session_new, error)),
};
log_undelivered(response_tx.send(result), AGENT_METHOD_NAMES.session_new);
}
@@ -1273,7 +1283,7 @@ async fn handle_requests(
}
Err(e) => {
log_undelivered(
response_tx.try_send(AcpUpdate::Error(e.to_string())),
response_tx.try_send(AcpUpdate::Error(e)),
AGENT_METHOD_NAMES.session_prompt,
);
}
@@ -1800,7 +1810,7 @@ mod tests {
use super::*;
use crate::agents::extension::Envs;
use agent_client_protocol::schema::v1::{
SessionConfigSelectOption, SessionMode, SessionModeId,
ErrorCode, SessionConfigSelectOption, SessionMode, SessionModeId,
};
use test_case::test_case;
@@ -1812,6 +1822,68 @@ mod tests {
}
}
fn acp_error_code(error: &anyhow::Error) -> Option<ErrorCode> {
error.chain().find_map(|source| {
source
.downcast_ref::<agent_client_protocol::Error>()
.map(|error| error.code)
})
}
#[test]
fn session_new_error_preserves_auth_required_code() {
let error = acp_method_error(
AGENT_METHOD_NAMES.session_new,
agent_client_protocol::Error::auth_required(),
);
assert_eq!(
error.to_string(),
"ACP session/new failed: Authentication required"
);
assert_eq!(acp_error_code(&error), Some(ErrorCode::AuthRequired));
}
#[test]
fn session_new_error_preserves_non_authentication_code() {
let error = acp_method_error(
AGENT_METHOD_NAMES.session_new,
agent_client_protocol::Error::internal_error(),
);
assert_eq!(acp_error_code(&error), Some(ErrorCode::InternalError));
}
#[tokio::test]
async fn prompt_error_update_preserves_auth_required_code() {
let (tx, mut rx) = mpsc::channel(1);
tx.send(AcpUpdate::Error(
agent_client_protocol::Error::auth_required().data("sign in"),
))
.await
.unwrap();
let AcpUpdate::Error(error) = rx.recv().await.unwrap() else {
panic!("expected ACP error update");
};
assert_eq!(error.code, ErrorCode::AuthRequired);
assert_eq!(error.data, Some(serde_json::json!("sign in")));
}
#[test]
fn prompt_auth_error_maps_to_provider_authentication() {
let error = provider_error_from_acp(agent_client_protocol::Error::auth_required());
assert!(matches!(error, ProviderError::Authentication(_)));
}
#[test]
fn prompt_internal_error_remains_request_failed() {
let error = provider_error_from_acp(agent_client_protocol::Error::internal_error());
assert!(matches!(error, ProviderError::RequestFailed(_)));
}
fn test_provider() -> (AcpProvider, ModelConfig) {
test_provider_with_tx(None)
}
+53 -1
View File
@@ -157,6 +157,14 @@ impl<T, E: std::fmt::Display> ResultExt<T> for Result<T, E> {
}
}
fn agent_creation_error(error: anyhow::Error, context: &str) -> agent_client_protocol::Error {
if crate::acp::is_auth_required(&error) {
agent_client_protocol::Error::auth_required()
} else {
agent_client_protocol::Error::internal_error().data(format!("{context}: {error}"))
}
}
pub(super) const DEFAULT_PROVIDER_ID: &str = "goose";
pub(super) const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)";
const PROVIDER_CONFIG_STATUS_CHECK_CONCURRENCY: usize = 16;
@@ -760,7 +768,7 @@ impl GooseAcpAgent {
},
)
.await
.internal_err_ctx("Failed to create agent")
.map_err(|error| agent_creation_error(error, "Failed to create agent"))
}
fn initial_session_extensions(
@@ -1337,6 +1345,11 @@ fn prompt_error_from_message_content(
content_item: &MessageContent,
) -> Option<agent_client_protocol::Error> {
match content_item {
MessageContent::Error(error)
if error.kind == crate::conversation::message::MessageErrorKind::Authentication =>
{
Some(agent_client_protocol::Error::auth_required())
}
MessageContent::SystemNotification(notification)
if notification.notification_type == SystemNotificationType::CreditsExhausted =>
{
@@ -2358,6 +2371,30 @@ mod tests {
use tempfile::NamedTempFile;
use test_case::test_case;
#[test]
fn agent_creation_auth_error_maps_to_auth_required() {
let error = anyhow::Error::new(agent_client_protocol::Error::auth_required());
let error = agent_creation_error(error, "Failed to create agent");
assert_eq!(
error.code,
agent_client_protocol::schema::v1::ErrorCode::AuthRequired
);
}
#[test]
fn agent_creation_non_auth_error_remains_internal() {
let error = anyhow::Error::new(agent_client_protocol::Error::internal_error());
let error = agent_creation_error(error, "Failed to create agent");
assert_eq!(
error.code,
agent_client_protocol::schema::v1::ErrorCode::InternalError
);
}
fn config_with_yaml(yaml: &str) -> (Config, NamedTempFile, NamedTempFile) {
let config_file = NamedTempFile::new().unwrap();
let secrets_file = NamedTempFile::new().unwrap();
@@ -2607,6 +2644,21 @@ print(\"hello, world\")
);
}
#[test]
fn test_authentication_message_maps_to_auth_required() {
let content = MessageContent::error(
crate::conversation::message::MessageErrorKind::Authentication,
"Authentication required",
);
let error = prompt_error_from_message_content(&content).expect("expected prompt error");
assert_eq!(
error.code,
agent_client_protocol::schema::v1::ErrorCode::AuthRequired
);
}
#[test]
fn test_non_credit_system_notification_does_not_map_to_prompt_error() {
let content = MessageContent::SystemNotification(SystemNotificationContent {
+48 -8
View File
@@ -86,6 +86,11 @@ const EMPTY_TURN_MESSAGE: &str =
"The model returned an empty response. Please resend your message to continue.";
const DEFAULT_FRONTEND_INSTRUCTIONS: &str = "The following tools are provided directly by the frontend and will be executed by the frontend when called.";
fn provider_creation_error(error: anyhow::Error, context: impl fmt::Display) -> anyhow::Error {
let message = format!("{context}: {error}");
error.context(message)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ToolCategory {
Shell,
@@ -3045,6 +3050,21 @@ impl Agent {
exit_chat = true;
break;
}
Err(ref provider_err @ ProviderError::Authentication(_)) => {
provider_errored = true;
#[cfg(feature = "telemetry")]
crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string());
error!("Error: {}", provider_err);
let message = persist_and_push_message_with_id(
&session_manager,
&session_config.id,
&mut conversation,
Message::from_provider_error(provider_err),
)
.await?;
yield AgentEvent::Message(message);
break;
}
Err(ref provider_err @ ProviderError::NetworkError(_)) => {
provider_errored = true;
#[cfg(feature = "telemetry")]
@@ -3478,7 +3498,7 @@ impl Agent {
session.working_dir.clone(),
)
.await
.map_err(|e| anyhow!("Could not create provider: {}", e))?;
.map_err(|error| provider_creation_error(error, "Could not create provider"))?;
self.update_provider(provider, model_config, session_id)
.await?;
@@ -3555,7 +3575,7 @@ impl Agent {
session.working_dir.clone(),
)
.await
.map_err(|e| anyhow!("Could not create provider: {}", e))?;
.map_err(|error| provider_creation_error(error, "Could not create provider"))?;
(p, model_config, false)
} else {
let fallback_provider_name = config
@@ -3592,12 +3612,12 @@ impl Agent {
session.working_dir.clone(),
)
.await
.map_err(|e| {
anyhow!(
"Could not create provider '{}' or fallback '{}': {}",
provider_name,
fallback_provider_name,
e
.map_err(|error| {
provider_creation_error(
error,
format!(
"Could not create provider '{provider_name}' or fallback '{fallback_provider_name}'"
),
)
})?;
@@ -3957,6 +3977,26 @@ mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::TempDir;
#[test]
fn provider_creation_context_preserves_acp_error_code() {
let source = anyhow::Error::new(agent_client_protocol::Error::auth_required())
.context("ACP session/new failed: Authentication required");
let error = provider_creation_error(source, "Could not create provider");
assert_eq!(
error.to_string(),
"Could not create provider: ACP session/new failed: Authentication required"
);
assert!(error.chain().any(|source| {
source
.downcast_ref::<agent_client_protocol::Error>()
.is_some_and(|error| {
error.code == agent_client_protocol::schema::v1::ErrorCode::AuthRequired
})
}));
}
#[test]
fn provider_session_id_comes_from_latest_inference() {
let messages = vec![
+5 -2
View File
@@ -214,11 +214,14 @@ impl AgentManager {
"Restoring evicted session {} (provider: {:?})",
session_id, session.provider_name
);
if let Err(e) = agent.restore_provider_from_session(&session).await {
if let Err(error) = agent.restore_provider_from_session(&session).await {
if crate::acp::is_auth_required(&error) {
return Err(error);
}
tracing::warn!(
"Failed to restore provider for session {}: {}",
session_id,
e
error
);
}
}
+17 -2
View File
@@ -422,10 +422,25 @@ impl GatewayHandler {
.await?;
}
let agent = self
let agent = match self
.agent_manager
.get_or_create_agent(session_id.to_string())
.await?;
.await
{
Ok(agent) => agent,
Err(error) if crate::acp::is_auth_required(&error) => {
self.gateway
.send_message(
&message.user,
OutgoingMessage::Text {
body: format!("⚠️ Failed to configure provider: {error}"),
},
)
.await?;
return Ok(());
}
Err(error) => return Err(error),
};
// Re-read the session after sync so restore picks up the new values.
let session = self
@@ -169,6 +169,20 @@ describe('acpChatSessionController.loadSession', () => {
);
});
it('retries a failed cached session load', async () => {
vi.mocked(acpChatSessionStore.getSnapshot).mockReturnValue({
...snapshotWithActivePrompt(null),
session: loadedSession(),
sessionLoadError: 'Sign in to your provider, then try again.',
});
vi.mocked(isAcpSessionLoadInFlight).mockReturnValue(false);
await acpChatSessionController.loadSession(SESSION_ID);
expect(acpChatSessionActions.startSessionLoad).toHaveBeenCalledWith(SESSION_ID);
expect(acpLoadSession).toHaveBeenCalledWith(SESSION_ID);
});
it('restores a cached session from the server', async () => {
vi.mocked(acpChatSessionStore.getSnapshot).mockReturnValue({
...snapshotWithActivePrompt(null),
@@ -252,6 +266,7 @@ describe('acpChatSessionController.submitMessage', () => {
expect(acpChatSessionActions.startPromptAttempt).not.toHaveBeenCalled();
expect(acpPromptSession).not.toHaveBeenCalled();
});
});
describe('acpChatSessionController.updateMessage', () => {
@@ -242,13 +242,9 @@ describe('acpChatSessionStore', () => {
acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-a');
acpChatSessionActions.startPromptAttempt(currentSessionId, 'attempt-b');
expect(
acpChatSessionActions.finishPromptAttemptIfCurrent(
currentSessionId,
'attempt-a',
'late error'
)
).toBe(false);
expect(acpChatSessionActions.finishPromptAttemptIfCurrent(currentSessionId, 'attempt-a')).toBe(
false
);
expect(acpChatSessionStore.getSnapshot(currentSessionId)).toMatchObject({
activePromptAttemptId: 'attempt-b',
+10 -1
View File
@@ -1,5 +1,14 @@
import { describe, expect, it } from 'vitest';
import { parseAcpCreditsExhaustedError } from '../errors';
import { RequestError } from '@agentclientprotocol/sdk';
import { formatAcpError, parseAcpCreditsExhaustedError } from '../errors';
describe('formatAcpError', () => {
it('explains how to recover from an authentication error', () => {
expect(formatAcpError(RequestError.authRequired())).toBe(
'Sign in to your provider, then try again.'
);
});
});
describe('parseAcpCreditsExhaustedError', () => {
it('parses structured ACP credits exhausted errors', () => {
+9 -8
View File
@@ -3,7 +3,6 @@ import type { GooseExtension } from '@aaif/goose-sdk';
import { AppEvents } from '../constants/events';
import { ChatState } from '../types/chatState';
import type { Session } from '../types/session';
import { errorMessage } from '../utils/conversionUtils';
import { showExtensionLoadResults } from '../utils/extensionErrorUtils';
import {
createUserMessage,
@@ -17,7 +16,11 @@ import {
type AcpChatSessionSnapshot,
} from './chatSessionStore';
import { cancelAcpElicitationRequestsForSession } from './elicitationRequests';
import { parseAcpCreditsExhaustedError, type AcpCreditsExhaustedError } from './errors';
import {
formatAcpError,
parseAcpCreditsExhaustedError,
type AcpCreditsExhaustedError,
} from './errors';
import { cancelAcpPermissionRequestsForSession } from './permissionRequests';
import { acpCancelPrompt, acpPromptSession } from './prompt';
import {
@@ -128,7 +131,7 @@ async function createSession(
async function loadSession(sessionId: string, options: AcpLoadSessionOptions = {}): Promise<void> {
const cached = acpChatSessionStore.getSnapshot(sessionId);
if (cached?.session) {
if (cached?.session && !cached.sessionLoadError) {
window.dispatchEvent(
new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED, { detail: { sessionId } })
);
@@ -162,7 +165,7 @@ async function loadSessionFromServer(
options.onSessionLoaded?.();
} catch (error) {
console.error('Failed to load ACP session:', error);
acpChatSessionActions.failSessionLoad(sessionId, errorMessage(error));
acpChatSessionActions.failSessionLoad(sessionId, formatAcpError(error));
}
}
@@ -211,10 +214,8 @@ async function submitMessage(
return;
}
const submitError = 'Submit error: ' + errorMessage(error);
if (
acpChatSessionActions.finishPromptAttemptIfCurrent(sessionId, promptAttemptId, submitError)
) {
const submitError = formatAcpError(error);
if (acpChatSessionActions.finishPromptAttemptIfCurrent(sessionId, promptAttemptId)) {
void options.onFinish(submitError);
}
}
+2 -4
View File
@@ -104,7 +104,7 @@ export interface AcpChatSessionActions {
promptAttemptId: string
): AcpChatSessionSnapshot | undefined;
waitForPromptCancellation(sessionId: string, promptAttemptId: string): Promise<void>;
finishPromptAttemptIfCurrent(sessionId: string, promptAttemptId: string, error?: string): boolean;
finishPromptAttemptIfCurrent(sessionId: string, promptAttemptId: string): boolean;
clearActivePromptAttempt(sessionId: string): AcpChatSessionSnapshot | undefined;
isCurrentPromptAttempt(sessionId: string, promptAttemptId: string): boolean;
}
@@ -403,8 +403,7 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal {
const finishPromptAttemptIfCurrent: AcpChatSessionActions['finishPromptAttemptIfCurrent'] = (
sessionId,
promptAttemptId,
error
promptAttemptId
) => {
const entry = sessionsById.get(sessionId);
if (!entry || entry.activePromptAttemptId !== promptAttemptId) {
@@ -419,7 +418,6 @@ function createAcpChatSessionStoreInternal(): AcpChatSessionStoreInternal {
discardPendingLocalSteerMessages(entry);
entry.progressMessage = undefined;
entry.chatState = ChatState.Idle;
entry.sessionLoadError = error;
notify(sessionId, entry);
return true;
};
+11
View File
@@ -1,9 +1,13 @@
import { RequestError } from '@agentclientprotocol/sdk';
import { errorMessage } from '../utils/conversionUtils';
export interface AcpCreditsExhaustedError {
message: string;
url?: string;
}
const CREDITS_EXHAUSTED_REASON = 'credits_exhausted';
const AUTH_REQUIRED_CODE = -32000;
// Kept in sync with RECIPE_PARAMS_CANCELLED_REASON in crates/goose/src/acp/server/recipe.rs.
const RECIPE_PARAMS_CANCELLED_REASON = 'recipe_params_cancelled';
@@ -42,6 +46,13 @@ export function parseAcpCreditsExhaustedError(error: unknown): AcpCreditsExhaust
};
}
export function formatAcpError(error: unknown): string {
if (error instanceof RequestError && error.code === AUTH_REQUIRED_CODE) {
return 'Sign in to your provider, then try again.';
}
return errorMessage(error);
}
interface AcpJsonRpcError {
message: string;
data: Record<string, unknown>;
+14 -8
View File
@@ -8,6 +8,7 @@ import ProgressiveMessageList from './ProgressiveMessageList';
import { MainPanelLayout } from './Layout/MainPanelLayout';
import ChatInput from './ChatInput';
import { ChatInputCard } from './ChatInputCard';
import { Button } from './ui/button';
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
import { useFileDrop } from '../hooks/useFileDrop';
import { ChatState } from '../types/chatState';
@@ -45,6 +46,10 @@ const i18n = defineMessages({
id: 'baseChat.goHome',
defaultMessage: 'Go home',
},
retry: {
id: 'baseChat.retry',
defaultMessage: 'Retry',
},
reconnecting: {
id: 'baseChat.reconnecting',
defaultMessage: 'Connection lost. Reconnecting…',
@@ -106,6 +111,7 @@ export default function BaseChat({
onSteerQueuedMessage,
submitElicitationResponse,
stopStreaming,
retrySessionLoad,
sessionLoadError,
tokenState,
notifications: toolCallNotifications,
@@ -384,14 +390,14 @@ export default function BaseChat({
</h3>
<p className="text-sm">{sessionLoadError}</p>
</div>
<button
onClick={() => {
setView('chat');
}}
className="px-4 py-2 text-center cursor-pointer text-text-primary border border-border-primary hover:bg-background-secondary rounded-lg transition-all duration-150"
>
{intl.formatMessage(i18n.goHome)}
</button>
<div className="flex gap-2">
<Button variant="outline" onClick={() => void retrySessionLoad()}>
{intl.formatMessage(i18n.retry)}
</Button>
<Button variant="outline" onClick={() => setView('chat')}>
{intl.formatMessage(i18n.goHome)}
</Button>
</div>
</div>
</div>
</div>
+4 -1
View File
@@ -1118,7 +1118,9 @@ export default function ChatInput({
setLastInterruption(null);
}
clearInputState();
if (sessionId !== null) {
clearInputState();
}
setHistoryIndex(-1);
setSavedInput('');
setIsInGlobalHistory(false);
@@ -1133,6 +1135,7 @@ export default function ChatInput({
handleSubmit,
lastInterruption,
clearInputState,
sessionId,
]
);
+3
View File
@@ -25,6 +25,8 @@ import {
selectNextChatExtensions,
type NextChatExtensionDraft,
} from '../utils/nextChatExtensions';
import { formatAcpError } from '../acp/errors';
import { toastError } from '../toasts';
const i18n = defineMessages({
goodMorning: { id: 'hub.goodMorning', defaultMessage: 'Good morning' },
@@ -116,6 +118,7 @@ export default function Hub({
});
} catch (error) {
console.error('Failed to create session:', error);
toastError({ title: "Couldn't start chat", msg: formatAcpError(error) });
setIsCreatingSession(false);
}
};
+12 -6
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { defineMessages, useIntl } from '../i18n';
import { AppEvents } from '../constants/events';
import { toastError } from '../toasts';
import { ChatState } from '../types/chatState';
import type { TokenState } from '../types/chat';
@@ -108,7 +109,9 @@ export function useChatSession({
const onFinish = useCallback(
async (error?: string): Promise<void> => {
if (!error) {
if (error) {
toastError({ title: "Couldn't send message", msg: error });
} else {
try {
const [notificationsEnabled, anyWindowFocused] = await Promise.all([
window.electron.getSetting('enableNotifications'),
@@ -143,12 +146,15 @@ export function useChatSession({
[getCurrentSnapshot, onFinish, sessionId]
);
const retrySessionLoad = useCallback(
() => acpChatSessionController.loadSession(sessionId, { onSessionLoaded }),
[sessionId, onSessionLoaded]
);
// Load session on mount or sessionId change
useEffect(() => {
if (!sessionId) return;
void acpChatSessionController.loadSession(sessionId, { onSessionLoaded });
}, [sessionId, onSessionLoaded]);
void retrySessionLoad();
}, [retrySessionLoad]);
const handleSubmit = useCallback(
async (input: UserInput) => {
@@ -302,7 +308,6 @@ export function useChatSession({
} catch (error) {
const errorMsg = errorMessage(error);
console.error('Failed to edit message:', error);
const { toastError } = await import('../toasts');
toastError({
title: 'Failed to edit message',
msg: errorMsg,
@@ -344,6 +349,7 @@ export function useChatSession({
onSteerQueuedMessage,
submitElicitationResponse,
stopStreaming,
retrySessionLoad,
tokenState,
notifications: notificationsMap,
pauseQueueOnStop: false,
@@ -22,6 +22,7 @@ export interface UseChatSessionResult {
userData: Record<string, unknown>
) => Promise<boolean>;
stopStreaming: () => void;
retrySessionLoad: () => Promise<void>;
sessionLoadError?: string;
tokenState: TokenState;
notifications: Map<string, NotificationEvent[]>;
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Verbindung unterbrochen. Verbindung wird wiederhergestellt…"
},
"baseChat.retry": {
"defaultMessage": "Wiederholen"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Erweiterung aktualisiert"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Connection lost. Reconnecting…"
},
"baseChat.retry": {
"defaultMessage": "Retry"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Extension Updated"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Conexión perdida. Reconectando…"
},
"baseChat.retry": {
"defaultMessage": "Reintentar"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Extensión actualizada"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Connexion perdue. Reconnexion…"
},
"baseChat.retry": {
"defaultMessage": "Réessayer"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Extension mise à jour"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "कनेक्शन टूट गया। फिर से कनेक्ट किया जा रहा है…"
},
"baseChat.retry": {
"defaultMessage": "पुनः प्रयास करें"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "एक्सटेंशन अपडेट किया गया"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Koneksi terputus. Menghubungkan kembali…"
},
"baseChat.retry": {
"defaultMessage": "Coba lagi"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Ekstensi Diperbarui"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Connessione persa. Riconnessione…"
},
"baseChat.retry": {
"defaultMessage": "Riprova"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Estensione aggiornata"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "接続が失われました。再接続しています…"
},
"baseChat.retry": {
"defaultMessage": "再試行"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "拡張機能を更新しました"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "연결이 끊어졌습니다. 다시 연결하는 중…"
},
"baseChat.retry": {
"defaultMessage": "다시 시도"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "익스텐션이 업데이트되었습니다."
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Sambungan terputus. Menyambung semula…"
},
"baseChat.retry": {
"defaultMessage": "Cuba semula"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Sambungan Dikemas Kini"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Conexão perdida. Reconectando…"
},
"baseChat.retry": {
"defaultMessage": "Tentar novamente"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Extensão atualizada"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Соединение потеряно. Повторное подключение…"
},
"baseChat.retry": {
"defaultMessage": "Повторить"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Расширение обновлено"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Bağlantı kesildi. Yeniden bağlanılıyor…"
},
"baseChat.retry": {
"defaultMessage": "Yeniden dene"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Uzantı Güncellendi"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "Mất kết nối. Đang kết nối lại…"
},
"baseChat.retry": {
"defaultMessage": "Thử lại"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "Đã cập nhật tiện ích mở rộng"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "连接已断开。正在重新连接…"
},
"baseChat.retry": {
"defaultMessage": "重试"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "扩展已更新"
},
+3
View File
@@ -134,6 +134,9 @@
"baseChat.reconnecting": {
"defaultMessage": "連線已中斷。正在重新連線…"
},
"baseChat.retry": {
"defaultMessage": "重試"
},
"bottomMenuExtensionSelection.extensionUpdated": {
"defaultMessage": "擴充功能已更新"
},