Skip to content

feat: session replay tab + JSONL transcript import#155

Merged
rohitg00 merged 2 commits intomainfrom
feat/replay-tab
Apr 17, 2026
Merged

feat: session replay tab + JSONL transcript import#155
rohitg00 merged 2 commits intomainfrom
feat/replay-tab

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented Apr 16, 2026

Summary

  • New Replay tab in the viewer plays back any stored session as a scrubbable timeline of prompts, tool calls, tool results, and responses. Play/pause, speed (0.5×–4×), and keyboard shortcuts (space, ←/→).
  • New agentmemory import-jsonl [path] CLI and POST /agentmemory/replay/import-jsonl endpoint backfill sessions from Claude Code JSONL transcripts. Default scans ~/.claude/projects; pass an explicit file or directory to scope it.
  • Three new iii functions — mem::replay::load, mem::replay::sessions, mem::replay::import-jsonl — exposed through the existing API trigger. No side-channel servers; everything flows through iii-sdk v0.11.

Why

agentmemory already captures every observation (hookType, tool_name, tool_input, tool_output) per session. Replay is the missing visualization for data the engine already has — one extra tab, zero new storage, with an import path for transcripts that predate installation.

Security

  • JSONL import rejects symlinks and paths containing sensitive terms (secret, credential, .env, id_rsa, token).
  • Malformed lines are skipped, not fatal.
  • Every import records an audit entry via safeAudit.
  • Viewer tab is nonce-scoped; test/viewer-security.test.ts still passes.

Test plan

  • npx vitest run test/replay.test.ts — 9/9 (parser + timeline projection + malformed-line tolerance + pacing)
  • npx vitest run test/viewer-security.test.ts — 2/2 (CSP regression)
  • npx vitest run test/compress-file.test.ts test/export-import.test.ts — 13/13
  • Full suite: 751 passed, 14 skipped, 1 file (integration) requires live engine — excluded by default pnpm test
  • npx tsc --noEmit — no new errors in touched files
  • Manual: visit http://localhost:3113, Replay tab, pick session, verify playback/step/speed/keyboard
  • Manual: agentmemory import-jsonl ~/.claude/projects/<something>/<sess>.jsonl then replay it

Files

  • New: src/functions/replay.ts, src/replay/jsonl-parser.ts, src/replay/timeline.ts, test/replay.test.ts, test/fixtures/jsonl/*.jsonl
  • Modified: src/viewer/index.html (tab + CSS + replay runtime), src/triggers/api.ts (3 REST routes), src/cli.ts (import-jsonl subcommand + help), src/index.ts (wire registerReplayFunctions), version bumps (0.8.13), CHANGELOG, README

Summary by CodeRabbit

  • New Features

    • Session Replay: Replay tab with scrubbable timeline, discrete events, play/pause, step controls, speed selection, and keyboard shortcuts.
    • JSONL Transcript Import: Import JSONL transcripts via CLI command or REST endpoint; imports surface in the viewer and log the import activity.
  • Security

    • Import validation rejects symlinks and paths containing sensitive terms; malformed JSONL lines are skipped without aborting the batch.

Adds a Replay tab to the viewer that plays any stored session as a
scrubbable timeline (prompts, tool calls, tool results, responses)
with play/pause, speed control, and keyboard navigation.

Adds `agentmemory import-jsonl [path]` CLI subcommand and a matching
POST /agentmemory/replay/import-jsonl endpoint to backfill sessions
from Claude Code JSONL transcripts (default ~/.claude/projects).

All new logic routes through iii-sdk primitives: registerFunction for
mem::replay::load / mem::replay::sessions / mem::replay::import-jsonl,
and the existing HTTP trigger in api.ts for REST exposure.

Security: JSONL import rejects symlinks, paths containing sensitive
terms, and tolerates malformed lines without aborting the batch.
All imports are recorded in the audit log.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Adds session replay: viewer "Replay" tab with scrubbable timelines and keyboard controls, JSONL import via CLI and REST, backend replay SDK functions/KV storage, JSONL parsing and timeline projection, package version bumped to 0.8.13.

Changes

Cohort / File(s) Summary
Version & Manifests
package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json, src/version.ts, src/types.ts
Bumped package versions to 0.8.13; updated exported VERSION and added "0.8.13" to ExportData.version.
Changelog / README
CHANGELOG.md, README.md
Documented Session Replay, CLI import-jsonl workflow, REST endpoint additions, and security constraints for JSONL imports.
CLI
src/cli.ts
Added import-jsonl [path] command: probes GET /agentmemory/livez, posts POST /agentmemory/replay/import-jsonl, shows spinner, handles timeouts, prints viewer URL when sessions imported.
Replay SDK / Backend
src/functions/replay.ts, src/functions/export-import.ts
Registered mem::replay::load, mem::replay::sessions, mem::replay::import-jsonl; added path-sensitivity checks, symlink detection, JSONL discovery/import, KV writes, and included 0.8.13 in supported import versions.
API Triggers
src/triggers/api.ts, src/index.ts
Added authenticated endpoints: GET /agentmemory/replay/sessions, GET /agentmemory/replay/load, POST /agentmemory/replay/import-jsonl; registered replay functions in initialization; updated REST count.
JSONL Parsing & Timeline
src/replay/jsonl-parser.ts, src/replay/timeline.ts
Added JSONL parser producing ParsedTranscript/RawObservation sequences (skips malformed lines) and projectTimeline to convert observations into timed TimelineEvents with offsets/durations.
Viewer UI
src/viewer/index.html
Added Replay tab UI, session picker, playback controls (play/pause, step, speed), keyboard shortcuts, lazy loading and timer-based playback, import action hooking to API.
Tests & Fixtures
test/*.ts, test/fixtures/jsonl/*
New tests for parsing, timeline projection, and sensitive-path guards; added JSONL fixtures; updated export test expectation to 0.8.13.

Sequence Diagram(s)

sequenceDiagram
    participant UserCLI as User (CLI)
    participant CLI as CLI Handler
    participant API as Service API
    participant KV as KV Store
    participant Audit as Audit Log

    UserCLI->>CLI: agentmemory import-jsonl [path]
    CLI->>API: GET /agentmemory/livez (2s probe)
    API-->>CLI: 200 OK
    CLI->>API: POST /agentmemory/replay/import-jsonl { path?, maxFiles? }
    API->>API: validate path (sensitive/symlink)
    API->>API: discover .jsonl files
    loop for each discovered file
        API->>API: parse JSONL → observations
        API->>KV: write session metadata
        API->>KV: write observations (by id)
    end
    API->>Audit: safeAudit(import metadata)
    API-->>CLI: 202 { imported, sessionIds, observations }
    CLI->>UserCLI: print success + viewer URL
Loading
sequenceDiagram
    participant User as Browser User
    participant Viewer as Replay Tab UI
    participant API as Service API
    participant KV as KV Store

    User->>Viewer: switch to Replay tab
    Viewer->>API: GET /agentmemory/replay/sessions
    API->>KV: read sessions
    KV-->>API: sessions[]
    API-->>Viewer: sessions[]
    User->>Viewer: select session
    Viewer->>API: GET /agentmemory/replay/load?sessionId=...
    API->>KV: fetch session + observations
    KV-->>API: session + observations[]
    API->>API: projectTimeline(observations)
    API-->>Viewer: timeline { events, startedAt, totalDurationMs }
    User->>Viewer: press Play
    Viewer->>Viewer: start timer loop (100ms)
    loop until end
      Viewer->>Viewer: advance offset, update cursor, render
    end
    Viewer->>Viewer: stop playback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through lines of JSON light,

stitched sessions into play at night;
with scrub and speed the story sings,
keys and clicks bring back old things —
replayed by a rabbit's gentle bite.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: session replay tab + JSONL transcript import' directly and clearly summarizes the two main features added in this changeset: a new session replay tab UI and JSONL transcript import capability.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/replay-tab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
src/functions/export-import.ts (1)

179-180: Centralize the export version allowlist.

This list now has to stay in sync with ExportData.version in src/types.ts, which makes every release bump a multi-file footgun. Hoist the versions into one shared const and derive both the runtime Set and the type from it.

Based on learnings: When bumping version, update src/types.ts (ExportData version union) and src/functions/export-import.ts (supportedVersions set).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/export-import.ts` around lines 179 - 180, Extract the
hard-coded version list into a single exported constant array (e.g.,
EXPORT_VERSIONS) and import it where needed; replace the local supportedVersions
Set in export-import.ts with new Set(EXPORT_VERSIONS) (so the runtime check uses
that shared array) and change the ExportData.version union in src/types.ts to
derive its type from the same array (e.g., export type ExportVersion = typeof
EXPORT_VERSIONS[number]; then use ExportVersion in ExportData). Update
references to supportedVersions and ExportData.version to use the shared symbols
(EXPORT_VERSIONS and ExportVersion) so a single update bumps both runtime and
type checks.
src/replay/jsonl-parser.ts (1)

172-179: Capture the fallback now timestamp once.

new Date().toISOString() is evaluated twice here as independent fallbacks. As per coding guidelines — "Capture timestamps once with new Date().toISOString() and reuse instead of calling Date multiple times" — hoist a single value so startedAt and endedAt use the same fallback.

♻️ Proposed fix
+  const nowIso = new Date().toISOString();
   return {
     sessionId: effectiveSessionId,
     project: deriveProject(cwd),
     cwd: cwd || process.cwd(),
-    startedAt: firstTs || new Date().toISOString(),
-    endedAt: lastTs || new Date().toISOString(),
+    startedAt: firstTs || nowIso,
+    endedAt: lastTs || nowIso,
     observations,
   };

As per coding guidelines: "Capture timestamps once with new Date().toISOString() and reuse instead of calling Date multiple times".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/replay/jsonl-parser.ts` around lines 172 - 179, The two fallbacks call
new Date().toISOString() independently; capture one timestamp once and reuse it
so startedAt and endedAt share the same fallback value. In the function that
returns the object (referencing keys sessionId, project via deriveProject(cwd),
cwd, startedAt, endedAt, observations and variables
effectiveSessionId/firstTs/lastTs), introduce a single variable (e.g., nowIso)
assigned to new Date().toISOString() before the return and use nowIso as the
fallback for both startedAt and endedAt (i.e., startedAt: firstTs || nowIso,
endedAt: lastTs || nowIso).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli.ts`:
- Around line 829-833: The import POST fetch call (const res = await
fetch(`${base}/agentmemory/replay/import-jsonl`, { ... })) can hang
indefinitely; wrap it with an AbortSignal.timeout and pass the signal into fetch
so it aborts after a generous timeout (matching the /livez check). Create a
signal via AbortSignal.timeout(<ms>) and add it to the fetch options (signal),
and ensure any spinner/error handling treats an AbortError as a timeout; update
the fetch invocation that uses base, headers, and body to include this signal.

In `@src/functions/replay.ts`:
- Around line 17-29: The current isSensitive() uses raw substring matches in
SENSITIVE_PATH_TERMS (including "token"), causing false positives for paths like
"jsonwebtoken-demo"; update SENSITIVE_PATH_TERMS and isSensitive() to use
scoped, case-insensitive pattern matching instead of plain includes — e.g.,
replace the raw "token" entry with a boundary-aware regex or explicit filename
patterns and implement matching in isSensitive() using regex tests that check
path/word boundaries and path separators (for example, a regex like
/(^|[\\/_.-])token([\\/_.-]|$)/i or explicit checks for filenames like ".env",
"id_rsa", "*_secret*", etc.), ensuring other entries still match correctly.
- Around line 208-213: The loop currently awaits each kv.set sequentially for
parsed.observations (using kv.set and KV.observations(parsed.sessionId)),
causing N serial KV round-trips; replace the per-iteration await with collecting
promises (e.g., map parsed.observations to kv.set(...) promises), await
Promise.all on that array, and then increment observationCount by the number of
observations (or sum results) after the Promise.all completes; keep the existing
sessionIds.push(parsed.sessionId) behavior and ensure any error handling around
the batched writes is preserved.
- Around line 161-178: The directory-walked files returned by findJsonlFiles()
are not re-checked for sensitive paths; add a per-file sensitive check inside
the import loop: after skipping symlinks (the isSymlink(file) check) call
isSensitive(file) and skip/return an error when true (mirroring the existing
behavior used for the root abs), ensuring you reference the same variables
(files, file, abs) and keep the check before reading/processing each file so
descendants with sensitive names are not imported.
- Around line 193-206: The current import only creates a Session when none
exists, so importing into an existing session leaves Session.observationCount
stale; modify the logic after fetching existing via kv.get<Session>(KV.sessions,
parsed.sessionId) to handle the existing case by updating
existing.observationCount (e.g., existing.observationCount +=
parsed.observations.length or recomputing from KV.observations if you prefer
accuracy), update any changed metadata (endedAt/status if needed), then persist
the updated session with await kv.set(KV.sessions, existing.id, existing) so the
viewer reflects the new count; reference the existing variable, KV.sessions, and
parsed.observations in the fix.
- Line 146: The ternary assigning abs is redundant and doesn't expand a leading
'~', causing paths like '~/file.jsonl' to resolve incorrectly; replace the
isAbsolute(...) ? resolve(...) : resolve(...) expression with a single resolve
call that first expands a leading '~' (e.g., if rawPath startsWith('~'), replace
the leading '~' with os.homedir()), then call resolve(expandedPath) and assign
to abs; update references to rawPath/abs accordingly and remove the unnecessary
isAbsolute check.

In `@src/triggers/api.ts`:
- Around line 365-372: Validation trims whitespace but payload currently assigns
the raw string; change the assignment so payload.path receives the trimmed value
(use body.path.trim()) instead of body.path to ensure downstream lookups don't
fail due to leading/trailing whitespace; update the assignment to payload.path =
body.path.trim() in the same block that validates body.path.
- Around line 322-394: You added three HTTP routes in triggers/api.ts (functions
"api::replay::load", "api::replay::sessions", "api::replay::import") which push
the public REST count past the hardcoded value; update the startup banner in
src/index.ts and the API/docs count in README.md to reflect the new total
(increment the previous REST endpoint count by 3) so the displayed endpoint
totals are accurate.

In `@src/version.ts`:
- Line 1: The VERSION union in export const VERSION is missing the "0.7.9" and
"0.8.0" literals that are present on ExportData.version in src/types.ts; update
the VERSION type union to include "0.7.9" and "0.8.0" so both enumerations stay
in sync (keep the assigned value "0.8.13" as-is).

In `@src/viewer/index.html`:
- Around line 3186-3211: The replay interval started by startReplayTimer keeps
running after switching away from the replay tab; update switchTab to call
stopReplayTimer (clearing state.replay.timer via stopReplayTimer) when leaving
the 'replay' tab, or modify the tick in startReplayTimer to short-circuit
immediately if state.activeTab !== 'replay' (and avoid calling renderReplay or
updating state.replay.offsetAt), referencing the functions startReplayTimer,
stopReplayTimer, switchTab, state.replay.timer, state.activeTab, and
renderReplay to locate the changes.
- Line 3129: The placeholder HTML uses class="empty" which has no CSS, so update
the markup to reuse the existing .empty-state styles (and its expected inner
structure) or add a matching .empty rule; specifically replace occurrences of
<div class="empty"> (and its closing tag) with the .empty-state structure that
includes an inner element for .empty-icon and a <p> for the hint text, or
alternatively add a CSS rule for .empty mirroring .empty-state styling so the
Replay tab hint renders styled; ensure you update both occurrences where
class="empty" appears.

---

Nitpick comments:
In `@src/functions/export-import.ts`:
- Around line 179-180: Extract the hard-coded version list into a single
exported constant array (e.g., EXPORT_VERSIONS) and import it where needed;
replace the local supportedVersions Set in export-import.ts with new
Set(EXPORT_VERSIONS) (so the runtime check uses that shared array) and change
the ExportData.version union in src/types.ts to derive its type from the same
array (e.g., export type ExportVersion = typeof EXPORT_VERSIONS[number]; then
use ExportVersion in ExportData). Update references to supportedVersions and
ExportData.version to use the shared symbols (EXPORT_VERSIONS and ExportVersion)
so a single update bumps both runtime and type checks.

In `@src/replay/jsonl-parser.ts`:
- Around line 172-179: The two fallbacks call new Date().toISOString()
independently; capture one timestamp once and reuse it so startedAt and endedAt
share the same fallback value. In the function that returns the object
(referencing keys sessionId, project via deriveProject(cwd), cwd, startedAt,
endedAt, observations and variables effectiveSessionId/firstTs/lastTs),
introduce a single variable (e.g., nowIso) assigned to new Date().toISOString()
before the return and use nowIso as the fallback for both startedAt and endedAt
(i.e., startedAt: firstTs || nowIso, endedAt: lastTs || nowIso).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c33773b8-4e58-4379-9d79-386aed1806e0

📥 Commits

Reviewing files that changed from the base of the PR and between 269fa65 and 11bef18.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • README.md
  • package.json
  • packages/mcp/package.json
  • plugin/.claude-plugin/plugin.json
  • src/cli.ts
  • src/functions/export-import.ts
  • src/functions/replay.ts
  • src/index.ts
  • src/replay/jsonl-parser.ts
  • src/replay/timeline.ts
  • src/triggers/api.ts
  • src/types.ts
  • src/version.ts
  • src/viewer/index.html
  • test/export-import.test.ts
  • test/fixtures/jsonl/basic.jsonl
  • test/fixtures/jsonl/errors.jsonl
  • test/fixtures/jsonl/tool-use.jsonl
  • test/replay.test.ts

Comment thread src/cli.ts
Comment thread src/functions/replay.ts Outdated
Comment thread src/functions/replay.ts Outdated
Comment thread src/functions/replay.ts
Comment thread src/functions/replay.ts
Comment thread src/triggers/api.ts
Comment on lines +322 to +394
sdk.registerFunction("api::replay::load",
async (req: ApiRequest): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
const sessionId = asNonEmptyString(req.query_params?.["sessionId"]);
if (!sessionId) {
return { status_code: 400, body: { error: "sessionId is required" } };
}
const result = await sdk.trigger({
function_id: "mem::replay::load",
payload: { sessionId },
});
return { status_code: 200, body: result };
},
);
sdk.registerTrigger({
type: "http",
function_id: "api::replay::load",
config: { api_path: "/agentmemory/replay/load", http_method: "GET" },
});

sdk.registerFunction("api::replay::sessions",
async (req: ApiRequest): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
const result = await sdk.trigger({ function_id: "mem::replay::sessions" });
return { status_code: 200, body: result };
},
);
sdk.registerTrigger({
type: "http",
function_id: "api::replay::sessions",
config: { api_path: "/agentmemory/replay/sessions", http_method: "GET" },
});

sdk.registerFunction("api::replay::import",
async (
req: ApiRequest<{ path?: string; maxFiles?: number }>,
): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
const body = (req.body ?? {}) as Record<string, unknown>;
const payload: { path?: string; maxFiles?: number } = {};
if (body.path !== undefined) {
if (typeof body.path !== "string" || body.path.trim().length === 0) {
return {
status_code: 400,
body: { error: "path must be a non-empty string" },
};
}
payload.path = body.path;
}
if (body.maxFiles !== undefined) {
if (!Number.isInteger(body.maxFiles) || (body.maxFiles as number) < 1) {
return {
status_code: 400,
body: { error: "maxFiles must be a positive integer" },
};
}
payload.maxFiles = body.maxFiles as number;
}
const result = await sdk.trigger({
function_id: "mem::replay::import-jsonl",
payload,
});
return { status_code: 202, body: result };
},
);
sdk.registerTrigger({
type: "http",
function_id: "api::replay::import",
config: { api_path: "/agentmemory/replay/import-jsonl", http_method: "POST" },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the hardcoded REST endpoint counts.

These three new routes push the public REST total past 104, so the startup banner in src/index.ts and the API/docs copy in README.md are stale again.

As per coding guidelines: When adding REST endpoints, update: triggers/api.ts, index.ts, and README.md with endpoint counts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/triggers/api.ts` around lines 322 - 394, You added three HTTP routes in
triggers/api.ts (functions "api::replay::load", "api::replay::sessions",
"api::replay::import") which push the public REST count past the hardcoded
value; update the startup banner in src/index.ts and the API/docs count in
README.md to reflect the new total (increment the previous REST endpoint count
by 3) so the displayed endpoint totals are accurate.

Comment thread src/triggers/api.ts Outdated
Comment thread src/version.ts Outdated
Comment thread src/viewer/index.html
'</div>' +
'<div class="replay-detail">' + renderReplayDetail(cursorEvent) + '</div>' +
'</div>'
: '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Placeholder element uses an undefined CSS class.

class="empty" has no matching style in this file (only .empty-state is defined earlier). The empty-state hint for the Replay tab will render unstyled. Either reuse .empty-state (with the inner .empty-icon/<p> structure) or add an .empty rule.

💅 Proposed fix
-          : '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>');
+          : '<div class="empty-state"><p>Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</p></div>');

Same applies to class="empty" at Line 3136.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
: '<div class="empty">Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</div>');
: '<div class="empty-state"><p>Pick a session to replay, or import Claude Code JSONL transcripts from ~/.claude/projects.</p></div>');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/viewer/index.html` at line 3129, The placeholder HTML uses class="empty"
which has no CSS, so update the markup to reuse the existing .empty-state styles
(and its expected inner structure) or add a matching .empty rule; specifically
replace occurrences of <div class="empty"> (and its closing tag) with the
.empty-state structure that includes an inner element for .empty-icon and a <p>
for the hint text, or alternatively add a CSS rule for .empty mirroring
.empty-state styling so the Replay tab hint renders styled; ensure you update
both occurrences where class="empty" appears.

Comment thread src/viewer/index.html
Security:
- Tighten sensitive-path guard to boundary-aware regexes (no more
  false positive on project names containing "token", "secret", etc.)
- Re-check every walked JSONL file against the guard, not just the
  root argument
- Expand leading "~" in user-supplied paths before resolve()

Correctness:
- Update existing Session.observationCount on re-import instead of
  silently leaving it stale
- Parallelize per-session observation writes via Promise.all
- Capture a single fallback timestamp in the parser so startedAt and
  endedAt agree when metadata is missing
- Trim incoming path in the import REST handler

Robustness:
- Add AbortSignal.timeout(120s) to the CLI import fetch and surface
  TimeoutError cleanly
- Stop the replay interval when switching away from the replay tab
- Style the Replay tab empty state with a new .empty rule

Sync:
- Bump REST endpoint count (104 -> 107) in startup banner + README
- Add 0.7.9 and 0.8.0 to VERSION union so it matches ExportData.version

Tests:
- Add replay-sensitive.test.ts covering false-positive avoidance
- Add parser test for shared fallback timestamp
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/functions/replay.ts (1)

148-150: ⚠️ Potential issue | 🟡 Minor

~user style paths are mishandled.

rawPath.startsWith("~") matches ~alice/foo and then slice(1) yields alice/foo, which gets joined under the current user's home — silently redirecting a different-user lookup rather than rejecting it. Guard against this by restricting expansion to exactly ~ or a ~/ prefix.

🛡️ Proposed fix
-      const expanded = rawPath.startsWith("~")
-        ? join(homedir(), rawPath.slice(1))
-        : rawPath;
+      const expanded =
+        rawPath === "~" || rawPath.startsWith("~/")
+          ? join(homedir(), rawPath.slice(1).replace(/^\/+/, ""))
+          : rawPath;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/replay.ts` around lines 148 - 150, The path expansion
incorrectly treats "~alice/foo" as the current user's home; update the expansion
logic in replay.ts where rawPath is converted to expanded so it only expands for
exactly "~" or paths starting with "~/". Replace the condition
rawPath.startsWith("~") with a strict check (rawPath === "~" ||
rawPath.startsWith("~/")) and continue to use join(homedir(), rawPath.slice(1))
only in that allowed case so "~user" style paths are left unchanged (or
handled/rejected elsewhere).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/replay.ts`:
- Around line 17-27: The existing SENSITIVE_PATH_PATTERNS entry for "secret"
doesn't match directories named "secrets" because the current alternation only
permits an optional "s" at end-of-string; update the pattern in
SENSITIVE_PATH_PATTERNS so it accepts either "secret" or "secrets" when followed
by a path separator or end-of-string (e.g., change the "secret" pattern to allow
the plural before a separator or $), ensuring the regex still respects the
existing anchors and separators; modify the entry inside the
SENSITIVE_PATH_PATTERNS array so functions referencing that constant will
correctly catch "secrets/" directories.

In `@src/replay/jsonl-parser.ts`:
- Line 103: The code computes fallback timestamps per-iteration with const ts =
entry.timestamp || new Date().toISOString();; instead capture a single fallback
value before the loop (e.g., const fallbackTs = new Date().toISOString();) and
then inside the loop use const ts = entry.timestamp || fallbackTs; to ensure all
entries in the batch share the same ISO timestamp and follow the project's
timestamp guideline.

---

Duplicate comments:
In `@src/functions/replay.ts`:
- Around line 148-150: The path expansion incorrectly treats "~alice/foo" as the
current user's home; update the expansion logic in replay.ts where rawPath is
converted to expanded so it only expands for exactly "~" or paths starting with
"~/". Replace the condition rawPath.startsWith("~") with a strict check (rawPath
=== "~" || rawPath.startsWith("~/")) and continue to use join(homedir(),
rawPath.slice(1)) only in that allowed case so "~user" style paths are left
unchanged (or handled/rejected elsewhere).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9560e91a-190e-4ed0-a8e7-0b2fde0cb361

📥 Commits

Reviewing files that changed from the base of the PR and between 11bef18 and 50817ad.

📒 Files selected for processing (10)
  • README.md
  • src/cli.ts
  • src/functions/replay.ts
  • src/index.ts
  • src/replay/jsonl-parser.ts
  • src/triggers/api.ts
  • src/version.ts
  • src/viewer/index.html
  • test/replay-sensitive.test.ts
  • test/replay.test.ts
✅ Files skipped from review due to trivial changes (2)
  • test/replay.test.ts
  • src/triggers/api.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • README.md
  • src/version.ts
  • src/viewer/index.html
  • src/cli.ts

Comment thread src/functions/replay.ts
Comment on lines +17 to +27
const SENSITIVE_PATH_PATTERNS: RegExp[] = [
/(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
/(^|[\\/_.-])credentials?([\\/_.-]|$)/i,
/(^|[\\/_.-])private[_-]?key([\\/_.-]|$)/i,
/(^|[\\/])\.env(\.[\w-]+)?$/i,
/(^|[\\/_.-])id_rsa([\\/_.-]|$)/i,
/(^|[\\/])auth[_-]?token([\\/_.-]|$)/i,
/(^|[\\/])bearer[_-]?token([\\/_.-]|$)/i,
/(^|[\\/])access[_-]?token([\\/_.-]|$)/i,
/(^|[\\/])api[_-]?token([\\/_.-]|$)/i,
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

secrets/ directory not caught by the secret pattern.

/(^|[\\/_.-])secret([\\/_.-]|s?$)/i accepts s? only when anchored at end-of-string. A path like /repo/secrets/foo.jsonl fails both branches (s is not a separator, and s isn't followed by $), so a transcript tucked under a secrets/ directory will sail through the guard. Consider allowing the plural before a separator as well.

🛡️ Proposed fix
-  /(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
+  /(^|[\\/_.-])secrets?([\\/_.-]|$)/i,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/replay.ts` around lines 17 - 27, The existing
SENSITIVE_PATH_PATTERNS entry for "secret" doesn't match directories named
"secrets" because the current alternation only permits an optional "s" at
end-of-string; update the pattern in SENSITIVE_PATH_PATTERNS so it accepts
either "secret" or "secrets" when followed by a path separator or end-of-string
(e.g., change the "secret" pattern to allow the plural before a separator or $),
ensuring the regex still respects the existing anchors and separators; modify
the entry inside the SENSITIVE_PATH_PATTERNS array so functions referencing that
constant will correctly catch "secrets/" directories.

for (const entry of entries) {
if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
if (entry.cwd && !cwd) cwd = entry.cwd;
const ts = entry.timestamp || new Date().toISOString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Capture the fallback timestamp once before the loop.

new Date().toISOString() is evaluated on every iteration when entry.timestamp is missing, producing slightly different ISO strings for entries in the same batch and violating the project's timestamp guideline.

♻️ Proposed fix
+  const nowIso = new Date().toISOString();
   const observations: RawObservation[] = [];

   for (const entry of entries) {
     if (entry.sessionId && !sessionId) sessionId = entry.sessionId;
     if (entry.cwd && !cwd) cwd = entry.cwd;
-    const ts = entry.timestamp || new Date().toISOString();
+    const ts = entry.timestamp || nowIso;
@@
-  const nowIso = new Date().toISOString();
   return {

As per coding guidelines: "Capture timestamps once with new Date().toISOString() and reuse instead of calling Date multiple times".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/replay/jsonl-parser.ts` at line 103, The code computes fallback
timestamps per-iteration with const ts = entry.timestamp || new
Date().toISOString();; instead capture a single fallback value before the loop
(e.g., const fallbackTs = new Date().toISOString();) and then inside the loop
use const ts = entry.timestamp || fallbackTs; to ensure all entries in the batch
share the same ISO timestamp and follow the project's timestamp guideline.

@rohitg00 rohitg00 merged commit 6b129dd into main Apr 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant