From 9aecb116fdef3d2b062fdcc2314345de7f8805dc Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Fri, 2 Oct 2026 16:29:44 +0900
Subject: [PATCH 1/7] Add a shared activity timeline to instance pages
Show incoming and outgoing delivery logs below the actor list in one
chronological, scrollable history. Align each entry left or right by
direction, start at the latest entries, and load older pages on upward
scroll while preserving the reader's position. Include empty, loading,
and error states with basic log metadata.
AI provenance: I requested a basic activity log UI and refined it
through local review to one shared scrollbar with one box per row.
Codex generated the Solid Relay components and route integration and
regenerated Relay artifacts. Codex verified the production build, static
checks, and diff whitespace. I inspected the local UI and provided
layout corrections; full manual verification of pagination was not recorded.
Assisted-by: Codex:gpt-6-astra
---
.../src/components/ActivityLogTimeline.tsx | 195 ++++++++++++++++++
.../src/components/InstanceActivityLog.tsx | 44 ++++
packages/web/src/routes/instance/[slug].tsx | 3 +
3 files changed, 242 insertions(+)
create mode 100644 packages/web/src/components/ActivityLogTimeline.tsx
create mode 100644 packages/web/src/components/InstanceActivityLog.tsx
diff --git a/packages/web/src/components/ActivityLogTimeline.tsx b/packages/web/src/components/ActivityLogTimeline.tsx
new file mode 100644
index 0000000..6572da2
--- /dev/null
+++ b/packages/web/src/components/ActivityLogTimeline.tsx
@@ -0,0 +1,195 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { graphql } from "relay-runtime";
+import {
+ For,
+ Show,
+ createEffect,
+ createMemo,
+ createSignal,
+ onCleanup,
+} from "solid-js";
+import { createPaginationFragment } from "solid-relay";
+
+import type { ActivityLogTimeline_instance$key } from "./__generated__/ActivityLogTimeline_instance.graphql.ts";
+
+/**
+ * A chronological delivery list that loads older entries when scrolled upward.
+ * @returns The shared scrollable activity timeline.
+ */
+export function ActivityLogTimeline(props: {
+ $instance: ActivityLogTimeline_instance$key;
+}) {
+ const data = createPaginationFragment(
+ graphql`
+ fragment ActivityLogTimeline_instance on Instance
+ @throwOnFieldError
+ @argumentDefinitions(
+ count: { type: "Int", defaultValue: 20 }
+ cursor: { type: "String" }
+ )
+ @refetchable(queryName: "ActivityLogTimelinePaginationQuery") {
+ activityLogs(first: $count, after: $cursor)
+ @connection(key: "ActivityLogTimeline_activityLogs") {
+ edges {
+ node {
+ id
+ direction
+ type
+ remoteActorIri
+ remoteHost
+ status
+ created
+ }
+ }
+ }
+ }
+ `,
+ () => props.$instance,
+ );
+ const entries = createMemo(() =>
+ (data()?.activityLogs.edges ?? []).toReversed(),
+ );
+ const [failed, setFailed] = createSignal(false);
+ let viewport: HTMLElement | undefined;
+ let initialized = false;
+ let anchor: { height: number; top: number } | undefined;
+ let frame: number | undefined;
+
+ // Older pages are prepended visually; compensate for their height so the
+ // entry the reader was looking at stays in place.
+ createEffect(() => {
+ entries();
+ if (frame !== undefined) cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(() => {
+ if (!viewport) return;
+ if (!initialized) {
+ viewport.scrollTop = viewport.scrollHeight;
+ initialized = true;
+ } else if (anchor) {
+ viewport.scrollTop = anchor.top + viewport.scrollHeight - anchor.height;
+ anchor = undefined;
+ }
+ });
+ });
+ onCleanup(() => {
+ if (frame !== undefined) cancelAnimationFrame(frame);
+ });
+
+ function loadOlder() {
+ if (!viewport || !data.hasNext || data.isLoadingNext) return;
+ anchor = { height: viewport.scrollHeight, top: viewport.scrollTop };
+ setFailed(false);
+ data.loadNext(20, {
+ onComplete(error) {
+ if (error) {
+ anchor = undefined;
+ setFailed(true);
+ }
+ },
+ });
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/web/src/components/InstanceActivityLog.tsx b/packages/web/src/components/InstanceActivityLog.tsx
new file mode 100644
index 0000000..e7759b0
--- /dev/null
+++ b/packages/web/src/components/InstanceActivityLog.tsx
@@ -0,0 +1,44 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { ErrorBoundary } from "solid-js";
+
+import type { ActivityLogTimeline_instance$key } from "./__generated__/ActivityLogTimeline_instance.graphql.ts";
+import { ActivityLogTimeline } from "./ActivityLogTimeline.tsx";
+
+/**
+ * Displays the instance's incoming and outgoing delivery logs.
+ * @returns The activity log section with a shared chronological timeline.
+ */
+export function InstanceActivityLog(props: {
+ $instance: ActivityLogTimeline_instance$key;
+}) {
+ return (
+
+
Activity log
+
+ Activity logs could not be loaded. Access requires instance
+ membership or administrator permissions.
+
+ }
+ >
+
+
+
+ );
+}
diff --git a/packages/web/src/routes/instance/[slug].tsx b/packages/web/src/routes/instance/[slug].tsx
index 9370cee..099cabe 100644
--- a/packages/web/src/routes/instance/[slug].tsx
+++ b/packages/web/src/routes/instance/[slug].tsx
@@ -31,6 +31,7 @@ import {
import { ActorCard } from "~/components/ActorCard.tsx";
import { CopyButton } from "~/components/CopyButton.tsx";
+import { InstanceActivityLog } from "~/components/InstanceActivityLog.tsx";
import type { InstanceDetailQuery } from "./__generated__/InstanceDetailQuery.graphql.ts";
@@ -41,6 +42,7 @@ const instanceDetailQuery = graphql`
query InstanceDetailQuery($slug: String!) {
localInstanceBySlug(slug: $slug) {
instance {
+ ...ActivityLogTimeline_instance
id
host
url
@@ -181,6 +183,7 @@ export default function InstanceDetailPage(
+
)}
From 4e31a84a1cea3fe55e22449fabf5d91a7624abfe Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Mon, 5 Oct 2026 15:53:36 +0900
Subject: [PATCH 2/7] Show delivery endpoints and expandable activity source
Extract activity boxes into a Relay fragment component. Show sender and
recipient IRIs, include the object type in labels such as Create(Note),
and expand each box to display and copy the recorded JSON-LD. Fall back
to the raw received body when parsed JSON is unavailable and label
outgoing payloads as before signing.
AI provenance: I requested From/To IRIs, more specific activity
types, and click-to-view JSON-LD. Codex generated the component and
updated the timeline fragment, then regenerated Relay artifacts.
Assisted-by: Codex:gpt-6-astra
---
.../web/src/components/ActivityLogBox.tsx | 145 ++++++++++++++++++
.../src/components/ActivityLogTimeline.tsx | 33 +---
2 files changed, 151 insertions(+), 27 deletions(-)
create mode 100644 packages/web/src/components/ActivityLogBox.tsx
diff --git a/packages/web/src/components/ActivityLogBox.tsx b/packages/web/src/components/ActivityLogBox.tsx
new file mode 100644
index 0000000..9950ef4
--- /dev/null
+++ b/packages/web/src/components/ActivityLogBox.tsx
@@ -0,0 +1,145 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { graphql } from "relay-runtime";
+import { For, Show, createMemo, createSignal } from "solid-js";
+import { createFragment } from "solid-relay";
+
+import type { ActivityLogBox_log$key } from "./__generated__/ActivityLogBox_log.graphql.ts";
+import { CopyButton } from "./CopyButton.tsx";
+
+/**
+ * Displays delivery endpoints and expands to show the recorded JSON-LD.
+ * @returns A keyboard-accessible activity disclosure.
+ */
+export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
+ const data = createFragment(
+ graphql`
+ fragment ActivityLogBox_log on ActivityLog @throwOnFieldError {
+ direction
+ type
+ objectType
+ actor {
+ iri
+ }
+ remoteActorIri
+ recipientIris
+ inboxUrl
+ status
+ created
+ payload
+ rawBody
+ }
+ `,
+ () => props.$log,
+ );
+ const [open, setOpen] = createSignal(false);
+ const source = createMemo(() => {
+ if (!open()) return "";
+ const log = data();
+ const payload: unknown = log?.payload;
+ return payload == undefined
+ ? (log?.rawBody ?? undefined)
+ : JSON.stringify(payload, undefined, 2);
+ });
+ const from = createMemo(() => {
+ const log = data();
+ if (log?.direction === "inbound") return log.remoteActorIri;
+ // Keep the sender visible even if its local actor has since been deleted.
+ const payload: unknown = log?.payload;
+ const actor: unknown =
+ payload != undefined && typeof payload === "object" && "actor" in payload
+ ? payload.actor
+ : undefined;
+ return log?.actor?.iri ?? (typeof actor === "string" ? actor : undefined);
+ });
+ const to = createMemo(() => {
+ const log = data();
+ if (!log) return [];
+ if (log.direction === "inbound") return [log.actor?.iri ?? log.inboxUrl];
+ return log.recipientIris.length > 0 ? log.recipientIris : [log.inboxUrl];
+ });
+
+ return (
+
+ {(log) => (
+ setOpen(event.currentTarget.open)}
+ style={{
+ border: "1px solid var(--line)",
+ "border-radius": "0.5rem",
+ padding: "0.75rem",
+ }}
+ >
+
+
+ {log().type ?? "Unknown activity"}
+ {log().objectType != undefined && log().objectType !== ""
+ ? `(${log().objectType})`
+ : ""}
+
+ · {log().status}
+
+ From: {from() ?? "Unknown sender"}
+
+
+ To:{" "}
+
+ {(iri, index) => (
+ <>
+ {index() > 0 ? ", " : ""}
+ {iri}
+ >
+ )}
+
+
+
+
+ {open() ? "Hide source" : "View source"}
+
+
+
+
-
-
+
+
)}
From 533ceac5380c868888f51ffc7aa5fd8a8c505b21 Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Mon, 5 Oct 2026 16:35:39 +0900
Subject: [PATCH 3/7] Extract activity log styles into a CSS module
Move the timeline and activity box inline styles into activity-log.module.css.
Use CSS classes for scrolling, directional placement, disclosures, and source
formatting. Preserve my existing 32rem timeline height adjustment.
AI provenance: I asked Codex to extract the inline styles from both activity
log components into a shared CSS module. Codex generated the stylesheet and
updated the class references, preserving the current layout and behavior.
Codex checked formatting, targeted linting, and diff whitespace. I requested
this commit; manual browser verification of the extraction was not recorded.
Assisted-by: Codex:gpt-6-astra
---
.../web/src/components/ActivityLogBox.tsx | 32 ++++----
.../src/components/ActivityLogTimeline.tsx | 46 ++++--------
.../web/src/styles/activity-log.module.css | 75 +++++++++++++++++++
3 files changed, 103 insertions(+), 50 deletions(-)
create mode 100644 packages/web/src/styles/activity-log.module.css
diff --git a/packages/web/src/components/ActivityLogBox.tsx b/packages/web/src/components/ActivityLogBox.tsx
index 9950ef4..7a1331c 100644
--- a/packages/web/src/components/ActivityLogBox.tsx
+++ b/packages/web/src/components/ActivityLogBox.tsx
@@ -21,6 +21,8 @@ import { createFragment } from "solid-relay";
import type { ActivityLogBox_log$key } from "./__generated__/ActivityLogBox_log.graphql.ts";
import { CopyButton } from "./CopyButton.tsx";
+import styles from "~/styles/activity-log.module.css";
+
/**
* Displays delivery endpoints and expands to show the recorded JSON-LD.
* @returns A keyboard-accessible activity disclosure.
@@ -32,6 +34,7 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
direction
type
objectType
+ objectIri
actor {
iri
}
@@ -78,13 +81,9 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
{(log) => (
setOpen(event.currentTarget.open)}
- style={{
- border: "1px solid var(--line)",
- "border-radius": "0.5rem",
- padding: "0.75rem",
- }}
+ class={styles.box}
>
-
+
{log().type ?? "Unknown activity"}
{log().objectType != undefined && log().objectType !== ""
@@ -92,10 +91,13 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
: ""}
· {log().status}
-
+
+ {(iri) => Object: {iri()}}
+
+
From: {from() ?? "Unknown sender"}
-
+
To:{" "}
{(iri, index) => (
@@ -106,13 +108,10 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
)}
-
@@ -128,12 +127,7 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
fallback={
No source recorded.
}
>
-
+
{source()}
diff --git a/packages/web/src/components/ActivityLogTimeline.tsx b/packages/web/src/components/ActivityLogTimeline.tsx
index 3917a49..42dff39 100644
--- a/packages/web/src/components/ActivityLogTimeline.tsx
+++ b/packages/web/src/components/ActivityLogTimeline.tsx
@@ -28,6 +28,8 @@ import { createPaginationFragment } from "solid-relay";
import type { ActivityLogTimeline_instance$key } from "./__generated__/ActivityLogTimeline_instance.graphql.ts";
import { ActivityLogBox } from "./ActivityLogBox.tsx";
+import styles from "~/styles/activity-log.module.css";
+
/**
* A chronological delivery list that loads older entries when scrolled upward.
* @returns The shared scrollable activity timeline.
@@ -64,11 +66,11 @@ export function ActivityLogTimeline(props: {
const [failed, setFailed] = createSignal(false);
let viewport: HTMLElement | undefined;
let initialized = false;
- let anchor: { height: number; top: number } | undefined;
+ let anchor: { height: number } | undefined;
let frame: number | undefined;
// Older pages are prepended visually; compensate for their height so the
- // entry the reader was looking at stays in place.
+ // current reading position stays in place, including scrolling during the request.
createEffect(() => {
entries();
if (frame !== undefined) cancelAnimationFrame(frame);
@@ -78,7 +80,7 @@ export function ActivityLogTimeline(props: {
viewport.scrollTop = viewport.scrollHeight;
initialized = true;
} else if (anchor) {
- viewport.scrollTop = anchor.top + viewport.scrollHeight - anchor.height;
+ viewport.scrollTop += viewport.scrollHeight - anchor.height;
anchor = undefined;
}
});
@@ -89,7 +91,7 @@ export function ActivityLogTimeline(props: {
function loadOlder() {
if (!viewport || !data.hasNext || data.isLoadingNext) return;
- anchor = { height: viewport.scrollHeight, top: viewport.scrollTop };
+ anchor = { height: viewport.scrollHeight };
setFailed(false);
data.loadNext(20, {
onComplete(error) {
@@ -111,12 +113,7 @@ export function ActivityLogTimeline(props: {
// Keyboard users must be able to focus and scroll this region.
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
- style={{
- height: "24rem",
- overflow: "auto",
- "overflow-anchor": "none",
- "overflow-wrap": "anywhere",
- }}
+ class={styles.timeline}
onScroll={() => {
if (initialized && viewport && viewport.scrollTop < 64 && !failed()) {
loadOlder();
@@ -136,30 +133,17 @@ export function ActivityLogTimeline(props: {
when={entries().length > 0}
fallback={
No activity recorded yet.
}
>
-
+
{(edge) => (
-
+
diff --git a/packages/web/src/styles/activity-log.module.css b/packages/web/src/styles/activity-log.module.css
new file mode 100644
index 0000000..290b273
--- /dev/null
+++ b/packages/web/src/styles/activity-log.module.css
@@ -0,0 +1,75 @@
+/*
+DrFed: A web-based platform for developing and debugging ActivityPub apps
+Copyright (C) 2026 DrFed team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+
+.timeline {
+ height: 40rem;
+ overflow: auto;
+ overflow-anchor: none;
+ overflow-wrap: anywhere;
+}
+
+.list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: grid;
+ gap: 0.75rem;
+}
+
+.row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 1rem;
+}
+
+.entry {
+ min-width: 0;
+}
+
+.incoming {
+ grid-column: 1;
+}
+
+.outgoing {
+ grid-column: 2;
+}
+
+.box {
+ border: 1px solid var(--line);
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+}
+
+.summary {
+ cursor: pointer;
+ overflow-wrap: anywhere;
+}
+
+.detailLine {
+ display: block;
+ margin-top: 0.5rem;
+}
+
+.recipientLine {
+ display: block;
+}
+
+.source {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
From b63cc02fd0e2257e7d9017c53e464432a8dcda41 Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Tue, 6 Oct 2026 13:37:11 +0900
Subject: [PATCH 4/7] Contain expanded activity log overflow within the
timeline
Make the timeline a positioning container so the copy button accessibility text stays inside its scrollable area instead of extending the page.
AiI provenance: I reported extra page space when expanding activity logs and asked Codex to fix and commit it. Codex added the positioning rule and verified it with a local headless Chrome fixture, formatting, and diff whitespace checks.
Assisted-by: Codex:gpt-6-astra
---
packages/web/src/styles/activity-log.module.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/web/src/styles/activity-log.module.css b/packages/web/src/styles/activity-log.module.css
index 290b273..4979f61 100644
--- a/packages/web/src/styles/activity-log.module.css
+++ b/packages/web/src/styles/activity-log.module.css
@@ -17,6 +17,7 @@ along with this program. If not, see .
*/
.timeline {
+ position: relative;
height: 40rem;
overflow: auto;
overflow-anchor: none;
From 6bca43e992518636668ec88ed7a834a96d025051 Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Tue, 6 Oct 2026 13:53:59 +0900
Subject: [PATCH 5/7] Highlight activity log JSON with Shiki
Render expanded activity sources with JSON syntax highlighting and automatic light and dark themes. Keep raw bodies as plain text and preserve source copying and wrapping. Isolate highlighting in ActivityLogSource and bundle only JSON and the selected themes.
AI provenance: I asked Codex to apply Shiki JSON highlighting and commit the result. Codex added the source component, styles, dependency, and lockfile update. Codex verified the build, repository checks, and source escaping with JSON and plain-text fixtures.
Assisted-by: Codex:gpt-6-astra
---
packages/web/package.json | 1 +
.../web/src/components/ActivityLogBox.tsx | 8 +--
.../web/src/components/ActivityLogSource.tsx | 51 +++++++++++++++++++
.../web/src/styles/activity-log.module.css | 16 +++++-
pnpm-lock.yaml | 3 ++
5 files changed, 75 insertions(+), 4 deletions(-)
create mode 100644 packages/web/src/components/ActivityLogSource.tsx
diff --git a/packages/web/package.json b/packages/web/package.json
index dc88a13..d53024c 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -55,6 +55,7 @@
"nitro": "3.0.260610-beta",
"relay-runtime": "^21.0.1",
"seroval": "1.5.4",
+ "shiki": "4.4.1",
"solid-js": "^1.9.5",
"solid-relay": "1.0.0-beta.29",
"valibot": "1.4.2",
diff --git a/packages/web/src/components/ActivityLogBox.tsx b/packages/web/src/components/ActivityLogBox.tsx
index 7a1331c..907114c 100644
--- a/packages/web/src/components/ActivityLogBox.tsx
+++ b/packages/web/src/components/ActivityLogBox.tsx
@@ -19,6 +19,7 @@ import { For, Show, createMemo, createSignal } from "solid-js";
import { createFragment } from "solid-relay";
import type { ActivityLogBox_log$key } from "./__generated__/ActivityLogBox_log.graphql.ts";
+import { ActivityLogSource } from "./ActivityLogSource.tsx";
import { CopyButton } from "./CopyButton.tsx";
import styles from "~/styles/activity-log.module.css";
@@ -127,9 +128,10 @@ export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
fallback={
No source recorded.
}
>
-
- {source()}
-
+
diff --git a/packages/web/src/components/ActivityLogSource.tsx b/packages/web/src/components/ActivityLogSource.tsx
new file mode 100644
index 0000000..007561e
--- /dev/null
+++ b/packages/web/src/components/ActivityLogSource.tsx
@@ -0,0 +1,51 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { createHighlighterCoreSync } from "shiki/core";
+import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
+import json from "shiki/langs/json.mjs";
+import githubDark from "shiki/themes/github-dark.mjs";
+import githubLight from "shiki/themes/github-light.mjs";
+import { createMemo } from "solid-js";
+
+import styles from "~/styles/activity-log.module.css";
+
+let highlighter: ReturnType | undefined;
+
+/**
+ * Render recorded source with JSON colours, or plain text for raw bodies.
+ * @returns Highlighted, escaped source markup.
+ */
+export function ActivityLogSource(props: { source: string; json: boolean }) {
+ // oxlint-disable-next-line node/no-sync
+ highlighter ??= createHighlighterCoreSync({
+ langs: [json],
+ themes: [githubLight, githubDark],
+ engine: createJavaScriptRegexEngine(),
+ });
+ const renderer = highlighter;
+ const html = createMemo(() =>
+ renderer.codeToHtml(props.source, {
+ lang: props.json ? "json" : "text",
+ themes: { light: "github-light", dark: "github-dark" },
+ defaultColor: false,
+ }),
+ );
+
+ // Shiki escapes source text before producing the highlighted markup.
+ // oxlint-disable-next-line solid/no-innerhtml
+ return ;
+}
diff --git a/packages/web/src/styles/activity-log.module.css b/packages/web/src/styles/activity-log.module.css
index 4979f61..2bb5f35 100644
--- a/packages/web/src/styles/activity-log.module.css
+++ b/packages/web/src/styles/activity-log.module.css
@@ -70,7 +70,21 @@ along with this program. If not, see .
display: block;
}
-.source {
+.source pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
+
+.source pre,
+.source span {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+}
+
+@media (prefers-color-scheme: dark) {
+ .source pre,
+ .source span {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f310962..17278d5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -311,6 +311,9 @@ importers:
seroval:
specifier: 1.5.4
version: 1.5.4
+ shiki:
+ specifier: 4.4.1
+ version: 4.4.1
solid-js:
specifier: ^1.9.5
version: 1.9.14
From e2ffc37eafb14eb4506d30b4c6d9f033c6cdb77d Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Tue, 6 Oct 2026 13:59:38 +0900
Subject: [PATCH 6/7] Clean up comments
---
packages/web/src/components/ActivityLogTimeline.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/web/src/components/ActivityLogTimeline.tsx b/packages/web/src/components/ActivityLogTimeline.tsx
index 42dff39..57ea811 100644
--- a/packages/web/src/components/ActivityLogTimeline.tsx
+++ b/packages/web/src/components/ActivityLogTimeline.tsx
@@ -110,7 +110,6 @@ export function ActivityLogTimeline(props: {
viewport = element;
}}
aria-label="Activity log history"
- // Keyboard users must be able to focus and scroll this region.
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
class={styles.timeline}
From 9f63ace9a5a67c53591ff1f82bca68496a3d508d Mon Sep 17 00:00:00 2001
From: Jiwon Kwon
Date: Tue, 6 Oct 2026 15:53:12 +0900
Subject: [PATCH 7/7] Update activity timeline queries for renamed delivery API
Use ActivityDelivery and activityDeliveries in the box fragment, pagination connection, and timeline data access to match the merged API rename.
AI provenance: I requested rebasing the frontend branch and committing the API compatibility updates. Codex updated the two frontend components and regenerated Relay artifacts through the build. Codex verified the build, repository checks, and diff whitespace.
Assisted-by: Codex:gpt-6-astra
---
packages/web/src/components/ActivityLogBox.tsx | 2 +-
packages/web/src/components/ActivityLogTimeline.tsx | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/web/src/components/ActivityLogBox.tsx b/packages/web/src/components/ActivityLogBox.tsx
index 907114c..2e36e96 100644
--- a/packages/web/src/components/ActivityLogBox.tsx
+++ b/packages/web/src/components/ActivityLogBox.tsx
@@ -31,7 +31,7 @@ import styles from "~/styles/activity-log.module.css";
export function ActivityLogBox(props: { $log: ActivityLogBox_log$key }) {
const data = createFragment(
graphql`
- fragment ActivityLogBox_log on ActivityLog @throwOnFieldError {
+ fragment ActivityLogBox_log on ActivityDelivery @throwOnFieldError {
direction
type
objectType
diff --git a/packages/web/src/components/ActivityLogTimeline.tsx b/packages/web/src/components/ActivityLogTimeline.tsx
index 57ea811..5aa890f 100644
--- a/packages/web/src/components/ActivityLogTimeline.tsx
+++ b/packages/web/src/components/ActivityLogTimeline.tsx
@@ -46,8 +46,8 @@ export function ActivityLogTimeline(props: {
cursor: { type: "String" }
)
@refetchable(queryName: "ActivityLogTimelinePaginationQuery") {
- activityLogs(first: $count, after: $cursor)
- @connection(key: "ActivityLogTimeline_activityLogs") {
+ activityDeliveries(first: $count, after: $cursor)
+ @connection(key: "ActivityLogTimeline_activityDeliveries") {
edges {
node {
id
@@ -61,7 +61,7 @@ export function ActivityLogTimeline(props: {
() => props.$instance,
);
const entries = createMemo(() =>
- (data()?.activityLogs.edges ?? []).toReversed(),
+ (data()?.activityDeliveries.edges ?? []).toReversed(),
);
const [failed, setFailed] = createSignal(false);
let viewport: HTMLElement | undefined;