Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .artifacts/chrome-extension/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# chrome-extension / Chrome拡張ワンクリックPera1展開

Created: 2026-06-12
Branch: feature/chrome-extension
Status: Awaiting Review (Round 2)

## 🔄 User Request ⇄ Response (修正依頼と対処)

| # | User Request (原文) | Response (対処内容) | 検証方法 |
|---|---------------------|---------------------|----------|
| 1 | 「この位置のボタン微妙。」(スクショ: リポジトリ名の下に折り返して表示) | `extension/content.js` の挿入先を「リポジトリ名の横」→「Watch/Fork/Starが並ぶ `ul.pagehead-actions` の先頭」に変更。高さもGitHub小ボタンと同じ28pxに統一 | E2E: `el.closest("ul.pagehead-actions")` でアクションエリア内に配置されることをアサート。ヘッダー部のライト/ダーク両モードのスクショを取得(下記Evidence) |

### Round 2 ビフォーアフター

| Before(指摘時: タイトル下に折り返し) | After(Notifications/Fork/Starの並びに統合) |
|--------|-------|
| リポジトリ名の直下にボタンが落ちてレイアウト崩れ | ![Header light](./images/01b-header-light.png) |
| (ユーザー提供スクショ参照) | ![Header dark](./images/01c-header-dark.png) |

## 📦 依頼内容(これは何のアウトプット?)

> 「この拡張機能を開発してほしいです。chromeの拡張を押した瞬間に、githubのURLをpera1のパラメーターにしてもらい、展開したやつを表示してほしいです。」

→ **ツールバーの拡張アイコンをクリックした瞬間**、現在タブのGitHub URLを `https://pera1.kazu-san.workers.dev/github.com/...` に変換し、新規タブでコード1ページ展開ビューを開くChrome拡張(Manifest V3)を実装しました。

## 📌 Attention Required(今回の確認項目)

| # | 確認ポイント | 前提・判断材料 | 質問 |
|---|------|---------------|------|
| 1 | **Pera1のドメインを `pera1.kazu-san.workers.dev` に統一した** | wrangler.toml にカスタムドメイン設定なし。Cloudflare APIでworkers.devサブドメインが `kazu-san` であることを確認し、実際に200応答することをcurlで検証済み。既存 `extension/content.js` が向いていた `pera1.pages.dev` は**DNSすら解決しない死にホスト**だった | この統一でOK?カスタムドメインの予定があれば教えて |
| 2 | **既存の `extension/` に統合**(新規ディレクトリを作らず) | リモートmainに既に content script型(ページ内ボタン挿入)の拡張があったため、そこへ action(アイコンクリック)機能を追加する形にした。ページ内ボタンも引き続き動作する | 統合方針でOK? |
| 3 | **Issues/PR等のページではリポジトリルートにフォールバック** | 例: `github.com/owner/repo/issues` でクリック → `pera1/github.com/owner/repo` を開く。GitHub以外のタブではバッジ「!」を2秒表示して何も開かない | この挙動でOK? |
| 4 | **アイコンを favicon.svg 由来に刷新** | 既存の icon48/128 はImageMagick製の「P1」文字プレースホルダーだった。`public/favicon.svg`(青丸+書類)から16/32/48/128pxを生成して統一 | 見た目OK? |

## 🔀 変換仕様(クリック時の挙動)

```mermaid
flowchart LR
A[拡張アイコンをクリック] --> B{現在タブのURL}
B -->|github.com/owner/repo| C[pera1/github.com/owner/repo]
B -->|.../tree/branch/dir| D[pera1/.../tree/branch/dir<br/>そのまま渡す]
B -->|.../blob/branch/file#L10| E[pera1/.../blob/branch/file<br/>ハッシュ・クエリは除去]
B -->|.../issues や /pulls 等| F[pera1/github.com/owner/repo<br/>ルートにフォールバック]
B -->|GitHub以外・gist等| G[何も開かずバッジ「!」を2秒表示]
C & D & E & F --> H[新規タブで1ページ展開ビュー]
```

## 🛠 変更前 → 変更後

| 項目 | Before | After |
|------|--------|-------|
| アイコンクリック | ❌ 何も起きない(actionなし) | ✅ **押した瞬間**Pera1の展開ビューが新規タブで開く |
| ページ内ボタンのリンク先 | ❌ `pera1.pages.dev`(DNS解決不可の死にホスト) | ✅ `pera1.kazu-san.workers.dev`(200応答を実測確認) |
| アイコン画像 | 「P1」文字のプレースホルダー(48/128のみ) | favicon.svg由来の統一デザイン(16/32/48/128) |
| URL変換テスト | なし | vitest 9ケース追加(モックなし純粋関数) |
| 拡張のE2E | なし | Playwright実ブラウザE2E(`tests/e2e/extension.e2e.mjs`) |

## 🖼 Evidence

| ① GitHubリポジトリページ(ボタンはアクションエリアに配置) | ② アイコンクリック相当 → Pera1展開ビュー |
|--------|-------|
| ![GitHub with button](./images/01-github-with-button.png) | ![Pera1 expanded](./images/02-pera1-expanded.png) |

①は「Go to Pera1」ボタンがNotifications/Fork/Starの並びに表示されている実GitHubページ。②はクリック後に開いた実Pera1(File Tree+全コード展開)。**モック・スタブなし**、実GitHub・実Workerに対するE2Eのスクショです。

### Test Results

```bash
# ユニット(既存33 + 追加9 = 42件)
pnpm test
# Test Files 3 passed (3)
# Tests 42 passed (42)

# 型チェック
pnpm typecheck # エラーなし

# E2E(実Chromium + 実GitHub + 実Pera1 worker)×2回連続
node tests/e2e/extension.e2e.mjs
# ✔ 拡張のservice workerがロードされた
# ✔ content scriptのボタン表示・リンク先OK
# ✔ Pera1タブが開きコードが展開された
# ✔ GitHub以外のタブでは何も開かない
# E2E ALL GREEN 🎉
```

### Verification Checklist
- [x] vitest 42件パス(既存テストも全て通過)
- [x] `tsc --noEmit` パス
- [x] Playwright E2Eを2回連続実行しflakeなし
- [x] `pera1.kazu-san.workers.dev` の死活をcurlで実測(200)
- [x] スクショを自分の目で確認

<details>
<summary>📂 変更ファイル一覧(WHAT詳細)</summary>

| ファイル | 種別 | 内容 |
|---------|------|------|
| `extension/pera1-url.js` | 新規 | GitHub URL→Pera1 URL変換ロジック(tree/blob/raw対応、予約パス処理、クエリ・ハッシュ除去) |
| `extension/background.js` | 新規 | service worker。`chrome.action.onClicked` で変換→`chrome.tabs.create`。変換不可時はバッジ「!」 |
| `extension/manifest.json` | 変更 | v1.1.0。`action` / `background`(module) / `permissions:["activeTab"]` / icons 16〜128 を追加。content_scriptsは維持 |
| `extension/content.js` | 修正 | `PERA1_HOST` を死にホスト `pera1.pages.dev` → `pera1.kazu-san.workers.dev` |
| `extension/generate-icons.sh` | 変更 | favicon.svgからrsvg-convertで4サイズ生成する方式に刷新 |
| `extension/icon{16,32,48,128}.png` | 新規/差替 | favicon.svg由来のアイコン |
| `tests/unit/extension-url.spec.ts` | 新規 | 変換ロジックのvitestユニットテスト9件 |
| `tests/e2e/extension.e2e.mjs` | 新規 | 拡張実ロードのPlaywright E2E |

権限は `activeTab` のみ(クリックした瞬間だけ現在タブのURLを読める最小権限)。`tabs` 権限や全サイトアクセスは要求しません。

</details>

<details>
<summary>⚠️ 既知の制約</summary>

- ツールバーの実クリックそのものはPlaywrightから発火できない(Chromeの仕様)ため、E2Eでは `onClicked` に渡るtabオブジェクトと同形でハンドラを直接呼んで検証。実クリックは手元での読み込み確認を推奨:
1. `chrome://extensions` → デベロッパーモードON → 「パッケージ化されていない拡張機能を読み込む」→ `extension/` を選択
2. GitHubの任意のリポジトリページでツールバーのPera1アイコンをクリック
- gist.github.com は Pera1 worker側が未対応のため対象外(バッジ「!」表示)

</details>

### How to Reproduce
```bash
pnpm install
pnpm test # ユニット42件
node tests/e2e/extension.e2e.mjs # E2E(要playwright)
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { toPera1Url } from "./pera1-url.js";

// 変換できないタブで押されたときに、バッジで「!」を短時間表示して知らせる
async function flashErrorBadge(tabId) {
await chrome.action.setBadgeBackgroundColor({ color: "#d73a49", tabId });
await chrome.action.setBadgeText({ text: "!", tabId });
setTimeout(() => {
chrome.action.setBadgeText({ text: "", tabId }).catch(() => {});
}, 2000);
}

async function openInPera1(tab) {
const pera1Url = toPera1Url(tab?.url ?? "");
if (!pera1Url) {
if (tab?.id != null) await flashErrorBadge(tab.id);
return null;
}
// tabが部分的なオブジェクトでも壊れないようにindex/openerTabIdはガードする
// (tab.id === chrome.tabs.TAB_ID_NONE(-1) のタブはopenerに指定できない)
return chrome.tabs.create({
url: pera1Url,
index: typeof tab.index === "number" && tab.index >= 0 ? tab.index + 1 : undefined,
openerTabId: typeof tab.id === "number" && tab.id >= 0 ? tab.id : undefined,
});
}

chrome.action.onClicked.addListener((tab) => {
openInPera1(tab).catch((err) => {
console.error("[Pera1] failed to open tab:", err);
});
});

// E2Eテスト(Playwright)からservice worker経由で呼び出すために公開
globalThis.openInPera1 = openInPera1;
32 changes: 22 additions & 10 deletions extension/content.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(function () {
"use strict";

const PERA1_HOST = "pera1.pages.dev";
const PERA1_HOST = "pera1.kazu-san.workers.dev";
const BUTTON_ID = "pera1-goto-btn";

function getRepoPath() {
Expand All @@ -14,30 +14,32 @@
return segments.slice(0, 2).join("/");
}

function createButton(repoPath) {
function createButton(repoPath, inActions) {
const btn = document.createElement("a");
btn.id = BUTTON_ID;
btn.href = `https://${PERA1_HOST}/github.com/${repoPath}`;
btn.target = "_blank";
btn.rel = "noopener noreferrer";
btn.textContent = "Go to Pera1";
// GitHubの小サイズボタン(高さ28px)に合わせ、Watch/Fork/Starの並びに馴染ませる
btn.style.cssText = [
"display: inline-flex",
"align-items: center",
"gap: 4px",
"margin-left: 8px",
"padding: 3px 10px",
"height: 28px",
"padding: 0 12px",
"font-size: 12px",
"font-weight: 600",
"color: #fff",
"background: #0969da",
"border-radius: 6px",
"text-decoration: none",
"vertical-align: middle",
"line-height: 1.5",
"line-height: 1",
"cursor: pointer",
"transition: background 0.15s",
].join(";");
inActions ? "" : "margin-left: 8px",
]
.filter(Boolean)
.join(";");

btn.addEventListener("mouseenter", () => {
btn.style.background = "#0550ae";
Expand All @@ -56,7 +58,17 @@
const repoPath = getRepoPath();
if (!repoPath) return;

// Try multiple selectors for GitHub's repo heading area
// 第一候補: Watch/Fork/Star が並ぶアクションエリアの先頭に置く
// (リポジトリ名の横はレイアウト次第で折り返して崩れるため)
const actions = document.querySelector("ul.pagehead-actions");
if (actions) {
const li = document.createElement("li");
li.appendChild(createButton(repoPath, true));
actions.prepend(li);
return;
}

// フォールバック: リポジトリ名の横
const selectors = [
"#repository-container-header strong[itemprop='name'] a",
"#repository-container-header .AppHeader-context-item-label",
Expand All @@ -72,7 +84,7 @@

if (!anchor) return;

const btn = createButton(repoPath);
const btn = createButton(repoPath, false);
// Insert after the repo name element
const parent = anchor.closest("li") || anchor.parentElement;
if (parent) {
Expand Down
25 changes: 12 additions & 13 deletions extension/generate-icons.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#!/bin/bash
# Generate simple placeholder icons for the extension
# Requires ImageMagick (convert command)
# If not available, create the icons manually
# Generate extension icons from public/favicon.svg
# Requires rsvg-convert (brew install librsvg)
set -euo pipefail
cd "$(dirname "$0")/.."

if command -v magick &> /dev/null; then
magick -size 48x48 xc:'#0969da' -fill white -font Helvetica -pointsize 24 -gravity center -annotate 0 'P1' extension/icon48.png
magick -size 128x128 xc:'#0969da' -fill white -font Helvetica -pointsize 64 -gravity center -annotate 0 'P1' extension/icon128.png
echo "Icons generated!"
elif command -v convert &> /dev/null; then
convert -size 48x48 xc:'#0969da' -fill white -font Helvetica -pointsize 24 -gravity center -annotate 0 'P1' extension/icon48.png
convert -size 128x128 xc:'#0969da' -fill white -font Helvetica -pointsize 64 -gravity center -annotate 0 'P1' extension/icon128.png
echo "Icons generated!"
else
echo "ImageMagick not found. Please create icon48.png and icon128.png manually."
if ! command -v rsvg-convert &> /dev/null; then
echo "rsvg-convert not found. Install with: brew install librsvg" >&2
exit 1
fi

for size in 16 32 48 128; do
rsvg-convert -w "$size" -h "$size" public/favicon.svg -o "extension/icon${size}.png"
echo "Generated extension/icon${size}.png"
done
Binary file modified extension/icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/icon16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/icon32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extension/icon48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
{
"manifest_version": 3,
"name": "Go to Pera1",
"version": "1.0.0",
"description": "Add a 'Go to Pera1' button on GitHub repository pages to quickly fetch code as plain text for LLMs.",
"version": "1.1.0",
"description": "Open the current GitHub repository in Pera1 (single-page code view for LLMs) with one click on the toolbar icon, or via the in-page button.",
"action": {
"default_title": "Open in Pera1"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["activeTab"],
"content_scripts": [
{
"matches": ["https://github.com/*/*"],
Expand All @@ -11,6 +19,8 @@
}
],
"icons": {
"16": "icon16.png",
"32": "icon32.png",
"48": "icon48.png",
"128": "icon128.png"
}
Expand Down
Loading
Loading