# Changelog — ASA Bridge for ChatGPT

本拡張の version 別変更履歴。`manifest.json` の version は **release 単位で bump**、
その間の変更は同一 version 配下に蓄積する。詳細は memory `project_extension_version_policy.md` 参照。

## 1.0.5.5 (未リリース)

- 会話の読み取り部分を `conversation_parse.js` へ分離し、拡張と公式エクスポート取り込み画面が同じ処理を通るようにした（`content.js` は委譲のみ）。

- PL-134: 自分でアップロードした画像が、同じ会話に複数枚あると 2 枚目以降が保存物から消えることがあった不具合を修正。画像を見分けるための符号を作る処理が、アップロード画像に付く形の参照だけ取り違え、全部を同じ画像として扱っていた。

## 1.0.5.4 (2026-08-14)

- PL-133: 思考を挟む画像生成（`Worked for 1m 38s` 等）で、画像だけの回答が保存物から丸ごと消えていた不具合を修正。この形では画像が単独のノードとして付き、従来の起点（画像生成の呼び出し）が存在しないため、発言枠ごと落ちていた。あわせて、画像の拾い方を起点依存から「画像を持ち画面に見えるノードは拾う」に変更（同じ生成の候補は 1 発言にまとめる）。中身のあるノードを捨てた時だけ件数をログに出すようにし、次に構造が変わった時に無言で欠落しないようにした。
- ストア掲載の短い説明（`extDescription`、ja/en）を書き換え。1.0.5.3 の「要デスクトップアプリ」先頭型は、検索から来た人が最初に読む 1 行が言い訳になっており、競合と並べた時に何ができる拡張か伝わっていなかった。1 文目で保存できる事、2 文目でキャプチャモードなら閉じた後の一時チャットも保存できる事（他が持たない差）を出し、単体では動かない旨は文末に残す。

## 1.0.5.3 (2026-07-09)

- ストア掲載の短い説明（`extDescription`、ja/en）を「要デスクトップアプリ」先頭型に変更。ストア一般公開に伴い、単体では動作しないことを最初に明示する。
- PL-120: 画像編集の参照に使ったユーザー添付画像が、無関係な生成候補と混ざって「候補」ラベル付きで表示される不具合を修正。ユーザー添付画像は候補ラベルなしで正しく表示されるようにした。
- PL-119: 共有ページ（`/share/`）を第一級の保存元として対応。埋め込まれた会話データ（RSC）から復元し、画像を含めて保存できるようにした。
- PL-118: 画像生成の途中に挟まる中間ノードを跨いで走査し、生成画像が一部ドロップする不具合を修正。
- PL-117: 保存前の「最新か」判定で数えるターンを発言ターンのみに限定し、無駄な取り直しを抑制。
- PL-116: 保存待ちの取得中に、一定時間を超えたら処理中スピナーを表示するようにした。
- PL-114: グラフを取得して PDF に忠実に描画するようにした。
- PL-111: ショッピングの商品カルーセルを横並びカードで保存。検索リンク化・項目の出し分け・カルーセル説明文の付与に対応。
- PL-110: 複数の画像を均一な正方タイルの格子で並べて表示するようにした（HTML / PDF 共通）。
- PL-109: 無料版の広告が会話に挟まると通常保存で広告以降の発言が欠落していた不具合を修正。保存時に画面の最新状態と取得済みの差を検知し、遅れていれば一度だけ取り直してから保存するようにした。
- PL-101: グループチャットで他ユーザーが添付した画像を実表示。発言を名前ブロック化（送信者名をバブル外ヘッダに、添付は素貼り）し、主役発言と同体裁に整えた。
- PL-099: 1 回のプロンプトで複数枚生成したとき、選ばなかった候補画像も保存するように変更。
- PL-078 Phase B: ChatGPT の AI 生成画像を別チャネルで取得し、出力に埋め込み。
- PL-097: 保存ボタン押下時のフリーズを、画像取得（enrich）の並列化で解消。
- PL-098: 画像が描画される前に保存するとドロップしていた不具合を防止。取得件数を可視化。
- PL-095: ツールバーアイコンのポップアップに「保存」「設定」ボタンを追加。英語環境の保存矢印グリフを全 locale で統一。
- 対象サービス外のページでツールバーポップアップに案内表示（中央寄せ＋アイコン）。保存ボタンの文言を短縮。

## 1.0.4 (2026-05-30)

- ツールバーアイコン押下で「ご意見・不具合報告」ポップアップを追加 (`action.default_popup` = `popup.html` 新設、 従来は押下無反応)。 ボタン押下で共有 Google フォームを新規タブで開き、 対象版は事前入力 (`?usp=pp_url&entry.634899730=ChatGPT`) で自動セット。 新規 `popup.html` / `popup.js` + `_locales` ja/en に `popupHeading` / `popupDesc` / `popupButton` 追加。 権限追加なし (`chrome.tabs.create` は permission 不要)、 既存 content_scripts / NMH / widget 経路は touched 0。 3 版共通実装で `PRODUCT` 定数のみ各版差替 (ChatGPT/Claude/Gemini)。 フォーム本体は日本語のみ (英語需要は実観測後に追加判断)

## 1.0.3 (2026-05-25)
- PL-067 (ChatGPT 「画像 markdown link を多数並べる」 task で AI assistant 発言が完全 drop する root を修正): root = `parts[0] = "image_group{\"layout\":\"carousel\",\"aspect_ratio\":\"1:1\",\"query\":[...], \"num_per_query\":2}"` (= ChatGPT 内部 directive 生 string) を ASA が assistant text として save しようとする一方、 **真の markdown は `metadata.content_references[0].alt` field に格納** されており、 ChatGPT site renderer は `matched_text` を `.alt` で置換 render するが ASA `parseConversationApiResponse` (`content.js:1020-1146`) は `content_references` 未認識で directive 生 string のみ採用 + 後段 layer で directive 検知 drop の設計欠陥。 競合拡張でも同様 drop = 業界共通 root。 fix = `parseConversationApiResponse` assistant msg 処理で parts 集約後・`cleanChatGPTApiMarkers` 通過前に `metadata.content_references[]` を走査 + `matched_text` を `.alt` で置換する layer 追加 (generic 設計 = `type` 列挙不要、 `matched_text` + `alt` 両方 string で非空の ref を全部置換)、 将来の content_references type 追加にも自動追従。 影響範囲 = ChatGPT 単版 (= Claude/Gemini に content_references 構造なし、 別 API spec)、 既存 PL-038 Phase 3.5 (= dalle asset_pointer 救済) + PL-050 (= image_gen_title placeholder) との干渉なし (= `recipient='all'` branch で動作、 別経路)、 通常 chat regression 0、 画像生成 turn regression 0。 副次 positive = 外部公開 CDN URL (= `cdn-icons-png.flaticon.com` 等永続 URL) は browser が `<img>` で直 fetch = log 開閲時に実画像表示、 dalle 内部 URL (= 認証 + 期限切れ) と違い `[画像]` placeholder 不要で site と同等の inline image carousel UX 達成
- PL-065 (group chat realtime mode 動作不可 fix): `chatgpt.com/gg/<id>` (= 共有グループチャット) で widget の ⦿ realtime toggle button click が無反応 (= 背景色 ON にならない / 自動取込進まない / dialog 出ない / desktop 側 message count 0 のまま)。 root = 2 層: **層 1 (= 拡張側)** ChatGPT 内部 group chat 用 API endpoint は `/backend-api/calpico/...` で page_hook `CONVERSATION_URL_RE` (= 通常 chat `/backend-api/conversation/<cid>` 専用) に match せず `conversationCache` 空 + `getCurrentConversationId()` も `/c/<id>` only で `/gg/<id>` 不対応 → realtimeRefreshViaApi が cid null で早期 return + start_realtime_capture も初期 buffer 蓄積失敗 + get_realtime_buffer も cid match 不成立で空返却。 **層 2 (= desktop 側)** `CHATGPT_CONV_ID_REGEX` (= `r"/c/(...)"`) + `_is_restorable_chat_url` の `"/c/" in low` predicate が共に group chat URL を非 chat 扱いで弾く → snapshot_acquire が `page_has_chat=False` 判定で `Skip snapshot request because no restorable conversation is selected` log で skip = 拡張側で buffer 14 件入っても desktop 側 polling 自体 走らず message count 0 から動かない症状 = 拡張側 diag log でのみ buffer 蓄積確認、 desktop UI 上は 0 表示固定。 fix = case A (= DOM scrape fallback、 一時 chat `isTemporaryChat` → `mergeEphemeralIntoBuffer` の precedent 同 pattern) を採用、 page_hook.js (= calpico endpoint sniff) は scope 外 (= demand 観測時に PL 再起票判断、 案 B carry forward)。 **層 1 fix (= content.js +44 行)**: 新 `isGroupChat()` (= `/gg/<id>` URL 判定)、 新 `getGroupChatId()` (= gg id 抽出)、 新 `mergeGroupChatIntoBuffer()` (= `extractMessages()` 結果を `state.realtimeBuffer` 上書き + conv 切替時 reset、 PL-064 で group chat node 既対応の DOM 経路を流用、 一時 chat と違い server persist 済 = shrink 保護不要で単純置換)、 `realtimeRefreshViaApi` 冒頭 `isTemporaryChat` branch 直後に group chat 分岐追加、 `start_realtime_capture` handler に `else if (isGroupChat())` 分岐で初期化、 `get_realtime_buffer` handler に group chat 用 cid match 分岐追加。 **層 2 fix (= desktop +2 行)**: `chatgpt_asa/app/main_loop_spec.py` `CHATGPT_CONV_ID_REGEX` を `r"/(?:c|gg)/([A-Za-z0-9\-]+)"` に拡張 (= alternation で /c/ + /gg/ 両 match、 group 1 = id)、 `ChatGPT_ASA.py` `_is_restorable_chat_url` の return predicate を `"/c/" in low or "/gg/" in low` に拡張 = group chat URL を「restorable chat」扱いで snapshot 経路通過 + PL-002 restore-last-url にも自動波及 (= 次回起動 group chat も復元対象)。 通常 chat / 一時 chat / save (= 非 realtime) = touched 0 で regression 0、 Claude/Gemini に group chat 概念なし ChatGPT 単版 scope。 **副次 fix (= 添付物 抜け対応)**: 別 account から投稿された image / file attachment が participant bubble に DOM 存在するが、 readMessageDeep の text 取得経路では img / button child 無視で完全 drop。 全 attachment 媒体を `[添付ファイル]` 系統で統一 placeholder 化 (= user 判断、 group chat scope で表記を揃える方針)。 (1) **画像**: `<img alt="画像の添付" src="https://chatgpt.com/backend-api/estuary/public_content/...">` で識別 (= filename 情報 不在で plain `[添付ファイル]`)、 (2) **ファイル**: `<button class="border-token-border-light ... max-w-sm ...">` 配下に svg icon + filename text の bordered card UI (= .txt / .pdf / .docx 等 拡張子問わず共通)、 button textContent から filename 抽出して `[添付ファイル: <name>]`。 新 `extractGroupChatAttachments(node)` helper 追加 (= avatar img (= `cdn.auth0.com/avatars/` + alt="プロファイル画像") は分離抽出で除外)、 extractMessages の participant 分岐で placeholder を text に追記 (= 件数分繰返、 text 不在 case は placeholder のみで text 構築)。 DOM 経路で生 URL / file body 抽出は cookie / 期限切れ依存で復元保証なし placeholder のみ採用。 placeholder 文言は ASA UI 言語 (= `state.widgetLang`、 WIDGET_I18N 経由 ja/en 切替) と整合させ、 `attached_file` key 追加 (ja=「添付ファイル」 / en=「Attached File」)。 (3) **filename 整形**: file 添付 button の textContent 直取りは末尾に site native type label (= 「テキスト」/「PDF」 等の媒体表示) が連結する罠あり (= 実機例 `*.txtテキスト`)、 button 配下の leaf `<div>` の最初の non-empty text node を採用 (= filename が長 + 先頭、 type label は別 leaf で除外)。 (4) **sender 名抽出 fallback**: file-only msg (= text bubble 不在で file button が `bg-token-bg-tertiary` を保持) では既存 walk-up logic が file button 領域を bubble と誤判定し sender span 到達不能 → 表示が default 「Other User」 fallback に劣化、 直接 selector `span[class*="text-token-text-secondary"][class*="text-xs"]:not([title])` で sender span を timestamp span (= title 属性持ち) と分離取得する段 1 を冒頭追加 + 既存 walk-up を段 2 fallback として残置 (= 構造変動 risk 想定) + 連続同一 sender 発言 (= chat UI 慣習で 2 件目以降の sender header 省略) を救済する段 3 fallback 追加 (= 直前 sibling `[data-message-id]` を最大 20 件遡って sender 名持つ msg を探索 → 継承表示)。 (5) **date divider format 揺れ救済**: 既存 regex `^\d+月\d+日\s*\d+:\d+$` は 「3月8日 22:20」 形式専用、 「今日 21:32」 / 「昨日 HH:MM」 / 英語 locale 「Today HH:MM」 等 site の relative date 表記を弾いて divider 抜け症状あり、 site の `<time>` element + class signal を信頼して text format check を緩和 (= `\d+:\d+` 含 + 30 字以下) で format 揺れ全網羅。 inferRole 内の同 regex も同条件に緩和 (= 「今日 21:32」 entry が `\d+月\d+日` 不一致で role='message' fallback されて system 中央寄せ render されない症状 fix)
- PL-064 (group chat 他人発言 完全抜け fix): `chatgpt.com/gg/<id>` (= 共有グループチャット) の HTML save で 自分発言 + ChatGPT 発言は出るが他人 (= 別 participant) 発言が 0 件で完全消失する問題。 root = 4 段経路解析で確定: (1) ChatGPT 内部で group chat 用 endpoint は別 service `/backend-api/calpico/chatgpt/rooms/<room_id>/messages` (= 通常 chat の `/backend-api/conversation/<cid>` と異)、 `page_hook.js:31` `CONVERSATION_URL_RE` は通常 chat endpoint のみ match で calpico 経路 sniff 0 件 → `conversationCache` 空 → `parseConversationApiResponse` 起動せず → API 経路 0 件、 (2) `save_open` handler (`content.js:1592-1617`) が API 経路 0 件時 DOM 経路 `extractMessages()` fallback、 (3) `collectMessageNodes` (`content.js:449-471`) の 3 selector (`[data-message-author-role]` / `[data-testid^="conversation-turn"]` / `[class*="user-message-bubble-color"]`) は group chat 他人発言 bubble に全て match せず素通り (= console probe で `turn=0 / role=2` 確定、 自分発言は user-message-bubble-color で hit / assistant は data-message-author-role で hit / 他人発言は 3 selector 全て不在)、 (4) probe 4 段で他人発言 bubble の DOM 構造確定 = `[data-message-id="<room_id>~<msg_id>"]` root + 配下 `bg-token-bg-tertiary` class bubble + 兄弟 div の sender 名 label。 fix = `collectMessageNodes` に 4 番目 source `groupOtherBubbleNodes` 追加 (= `[data-message-id]` + `bg-token-bg-tertiary` 配下 + 自分発言印 / assistant 印 いずれも不在 で識別、 既存 3 selector hit と重複排除)、 `inferRole` に同条件分岐追加で `'user'` role 返却、 `extractGroupChatSenderName` 新 helper で bubble 親 wrapper を 4 階層遡り text-only leaf (< 40 字) を sender 名候補として抽出 (= 構造変動 risk 想定 fallback chain)、 `extractMessages` で role=='user' + `[data-message-id]` 持ち + user-message-bubble-color 不在 (= 他人発言判定) の時 sender 名を text 前 prepend (= 「梵太郎 St.: お邪魔します」 形式) で識別性確保。 自分発言 / assistant / 通常 chat = touched 0 selector 追加のみで regression 0、 Claude/Gemini に group chat 概念なし ChatGPT 単版 scope。 Phase 2 (= calpico API 経路 sniff + pagination 全件取得) は会話量増 + scroll unmount 顕在化したら別 PL 候補で carry forward
- PL-063 (group chat widget mount 不在 fix): `chatgpt.com/gg/<id>` (= 共有グループチャット) で widget icon が出現しない問題。 root = (1) `findComposerForm` の `.closest("form")` 固定経路 + group chat composer は `<form>` 完全不在 + `data-testid="prompt-textarea"` 剥離 + ProseMirror editor (`<div class="ProseMirror" contenteditable="true">`) 単体構造で anchor 4 selector 全 null + form fallback 永遠不能、 (2) ProseMirror 祖先の composer wrapper を anchor とした第 1 段 fix では実機 smoke で widget が DOM 挿入されたが composer 全体を覆う `shadow-short bg-token-bg-elevated-primary` card が `overflow:hidden` で widget 左外側 floating を clip = 不可視判明、 (3) viewport 右下 fixed pin の第 2 段 fix で可視化達成も user UX reject (= 通常 chat と同位置感が要件、 viewport pin は ダメ過ぎる)、 (4) Codex Round 1 review verdict 受領で composer rect 追従 tracked floating 案採用。 fix 3 段 = **段 1** = `findComposerForm` → `findComposerAnchor` 改名 + 既存 form 経路完全保持 + ProseMirror fallback chain 追加 (ProseMirror editor の祖先を 6 階層遡り composer-plus-btn を含む grid wrapper を anchor として返却)、 **段 2** = anchor 種別 (form / fallback) で mount 戦略分岐 = form anchor (= 通常 chat) は既存 position:absolute 配置維持、 fallback anchor (= group chat) は `body` 直挿入 + `position:fixed` で composer card 制約完全回避、 **段 3** = viewport pin を廃して composer rect 追従 tracked floating に変更 = `widgetHostStyle("fixed")` を `position:fixed; left:0; top:0; z-index:2147483647` に変更 (left/top は JS で算出)、 `updateTrackedWidgetPos()` 新規 = anchor.getBoundingClientRect から composer 左外側 8px gap (widgetSide right なら右外側) + 垂直 center で widget 位置算出 + viewport clamp (= 画面端 8px margin 内に収める)、 `scheduleTrackedUpdate()` = requestAnimationFrame throttle、 `attachTrackedListeners()` = ResizeObserver (anchor + host) + window scroll (capture phase) + window resize の 4 trigger で位置更新発火、 `detachTrackedListeners()` = host 再生成 / form mode 切替時の cleanup。 `buildWidgetHost(mode)` + `widgetHostStyle(mode)` に mode 引数追加 (`anchor` / `fixed`)、 `injectWidget` で `anchor.tagName === "FORM"` 判定で parent (= form / body) + mode 決定 + tracked listeners attach/detach、 attachAnchorObserver の MutationObserver + sentinel interval の existence check も expectedParent ベース判定に更新 (= fallback mode で永続再 inject 発火 抑止)。 `ensureFormPositioned` → `ensureAnchorPositioned` に意味再定義 (position relative 化 logic は form mode のみ呼出、 fallback mode では不要)。 視覚効果 = group chat でも widget が composer 左外側 8px gap floating で通常 chat と同位置感達成、 composer の scroll / resize / DOM mutation に追従。 通常 chat = 既存 form 経路で hit、 regression 0。 Claude/Gemini に group chat 概念なしで 3 版横展開不要、 ChatGPT 単版 scope
- PL-058 (widget の `△ scroll_snap` icon を撤去、 代わりに `📌 toggle_topmost` icon 追加): widget HTML から scroll_snap button 削除、 同位置に dialog 最前面切替 button 追加。 click で `widget_toggle_topmost` event 送信 → asa_core `handle_widget_toggle_topmost` で `DIALOG_TOPMOST` setting toggle + `set_widget_state({dialog_topmost: <new>})` で widget 即時同期、 既存 `⦿ toggle_realtime` と同 pattern で `data-active="true"` 背景赤化により state 視認可能 (ON = 最前面固定、 OFF = 通常)。 `WIDGET_I18N` から `scroll_snap` key 削除、 `toggle_topmost` key 追加 (= ja/en)。 `applyWidgetState` 拡張 = `caps.toggle_topmost === false` で button hide + `ws.dialog_topmost` で `data-active` 同期。 scroll_snap backend (= `handle_widget_scroll_snap` + dispatcher elif + `scroll_snap_dialog.py` 等) は残置 (= 5/24 user 判定 「backend 維持」 継承)
- PL-050 (image placeholder title 取得抜け fix): PL-038 Phase 3.5 で導入した `[画像]` 固定文字列 placeholder に title 抽出を後追い追加、 `[画像: <title>]` 形式化。 起票 trigger = user 実機比較 (= 競合 `ChatGPT Exporter` log `![海沿いの静かな鉄道横断](URL)` / `![星空の下の静かな町並み](URL)` vs ASA log `[画像]` x 2 で 2 枚識別不能)、 Claude PL-039 Phase 1.1 で確立した `[AI生成: <title>]` contract との 3 版整合性視点が活性化した瞬間に劣化部分浮上。 Phase 0 probe (= ChatGPT 該当 chat `6a118a9a-...` の page_hook cache JSON dump 28 KB 解析) で title 格納 field 確定 = 子 tool msg (= `recipient='all'` / `ctype='multimodal_text'` / parts に `asset_pointer`) の `metadata.image_gen_title` (or `async_task_title` 同値)。 fix = `parseConversationApiResponse` 内 imageDetected block で child node の `metadata.image_gen_title` 取得、 title 不在 case (= 旧形式 / 生成失敗) は `[画像]` fallback で枠保持。 URL 埋め込みは非採用 (= log meta block の元 chat URL から辿れる + 外部 cookie 依存で expire risk + log 純度低下、 user 判断)。 Gemini 横展開 audit は別 cycle 候補
- PL-046 (ephemeral 一時 chat 1番目 user 発言消失 / 「最大6メッセージしか取れない」 root fix): probe 2026-05-23 で root 確定 = ChatGPT 一時 chat は virtual-scroll で 3 turn 続けると `conversation-turn-1` (= 1 番目 user 発言枠) を中身ごと unmount (= `data-message-author-role` 属性ごと消失、 section shell だけ残る) する挙動。 旧 `realtimeRefreshViaApi` ephemeral 分岐は毎周期 `state.realtimeBuffer = extractMessages()` で DOM 直 overwrite していたため、 unmount 後の poll で buffer が 1 件分縮んで 1 番目 user 発言が永久 loss、 user 観測 「3 turn なのに 5 件 / 最初 user 発言だけ消える / title に食い込む」 = 全て同一 root。 fix = 3 層構造で対処: **(1) identity = `turn_id` (= parent section の `data-turn-id`、 streaming/completion 通じて安定)**。 当初試行で msg_id (= 内側 `data-message-id`) key を採用したが streaming → completion で値変動の観測あり (= 同 turn が複数 entry 化、 黒 `<pre><code></code></pre>` 空 block leak で実機 NG)、 section 識別子に切替。 **(2) ephemeral buffer 永続化**: realtime `stop_realtime_capture` で buffer clear せず保持 (= 非 ephemeral のみ clear)、 `start_realtime_capture` で既存 buffer 維持 + DOM merge、 stop → 別操作 → restart の round-trip で 1 番目 user 発言を保護。 **(3) 通常保存 snapshot path も buffer 経由**: `capture_snapshot` / `buildDomMessagesResult` で一時 chat 検知時 `mergeEphemeralIntoBuffer` 呼出 + buffer 返却、 realtime 経由蓄積した unmount 前 entry を保存出力に反映。 新 chat 検知 = DOM の conversation-turn 0 件 + buffer 非空で reset (= 「新しい一時 chat」 button 等で同 tab 内 session 切替の case)。 **(4) ephemeral auto-capture observer**: content script load 時点から `document.body` subtree mutation を 300ms debounce で監視 (`attachEphemeralAutoCapture`)、 isTemporaryChat 時に逐次 `mergeEphemeralIntoBuffer`、 realtime ON/OFF と独立して全 turn を buffer 蓄積。 これで realtime 未起動の通常保存でも 1 番目 user 発言を保護 (= unmount 前に observer 経由で buffer 取込済)。 user 観測 = 競合拡張は短時間 (= 3 turn 直後) なら通常保存も全件取れるが「一定時間経過後」 ASA 通常保存と同じ 5 件症状に陥る = 競合も DOM scrape 経路依存 + buffer 寿命制限あり、 ASA realtime は永続 buffer (= 本 fix 3) で時間経過後も全件、 本 fix 4 は通常保存 path も同 buffer に統合することで競合超えの常時保護を狙う。 新 helper 3 件追加: `extractEphemeralMessagesByTurnId` (= section walk + inner role 取得、 unmount 済は skip)、 `mergeEphemeralIntoBuffer` (= turn_id key merge + buffer 絶対 shrink せず + 新 chat 検知 reset)、 `attachEphemeralAutoCapture` (= page load 時 MutationObserver install)。 一時 chat は server-side 永続化なし (= 通常 conv 用 API hook PL-038 Phase 3 適用不可) のため DOM scrape merge が唯一の解。 元 PL-046 charter §streaming race (= `.result-streaming` marker skip) は別 root として残置
- widget theme を OS `prefers-color-scheme` から **ChatGPT site theme 追従**に変更 + dark theme 時 button hover bg を明灰系 overlay 化。ChatGPT site dark theme 時に widget hover bg (灰半透明 `rgba(127,127,127,.18)`) が site native button hover (e.g., 「何かを調べる」 button hover の明灰 chip) と整合せずユーザーが違和感を覚えていた問題を解消。`bindWidgetTheme` を `getComputedStyle(document.documentElement).colorScheme` 判定 + `<html>` `style` / `class` 属性 MutationObserver に置換 (ChatGPT site は html element に `class="dark|light"` + `color-scheme: dark|light` 両方設定で theme 切替、 colorScheme 経路は Claude 既存実装と symmetric)。widget CSS に `:host([data-theme="dark"]) .asa-dock button:hover { background: rgba(255,255,255,.28); }` 追加 (light default `rgba(127,127,127,.18)` から差替、 値は `.1` → `.2` → `.35` → `.28` で user 体感調整、 native 「何かを調べる」 hover 明灰と整合)。tooltip / active / icon 配色は touched せず default 維持 (user spec で hover bg のみ明示)。Gemini は既に site 追従済 = ChatGPT 単版 fix で完結
- PL-038 Phase 3.5 (image gen 発言枠 placeholder 補完): Phase 3.4 の recipient='all' filter で image gen prompt JSON drop は解消したが、 assistant 発言枠が完全に消失して user 発話の直後に次 user 発話が並ぶ不自然な構造 (turn 番号 25 で本来 image 表示の assistant 発言枠が抜け落ちる) と user 指摘。 修正 = recipient !== 'all' で drop する直前に node.children を走査、 child に `tool ctype=multimodal_text + parts に asset_pointer (sediment://file_...) を持つ dict` がある場合 = image gen tool call と判定、 親 assistant node を `[画像]` placeholder text で emit (発言枠保持)。 search tool 等の child に image asset 不在なら発言枠不在で drop 維持。 legacy DOM 経路は site UI が image を inline `<img>` rendering しているため innerText から外れて drop も placeholder も不要だったが、 API path では tool node の asset_pointer を能動的に detect する必要がある
- PL-038 Phase 3.4 (image gen prompt leak / tool call assistant msg drop): Phase 3.3 smoke で 28 turn 全件取得確認済だが assistant msg (turn 24) に DALL-E image gen の prompt JSON (`{"prompt":"AI時代のホムンクルス...","size":"1792x1024","n":1}`) が raw 表出。 probe data 解析で当該 node = ctype=text + `recipient='t2uay3k.sj1i4kz'` (= hashed tool target identifier) + end_turn=False、 通常 assistant 最終 reply は `recipient='all'` + end_turn=True と判明、 ChatGPT API canonical pattern 「recipient='all' = user-facing / それ以外 = tool target」 が確定。 修正 = parseConversationApiResponse role check 直後に `msg.recipient === 'all'` filter 追加 (recipient 不在 / null は許容、 explicit な非 'all' のみ drop)、 search tool (`recipient='web'`) や image gen tool (`recipient='t2uay3k.sj1i4kz'` 等) の assistant ctype=text msg を捕捉。 legacy DOM 経路は site UI が tool call message 自体を別 styled rendering (image inline preview / search status indicator 等) で hidden にしていたため leak 不発、 API path 採用に伴う必須 normalize 工程 Phase 3 シリーズの 4 件目
- PL-038 Phase 3.3 (file-only attachment user msg 復元): Phase 3.2 smoke で legacy 28 turn / master 27 turn の 1 turn drop = 添付 file のみ user msg が parts=[''] (text 空) で API 経路 filter 段で drop されると判明、 metadata.attachments に file 名 (1 件: `### AIと錬金術ーー現代のホムンクルスとは.txt`) 格納されているため text 空判定の前に attachments 存在チェック、 non-empty なら `[添付ファイル: <name>]` placeholder で text 再構築 (Gemini PL-017 / Claude PL-012 と同 contract)。 legacy DOM innerText 経路は React render の attachment chip から file 名を innerText で吸収していたため drop 不発、 API path 採用に伴う必須 normalize 工程 Phase 3 シリーズの 3 件目
- PL-038 Phase 3.2 (API marker PUA delimiter fix): Phase 3.1 の cleanChatGPTApiMarkers regex は literal `entity` / `cite` 文字列 match で書いていたが、 user smoke で render 残存報告 (`□entity□["organization", "Microsoft", 0]□` 形式) → raw bytes 解析で marker は **Private Use Area 文字 `\uE200` (開始) / `\uE201` (終了) / `\uE202` (内部 separator)** で囲まれた構造化 markup と判明、 literal match では marker 全体を吸収できていなかった。 修正 = 3 段 regex chain に再構成: (1) `\uE200entity\uE202\[..., "name", \d+\]\uE201` で entity marker 全体を表示名 (capture group $1) に置換、 (2) 残 PUA-block (`\uE200[\s\S]*?\uE201`) は cite / 他 marker 含めて全削除、 (3) 最終 strip pass で stray PUA chars `[\uE200-\uE207]` を除去 (delimiter 単独残置で `□` glyph 化する case 対策)
- PL-038 Phase 3.1 (API mapping render artifacts 解消): Phase 3 user smoke で 通常保存末尾数件問題は解消したが render 側で 3 件 noise 残: (1) tool call leak (`search("\u30de...")` 等の raw query 表出) = parser が `content_type='code'` の tool call message を assistant text として取り込んでいた、 (2) entity recognition marker (`entity["organization", "Microsoft", 0]`) raw 表出 = ChatGPT site UI が styled span 化する inline marker、 (3) cite turn marker (`citeturn0search0turn0search11`) raw 表出 = UI が footnote link 化する inline marker。 fix = parseConversationApiResponse で role filter を user / assistant 限定 (system / tool drop)、 content_type filter を text / multimodal_text 限定 (code / *_editable_context / tether_* drop)、 cleanChatGPTApiMarkers helper 追加で entity marker は表示名抽出 (capture group $1)、 cite marker は全削除 (regex `cite(?:turn\d+(?:search|news|web|file)\d+)+` で吸収)。 multimodal 非 string parts (image / file asset) 復元は Phase 3.2 候補で carry forward
- PL-038 Phase 3 (ChatGPT lazy mount fix via API hook、 release blocker regression 解消): site UI 5/7-5/10 仕様変更で 旧 turn の `data-message-author-role` / `text-message` 子孫が scroll mount 時 attach + mount 前不在 (= lazy mount + lazy attribute attachment) と判明 (Phase 1 probe `aed0c71` で確定)、 通常保存で末尾数件しか取れない release blocker 解消。 設計 = Gemini PL-036 形 page_hook.js + active replay pattern を ChatGPT 移植、 PL-036 §invariant #4 (auto-scroll 不採用 厳守) 整合。 Phase 2 試作 (auto-scroll expand loop in extractMessages、 commit 195e120) は invariant 違反で revert (ed14d8e)、 Phase 3 で API 経路に pivot。 実装: (1) `page_hook.js` 新規 = MAIN world で document_start install、 `/backend-api/conversation/<cid>` の fetch / XHR を passive sniff、 200 response の resp_text を cid 別 cache、 完全な mapping JSON 取得。 site 自身が page load 時 auto-fetch するため通常 cache hit。 cache miss 時は active replay = saved Request clone (site auth chain 込み Authorization / OAI-Device-Id / Sentinel header 全保持) で再 fetch、 console から単純 `fetch('/backend-api/conversation/<cid>')` は 404 (site auth header 不在) と probe で検証済。 (2) `manifest.json` `content_scripts` に page_hook.js MAIN world entry 追加 + content.js を `document_start` に変更 + `web_accessible_resources` に page_hook.js 追加。 (3) `content.js` bridge 機構 (postMessage 双方向、 reqId tracking + timeout) + `parseConversationApiResponse` (mapping から current_node parent chain walk → 時系列 messages 化、 hidden system / tool / multimodal asset 部分は除外、 user / assistant / system / tool role を保持、 raw markdown text 形式) + `extractMessagesViaApi` (cache 読出 → miss なら active replay → mapping parse) + save_open click handler async 化で API 経路優先、 API fail 時のみ DOM fallback。 (4) 副次 fix = `deepText` collect 内に collapsible UI label substring match skip (`class*="showMoreLabel"` / `showLessLabel`) 追加、 主経路 API 化で UI noise 不在だが DOM fallback 経路用の防御。 当初 charter §設計 (Gemini PL-036 形 paginate / event-driven wait / fullHistoryReady signal) は ChatGPT 全件 1 fetch で完結するため簡略化、 release blocker 解消 scope のみに集中
- widget tooltip を native `title=` 属性から自前 CSS tooltip (`::after` pseudo-element + `data-tooltip` attr) に置換。OS native tooltip の遅延 (~500ms) / 配色 (Win11 = 白bg黒文字) / 位置 (cursor 右下、ばらつき) の 3 症状を一括解消。Shadow DOM scope 内で `padding: 3px 8px 1px` (日本語 baseline 視覚補正) / `line-height: 1.3` (button line-height 36px 継承遮断) / 黒bg `#1f1f1f` 白文字固定 / button 直上 default + 上端余白 < 40px で下方向 flip (`mouseenter` で `getBoundingClientRect` → `data-tooltip-pos` attr toggle) / 120ms hover delay。`applyWidgetI18n` の `el.title = text` は `el.dataset.tooltip = text` に置換 (`aria-label` は維持)
- PL-029 CP2 (`background.js` ASA pre-warm on tab probe): `probeChatGPTTab` 関数先頭に `void ensureAsaRunning()` (fire-and-forget) を挿入。tab probe = ASA 必要性 signal の proactive trigger 化、tab probe 完了時点 ≈ ASA spawn 並行完了で 1st widget click 時の体感 lag (~300-500ms) 解消。`ensureAsaRunning` singleton (PL-025 CP3 fix-A) + `state.token` early return guard で重複 spawn / 副作用 0、onUpdated 連発 / bootstrap 起動時 4 回 trigger でも in-flight share or token guard で抑制完備。reactive 経路 (postEvent 401/403 + PL-028 recovery) は touched せず共存。Claude/Gemini 横展開は CP3
- PL-029 CP4 (`background.js` NMH old port disconnect 化): `ensureAsaRunning` 内 `state.nmhPort = port` 上書き前に旧 port を `disconnect()` 呼出。`chrome.runtime.connectNative` で得た port は明示 disconnect しない限り alive 維持 = NMH helper (bat + python.exe = 2 process) も alive 維持で service tab close + 再 open 反復で **NMH helper process が累積する root** を能動 disconnect で旧 helper の stdin EOF → exit させて回避。PL-025 CP3 fix-C 「NMH disconnect ≠ ASA exit」と整合 (ASA 本体は別 process で alive 維持)。Claude/Gemini 横展開は同 commit
- PL-029 CP5 (`background.js` tab_closed gate 内 NMH disconnect + token reset): `tab_closed` gate (PL-021 CP2、残 service tab 0 で `postEvent('tab_closed', ...)` 発火) の **直後**に `state.nmhPort.disconnect()` + `state.token = null` reset を追加。ASA shutdown 確定時に NMH helper も同時 exit で **Python 残置 0** 化、かつ次 service tab open 時の `ensureAsaRunning` が `state.token === null` 経路で **pre-warm 段階で full spawn 発火** = postEvent recovery (PL-028) 待ちの 1st click lag 解消。「(2) 残置 + 再 open で slow spawn」症状を根治。Claude/Gemini 横展開は同 commit
- PL-028 CP2 (`background.js` ASA crash 時 token stale 復旧): `postEvent` に `_retried` guard 付き 1 回 retry 経路追加。`fetchError === true` / `status === 0` (ECONNREFUSED = ASA 本体 HTTP server 消失) または `status === 403` (token mismatch、token file 削除等の限定 case) を検出して `state.token = null` clear + `ensureAsaRunning` (PL-025 CP3 fix-A singleton 流用) 再実行 + 元 `postEvent` を 1 回 retry。recursion guard で無限 loop 防止、通常 4xx/5xx (payload error 等) は token clear せず素通し。PL-025 CP3 fix-C で削除した `port.onDisconnect` token reset の副作用 (異常 disconnect 時に stale token 永続化) を別軸で補完。Claude/Gemini 横展開は CP3/CP4
- PL-025 CP3 (widget save fetching dialog、ChatGPT scope は撤去判断): `bindWidgetButtons` の split events 化を試行したが user smoke で **sync push (ChatGPT は buildSnapshot ms 完了) に対して `min_show_ms` 300ms が逆 UX 劣化**と判明、ChatGPT/Claude 経路では split events 不採用に scope 縮退。content.js は元の PL-024 形 (save_open 1 発、push model 直送) を維持、`asa_core/dialogs/save_pending_dialog.py` + 3 版 dispatch は Gemini async 経路 (CP5) 専用に位置付け変更。詳細判断は `Dev_Journal/problem_logs/PL-025-cross-cutting-fetching-dialog.md` §scope 縮退判断
- PL-025 CP3 fix-A (`background.js` `ensureAsaRunning` race 根治): singleton in-flight promise 化。旧実装は `fetchBridgeInfo` の await 中 `state.nmhConnecting` lock 未設定で並行 2 invocation が race 通過 → ASA 2 個 spawn の bug。CP3 split events 試行 (sendMessage 2 連) で初顕在化、症状 = pending dialog freeze + Python process 増殖 + `widget_save_open` POST が別 process 着で dialog 永遠 timeout。`_ensureAsaRunningPromise` を share、並行 call 全部同 promise を await、`fetchBridgeInfo` も lock 内に取込。ChatGPT 撤去後も race 自体は将来別経路で再発し得るため fix 維持 (Claude/Gemini への横展開は CP4/CP5)
- PL-025 CP3 fix-C (`background.js` NMH disconnect 時の token 維持): `port.onDisconnect.addListener` 内の `state.token = null` reset を削除。NMH stub host は ready 報告後すぐ exit する設計 = disconnect 自体は仕様通りで ASA 本体は alive、`/bridge-info` 取得も valid。旧 reset は次 sendMessage 時 fix-A の token check を skip させ connectNmh 経路に再突入 → NMH が既存 ASA を detect せず 2 個目 spawn する root の本命。Chrome devtools log で 1 click 内 `connectNative start` が 2 回連続 (pid 別 ASA spawn 2 個 = Python 4 process) を観察、本 fix で `if (state.token) return true` skip 経路を健全化。ASA crash 時の token stale 復旧は postEvent 401/403 検出 + clear logic で対応する別 PL 起票候補
- PL-021 CP2: chrome multi-window / multi-tab で 1 枚閉じ → ASA 即 shutdown + 残ウィンドウ連鎖閉鎖の UX blocker 解消。`background.js` `tab_closed` handler に `chrome.tabs.query({url: 'https://chatgpt.com/*'})` gate 追加 = sender tab を除く残 service tab 数 0 のときだけ `postEvent('tab_closed', ...)` 発火。`pagehide` (tab 単位 fire) → ASA `tab_closed` flag → main_loop break → `driver.quit()` + 同 profile 全 window close 連鎖の root cause を event 元で抑止。manifest `host_permissions` `https://chatgpt.com/*` 既存 = 追加権限不要
- PL-007: DOM 経路の `<table>` を markdown pipe 構文に変換し保存 HTML での表フラット化を解消 (`84a158d`)。`readMessageText` 内 `clone.querySelectorAll('table')` を `tableToMarkdown` 結果のテキストノードに置換、`<thead>/<tbody>/<tfoot>` 跨ぎ・列数不揃い・header 検出に対応
- PL-035: code-block の内部改行消失 + 言語ラベル / コピー / 実行する ボタン混入を解消。`<pre>` を code container として捕捉、inner `<code>` を `<br>→\n` で body 化し `code_blocks` 配列に積み marker `[[ASA_CODE_BLOCK_<idx>]]` で本文中を置換。html serializer のみ fenced code block 化、md は marker→平文戻し（PL-016 と同じ contract）。`<button>` と `role=button` を walker で全 skip
- PL-035 Step 2: 外側 `<pre>` 到達時 (alreadyWrapped 経路) に header の `div.text-token-text-primary` から言語ラベル ("Python"/"JavaScript" 等) を抽出し `code_blocks[idx].lang` に格納。html で `<pre><code class="language-X">` として描画。3 版で `code_blocks[].lang` contract 完全整合 (Claude PL-011 / Gemini PL-016 Step 2 と同形)
- PL-037: file 添付 (file-tile) の描画崩れを解消。ChatGPT は file-tile 外側 div の sibling 可視 text node に file 名を露出するため UI_SKIP(button) でも raw text が漏れて markdown `#` 構文と衝突 (`# AIの食卓.txt` → `<h1>` 化、user turn 異常見出し化) していた。`deepText` 内 collect で `class~="group/file-tile"` の container を early intercept し、内部 `button[aria-label]` から完全 file 名を抽出して `[添付ファイル: <name>]` placeholder で本文に注入、container 配下は subtree skip。3 版 contract 整合 (Gemini PL-017 / Claude PL-012 と同形)
- PL-013 (横展開): URL-only msg (本文が外部 link 1 つのみ) の turn 完全消失を予防的に解消。`readMessageDeep` / `readMessageText` 内 `clone.querySelectorAll('a[href^="http"]').forEach(a => a.textContent = '')` を 2 箇所削除。citation 除去は次行の `[data-footnotes], cite, [class*="citation"], [class*="footnote"]` の `node.textContent = ''` で完結 (冗長 strip 排除)。Claude 版 PL-013 と同形 selector 同形 fix。Claude 版で実機再現 + fix POSITIVE 確認後の予防的横展開。Gemini 版は同形 selector 不在のため対象外
- PL-014 (Claude 由来横展開): `dedupeMessages` 内 `if (key === previousKey) continue;` の text-based adjacency dedup を撤去。Claude UI が regenerate / 連投で **異なる DOM node に同 text** を render する case で legit な user-meaningful turn を 1 つ消す副作用があった。同 DOM node の重複検出は `collectMessageNodes` の Set-based node identity dedup で実施済のため text-based collapse は冗長 + 過剰。Claude 版で実機再現 + fix POSITIVE 確認後の予防的横展開。Gemini 版 `gemini_extension/content.js` には `dedupeMessages` 不在 (異なる algo) のため対象外
- PL-015 (Claude 由来横展開): `chatgpt_asa/runtime/save_helpers.py` `merge_scroll_snap_batches` の text-based fingerprint dedupe を occurrence-based に書換。scroll_snap mode で template 連投応答の最終 turn 消失症状を Claude 版実機再現 + fix POSITIVE 後の予防的横展開。詳細は Claude PL-015 参照
- PL-013 CP1 (widget 化): composer form 内 abs child として 4 button widget (💾⚙️🔴📜) を inject。CSS pinning (`position:absolute; top:50%; right:0; transform: translate(calc(100% + 8px), -50%)`) で form 右外側 / 垂直中心に配置、composer height 変動は CSS が自動追従 (JS rect tracking 廃止)。`form.style.position = "relative"` を idempotent 設定 (computed が static の場合のみ) で site CSS 干渉ゼロ。Shadow DOM `closed` + theme auto (OS prefers-color-scheme) + emoji icon + tooltip。`localStorage["asa_widget_side"]` で left/right 切替。MutationObserver + 1000ms sentinel polling で SPA 遷移時に再 inject。dispatch (button click → desktop save flow) は CP2 以降。設計 doc: `Dev_Journal/2026-05-04_PL-013_widget_design.md`

## 1.0.2 (2026-04-08)

- 初回 Chrome Web Store 申請版
- `manifest.name` を「ASA Bridge for ChatGPT」に確定
- プライバシーポリシー URL: https://jellycotton.github.io/asa-privacy-policy/
