authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 18:06:49+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 18:06:49+02:00
log315b27385d7eb36a56b410fa8b624fc2ed246278
tree5621d9b87ba171649b450da2accc3c9fcc569792
parent719c905eacdcb04e11dd4ce1e515a44455ed0874
signature Signed by PGP key B5690EEEBB952194

[codex] Deduplicate connection failure messaging (#4367)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 25 insertions(+), 22 deletions(-)

apps/web/src/components/ChatView.tsx+2-2
...@@ -22,7 +22,7 @@ import {...@@ -22,7 +22,7 @@ import {
22 TerminalOpenInput,22 TerminalOpenInput,
23} from "@t3tools/contracts";23} from "@t3tools/contracts";
24import {24import {
25 connectionStatusText,25 connectionStatusTitle,
26 type EnvironmentConnectionPresentation,26 type EnvironmentConnectionPresentation,
27} from "@t3tools/client-runtime/connection";27} from "@t3tools/client-runtime/connection";
28import {28import {
...@@ -1815,7 +1815,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1815,7 +1815,7 @@ function ChatViewContent(props: ChatViewProps) {
1815 id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`,1815 id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`,
1816 variant: connection.phase === "error" ? "error" : "warning",1816 variant: connection.phase === "error" ? "error" : "warning",
1817 icon: <WifiOffIcon />,1817 icon: <WifiOffIcon />,
1818 title: `${activeEnvironmentUnavailableState.label}: ${connectionStatusText(connection)}`,1818 title: `${activeEnvironmentUnavailableState.label}: ${connectionStatusTitle(connection)}`,
1819 description:1819 description:
1820 connection.error ??1820 connection.error ??
1821 "Reconnect this environment before sending messages or running actions.",1821 "Reconnect this environment before sending messages or running actions.",
apps/web/src/components/chat/ChatComposer.tsx+6-13
...@@ -18,10 +18,7 @@ import {...@@ -18,10 +18,7 @@ import {
18 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,18 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
19 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,19 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
20} from "@t3tools/contracts";20} from "@t3tools/contracts";
21import {21import type { EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection";
22 connectionStatusText,
23 type EnvironmentConnectionPresentation,
24} from "@t3tools/client-runtime/connection";
25import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";22import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";
26import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";23import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";
27import {24import {
...@@ -2577,15 +2574,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2577,15 +2574,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2577 ? "Add feedback to refine the plan, or leave this blank to implement it"2574 ? "Add feedback to refine the plan, or leave this blank to implement it"
2578 : projectSelectionRequired2575 : projectSelectionRequired
2579 ? "Choose a project above to start a thread"2576 ? "Choose a project above to start a thread"
2580 : environmentUnavailable2577 : noProviderAvailable
2581 ? `${environmentUnavailable.label}: ${connectionStatusText(2578 ? "Enable a provider in Settings to send a message"
2582 environmentUnavailable.connection,2579 : phase === "disconnected"
2583 )}`2580 ? "Ask for follow-up changes or attach images"
2584 : noProviderAvailable2581 : "Ask anything, @tag files/folders, $use skills, or / for commands"
2585 ? "Enable a provider in Settings to send a message"
2586 : phase === "disconnected"
2587 ? "Ask for follow-up changes or attach images"
2588 : "Ask anything, @tag files/folders, $use skills, or / for commands"
2589 }2582 }
2590 disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}2583 disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}
2591 />2584 />
packages/client-runtime/src/connection/presentation.test.ts+10-7
...@@ -12,6 +12,7 @@ import {...@@ -12,6 +12,7 @@ import {
12 connectionCatalogDisplayUrl,12 connectionCatalogDisplayUrl,
13 connectionPhaseMessage,13 connectionPhaseMessage,
14 connectionStatusText,14 connectionStatusText,
15 connectionStatusTitle,
15 presentEnvironmentConnection,16 presentEnvironmentConnection,
16 presentConnectionState,17 presentConnectionState,
17} from "./presentation.ts";18} from "./presentation.ts";
...@@ -123,13 +124,15 @@ describe("connection presentation", () => {...@@ -123,13 +124,15 @@ describe("connection presentation", () => {
123 });124 });
124125
125 it("combines reconnect progress with the latest failure", () => {126 it("combines reconnect progress with the latest failure", () => {
126 expect(127 const connection = {
127 connectionStatusText({128 phase: "reconnecting",
128 phase: "reconnecting",129 error: "Relay request timed out.",
129 error: "Relay request timed out.",130 traceId: "trace-retry",
130 traceId: "trace-retry",131 } as const;
131 }),132 expect(connectionStatusText(connection)).toBe(
132 ).toBe("Failed to connect. Reconnecting... Reason: Relay request timed out.");133 "Failed to connect. Reconnecting... Reason: Relay request timed out.",
134 );
135 expect(connectionStatusTitle(connection)).toBe("Failed to connect. Reconnecting...");
133 });136 });
134137
135 it("presents the supervisor's offline state without consulting shell state", () => {138 it("presents the supervisor's offline state without consulting shell state", () => {
packages/client-runtime/src/connection/presentation.ts+7
...@@ -76,6 +76,13 @@ export function connectionStatusText(connection: EnvironmentConnectionPresentati...@@ -76,6 +76,13 @@ export function connectionStatusText(connection: EnvironmentConnectionPresentati
76 }76 }
77}77}
7878
79export function connectionStatusTitle(connection: EnvironmentConnectionPresentation): string {
80 if (connection.phase === "reconnecting" && connection.error) {
81 return "Failed to connect. Reconnecting...";
82 }
83 return connectionStatusText({ ...connection, error: null });
84}
85
79export function presentEnvironmentConnection(86export function presentEnvironmentConnection(
80 state: SupervisorConnectionState,87 state: SupervisorConnectionState,
81): EnvironmentConnectionPresentation {88): EnvironmentConnectionPresentation {