fix: remove composer footer error messages
This commit is contained in:
@@ -957,8 +957,9 @@ Deno.test("Worker Console keeps Notify beside Submit without persistent notices"
|
||||
!consolePage.includes("composer-notice") &&
|
||||
!consolePage.includes('class="composer-actions"') &&
|
||||
!consolePage.includes("Sent through Worker protocol.") &&
|
||||
consolePage.includes('{#if sendError}<p class="error" role="alert">'),
|
||||
"Composer must remove persistent notices and empty action rows but retain send errors",
|
||||
!consolePage.includes("sendError") &&
|
||||
consolePage.includes("reportComposerError(command.message)"),
|
||||
"Composer must not render notices or errors below its input shell; validation uses Workspace alerts",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
let fileInput: HTMLInputElement | null = null;
|
||||
let isDraggingFiles = $state(false);
|
||||
let sending = $state(false);
|
||||
let sendError = $state<string | null>(null);
|
||||
let rewindTargets = $state<RewindTarget[]>([]);
|
||||
let rewindHeadEntries = $state(0);
|
||||
let protocolState = $state<"connecting" | "open" | "closed" | "error">(
|
||||
@@ -765,7 +764,6 @@
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
sendError = message;
|
||||
pushWorkspaceAlert("error", message, {
|
||||
id: controlAlertId,
|
||||
title: "Worker control failed",
|
||||
@@ -780,7 +778,7 @@
|
||||
): ProtocolMethod | null {
|
||||
const state = consoleProjection.workerState;
|
||||
if (!state) {
|
||||
sendError = "Worker state snapshot is not available; reconnect before sending control.";
|
||||
reportComposerError("Worker state snapshot is not available; reconnect before sending control.");
|
||||
return null;
|
||||
}
|
||||
const commandId = Math.max(
|
||||
@@ -1082,21 +1080,28 @@
|
||||
if (event.dataTransfer?.files) addAttachmentFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
function reportComposerError(message: string) {
|
||||
pushWorkspaceAlert("error", message, {
|
||||
id: controlAlertId,
|
||||
title: "Console input",
|
||||
});
|
||||
}
|
||||
|
||||
async function submitDraft(
|
||||
value: ComposerDraftSnapshot,
|
||||
delivery: ComposerDelivery = "submit",
|
||||
) {
|
||||
if (delivery === "notify" && attachments.length > 0) {
|
||||
sendError = "Notify accepts text only; remove attachments or queue a Submit.";
|
||||
reportComposerError("Notify accepts text only; remove attachments or queue a Submit.");
|
||||
return;
|
||||
}
|
||||
const incompleteAttachment = attachments.find((attachment) =>
|
||||
attachment.state !== "uploaded" || !attachment.reference
|
||||
);
|
||||
if (incompleteAttachment) {
|
||||
sendError = incompleteAttachment.state === "uploading"
|
||||
reportComposerError(incompleteAttachment.state === "uploading"
|
||||
? "Wait for file uploads to finish before sending."
|
||||
: incompleteAttachment.error ?? "Retry or remove the failed attachment.";
|
||||
: incompleteAttachment.error ?? "Retry or remove the failed attachment.");
|
||||
return;
|
||||
}
|
||||
const attachmentSegments: Segment[] = attachments.map((attachment) => ({
|
||||
@@ -1109,7 +1114,7 @@
|
||||
preserveExactText: value.textPastes.length > 0,
|
||||
});
|
||||
if (!command.ok) {
|
||||
sendError = command.message;
|
||||
reportComposerError(command.message);
|
||||
return;
|
||||
}
|
||||
if (!command.request) {
|
||||
@@ -1134,18 +1139,17 @@
|
||||
|
||||
let request: WorkerConsoleInputRequest = command.request;
|
||||
if (delivery === "queue" && request.kind !== "user") {
|
||||
sendError = "Queue accepts ordinary input, not a Composer command.";
|
||||
reportComposerError("Queue accepts ordinary input, not a Composer command.");
|
||||
return;
|
||||
}
|
||||
if (delivery === "notify") {
|
||||
if (request.kind !== "user") {
|
||||
sendError = "Notify accepts ordinary text, not a Composer command.";
|
||||
reportComposerError("Notify accepts ordinary text, not a Composer command.");
|
||||
return;
|
||||
}
|
||||
request = { kind: "notify", content: request.content };
|
||||
}
|
||||
sending = true;
|
||||
sendError = null;
|
||||
try {
|
||||
const method = composerRequestToProtocolMethod(request);
|
||||
if (isCommand) {
|
||||
@@ -1160,7 +1164,7 @@
|
||||
liveWorkerState = "running";
|
||||
}
|
||||
} catch (error) {
|
||||
sendError = error instanceof Error ? error.message : String(error);
|
||||
reportComposerError(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
sending = false;
|
||||
}
|
||||
@@ -2065,7 +2069,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if sendError}<p class="error" role="alert">{sendError}</p>{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
+33
@@ -16,6 +16,7 @@ import type {
|
||||
} from "$lib/generated/protocol";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import * as alerts from "$lib/workspace/alerts/store";
|
||||
import ConsolePage from "./+page.svelte";
|
||||
|
||||
const multiplexer = vi.hoisted(() => {
|
||||
@@ -263,6 +264,38 @@ test.each(["live_protocol", "retained_snapshot"] as const)("overlapping history
|
||||
expect(view.container.querySelectorAll(".activity-summary")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ text: ":peer", message: "Invalid arguments. Usage: :peer <worker-name>", transportFailure: false },
|
||||
{ text: "keep this draft", message: "test transport failure", transportFailure: true },
|
||||
])("Composer keeps $text on failure without adding any footer message", async ({ text, message, transportFailure }) => {
|
||||
const alert = vi.spyOn(alerts, "pushWorkspaceAlert");
|
||||
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||
const view = render(ConsolePage, { data: pageData() });
|
||||
await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce());
|
||||
latestListener().onFrame(subscribedFrame(emptySession()));
|
||||
await screen.findByText("No conversation to display");
|
||||
const cm = EditorView.findFromDOM(view.container.querySelector(".cm-editor") as HTMLElement)!;
|
||||
const form = view.container.querySelector(".console-composer") as HTMLFormElement;
|
||||
cm.dispatch({ changes: { from: 0, insert: text }, selection: { anchor: text.length } });
|
||||
if (transportFailure) multiplexer.sendWorkerMethod.mockImplementationOnce(() => { throw new Error(message); });
|
||||
await fireEvent.submit(form);
|
||||
await waitFor(() => expect(alert).toHaveBeenCalledWith("error", message, expect.any(Object)));
|
||||
expect(cm.state.doc.toString()).toBe(text);
|
||||
expect(multiplexer.sendWorkerMethod).toHaveBeenCalledTimes(transportFailure ? 1 : 0);
|
||||
expect(form.children).toHaveLength(1);
|
||||
expect(form.firstElementChild?.classList.contains("composer-input-shell")).toBe(true);
|
||||
expect(form.querySelector('[role="alert"]')).toBeNull();
|
||||
expect(form.textContent).not.toContain(message);
|
||||
|
||||
cm.dispatch({ changes: { from: 0, to: cm.state.doc.length, insert: ":peer teammate" } });
|
||||
await fireEvent.submit(form);
|
||||
await waitFor(() => expect(multiplexer.sendWorkerMethod).toHaveBeenLastCalledWith({
|
||||
method: "register_peer", params: { name: "teammate" },
|
||||
}));
|
||||
expect(cm.state.doc.toString()).toBe("");
|
||||
expect(form.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("pending inputs show literal previews above Composer with revision-fenced icon actions", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||
const view = render(ConsolePage, { data: pageData() });
|
||||
|
||||
Reference in New Issue
Block a user