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("composer-notice") &&
|
||||||
!consolePage.includes('class="composer-actions"') &&
|
!consolePage.includes('class="composer-actions"') &&
|
||||||
!consolePage.includes("Sent through Worker protocol.") &&
|
!consolePage.includes("Sent through Worker protocol.") &&
|
||||||
consolePage.includes('{#if sendError}<p class="error" role="alert">'),
|
!consolePage.includes("sendError") &&
|
||||||
"Composer must remove persistent notices and empty action rows but retain send errors",
|
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 fileInput: HTMLInputElement | null = null;
|
||||||
let isDraggingFiles = $state(false);
|
let isDraggingFiles = $state(false);
|
||||||
let sending = $state(false);
|
let sending = $state(false);
|
||||||
let sendError = $state<string | null>(null);
|
|
||||||
let rewindTargets = $state<RewindTarget[]>([]);
|
let rewindTargets = $state<RewindTarget[]>([]);
|
||||||
let rewindHeadEntries = $state(0);
|
let rewindHeadEntries = $state(0);
|
||||||
let protocolState = $state<"connecting" | "open" | "closed" | "error">(
|
let protocolState = $state<"connecting" | "open" | "closed" | "error">(
|
||||||
@@ -765,7 +764,6 @@
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
sendError = message;
|
|
||||||
pushWorkspaceAlert("error", message, {
|
pushWorkspaceAlert("error", message, {
|
||||||
id: controlAlertId,
|
id: controlAlertId,
|
||||||
title: "Worker control failed",
|
title: "Worker control failed",
|
||||||
@@ -780,7 +778,7 @@
|
|||||||
): ProtocolMethod | null {
|
): ProtocolMethod | null {
|
||||||
const state = consoleProjection.workerState;
|
const state = consoleProjection.workerState;
|
||||||
if (!state) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
const commandId = Math.max(
|
const commandId = Math.max(
|
||||||
@@ -1082,21 +1080,28 @@
|
|||||||
if (event.dataTransfer?.files) addAttachmentFiles(event.dataTransfer.files);
|
if (event.dataTransfer?.files) addAttachmentFiles(event.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportComposerError(message: string) {
|
||||||
|
pushWorkspaceAlert("error", message, {
|
||||||
|
id: controlAlertId,
|
||||||
|
title: "Console input",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function submitDraft(
|
async function submitDraft(
|
||||||
value: ComposerDraftSnapshot,
|
value: ComposerDraftSnapshot,
|
||||||
delivery: ComposerDelivery = "submit",
|
delivery: ComposerDelivery = "submit",
|
||||||
) {
|
) {
|
||||||
if (delivery === "notify" && attachments.length > 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const incompleteAttachment = attachments.find((attachment) =>
|
const incompleteAttachment = attachments.find((attachment) =>
|
||||||
attachment.state !== "uploaded" || !attachment.reference
|
attachment.state !== "uploaded" || !attachment.reference
|
||||||
);
|
);
|
||||||
if (incompleteAttachment) {
|
if (incompleteAttachment) {
|
||||||
sendError = incompleteAttachment.state === "uploading"
|
reportComposerError(incompleteAttachment.state === "uploading"
|
||||||
? "Wait for file uploads to finish before sending."
|
? "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;
|
return;
|
||||||
}
|
}
|
||||||
const attachmentSegments: Segment[] = attachments.map((attachment) => ({
|
const attachmentSegments: Segment[] = attachments.map((attachment) => ({
|
||||||
@@ -1109,7 +1114,7 @@
|
|||||||
preserveExactText: value.textPastes.length > 0,
|
preserveExactText: value.textPastes.length > 0,
|
||||||
});
|
});
|
||||||
if (!command.ok) {
|
if (!command.ok) {
|
||||||
sendError = command.message;
|
reportComposerError(command.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!command.request) {
|
if (!command.request) {
|
||||||
@@ -1134,18 +1139,17 @@
|
|||||||
|
|
||||||
let request: WorkerConsoleInputRequest = command.request;
|
let request: WorkerConsoleInputRequest = command.request;
|
||||||
if (delivery === "queue" && request.kind !== "user") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (delivery === "notify") {
|
if (delivery === "notify") {
|
||||||
if (request.kind !== "user") {
|
if (request.kind !== "user") {
|
||||||
sendError = "Notify accepts ordinary text, not a Composer command.";
|
reportComposerError("Notify accepts ordinary text, not a Composer command.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
request = { kind: "notify", content: request.content };
|
request = { kind: "notify", content: request.content };
|
||||||
}
|
}
|
||||||
sending = true;
|
sending = true;
|
||||||
sendError = null;
|
|
||||||
try {
|
try {
|
||||||
const method = composerRequestToProtocolMethod(request);
|
const method = composerRequestToProtocolMethod(request);
|
||||||
if (isCommand) {
|
if (isCommand) {
|
||||||
@@ -1160,7 +1164,7 @@
|
|||||||
liveWorkerState = "running";
|
liveWorkerState = "running";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendError = error instanceof Error ? error.message : String(error);
|
reportComposerError(error instanceof Error ? error.message : String(error));
|
||||||
} finally {
|
} finally {
|
||||||
sending = false;
|
sending = false;
|
||||||
}
|
}
|
||||||
@@ -2065,7 +2069,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if sendError}<p class="error" role="alert">{sendError}</p>{/if}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+33
@@ -16,6 +16,7 @@ import type {
|
|||||||
} from "$lib/generated/protocol";
|
} from "$lib/generated/protocol";
|
||||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
|
import * as alerts from "$lib/workspace/alerts/store";
|
||||||
import ConsolePage from "./+page.svelte";
|
import ConsolePage from "./+page.svelte";
|
||||||
|
|
||||||
const multiplexer = vi.hoisted(() => {
|
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);
|
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 () => {
|
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" })));
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||||
const view = render(ConsolePage, { data: pageData() });
|
const view = render(ConsolePage, { data: pageData() });
|
||||||
|
|||||||
Reference in New Issue
Block a user