Compare commits

...
Author SHA1 Message Date
core-builder-b 32ffc41937 fix(tui): Feed times follow ui.timezone, as the header's do
The Feed printed raw ISO timestamps on each row, in a request's detail
("Observed:"), on a delivered update's "Delivered:" or "Accepted by
bridge:" line, and in "Read at". The header already draws times with
displayTime in the configured zone. The Feed now uses the same function.

The times are formatted when the page is drawn, not when it is read. The
zone setting arrives after start-up, so a time formatted at read time
could stay in the default zone until the next refresh. A delivered
update's detail used to print its time twice, as "Observed:" and as
"Delivered:". It now prints it once, under the delivery label. A missing
time reads "time unknown", as elsewhere in the TUI. The default zone is
unchanged.
2026-11-12 01:04:55 +00:00
2 changed files with 34 additions and 9 deletions

No files matched your search

+11 -5
View File
@@ -1,6 +1,7 @@
import type { AttentionRead, AttentionItem } from "@openrig/daemon/attention";
import type { Action, FleetSnapshot, ViewState } from "../types.js";
import { listItem, wrapDetailLines, type ContentLine } from "../detail.js";
import { displayTime } from "../time.js";
/** The bounded, confirmed-delivery or bridge-acceptance projection served by S02; no client classification. */
export interface DeliveredHumanUpdates {
@@ -11,6 +12,8 @@ export interface DeliveredHumanUpdates {
truncated: boolean;
}
const BRIDGE_ACCEPTED_SCOPE = " · accepted by bridge";
function isBridgeAcceptedUpdate(q: DeliveredHumanUpdates["items"][number]): boolean {
return q.deliveryOutcome === "accepted" || q.deliveryReceipt.startsWith("relay-owner-notification-accepted");
}
@@ -38,13 +41,13 @@ export function composeHumanUpdates(attention: AttentionRead | null, updates: De
const baseScope = projects.length === 1 ? `project ${projects[0]} (queue tag)` : "instance · project unknown";
const item: AttentionItem = { id: `human-update:${q.qitemId}`, kind: "update", summary: q.summary || q.body.trim().split(/\r?\n/).find(Boolean) || (accepted ? "Accepted by bridge" : "Delivered update"),
recipient: q.destinationSession, urgency: "update", unblocks: null, at: q.deliveredAt,
scope: accepted ? `${baseScope} · accepted by bridge` : baseScope, project: null,
scope: accepted ? `${baseScope}${BRIDGE_ACCEPTED_SCOPE}` : baseScope, project: null,
source: `/api/queue/${encodeURIComponent(q.qitemId)}` };
if (!read.items.some(current => current.id === `queue-update:${q.qitemId}`)) read.items.push(item);
if (wanted === item.id && read.detail?.item.id !== item.id) {
read.detailError = null;
read.detail = { item, lines: [q.body, ...(q.humanDetail ? ["Supplemental detail:", q.humanDetail] : []),
`From: ${q.sourceSession}`, accepted ? `Accepted by bridge: ${q.deliveredAt}` : `Delivered: ${q.deliveredAt}`, `Delivery receipt: ${q.deliveryReceipt}`, `Queue: ${q.qitemId}`, `Evidence: ${q.evidenceRef ?? "none recorded"}`],
`From: ${q.sourceSession}`, `Delivery receipt: ${q.deliveryReceipt}`, `Queue: ${q.qitemId}`, `Evidence: ${q.evidenceRef ?? "none recorded"}`],
files: q.evidenceRef?.startsWith("/") ? [{ label: "Update evidence", path: q.evidenceRef }] : [] };
}
}
@@ -71,7 +74,10 @@ export function attentionLines(state: ViewState, snap: FleetSnapshot, width: num
if (d.item.recipient) lines.push({ text: `To: ${d.item.recipient}` });
if (d.item.id.startsWith("human-update:") || d.item.id.startsWith("queue-update:")) lines.push({ text: "No action needed" });
if (d.item.unblocks) lines.push({ text: `Unblocks: ${d.item.unblocks}` });
lines.push({ text: `Scope: ${d.item.scope}` }, { text: `Observed: ${d.item.at ?? "unknown"}` });
// Times are drawn in the person's zone (ui.timezone), as the header's are; a delivered update's time is when it was
// delivered or accepted by the bridge.
const when = !d.item.id.startsWith("human-update:") ? "Observed" : d.item.scope.endsWith(BRIDGE_ACCEPTED_SCOPE) ? "Accepted by bridge" : "Delivered";
lines.push({ text: `Scope: ${d.item.scope}` }, { text: `${when}: ${displayTime(d.item.at, state.timeZone)}` });
lines.push(...questionLines(d.item).map(text => ({ text })));
lines.push(...d.lines.map(text => ({ text })));
for (const f of d.files) lines.push(listItem(f.label, { type: "attention-source", path: f.path }));
@@ -124,10 +130,10 @@ export function attentionLines(state: ViewState, snap: FleetSnapshot, width: num
if (i.humanQuestions?.length) lines.push({ text: ` Questions: ${i.humanQuestions.length}, ${unansweredCount(i)} unanswered` });
if (i.id.startsWith("human-update:") || i.id.startsWith("queue-update:")) lines.push({ text: " No action needed" });
if (i.unblocks) lines.push({ text: ` Unblocks: ${i.unblocks}` });
lines.push({ text: ` ${i.scope} · ${i.at ?? "time unknown"}` });
lines.push({ text: ` ${i.scope} · ${displayTime(i.at, state.timeZone)}` });
}
}
lines.push({ text: "" }, { text: `Read at ${read.readAt}` }, ...read.sources.map(s => ({ text: `${s.source}: ${s.state} · ${s.detail}` })));
lines.push({ text: "" }, { text: `Read at ${displayTime(read.readAt, state.timeZone)}` }, ...read.sources.map(s => ({ text: `${s.source}: ${s.state} · ${s.detail}` })));
}
lines.push({ text: "" }, { text: "Viewing is not approval. Each request's detail says whether it went to Slack and where to answer it." });
return wrapDetailLines(lines, width);
@@ -50,6 +50,24 @@ it("renders an unsent local FYI without inventing an approval or a delivery rece
expect(text).not.toContain("Delivery receipt:");
});
it("draws Feed times in the person's zone, as the header does, and redraws them when the zone changes", () => {
const item = current(), attention = read([item]);
attention.detail = { item, lines: ["Current queue body"], files: [] };
const merged = composeHumanUpdates(attention, delivered("other"), item.id)!;
const draw = (timeZone: string, attentionOpen: string | null) => attentionLines(
{ ...createViewState({ instanceId: "owned-fixture", timeZone }).get(), section: "needs" as const, attentionOpen },
{ ...emptySnapshot(), attentionRead: merged }, 140).map(l => l.text).join("\n");
const london = draw("Europe/London", null);
expect(london).toContain("instance · 2026-10-01 13:00:00 GMT+1");
expect(london).toContain("instance · project unknown · 2026-10-08 13:00:00 GMT+1");
expect(london).toContain("Read at 2026-10-09 13:00:00 GMT+1");
expect(london).not.toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:/);
// The same read, drawn after the zone setting arrives: nothing is re-read, and nothing keeps the earlier zone.
const angeles = draw("America/Los_Angeles", item.id);
expect(angeles).toContain("Observed: 2026-10-01 05:00:00 PDT");
expect(angeles).not.toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:|GMT\+1/);
});
it("does not retain delivered history beyond a successful empty receipt window when only current updates fail", () => {
const prior = composeHumanUpdates(read([]), delivered())!;
const failed = read([]);
@@ -155,8 +173,9 @@ it("labels relay bridge acceptance as accepted by the bridge rather than Deliver
],
};
const openedBridge = composeHumanUpdates(read([]), bridgeUpdates, "human-update:bridge-fyi")!;
const bridgeDetailLines = openedBridge.detail?.lines.join("\n") ?? "";
expect(bridgeDetailLines).toContain("Accepted by bridge: 2026-10-08T14:00:00Z");
const detailState = (id: string) => ({ ...createViewState({ instanceId: "owned-fixture", timeZone: "UTC" }).get(), section: "needs" as const, attentionOpen: id });
const bridgeDetailLines = attentionLines(detailState("human-update:bridge-fyi"), { ...emptySnapshot(), attentionRead: openedBridge }, 140).map(l => l.text).join("\n");
expect(bridgeDetailLines).toContain("Accepted by bridge: 2026-10-08 14:00:00 UTC");
expect(bridgeDetailLines).not.toContain("Delivered:");
expect(openedBridge.sources.find(s => s.source === "delivered updates")?.detail).toContain("accepted by the bridge");
expect(openedBridge.sources.find(s => s.source === "delivered updates")?.detail).not.toContain("confirmed delivered updates");
@@ -168,8 +187,8 @@ it("labels relay bridge acceptance as accepted by the bridge rather than Deliver
// Slack delivered wording remains unchanged
const openedSlack = composeHumanUpdates(read([]), delivered("slack-fyi"), "human-update:slack-fyi")!;
const slackDetailLines = openedSlack.detail?.lines.join("\n") ?? "";
expect(slackDetailLines).toContain("Delivered: 2026-10-08T12:00:00Z");
const slackDetailLines = attentionLines(detailState("human-update:slack-fyi"), { ...emptySnapshot(), attentionRead: openedSlack }, 140).map(l => l.text).join("\n");
expect(slackDetailLines).toContain("Delivered: 2026-10-08 12:00:00 UTC");
expect(slackDetailLines).not.toContain("Accepted by bridge:");
expect(openedSlack.sources.find(s => s.source === "delivered updates")?.detail).toBe(
"Latest 20 confirmed delivered updates; retained receipt window.",