fix(ui): show Workboard comments in edit modal

Show existing Workboard card comments in the edit modal and allow operators to append a new comment through the existing `workboard.cards.comment` gateway method.

Refs #88592.

Verification:
- node scripts/run-vitest.mjs ui/src/ui/views/workboard.test.ts
- pnpm tsgo:test:ui
- git diff --check origin/main...HEAD
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main

Co-authored-by: Ted Li <tl2493@columbia.edu>
This commit is contained in:
Ted Li
2026-05-31 13:54:26 -07:00
committed by GitHub
parent 8076eead77
commit e5acae4453
3 changed files with 126 additions and 8 deletions

View File

@@ -310,6 +310,7 @@ export type WorkboardUiState = {
draftAgentId: string;
draftSessionKey: string;
draftTemplateId: WorkboardTemplateId | "";
draftCommentBody: string;
busyCardId: string | null;
draggedCardId: string | null;
syncingCardIds: Set<string>;
@@ -352,6 +353,7 @@ function createDefaultState(): WorkboardUiState {
draftAgentId: "",
draftSessionKey: "",
draftTemplateId: "",
draftCommentBody: "",
busyCardId: null,
draggedCardId: null,
syncingCardIds: new Set(),
@@ -947,6 +949,7 @@ function resetDraftState(state: WorkboardUiState) {
state.draftAgentId = "";
state.draftSessionKey = "";
state.draftTemplateId = "";
state.draftCommentBody = "";
}
function normalizeDraftLabels(value: string): string[] {
@@ -1418,6 +1421,34 @@ export async function saveWorkboardCardDraft(params: {
}
}
export async function addWorkboardCardComment(params: {
host: WorkboardHost;
client: GatewayBrowserClient | null;
requestUpdate?: () => void;
}) {
const state = getWorkboardState(params.host);
const body = state.draftCommentBody.trim();
if (!state.editingCardId || !params.client || !body) {
return;
}
state.loading = true;
state.error = null;
params.requestUpdate?.();
try {
const payload = await params.client.request("workboard.cards.comment", {
id: state.editingCardId,
body,
});
replaceCard(state, normalizeCardPayload(payload));
state.draftCommentBody = "";
} catch (error) {
state.error = formatError(error);
} finally {
state.loading = false;
params.requestUpdate?.();
}
}
export async function moveWorkboardCard(params: {
host: WorkboardHost;
client: GatewayBrowserClient | null;

View File

@@ -609,16 +609,33 @@ describe("renderWorkboard", () => {
position: 1000,
createdAt: 1,
updatedAt: 1,
metadata: {
comments: [{ id: "comment-1", body: "Needs owner check", createdAt: 2 }],
},
},
];
const request = vi.fn(async () => ({
card: {
...state.cards[0],
title: "Renamed",
priority: "high",
updatedAt: 2,
},
}));
const request = vi.fn(async (method: string) =>
method === "workboard.cards.comment"
? {
card: {
...state.cards[0],
metadata: {
comments: [
...(state.cards[0]?.metadata?.comments ?? []),
{ id: "comment-2", body: "Ship after CI", createdAt: 3 },
],
},
},
}
: {
card: {
...state.cards[0],
title: "Renamed",
priority: "high",
updatedAt: 2,
},
},
);
const props = {
host,
client: { request } as unknown as GatewayBrowserClient,
@@ -638,6 +655,24 @@ describe("renderWorkboard", () => {
render(renderWorkboard(props), container);
expect(container.querySelector('[role="dialog"]')?.textContent).toContain("Edit card");
expect(container.querySelector('[role="dialog"]')?.textContent).toContain("Needs owner check");
const commentInput = container.querySelector<HTMLTextAreaElement>(".workboard-comments__input");
commentInput!.value = "Ship after CI";
commentInput!.dispatchEvent(new InputEvent("input", { bubbles: true }));
render(renderWorkboard(props), container);
[...container.querySelectorAll<HTMLButtonElement>("button")]
.find((button) => button.textContent?.includes("Create"))
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();
await Promise.resolve();
expect(request).toHaveBeenCalledWith("workboard.cards.comment", {
id: "card-1",
body: "Ship after CI",
});
expect(state.cards[0]?.metadata?.comments?.at(-1)?.body).toBe("Ship after CI");
render(renderWorkboard(props), container);
const title = container.querySelector<HTMLInputElement>(".workboard-draft__title");
expect(title?.value).toBe("Rename me");
title!.value = "Renamed";

View File

@@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { t } from "../../i18n/index.ts";
import {
addWorkboardCardComment,
archiveWorkboardCard,
deleteWorkboardCard,
dispatchWorkboard,
@@ -352,6 +353,7 @@ function resetDraft(state: WorkboardUiState) {
state.draftAgentId = "";
state.draftSessionKey = "";
state.draftTemplateId = "";
state.draftCommentBody = "";
}
function openCreateModal(state: WorkboardUiState) {
@@ -405,6 +407,7 @@ function openEditModal(state: WorkboardUiState, card: WorkboardCard) {
state.draftAgentId = card.agentId ?? "";
state.draftSessionKey = card.sessionKey ?? "";
state.draftTemplateId = card.metadata?.templateId ?? "";
state.draftCommentBody = "";
}
function applyTemplate(state: WorkboardUiState, templateId: WorkboardTemplateId) {
@@ -569,6 +572,10 @@ function renderCardModal(props: WorkboardProps) {
return nothing;
}
const editing = Boolean(state.editingCardId);
const editingCard = state.editingCardId
? (state.cards.find((card) => card.id === state.editingCardId) ?? null)
: null;
const comments = editingCard?.metadata?.comments ?? [];
return html`
<div
class="workboard-modal"
@@ -745,6 +752,51 @@ function renderCardModal(props: WorkboardProps) {
/>
</label>
</div>
${editing
? html`
<section
class="workboard-field workboard-field--wide"
aria-labelledby="workboard-card-comments-title"
>
<span id="workboard-card-comments-title">
${t("workboard.badgeComments", { count: String(comments.length) })}
</span>
${comments.length
? html`
<ol>
${comments.map((comment) => html`<li>${comment.body}</li>`)}
</ol>
`
: nothing}
<textarea
class="input workboard-comments__input"
aria-labelledby="workboard-card-comments-title"
maxlength="2000"
.value=${state.draftCommentBody}
@input=${(event: InputEvent) => {
state.draftCommentBody = (event.currentTarget as HTMLTextAreaElement).value;
props.onRequestUpdate?.();
}}
></textarea>
<div class="workboard-modal__actions">
<button
class="btn"
type="button"
?disabled=${state.loading || !state.draftCommentBody.trim()}
@click=${() => {
void addWorkboardCardComment({
host: props.host,
client: props.client,
requestUpdate: props.onRequestUpdate,
});
}}
>
${icons.plus} ${t("common.create")}
</button>
</div>
</section>
`
: nothing}
<div class="workboard-modal__actions">
<button class="btn primary" ?disabled=${state.loading || !state.draftTitle.trim()}>
${editing ? t("common.save") : t("common.create")}