Skip to content

[XKCD Comics] Add viewing history#26704

Open
ronak-guliani wants to merge 9 commits intoraycast:mainfrom
ronak-guliani:xkcd-history
Open

[XKCD Comics] Add viewing history#26704
ronak-guliani wants to merge 9 commits intoraycast:mainfrom
ronak-guliani:xkcd-history

Conversation

@ronak-guliani
Copy link
Copy Markdown

@ronak-guliani ronak-guliani commented Mar 27, 2026

Description

Adds a viewing history feature to the XKCD Comics extension.

  • History tracks which comics you have read and when, persisted in LocalStorage across sessions
  • Accessible via the View History action (Cmd+K) on any comic or command item
  • Shows comics newest-first with relative timestamps (just now, 4h ago, 3d ago)
  • Re-reading a comic moves it to the top (browser-style dedup), capped at 100 entries
  • History screen shows the full comic detail panel plus Open in Browser and Explain xkcd actions

Checklist

  • I read the extension guidelines
  • I read the documentation about publishing
  • I ran npm run build and tested this distribution build in Raycast
  • I checked that files in the assets folder are used by the extension itself
  • I checked that assets used by the README are placed in the metadata folder

CleanShot 2026-03-27 at 14 31 37

@raycastbot raycastbot added extension fix / improvement Label for PRs with extension's fix improvements extension: xkcd Issues related to the xkcd extension platform: macOS platform: Windows labels Mar 27, 2026
@raycastbot
Copy link
Copy Markdown
Collaborator

raycastbot commented Mar 27, 2026

Thank you for your first contribution! 🎉

🔔 @maggie-j-liu you might want to have a look.

You can use this guide to learn how to check out the Pull Request locally in order to test it.

📋 Quick checkout commands
BRANCH="xkcd-history"
FORK_URL="https://github.com/ronak-guliani/extensions.git"
EXTENSION_NAME="xkcd"
REPO_NAME="extensions"

git clone -n --depth=1 --filter=tree:0 -b $BRANCH $FORK_URL
cd $REPO_NAME
git sparse-checkout set --no-cone "extensions/$EXTENSION_NAME"
git checkout
cd "extensions/$EXTENSION_NAME"
npm install && npm run dev

We're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 10-15 business days.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR adds a viewing history feature to the XKCD Comics extension, persisting up to 100 entries in LocalStorage with relative timestamps and a dedicated HistoryView accessible from every action panel. The approach is well-structured (new historyAtom, HistoryEntry type, and a self-contained component), but there is a fundamental architectural issue that needs to be resolved before merging.

Key issues found:

  • History view corrupts history data (P1): HistoryView updates the shared currentComicAtom on every selection change, which triggers the currentComic useEffect in index.tsx. This effect re-timestamps and moves the selected entry to the top of the list, so merely scrolling through or hovering over items in the history view rewrites the historical record. The fix is to use local component state for the selected comic number in HistoryView instead of the shared atom.
  • Stale history closure in useEffect (P2): The currentComic effect in index.tsx uses history from its closure but omits it from the dependency array, creating a potential race with the LocalStorage loading effect that could silently drop all persisted history entries.
  • HistoryView receives a stale snapshot (P2): Passing history as a prop rather than reading historyAtom directly inside the component means the list won't update if the atom changes while the view is open.
  • accessoryTitle deprecated (P2): The new history.tsx uses the deprecated accessoryTitle prop; the accessories array should be used instead.

Confidence Score: 3/5

Not safe to merge — the history view's own navigation corrupts the history data on every use.

There is a P1 logic bug where selecting any item in HistoryView triggers the read-tracking side effect in index.tsx (via the shared currentComicAtom), re-timestamping and reordering entries. This means the feature is broken as shipped: the first time a user opens history and scrolls through it, their historical order is permanently overwritten.

extensions/xkcd/src/history.tsx and extensions/xkcd/src/index.tsx need attention for the shared atom / corrupted history bug.

Important Files Changed

Filename Overview
extensions/xkcd/src/history.tsx New HistoryView component with a critical bug: selecting items updates the shared currentComicAtom, triggering the read-tracking side effect in index.tsx and corrupting history order/timestamps on every navigation.
extensions/xkcd/src/index.tsx Adds history tracking to the main view; has a stale history closure in the currentComic useEffect and passes history as a snapshot prop rather than reading from the atom directly in HistoryView.
extensions/xkcd/src/atoms.ts Adds HistoryEntry interface and historyAtom; clean and straightforward addition.
extensions/xkcd/package.json Adds ronakguliani as a contributor; no other changes.
extensions/xkcd/CHANGELOG.md New CHANGELOG.md with correct {PR_MERGE_DATE} placeholder at the top; properly structured and ordered.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/xkcd/src/history.tsx
Line: 56-68

Comment:
**Browsing history view corrupts history data**

When a user selects an item in `HistoryView`, `setCurrentComic(num)` updates the shared `currentComicAtom`. Because `index.tsx` also subscribes to this atom via `useCurrentSelectedComic(currentComicNumber)`, it fetches that comic and its `useEffect([currentComic])` fires — adding the entry to history with a fresh timestamp and moving it to the top of the list.

This means simply scrolling through the history view (every selection change triggers `onSelectionChange`) re-timestamps and re-orders every hovered entry, completely corrupting the historical viewing order.

The fix is to decouple the history view's comic-fetching from the shared `currentComicAtom`. For example, use a local `useState` for the selected comic number in `HistoryView`:

```tsx
// In HistoryView — use local state instead of the shared atom
const [selectedNum, setSelectedNum] = useState<number>(-1);
const [currentComic, loadingComic] = useCurrentSelectedComic(selectedNum);

const onSelectionChange = useCallback((id: string | undefined) => {
  if (!id || selectedId.current === id) return;
  selectedId.current = id;
  const num = Number(id);
  if (!isNaN(num)) setSelectedNum(num);  // local state, not shared atom
}, []);
```

This way the history view can display a comic's detail without triggering the read-tracking side effects in `index.tsx`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/xkcd/src/index.tsx
Line: 30-33

Comment:
**Stale `history` closure in useEffect**

`history` is read inside this effect but is not listed in the dependency array (`[currentComic]`). If `setHistory(parsed)` from the LocalStorage loading effect fires concurrently with this effect, the `history.filter(...)` call may operate on the stale empty initial state `[]`, silently dropping all previously-persisted history entries before they are written back to storage.

Consider using a functional updater to always read the latest atom state:

```tsx
setHistory((prev) => {
  const newEntry = { num: currentComic.num, viewedAt: new Date().toISOString() };
  const updated = [newEntry, ...prev.filter((e) => e.num !== currentComic.num)].slice(0, 100);
  LocalStorage.setItem("history", JSON.stringify(updated));
  return updated;
});
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/xkcd/src/index.tsx
Line: 119

Comment:
**`history` prop snapshot may go stale in `HistoryView`**

`<HistoryView history={history} />` passes a snapshot of the atom value at the time the parent last rendered. If `historyAtom` is updated while the user is inside `HistoryView`, the displayed list won't reflect those changes because `HistoryView` doesn't subscribe to the atom.

A simpler, more resilient pattern is to read `historyAtom` directly inside `HistoryView` with `useAtom`, removing the `history` prop entirely:

```tsx
// In history.tsx
const [history] = useAtom(historyAtom);
```

This applies to all five `<HistoryView history={history} />` call sites in `index.tsx`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/xkcd/src/history.tsx
Line: 60

Comment:
**`accessoryTitle` is deprecated**

`accessoryTitle` was deprecated in the Raycast API in favor of the `accessories` array prop. Since this is a new file it's a good opportunity to use the current API:

```suggestion
            accessories={[{ text: relativeTime(entry.viewedAt) }]}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "xkcd: add history tracking and View Hist..." | Re-trigger Greptile

Comment on lines +56 to +68
<List.Item
id={entry.num.toString()}
key={entry.num}
title={`Comic #${entry.num}`}
accessoryTitle={relativeTime(entry.viewedAt)}
detail={detail}
actions={
<ActionPanel>
<OpenComicInBrowser />
<ExplainXkcd />
</ActionPanel>
}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Browsing history view corrupts history data

When a user selects an item in HistoryView, setCurrentComic(num) updates the shared currentComicAtom. Because index.tsx also subscribes to this atom via useCurrentSelectedComic(currentComicNumber), it fetches that comic and its useEffect([currentComic]) fires — adding the entry to history with a fresh timestamp and moving it to the top of the list.

This means simply scrolling through the history view (every selection change triggers onSelectionChange) re-timestamps and re-orders every hovered entry, completely corrupting the historical viewing order.

The fix is to decouple the history view's comic-fetching from the shared currentComicAtom. For example, use a local useState for the selected comic number in HistoryView:

// In HistoryView — use local state instead of the shared atom
const [selectedNum, setSelectedNum] = useState<number>(-1);
const [currentComic, loadingComic] = useCurrentSelectedComic(selectedNum);

const onSelectionChange = useCallback((id: string | undefined) => {
  if (!id || selectedId.current === id) return;
  selectedId.current = id;
  const num = Number(id);
  if (!isNaN(num)) setSelectedNum(num);  // local state, not shared atom
}, []);

This way the history view can display a comic's detail without triggering the read-tracking side effects in index.tsx.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/xkcd/src/history.tsx
Line: 56-68

Comment:
**Browsing history view corrupts history data**

When a user selects an item in `HistoryView`, `setCurrentComic(num)` updates the shared `currentComicAtom`. Because `index.tsx` also subscribes to this atom via `useCurrentSelectedComic(currentComicNumber)`, it fetches that comic and its `useEffect([currentComic])` fires — adding the entry to history with a fresh timestamp and moving it to the top of the list.

This means simply scrolling through the history view (every selection change triggers `onSelectionChange`) re-timestamps and re-orders every hovered entry, completely corrupting the historical viewing order.

The fix is to decouple the history view's comic-fetching from the shared `currentComicAtom`. For example, use a local `useState` for the selected comic number in `HistoryView`:

```tsx
// In HistoryView — use local state instead of the shared atom
const [selectedNum, setSelectedNum] = useState<number>(-1);
const [currentComic, loadingComic] = useCurrentSelectedComic(selectedNum);

const onSelectionChange = useCallback((id: string | undefined) => {
  if (!id || selectedId.current === id) return;
  selectedId.current = id;
  const num = Number(id);
  if (!isNaN(num)) setSelectedNum(num);  // local state, not shared atom
}, []);
```

This way the history view can display a comic's detail without triggering the read-tracking side effects in `index.tsx`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — HistoryView now uses local useState for the selected comic number instead of the shared currentComicAtom. Browsing the history list no longer triggers the read-tracking useEffect in index.tsx.

Comment on lines +30 to +33
const newEntry = { num: currentComic.num, viewedAt: new Date().toISOString() };
const updated = [newEntry, ...history.filter((e) => e.num !== currentComic.num)].slice(0, 100);
setHistory(updated);
LocalStorage.setItem("history", JSON.stringify(updated));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Stale history closure in useEffect

history is read inside this effect but is not listed in the dependency array ([currentComic]). If setHistory(parsed) from the LocalStorage loading effect fires concurrently with this effect, the history.filter(...) call may operate on the stale empty initial state [], silently dropping all previously-persisted history entries before they are written back to storage.

Consider using a functional updater to always read the latest atom state:

setHistory((prev) => {
  const newEntry = { num: currentComic.num, viewedAt: new Date().toISOString() };
  const updated = [newEntry, ...prev.filter((e) => e.num !== currentComic.num)].slice(0, 100);
  LocalStorage.setItem("history", JSON.stringify(updated));
  return updated;
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/xkcd/src/index.tsx
Line: 30-33

Comment:
**Stale `history` closure in useEffect**

`history` is read inside this effect but is not listed in the dependency array (`[currentComic]`). If `setHistory(parsed)` from the LocalStorage loading effect fires concurrently with this effect, the `history.filter(...)` call may operate on the stale empty initial state `[]`, silently dropping all previously-persisted history entries before they are written back to storage.

Consider using a functional updater to always read the latest atom state:

```tsx
setHistory((prev) => {
  const newEntry = { num: currentComic.num, viewedAt: new Date().toISOString() };
  const updated = [newEntry, ...prev.filter((e) => e.num !== currentComic.num)].slice(0, 100);
  LocalStorage.setItem("history", JSON.stringify(updated));
  return updated;
});
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — switched to a functional updater setHistory(prev => [...]) so the effect always operates on the latest state regardless of when the closure was captured.

<ActionPanel>
<OpenComicInBrowser />
<ExplainXkcd />
<Action.Push title="View History" icon={Icon.Clock} target={<HistoryView history={history} />} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 history prop snapshot may go stale in HistoryView

<HistoryView history={history} /> passes a snapshot of the atom value at the time the parent last rendered. If historyAtom is updated while the user is inside HistoryView, the displayed list won't reflect those changes because HistoryView doesn't subscribe to the atom.

A simpler, more resilient pattern is to read historyAtom directly inside HistoryView with useAtom, removing the history prop entirely:

// In history.tsx
const [history] = useAtom(historyAtom);

This applies to all five <HistoryView history={history} /> call sites in index.tsx.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/xkcd/src/index.tsx
Line: 119

Comment:
**`history` prop snapshot may go stale in `HistoryView`**

`<HistoryView history={history} />` passes a snapshot of the atom value at the time the parent last rendered. If `historyAtom` is updated while the user is inside `HistoryView`, the displayed list won't reflect those changes because `HistoryView` doesn't subscribe to the atom.

A simpler, more resilient pattern is to read `historyAtom` directly inside `HistoryView` with `useAtom`, removing the `history` prop entirely:

```tsx
// In history.tsx
const [history] = useAtom(historyAtom);
```

This applies to all five `<HistoryView history={history} />` call sites in `index.tsx`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — HistoryView now reads historyAtom directly via useAtom instead of receiving a snapshot prop. The prop has been removed from all call sites.

id={entry.num.toString()}
key={entry.num}
title={`Comic #${entry.num}`}
accessoryTitle={relativeTime(entry.viewedAt)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 accessoryTitle is deprecated

accessoryTitle was deprecated in the Raycast API in favor of the accessories array prop. Since this is a new file it's a good opportunity to use the current API:

Suggested change
accessoryTitle={relativeTime(entry.viewedAt)}
accessories={[{ text: relativeTime(entry.viewedAt) }]}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/xkcd/src/history.tsx
Line: 60

Comment:
**`accessoryTitle` is deprecated**

`accessoryTitle` was deprecated in the Raycast API in favor of the `accessories` array prop. Since this is a new file it's a good opportunity to use the current API:

```suggestion
            accessories={[{ text: relativeTime(entry.viewedAt) }]}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — replaced accessoryTitle with the current accessories={[{ text: relativeTime(entry.viewedAt) }]} API.

@ronak-guliani
Copy link
Copy Markdown
Author

All four review comments addressed in the latest push:

  • History view corrupts dataHistoryView now uses local useState for selected comic number; browsing history no longer fires the read-tracking side effect in index.tsx
  • Stale closure in useEffect — switched to functional updater setHistory(prev => ...)
  • Stale history propHistoryView reads historyAtom directly via useAtom; prop removed from all call sites
  • Deprecated accessoryTitle — replaced with accessories={[{ text: ... }]}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extension fix / improvement Label for PRs with extension's fix improvements extension: xkcd Issues related to the xkcd extension platform: macOS platform: Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants