<div align="center">

# dsh-session-notify

[简体中文](README.md) · [English](README.en.md) · [繁體中文](README.zh-TW.md) · **日本語** · [한국어](README.ko.md)

**DSH（DeepSeek Harness）セッション完了通知プラグイン —— 各ターンが終わったら、完了状態があなたの方へ来る。画面をじっと見て待つ必要はありません。**

[![npm version](https://img.shields.io/npm/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![npm downloads](https://img.shields.io/npm/dm/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![license](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](./LICENSE)
[![node](https://img.shields.io/node/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![DSH](https://img.shields.io/badge/DSH-Web%20Profile-4D6BFE)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/TelosmaYLX/dsh-session-notify/pulls)

各ターンの会話終了時に、「完了 / エラー / ブロック / 上限到達」を所要時間・トークン消費とともにセッションログへ書き込み、ブラウザのシステム通知とページ内トーストをプッシュします（**ウィンドウの非フォーカス時 / フォーカス時でチャンネルを個別に指定でき、どちらにも「通知しない」あり**）。**AI からの質問や承認依頼も即座にポップアップでお知らせ**します。5 言語、4 つのスタイルプリセット（顔文字 / アイルー / ネコ娘 / DeepSeekちゃん）、ビジュアルなメッセージテンプレートエディタ、カスタムプリセットライブラリを内蔵。キャッシュヒット率と生成速度は公式プロジェクションから取得し、ステータスバーと同じ口径です。

</div>

---

## 目次

- [機能概要](#機能概要)
- [環境要件](#環境要件)
- [インストール](#インストール)
- [アンインストール](#アンインストール)
- [クイックスタート](#クイックスタート)
- [通知の動作](#通知の動作)
  - [トリガー条件](#トリガー条件)
  - [通知本文の取得元](#通知本文の取得元)
  - [通知の例](#通知の例)
  - [通知の権限](#通知の権限)
- [設定](#設定)
  - [設定パネル](#設定パネル)
  - [メッセージテンプレートとプレースホルダー](#メッセージテンプレートとプレースホルダー)
  - [プリセットシステム](#プリセットシステム)
  - [ホスト設定項目](#ホスト設定項目)
- [動作原理](#動作原理)
- [プロジェクト構成](#プロジェクト構成)
- [開発とデバッグ](#開発とデバッグ)
- [よくある質問](#よくある質問)
- [更新履歴](#更新履歴)
- [謝辞](#謝辞)
- [コントリビューション](#コントリビューション)
- [関連リンク](#関連リンク)
- [ライセンス](#ライセンス)

---

## 機能概要

<div align="center">

<img src="screenshot/screenshot1.png" width="220" alt="タイトルエディタ">
<img src="screenshot/ScreenShot2png.png" width="220" alt="本文エディタ">
<img src="screenshot/ScreenShot3.png" width="220" alt="タスク完了通知">
<img src="screenshot/ScreenShot4.png" width="220" alt="AI 質問通知">
<img src="screenshot/ScreenShot5.png" width="220" alt="タスクエラー通知">

</div>

### 3 チャンネルで通知、取りこぼしなし

| チャンネル | 形式 | 説明 |
| --- | --- | --- |
| セッション内システムメッセージ | 折りたたみ可能な通知行 | 各ターン終了時に、終了理由・所要時間・消費量をプラグイン由来のシステムメッセージとしてセッションログへ追加し、JSONL とともに保存。セッションを復元・リプレイした後も表示されます。 |
| ブラウザシステム通知 | Web Notification | ネイティブポップアップ。各完了イベントで独立した `tag`（`dsh-session-notify:<timestamp>`）を使用するため、前回と置き換わらず、グループ項目に折りたたまれません。通知をクリックするとウィンドウにフォーカスが戻ります。 |
| ページ内トースト | 右下のフローティングポップアップ | 常に表示されるセーフティネット。システム通知がプラットフォームにサイレント化されたり、権限が拒否されたり、環境が非対応の場合でも、目に見えるフィードバックを提供します。同画面に最大 3 件（超えた場合は最古のものを削除）、10 秒で自動的に消え、クリックで閉じます。 |

> [!NOTE]
> 後ろ 2 つのチャンネルは**ウィンドウのフォーカス状態で振り分けられます**。設定パネルには「非フォーカス時」「フォーカス時」の 2 つのドロップダウンがあり、それぞれ `システム + ページ内` / `システムのみ` / `ページ内のみ` / `通知しない` から選択できます。非フォーカス判定の条件は `document.visibilityState === 'hidden'` または `!document.hasFocus()`——タブを切り替えた、ウィンドウを最小化した、別の場所をクリックした場合に「非フォーカス時」の経路が使われます。

### バックグラウンドセッションを完全カバー

- ホストはすべてのセッション（バックグラウンド、未表示ウィンドウを含む）について「最新の通知本文」のセッションプロジェクションを維持します（key = `session-complete-notify`）。プッシュ本文はセッション間で一貫し、たまたまそのウィンドウを開いているかどうかに依存しません。
- クライアントはセッションリストのスナップショットから全セッションの `running` ビットを監視し、`true → false` のエッジでプッシュをトリガーします。公式サイドバー通知と同じ戦略です（初回監視ではベースラインを記録するだけで、すでに idle のセッションには追って送信しません）。

### 質問の即時通知

- AI が `ask_user_question` を呼び出して質問すると、ホストは即座に「質問タイトル + 本文」を専用プロジェクション（key = `session-complete-notify-question`）へ書き込み、クライアントがリアルタイムにポーリングしてポップアップ通知します——**別のページを見ていても質問を見逃しません**。
- 質問の文言は完全にカスタマイズ可能です。タイトルは「理由別タイトル → グローバルタイトル → デフォルトタイトル」の順に解決され、本文は `{question}` プレースホルダー（AI の実際の質問を注入）に対応し、`{image}` / `{icon}` のメディアスイッチも同様に有効です。

### 承認の即時通知

- セッションが権限承認を要求した時点（`approval/asked`）で即通知し、`approval/decided` で解除——別のタブを見ていても承認を逃しません。
- 3 系統のフォールバック信号：harness ネイティブの `pendingInteractions`（ホストが提供する場合は最も確実）→ ホストの承認プロジェクション（key = `session-complete-notify-approval`、タイトルと本文はホストが現在の言語で描画）→ セッションリストスナップショットの `pendingInteraction === 'approval'`。
- 文言はツール名と任意の理由のみ（例：「セッションが Bash の承認を待っています。」）。**コマンド引数などの機密内容は含みません**。プッシュチャンネルとメディア設定も同様に適用されます。

### 一言一句までカスタマイズ可能

- **5 言語**：簡体中文、繁體中文、English、日本語、한국어 —— 通知メッセージ、所要時間・消費量の表現、設定パネルの UI がすべて言語に応じて切り替わります（切り替え時に即再レンダリング）。
- **ビジュアルテンプレートエディタ**（Chip カプセルエディタ）：動的情報をインラインカプセルとしてレンダリング（プレースホルダーコードは露出しません）。「+ 情報を挿入」でカーソル位置に挿入（テキストの途中にも挿入可）、カプセルをクリックで削除、各欄にリアルタイムプレビュー（情報がサンプル値として本文に流れ込みます）。
- **プリセットシステム**：「デフォルト」ベースライン + ワンクリックのスタイルプリセット 4 種（顔文字 / アイルー / ネコ娘 / DeepSeekちゃん——タイトルと 5 終了理由 + 質問の文面一式をスタイル化）。現在の設定をカスタムプリセットとして保存可能（`localStorage` に永続化）。自動採番される無名プリセット（`未命名`、`未命名 2`…）、「出自：xxx · 変更あり」の出所表示、プリセット削除に対応。
- **プッシュタイトルテンプレート**：空欄の場合は各理由でデフォルトタイトルを使用（完了＝タスク完了 / エラー＝タスクエラー / … / 質問＝AI からの質問が届きました）。`{title}` はセッションタイトルを参照します。

### 公式の口径と同じソース

- **キャッシュヒット率**は公式 `tokenUsage` プロジェクションから取得：キャッシュ読み込み /（非キャッシュ入力 + キャッシュ読み込み + キャッシュ書き込み）。
- **生成速度**は公式 `sessionStats` プロジェクションから取得：出力トークン ÷ デコード所要時間。
- どちらも dsh-web-ui のステータスバーと完全に同じ口径で、待機・準備・ツール実行時間は含みません。プロジェクションが利用不可、またはデータが未準備の場合は、ローカルの使用量集計による推定に自動フォールバックします。

> [!NOTE]
> キャッシュヒット率と速度は、カスタムテンプレートで `{cache}`、`{tps}` プレースホルダーを挿入した場合にのみ表示されます。内蔵デフォルトメッセージを使用する場合、本文には所要時間と消費量は含まれません（表示するにはカスタムテンプレートでプレースホルダーを挿入してください）。

### エンジニアリング品質

- **リアルタイムイベントのみに反応**：resume、replay で古い通知を再生せず、セッション読み込み時に画面をスパムしません。
- **自己ループ免疫**：プラグインが追加するメッセージタイプ（`user/message`）と、自身が監視する対象（`turn/*`）は交差しません。
- **外部依存ゼロ**：ホスト側で裸の import がゼロ。UserMessage は `dsh-llm` の `createUserMessage` 契約に従って手作業で構築。純粋ロジック層（`lib/core.js`）は依存ゼロで、単独でテスト可能です。
- **Cordis effect の規律**：リトライタイマーを `ctx.effect()` でラップし `clearTimeout` disposer を返すため、fiber のアンロードに伴い登録が自動解除され、HMR ホットリロードに対しても安全です。
- **インストール即マウント**：公式 `dsh.bundle` manifest を宣言しており、`dsh plugin add` の 1 コマンドでインストール完了後すぐ使用可能。手書きの patch は不要です。

---

## 環境要件

| 依存 | 要件 |
| --- | --- |
| DSH（DeepSeek Harness） | Web profile でデプロイ。公式 base bundle にはデフォルトで `@deepseek-ai/dsh-settings`（設定名前空間）とセッションプロジェクションが含まれており、追加設定は不要です |
| cordis | `>=4.0.0-rc <5`（peer dependency、ホスト側で提供） |
| Node.js | `>=22`（ホスト側） |
| ブラウザ | Web Notification 対応ならシステム通知あり。非対応、権限拒否、サイレント化の場合はトーストでフォールバック |

---

## インストール

> [!WARNING]
> 裸の `npm install` はパッケージを依存ツリーに追加するだけで、**プラグインを登録しません** —— これは DSH 公式の設計です（`npm install only adds the dependency; it does not register the plugin`）。自動マウントの唯一の公式手段は `dsh plugin add`：パッケージ内の `dsh.bundle` manifest（本プラグインは 0.1.3 以降で宣言、リポジトリ直下の `cordis.patch.yml` を指します）を読み取り、自動適用します。

### 方法 1：dsh plugin add（推奨）

パッケージのインストールと同時に `cordis.patch.yml` を自動適用し、プラグインを profile の組み立てにマウントします（host のイベント購読 + client の起動グラフ注入）。

```bash
dsh plugin --profile web add @telosmaylx/dsh-session-notify
```

### 方法 2：GitHub リポジトリからインストール

```bash
dsh plugin add github:TelosmaYLX/dsh-session-notify
```

DSH Web GUI のセッション内で実行することもできます：

```bash
dev_install_package github=TelosmaYLX/dsh-session-notify
```

### 方法 3：ローカルディレクトリのホットインストール（開発用）

パスを自分のクローン先ディレクトリに置き換え、DSH Web GUI のセッション内で実行します：

```bash
dev_install_package dir=/あなたの/クローン先/dsh-session-notify
```

### 方法 4：npm パッケージの手動インストール

まずパッケージングします：

```bash
npm pack @telosmaylx/dsh-session-notify
```

解凍後、ディレクトリを指定してインストールします（DSH Web GUI のセッション内で実行）：

```bash
dev_install_package dir=/解凍/ディレクトリ/package
```

### 方法 5：cordis patch の手動適用（インストーラ非依存）

`~/.dsh/profiles/web/cordis.patch.yml` に追記します：

```yaml
- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config: {}
```

> [!IMPORTANT]
> どの方法でも、インストール後は**ブラウザのページを 1 回リロード**する必要があります —— クライアント bundle は `__DSH_BOOT__` 起動グラフから注入されるためです。

## アンインストール

1 コマンドでプラグインとそのマウントを削除します（`cordis.patch.yml` から insert エントリを自動的に削除）：

```bash
dsh plugin --profile web remove @telosmaylx/dsh-session-notify
```

> [!NOTE]
> 手動インストール（方法 4 / 5）の場合は、`~/.dsh/profiles/web/cordis.patch.yml` から該当の insert エントリを削除し、ページをリロードしてください。

### アンインストール時に自動クリーンアップされる内容

プラグインは完全なライフサイクル終了処理を実装しています（Cordis effect の規律）。アンインストール / 無効化 / HMR ホットリロード時：

| 側面 | 自動解放されるリソース |
| --- | --- |
| host | `session/event` イベント購読、settings 名前空間、セッションプロジェクション、設定登録リトライタイマー（`ctx.effect` でラップ）。アンインストールフラグを立て、スケジュール済みのマイクロタスク追加を抑制します |
| client | セッションリスト購読、完了プッシュ本文のポーリングタイマー、`window.__dsch_notify_debug` デバッグフック（参照で削除、クロージャリーク防止）、ページ内トーストコンテナの DOM |

### アンインストール後も保持されるデータ

- **設定構成**（言語、メッセージテンプレート）は settings ドキュメントに残り、再インストール後に自動復元されます。
- **カスタムプリセット**はブラウザの `localStorage`（`dsh-scn-custom-presets`）に保存され、再インストール後も残ります。
- 過去のセッションに追加済みのシステムメッセージと JSONL ログは**ロールバックされません**（これらはセッションデータの一部であり、公式サイドバー通知と同じ意味づけです）。

---

## クイックスタート

1. 上記いずれかの方法でインストールし、ページをリロードします。
2. 任意の 1 ターンの会話を開始し、終了を待ちます —— 右下にトーストが表示され、ブラウザにシステム通知が届き、セッションログに折りたたみ可能なシステム通知行が現れます。
3. 初めて完了イベントを受け取ったとき、ブラウザが通知権限を要求します（ページごとに 1 回のみ）。許可すると、以降の完了でシステム通知が届きます。
4. **設定 → プラグイン → セッション完了通知**を開き、言語の切り替え、メッセージテンプレートの編集、プリセットの保存を行います。保存後、「クリックでリロード」をクリックしてホストとクライアントの両側で読み直すと、新しい設定が有効になります。

インストール直後は、セッションログに次のような折りたたみ可能な通知行が表示されます：

```text
会话「重构登录模块」已完成（用时 1 分 12 秒，消耗 1,240 输入 / 3,560 输出）。
```

> デフォルトメッセージは「セッション」の後にセッションタイトルのラベル（`{title}`）を埋め込みます。セッションにタイトルがない場合は「セッション完了」に自動フォールバックします。

---

## 通知の動作

### トリガー条件

各ターンの会話終了（`turn/end`）時に終了理由で判定し、ホワイトリストに一致すれば通知します：

| 終了理由 | 意味 | デフォルト |
| --- | --- | --- |
| `completed` | セッションが正常に完了 | 通知 |
| `aborted` | セッション中止 | 通知 |
| `blocked` | セッションがブロック | 通知 |
| `error` | セッションでエラー（エラー詳細付き、超長は切り詰め） | 通知 |
| `max-tokens` | 出力トークン上限に到達 | 通知 |
| `interrupted` | 中断（クラッシュ復旧後に永続化バックエンドが書き足す孤児ターンのクローズマーカー） | 通知しない（設定で追加可） |

**サブエージェントセッションはデフォルトでスキップ**（`header.origin === 'subagent'` または `delegationDepth > 0`）—— サブエージェントは親セッションがオーケストレーションするため、ターンごとの通知はノイズになります。ホスト設定でスキップを無効化できます。

**質問通知は独立したチャネルで、上記のホワイトリストには含まれません**：AI が `ask_user_question` を呼び出して回答を待つ間（`tool/call` イベント）、即座に通知し、`tool/result` が返ると通知は無効になります。質問はセッションログに書き込まれず、通知のみが表示されます。

**承認も独立したチャネルです**：セッションが権限承認を要求した時点（`approval/asked`）で即通知し、`approval/decided` で解除。こちらもセッションログには書き込まず、通知のみです。タイトルと本文はツール名と任意の理由のみで、コマンド引数は含みません。

### 通知本文の取得元

クライアントはセッションリストで `running: true → false` のエッジを観測したときにプッシュします。本文は以下の優先順位で取得します（最大 6 秒のポーリング、400ms 間隔）：

1. **ホストプロジェクション**（key = `session-complete-notify`）—— すべてのセッションにあり、バックグラウンドセッションでも全文を取得できます。
2. **セッションイベントウィンドウ内の notice ノード**（`kind=context` + `form=notice`）—— 表示中のセッションで、保存後すぐに利用可能。
3. **フォールバック** —— 「詳細はセッション内のシステムメッセージを参照」+ ワークスペース情報（`cwd` の最後のセグメント）。

質問通知の本文もホストプロジェクション（key = `session-complete-notify-question`、ホストがタイトルと本文をレンダリング済み）を優先します。古いホストにこのプロジェクションがない場合は、クライアントがタイトルと `{question}` テキストを自前で組み立てます。

承認通知は利用可能な順に 3 系統から取得します：harness ネイティブの `pendingInteractions` → ホストの承認プロジェクション（key = `session-complete-notify-approval`）→ セッションリストスナップショットの `pendingInteraction` フィールド。取得できた時点で通知し、同一の承認につき 1 回だけプッシュします。

### 通知の例

以下はすべて `lib/core.js` の `buildNotice` が実際に生成したものです。デフォルトメッセージは「セッション「{title}」〇〇。クリックして表示。」の形で統一（終了理由によって語彙が異なります。**所要時間・消費は含みません**）：

日本語デフォルトメッセージ：

```text
セッション「重构登录模块」完了。クリックして表示。                 ← 完了
セッション「重构登录模块」中止。クリックして表示。                 ← 中止
セッション「重构登录模块」がブロックされました。クリックして表示。   ← ブロック
セッション「重构登录模块」上限に到達。クリックして表示。           ← 上限到達
セッション「重构登录模块」エラー。クリックして表示。               ← エラー
```

> セッションにタイトルがない場合（`titleValue` が空）は「セッション完了。クリックして表示。」にフォールバック。所要時間・消費・キャッシュヒット率・速度は、カスタムテンプレートで `{duration}` `{usage}` `{cache}` `{tps}` を挿入した場合のみ表示されます。

カスタムテンプレート（設定パネルで編集。この例ではすべての情報枠を使用）：

```text
{title} 干完了！用时 {duration}，消耗 {usage}，缓存命中 {cache}，速度 {tps}
```

レンダリング結果：

```text
重构登录模块 干完了！用时 3 分 25 秒，消耗 103,600 输入 / 35,600 输出，缓存命中 96.5%，速度 92 tok/s
```

5 言語での同じイベント：

```text
会话「重构登录模块」已完成（用时 3 分 25 秒，消耗 1,240 输入 / 3,560 输出）。
會話「重構登入模組」已完成（用時 3 分 25 秒，消耗 1,240 輸入 / 3,560 輸出）。
Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
セッション「重构登录模块」完了（所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力）。
세션「重构登录模块」 완료（소요 3분 25초, 소모 1,240 입력 / 3,560 출력）。
```

### 通知の権限

| 権限ステータス | 動作 |
| --- | --- |
| `default`（未決定） | 完了イベントではトーストのみ送信。設定パネルの「通知権限」エリアに「許可をリクエスト」ボタンを提供（**ユーザージェスチャ内でリクエスト**——Chromium はジェスチャ以外の自動リクエストを無視するため、プラグインは自動リクエストを行いません） |
| `granted` | 「非フォーカス時」「フォーカス時」それぞれで選んだチャンネルでシステム通知を送信（独立した tag、互いに上書きしません。「通知しない」ならその経路では表示されません） |
| `denied`（ブラウザでブロック） | トーストのみ。設定パネルにアドレスバーの操作ガイドを表示（権限アイコン → サイト設定 → 通知 → 許可） |
| `undefined`（非セキュアコンテキスト / 非対応） | トーストのみ。「ページ内表示のみ」への切り替えを推奨 |

---

## 設定

ほとんどの設定は **DSH Web UI → 設定 → プラグイン → セッション完了通知** パネルで行います（保存後、「クリックでリロード」を押すと有効になります）。「トリガー理由のホワイトリスト」のみ、ホストの `cordis.patch.yml` の `config` で設定します（サブエージェントのスキップはパネルのチェックボックスで制御）。

### 設定パネル

パネルは公式の「設定 → プラグイン」パネルに登録されます（`settings.plugin.item` keyed slot、key = `session-complete-notify`）。スタイルはネイティブプラグインカードを 1 つ 1 つ再現しています（12px の角丸、開閉、回転する chevron、footer のステータス表示 + ゴーストボタン + メインカラーの保存ボタン）：

| エリア | 内容 |
| --- | --- |
| プリセット | ドロップダウンで内蔵またはカスタムプリセットを選択。「新規作成」で現在の設定をカスタムプリセットとして保存。現在のプリセットは「削除」可能 |
| 言語 | 5 言語の単一選択。切り替えでパネル全体が即再レンダリング |
| 非フォーカス時 | 4 択：**通知しない**（そのタイミングでは完全にサイレント）/ デュアルチャンネル（システム通知 + ページ内表示、デフォルト）/ システム通知のみ / ページ内表示のみ —— ウィンドウが非フォーカス（タブを切り替えた、最小化した、別の場所をクリックした）のときに有効 |
| フォーカス時 | 4 択：同上（デフォルトはデュアルチャンネル）—— ウィンドウがフォーカスのときに有効。2 つの経路は独立しており、「非フォーカス時はシステム通知 + フォーカス時は通知しない」など自由に組み合わせ可能 |
| 通知メディア | 大きな画像の 2 つのソース：**理由ごとのアップロード**——テンプレート内で「＋ 情報を挿入 → 画像」から `{image}` トークンを挿入しローカル画像を選択（エディタ内ではサムネイル付きチップとして表示、**512px 幅・通知表示比率 16:9 で中央クロップ**に自動圧縮、理由ごとに保存）。**グローバルの画像/アイコン**——2 つのアップロードカードを横並び 1 行に配置（**アイコンが先**。空欄は角丸の「+」タイルで、クリックでアップロード。**画像 512×288（16:9 中央クロップ）、アイコン 128×128（1:1 正方形中央クロップ）**。アップロード後はカードにサムネイルが表示され、**クリックで全画面プレビュー（等比・未クロップの元画像）**、右上の × で削除）。アイコンは空欄ならサイト既定のアイコン、またはテンプレートに `{icon}` トークンを挿入して**理由ごとのアイコン**を指定（グローバルより優先）。システム通知チャネルのみ有効（ページ内トーストはテキストカード）。「送信」テストボタンも同様に適用 |
| タイトル | 折りたたみセクション（**既定で折りたたみ**、クリックで展開）：**グローバルプッシュタイトル**（全理由共通。Chip エディタ——「＋ 情報を挿入」で挿入した情報は**カプセルタグ**として表示され、クリックで削除。**通知送信時はタイトル内の情報トークン（所要時間/消費/エラー/キャッシュヒット/速度）が実際の値に置き換えられ、コードが露出しません**。空欄の場合は各理由でデフォルトタイトル——完了＝タスク完了、エラー＝タスクエラー、中止＝タスク中止、ブロック＝タスクブロック、上限＝出力上限に到達、質問＝AI からの質問が届きました）＋ **理由ごとのタイトル**（6 つの理由それぞれに入力。各行に「+」挿入ボタン——挿入可能な情報トークン（「質問」含む、画像/アイコン除く）、カーソル位置に挿入；**グローバルタイトルより優先**。空欄 = グローバルまたは言語デフォルトを使用） |
| コンテンツ | 折りたたみセクション（**既定で折りたたみ**、クリックで展開）。展開すると各理由（完了、エラー、中止、ブロック、出力上限、質問）ごとに**1 行レイアウト**（理由ラベル + Chip エディタ + 「+」挿入ボタン——メニュー展開中は「−」に変化 + **紙飛行機の送信ボタン**。ボタンは角丸矩形で垂直中央揃え）：**テンプレートが空（既定プリセット）のときはエディタにデフォルト文面を表示**。テキスト + インライン情報カプセル、カーソル位置に挿入；`{image}`/`{icon}` チップは**サムネイルクリックで大図をプレビュー、× クリックでのみ削除**（誤削除防止）、他のチップはクリックで削除；**編集後に空にすると「空欄の場合はデフォルトの文面を使用」のプレースホルダーを表示（選択・削除不可）**。質問行のデフォルト文面は「AI からの質問：{question}」で、`{question}` は送信時に AI の実際の質問へ置き換わります（挿入メニューにも「質問」トークンがあり、他のトークンと同じ操作です） |
| サブエージェントセッションをスキップ | チェックボックス（保存時に設定ドキュメントへ一緒に書き込み） |
| 通知権限 | 状態をリアルタイム表示：許可済み（緑）/ 未許可（「許可をリクエスト」ボタン付き）/ ブラウザにブロック済み（アドレスバーの操作ガイド付き）/ 環境が非対応 |
| 理由別のタイトルカスタマイズ | 折りたたみエリア（デフォルトで収納）：終了理由ごとに独立したタイトル入力欄。空欄＝グローバルテンプレートまたは言語デフォルトのタイトルを使用 |
| 保存 | ホストの設定ドキュメントに書き込み（`language` / `templates` / `titleTemplate` / `titleTemplates` / `pushModeBlur` / `pushModeFocus` / `skipSubagents`）。保存後「クリックでリロード」リンクを表示 |
| リセット | 1 クリックでデフォルト値に復元（**言語は現在の選択を保持**、タイトル / テンプレート / 非フォーカス・フォーカス時のチャンネルはデフォルトに戻す）し、即座に保存 |

> [!NOTE]
> 「非フォーカス時」「フォーカス時」のチャンネルの取舍：`dual`（デフォルト）は Windows システム通知とページ内トーストを同時に表示します。トーストはセーフティネットとして、システム通知がプラットフォームにサイレント化されるのを防ぎます（集中アシスタント、通知バナー無効化など）。2 つの経路は独立しているため、「非フォーカス時はシステム通知、フォーカス時は通知しない」といった組み合わせも可能です。**既存の設定は影響を受けません**——設定ドキュメントに `pushModeBlur` / `pushModeFocus` がなければ、両方の経路が旧版の単一 `pushMode` の値にフォールバックします。ただし **QQ ブラウザなどの国産 Chromium シェルブラウザは `Notification` を「ブラウザ内蔵のページ内プッシュポップアップ」としてレンダリングします**（ページ上部 / 隅のバナーで、Windows 通知センターを経由しません）—— この場合、`dual` ではページ内に 2 つの通知（ブラウザ内蔵ポップアップ + プラグインのトースト）が表示されます。このようなブラウザでは「ページ内表示のみ」を選択してください（`Notification` を呼び出さないため、ブラウザ内蔵ポップアップは表示されず、ページ内にはプラグインの小さなトーストのみ表示されます）。「システム通知のみ」モードは QQ ブラウザでは無効です（常にページ内ポップアップとしてレンダリングされます）。設定パネルの各理由の「送信」テストボタンも同様の影響を受けます——テスト通知は「フォーカス時」の経路を優先し、それが「通知しない」の場合は「非フォーカス時」の経路に切り替え、両方ともオフなら `dual` にフォールバックするため、プレビューには常にフィードバックがあります。

> [!NOTE]
> システム通知（`Notification` API）が表示されるかどうかは、**ブラウザとサイトへのアクセス方法**の両方で決まります。Edge / Chrome は「馴染みのない」サイトへの通知を**自動的にブロック**します（アドレスバーに「通知がブロックされました」と表示）。アドレスバー左の権限アイコン → サイト設定 → 通知 → 許可で復元できます。`http://IP` のような非セキュアコンテキストでのアクセス時は `Notification` 自体が存在しないため、「ページ内表示のみ」に切り替えてください。設定パネルの「通知権限」エリアは現在の状態をリアルタイム表示し、対応する操作ガイドを提供します（1 クリックで権限をリクエスト可能）。Firefox ではウィンドウにフォーカスがある間は通知がページ内バナーとして表示され、フォーカスが外れて初めてシステム通知センターに入ります。

> [!NOTE]
> パネルの「サブエージェントセッションをスキップ」は設定ドキュメント内のブール値を保存します。ホスト `cordis.patch.yml` の `config.skipSubagents` は起動時のデフォルト値であり、どちらか一方でも true ならスキップされます。

### メッセージテンプレートとプレースホルダー

各終了理由ごとに独立したテンプレート入力欄があります。**ラベルがスイッチ**です —— テンプレートに対応する情報ラベルを挿入して初めて、そのデータが表示されます：

| プレースホルダー | 意味 | 例の値 |
| --- | --- | --- |
| `{title}` | セッションタイトル（プッシュタイトルテンプレートでも使用可） | `重构登录模块` |
| `{duration}` | 本ターンの所要時間（`turn/start` で計測開始 → `turn/end` で終了） | `3 分 25 秒` / `3m25s` |
| `{usage}` | トークン消費（入力 = 非キャッシュ + キャッシュ読み込み + キャッシュ書き込み） | `1,240 输入 / 3,560 输出` |
| `{error}` | エラー情報（エラーなしの場合は `none` と表示。単行化、80 文字で切り詰め） | `connection timeout` |
| `{cache}` | キャッシュヒット率（公式プロジェクション口径、データなしの場合は空） | `96.5%` |
| `{tps}` | 生成速度（公式プロジェクション口径、データなしの場合は空） | `92 tok/s` |
| `{image}` | カスタム通知イメージのスイッチ：「＋ 情報を挿入」から挿入してローカル画像を選択（512px に自動圧縮）、理由ごとに独立。本文レンダリング時には剥除され、セッションログには書き込まれません。トークンを削除するとその理由の画像データも消去されます | — |
| `{icon}` | カスタム通知アイコンのスイッチ：「＋ 情報を挿入」から挿入してローカル画像を選択（128×128 正方形に自動圧縮）、理由ごとに独立。本文レンダリング時には剥除され、セッションログには書き込まれません。グローバルの「通知アイコン」より優先。トークンを削除するとその理由のアイコンデータも消去されます | — |
| `{question}` | **質問行専用のプレースホルダー**：送信時に AI の実際の質問文へ置き換えられます。「＋ 情報を挿入」メニューと連動（「質問」トークンを直接選択可、手入力の `{question}` もカプセルとして認識）。質問チャネルでのみ使用可。他の理由行に挿入しても空文字に置き換えられます（リテラル漏れ防止） | `レポートの生成を続けますか？` |
| `{label}` | 非推奨 —— レンダリング時に自動的に除去され、旧テンプレートとの互換性を維持（挿入メニューからは削除済み） | — |

テンプレートを空欄にすると内蔵のデフォルトメッセージを使用します（「セッション「{title}」〇〇。クリックして表示。」の形で統一、所要時間と消費量は含みません）。折りたたみ行の `summary` は本文と同じソースです（レンダリング結果を 120 文字に切り詰め）—— 折りたたみ行だけを見るユーザーにも実際のタイトル・所要時間・消費量がわかります。

### プリセットシステム

- **内蔵プリセット**：「デフォルト」のみ。ベースラインとして使用。
- **カスタムプリセット**：`localStorage`（key = `dsh-scn-custom-presets`）に保存：
  - 「新規作成」で名前を付けてカスタムプリセットとして保存。保存後は「変更」で自動同期、「削除」で除去できます。
  - **自動採番される無名プリセット**：「デフォルト / 空白」から直接保存すると、`未命名`、`未命名 2`、`未命名 3`… が自動生成されます（番号は現在の最大値 + 1）。
  - フォームに「出自：xxx · 変更あり」の出所表示があります（プリセット由来だが内容が変更された場合）。
- **保存即同期**：保存時、フォームの出所がカスタムプリセットならそのプリセットを更新し、それ以外は新規作成するか無名プリセットの採番を続けます。

### ホスト設定項目

```yaml
- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config:
        reasons: [completed, aborted, blocked, error, max-tokens]
        skipSubagents: true
```

| フィールド | 型 | デフォルト値 | 説明 |
| --- | --- | --- | --- |
| `reasons` | `string[]` | `[completed, aborted, blocked, error, max-tokens]` | 通知をトリガーする `turn/end` 理由のホワイトリスト |
| `skipSubagents` | `boolean` | `true` | サブエージェントセッションをスキップ（`origin=subagent` または `delegationDepth>0`） |

---

## 動作原理

プラグインは**ホスト側**（Node）と**クライアント側**（ブラウザ）に分かれ、間をセッションログ（JSONL）と公式セッションプロジェクションで接続します：

```text
┌─────────────────── 宿主平面（lib/index.js，Node）──────────────────┐
│                                                                     │
│  session/event 火线                                                 │
│   ├─ turn/start        → tracker 起表（key: sessionId:turn）        │
│   ├─ assistant/message → 累加该轮 token 用量                        │
│   ├─ tool/call         → ask_user_question？写提问投影（标题+正文） │
│   └─ turn/end          → reason.kind ∈ reasons ？                   │
│                            ├─ 子代理会话？跳过                       │
│                            ├─ 读官方投影：cache / tps / title        │
│                            ├─ 按语言+模板构建通知（summary ≤120 字） │
│                            └─ queueMicrotask 追加系统消息            │
│                                 （避开 append 重入窗口）             │
│                                                                     │
│  settings.register   → 官方「设置 → 插件」命名空间（失败退避重试）   │
│  sessionProjections  → 注册投影单元（key=session-complete-notify）  │
│                        + 提问投影（key=session-complete-notify-     │
│                          question，等待回答期间持续推送）            │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ user/message (source: plugin, form: notice)
                               ▼  JSONL 持久化 + 投影推送
┌─────────────────── 客户端平面（lib/client.js，浏览器）──────────────┐
│                                                                     │
│  会话列表订阅：running true → false 边沿 → pushCompletion            │
│   ├─ 取正文：投影 → 事件窗口 notice → 降级（轮询 ≤6s）               │
│   ├─ Web Notification（独立 tag，点击聚焦）                          │
│   └─ 页内 toast（永远展示，≤3 条，10s 自动消失）                     │
│  提问投影轮询（key=session-complete-notify-question）：              │
│   有值 → 立即弹提醒（标题+正文），无值清空                            │
│                                                                     │
│  slots.inject('settings.plugin.item') → 设置卡片（预设/语言/模板）   │
└─────────────────────────────────────────────────────────────────────┘
```

### 主要な設計判断

- **再生しない**：リアルタイムイベントのみを処理し、resume、replay では過去の通知を送りません。
- **自己ループなし**：プラグインは `user/message` を追加し、自身は `turn/*` のみを監視するため、イベントタイプが交差しません。
- **外部 import ゼロ**：プラグインはリポジトリディレクトリから realpath で読み込まれるため、`@deepseek-ai/*` を裸で解決できません —— ホスト側は `createRequire` で profile 共有依存のハブ（`.dsh/profiles/node_modules`）に固定し、`schemastery`（設定 schema）と `zod`（プロジェクション schema）を取得します。UserMessage は `dsh-llm` の契約に従って手作業で構築します（`id = crypto.randomUUID()`、deep-freeze は `session.append` の adopt スナップショット段階で完了）。
- **append 再入の回避**：`session/event` のオブザーバーコールバックは、`turn/end` の append のパブリッシュ境界内で実行されます（dsh-session は dispatch の前に `entry.appending` を立て、`finally` でリセット）。同期 append は拒否されるため、`queueMicrotask` に延期します（マイクロタスクは今回の同期スタックが `finally` リセットを含めて完了した後に実行されます）。
- **effect の規律**：設定登録のバックオフリトライタイマーを `ctx.effect()` でラップし `clearTimeout` disposer を返します —— リトライウィンドウ中にプラグインがアンロードまたはホットリロードされた場合、タイマーは fiber とともに破棄され、解放済みの ctx に対して登録を発火しません（非常に古い環境で `ctx.effect` API がない場合は、裸のタイマー + ctx 破棄後のフォールバックキャッチに退化）。
- **HMR 安全**：`core.js` のインポートに `?v=1` のキャッシュバスターを付与（HMR リロードは URL をキーとして制御）。設定登録がホットリロードの競合（duplicate）に遭遇した場合は自動でバックオフリトライ（最大 8 回、間隔 `400ms × attempts`）。
- **プロジェクション登録の二重トラック**：優先的に `ctx.root.get('sessionProjections')`（ホストルートに最も近いもの）を使用し、取得できない場合は注入インスタンスにフォールバック。注入インスタンスのみに登録した場合、クライアントがプロジェクションを読み取れず、プッシュ本文はフォールバックパスを通る可能性があります —— ベストエフォートであり、セッション内のシステムメッセージには影響しません。

---

## プロジェクト構成

```text
dsh-session-notify/
├── lib/
│   ├── index.js      # 宿主平面（Node）：session/event 订阅 → 系统消息落盘；
│   │                 #   settings 命名空间注册（schemastery schema，退避重试）；
│   │                 #   sessionProjections 投影单元（后台会话推送正文）
│   ├── core.js       # 纯逻辑层（零依赖，可独立测试）：轮次计时与用量聚合、
│   │                 #   5 语言文案表、时长/用量/缓存/速度格式化、
│   │                 #   模板渲染（{title}{duration}{usage}{error}{cache}{tps}）、
│   │                 #   提问正文构建（buildQuestionBody，{question} + 媒体剥除）
│   └── client.js     # 浏览器平面：完成推送（系统通知 + toast）、
│                     #   设置卡片（Chip 模板编辑器 + 预设系统 + 实时预览）
├── scripts/
│   ├── build.sh                # 零构建：仅 node --check 语法校验
│   ├── verify-notice.mjs       # 校验会话日志落盘证据（zstd 多帧逐帧解压）
│   ├── probe-client.mjs        # 探针：客户端装配
│   ├── probe-client-e2e.mjs    # 探针：客户端端到端
│   ├── probe-card-render.mjs   # 探针：设置卡片渲染
│   ├── probe-settings-card.mjs # 探针：设置面板卡片
│   ├── probe-settings-check.mjs# 探针：设置面板检查
│   └── probe-diag-settings.mjs # 探针：settings 诊断
├── cordis.patch.yml  # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
├── package.json      # dsh.bundle（patch）+ dsh.client（web 注入）双 manifest；
│                     #   exports: "." / "./client" / "./core"
├── LICENSE           # MIT
└── README.md         # 本文档
```

---

## 開発とデバッグ

構文チェック（ゼロビルド、`prepublishOnly` と同じ検査）：

```bash
npm run build
```

公開（公開前に `prepublishOnly` の構文チェックを自動実行）：

```bash
npm publish --registry=https://registry.npmjs.org --access public
```

オフライン検証：セッションログからすべての plugin-source イベントと `turn/end` の末尾シーケンスを抽出（パスを渡さない場合は `~/.dsh/sessions` 配下の最新セッションを自動選択）：

```bash
node scripts/verify-notice.mjs <session.jsonl.zstd>
```

### デバッグ入口

| 入口 | 内容 |
| --- | --- |
| `~/.dsh/session-complete-notify.log` | ホスト診断ログ：設定登録、リトライと失敗、プロジェクション登録、追加失敗のスタック |
| ブラウザ console `[dsh-session-notify-client]` | クライアントログ：権限ステータス、通知表示、設定保存 |
| `window.__dsch_notify_debug.readNotice(id)` | 指定セッションの最新通知本文を手動で読み取り |
| `window.__dsch_notify_debug.snapshotDebug(id)` | セッション末尾のノードタイプ + notice 数 + 最新の本文（先頭 200 文字） |

---

## よくある質問

<details>
<summary><b>npm install の後に自動マウントされないのはなぜ？</b></summary>

これは DSH 公式の設計です：`npm install` はパッケージを依存ツリーに追加するだけで、プラグインを登録しません。自動マウントの唯一の手段は `dsh plugin add` —— パッケージ内の `dsh.bundle` manifest（本プラグインは 0.1.3 以降で宣言）を読み取り、`cordis.patch.yml` を自動適用します。[インストール](#インストール)を参照してください。

</details>

<details>
<summary><b>AI からの質問でもポップアップ通知されますか？</b></summary>

通知されます。AI が `ask_user_question` を呼び出して回答を待つと、ホストは即座に「質問タイトル + 本文」を専用プロジェクション（key = `session-complete-notify-question`）へ書き込み、クライアントがポーリングで取得すると即座に通知します——別のページを見ていても見逃しません。質問の文言は完了通知と同じく完全にカスタマイズ可能です。設定パネルの「タイトル / コンテンツ」折りたたみセクションにそれぞれ「質問」行があり、本文は `{question}` プレースホルダー（AI の実際の質問を注入）と `{image}` / `{icon}` メディアスイッチに対応します。回答後（`tool/result`）は通知が無効になり、残りません。

</details>

<details>
<summary><b>「中断」（interrupted）で通知されないのはなぜ？</b></summary>

`interrupted` はクラッシュ復旧後に永続化バックエンドが書き足す孤児ターンのクローズマーカーで、ユーザー視点の「完了」には含まれません（含めると復元セッションで誤通知が並んでしまいます）。必要な場合はホスト設定の `reasons` に追加してください。

</details>

<details>
<summary><b>バックグラウンドセッション（ウィンドウを開いていない）でもプッシュされますか？</b></summary>

はい。クライアントはセッションリストのスナップショットから全セッションの `running` エッジを監視します。本文は優先的にホストプロジェクションを取得します —— ホストがすべてのセッション（バックグラウンド含む）でプロジェクションを維持するため、プッシュ本文はセッション間で一貫します。プロジェクションが利用不可の場合は、イベントウィンドウまたはワークスペース情報にフォールバックします。

</details>

<details>
<summary><b>設定を保存したのにページのリロードを促されるのはなぜ？</b></summary>

ホストは名前空間の登録時に一度だけ設定を読み取り、クライアント bundle はページ読み込み時に組み立てられます。保存後、「クリックでリロード」をクリックして両側で読み直すと、新しい言語・テンプレートが有効になります。

</details>

<details>
<summary><b>キャッシュヒット率・速度のデータはどこから来ますか？なぜ空のことがあるのですか？</b></summary>

公式の `sessionProjections`（`tokenUsage`、`sessionStats`）から取得し、dsh-web-ui のステータスバーと同じ口径です。ホストがプロジェクションスナップショットの読み取りに失敗した場合やデータが未準備の場合は、ローカルの使用量集計による推定にフォールバックし、それでもデータがない場合はその項目を空にします（ラベルを挿入しても表示されません）。また、この 2 項目はカスタムテンプレートで `{cache}`、`{tps}` を挿入した場合にのみ現れ、デフォルトメッセージには含まれません。

</details>

<details>
<summary><b>通知本文のエラー情報が長すぎる、改行がある場合は？</b></summary>

サマリー行（折りたたみ行）とエラー詳細はどちらも単行化され切り詰められます：サマリー 120 文字、テンプレート `{error}` 80 文字、デフォルトメッセージのエラー詳細 40 文字。超長の場合は省略記号で終わります。

</details>

<details>
<summary><b>システム通知のアイコンやサウンドをカスタマイズできますか？</b></summary>

アイコンは**カスタマイズ可能**です：設定パネルの「通知画像」セクションで**通知ヒーロー画像**と**アイコン**をアップロードできます（グローバル）。各理由のテンプレートに `{icon}` タグを挿入すれば、その理由専用のアイコンも指定できます（グローバルより優先）。**サウンド**はカスタマイズできません（システム/ブラウザデフォルトを使用）。トーストは固定のダークカードです。その他のご要望は Issue または PR をお寄せください。

</details>

<details>
<summary><b>Edge でシステム通知が届かないのはなぜ？QQ ブラウザではページ内バナー（内蔵プッシュポップアップ）しか出ないのはなぜ？</b></summary>

どちらもブラウザの挙動であり、プラグインから強制はできません：

- **Edge / Chrome**：「馴染みのない」サイトへの通知を**自動的にブロック**します（アドレスバーに「通知がブロックされました」と表示）。アドレスバー左の権限アイコン → サイト設定 → 通知 → 許可で復元され、以降は Windows 通知センターに正常に表示されます。ブラウザの通知設定で「自動ブロック」をオフにすることもできます。
- **QQ ブラウザなどの国産 Chromium シェル**：`Notification` を**ブラウザ内蔵のページ内プッシュポップアップ**として固定レンダリングします（ページ上部 / 隅のバナー、Windows 通知センターを経由せず）、システム通知のオプションもありません。「非フォーカス時」「フォーカス時」の 2 つのドロップダウンは、このようなブラウザでは同じ挙動になります：
  - `システム + ページ内` → ブラウザ内蔵ポップアップ + プラグイントースト。ページ内に 2 つの通知。
  - `システムのみ` → 無効（QQ ブラウザでは常にページ内ポップアップとしてレンダリング）。
  - `ページ内のみ` → ブラウザ内蔵ポップアップは表示されず、ページ内にはプラグインの小さなトーストのみ（推奨）。
  - `通知しない` → そのタイミングは完全にサイレント。
  設定パネルの各理由の「送信」テストボタンも同じ規則でレンダリングされます。
- **Firefox**：ウィンドウにフォーカスがある間は通知がページ内バナーとして表示され、フォーカスが外れる / 最小化して初めてシステム通知センターに入ります。権限はアドレスバーで手動許可が必要です。
- もう一点：`http://IP` アクセス（非セキュアコンテキスト）時は `Notification` が存在せず、どのブラウザでもシステム通知を表示できません。

設定パネルの「通知権限」エリアは現在の状態と対応する操作ガイドをリアルタイム表示します。

</details>

---

## 更新履歴

| バージョン | 日付 | 変更内容 |
| --- | --- | --- |
| **0.1.21** | 2026-09-14 | **プッシュチャンネルを非フォーカス / フォーカスで分離**（[PR #3](https://github.com/TelosmaYLX/dsh-session-notify/pull/3) を [@YiHui-Liu](https://github.com/YiHui-Liu) 氏が提供）：独立した「非フォーカス時」「フォーカス時」の 2 つのドロップダウンを追加し、それぞれ `通知しない` / `システム + ページ内` / `システムのみ` / `ページ内のみ` を選択可能；旧版の単一「プッシュ方式」設定を削除し、新項目が未設定なら両経路とも旧 `pushMode` の値を引き継ぐ（既存設定の挙動は不変）；あるタイミングを「通知しない」にするとそのタイミングは完全にサイレント（質問と承認は重複排除を行わないため、ページがもう一方のタイミングに切り替われば同じイベントが改めて通知される。完了はエッジイベントのため、サイレントなら再送されない）；「送信」テスト通知は「フォーカス時」の経路を優先し、それが「通知しない」なら「非フォーカス時」の経路に切り替え |
| **0.1.20** | 2026-09-09 | **承認の即時通知 + 履歴セッション読み込みの修正**：権限承認通知を追加（[PR #2](https://github.com/TelosmaYLX/dsh-session-notify/pull/2) を [@YiHui-Liu](https://github.com/YiHui-Liu) 氏が提供——`approval/asked` プロジェクション + クライアント側 3 系統フォールバック）；0.1.19 のリグレッションを修正：プロジェクション登録を両世代の契約併記（`schema`/`view` と `stateSchema`/`wire`）に変更し、旧ホストで履歴セッションを開いても `undefined.parse` で失敗しなくなる；クライアントの `uiSession` を `inject` から外し `ctx.get` の任意参照に変更、サービス欠如時に通知・設定パネル全体が停止する問題を回避 |
| **0.1.19** | 2026-09-07 | **質問ポップアップの修正（ホストのプロジェクション断線）**：プロジェクション単位の登録を `stateSchema` + `wire: { viewSchema, view }` 契約へ移行——旧形状（トップレベルの `schema`/`view`）は新ホスト（dsh-session-projection）では host-only 単位となるため値がクライアントに届かず、完了・質問の両プロジェクションが無効化されていた；`tool/call` の `callId` が空文字列の場合（一部の OpenAI 互換プロキシルート）は質問 id を `turn:step` にフォールバックし、`tool/result` も turn/step 照合で解除；あわせて `stateSchema` 欠落時のプロジェクション checkpoint 復元経路の潜在的クラッシュを修正 |
| **0.1.18** | 2026-09-01 | **質問ポップアップ不発の修正**：一部の dsh バージョン（0.1.2）でホストのプロジェクションがクライアントに届かず、質問してもポップアップしない問題を修正。クライアントの質問プッシュに harness ネイティブの「質問待ち」マークによるフォールバックを追加し、プロジェクション欠落時も通知。完了プッシュ・設定パネルは変更なし |
| **0.1.17** | 2026-08-30 | **質問の即時通知（カスタマイズ版）**：AI の質問で即ポップアップ。質問文言は `{question}` プレースホルダーとメディアスイッチに対応。4 プリセットに 5 言語の質問文言を追加。旧ホストではフォールバック |
| **0.1.16** | 2026-08-30 | **操作修正**：Backspace 連打でタグを誤削除しなくなりました（カーソルとタグの間に文字がないときだけタグを削除） |
| **0.1.15** | 2026-08-30 | **操作改善**：「コンテンツ」をデフォルトで展開。タグ削除後、カーソルが実際の内容に直行し連続削除が可能に |
| **0.1.14** | 2026-08-30 | **コードレビュー修正**：プリセット削除確認、保存済み設定への復元、メディア「×」でプレビューも削除、プリセット照合に画像を含める、同名警告、変更時のみリセット可能、デバッグログ自動切り詰め |
| **0.1.13** | 2026-08-29 | ワンクリックスタイルプリセット 4 種追加（顔文字/アイルー/ネコ娘/DeepSeekちゃん）、5 言語対応 |
| **0.1.12** | 2026-08-29 | リリースパッケージの整理 |
| **0.1.11** | 2026-08-29 | **カスタム通知メディア**：`{image}`/`{icon}` トークン挿入と画像/アイコンのアップロード（自動クロップ）。タイトルに情報プレースホルダー。「本文テンプレート×5」を折りたたみに。レイアウト・操作を全面改善 |
| **0.1.10** | 2026-08-29 | プッシュタイトルをネイティブ入力に。多言語 README 追加（English/繁體/日本語/한국어） |
| **0.1.9** | 2026-08-29 | 理由別プッシュタイトル。投影をオブジェクト化。リセットで言語を維持。理由別「送信」テストボタン |
| **0.1.8** | 2026-08-29 | デフォルトタイトル「タスク完了」。理由別デフォルト文言。リセットボタン追加 |
| **0.1.7** | 2026-08-29 | 設定カードのクラッシュ修正（通知権限行のスコープ問題） |
| **0.1.6** | 2026-08-29 | 通知権限ステータス領域を追加。権限リクエストをユーザージェスチャー内に変更 |
| **0.1.5** | 2026-08-29 | 通知チャネル設定（二重/システムのみ/ページ内のみ）——QQ ブラウザの二重通知を解消 |
| **0.1.4** | 2026-08-28 | 完全なアンインストール対応（dispose ライフサイクル整理） |
| **0.1.3** | 2026-08-28 | dsh.bundle manifest を宣言。settings 再試行タイマーを ctx.effect() に |
| 0.1.2 | 2026-08-27 | `@telosmaylx` スコープに改名 |
| 0.1.1 | 2026-08-27 | GitHub / npm インストール方法を文書化 |
| 0.1.0 | 2026-08-26 | 初版：セッション内システムメッセージ + ブラウザ通知 + 公式設定パネル |

---

## 謝辞

[@YiHui-Liu](https://github.com/YiHui-Liu) 氏の 2 つの貢献に感謝します：[PR #2](https://github.com/TelosmaYLX/dsh-session-notify/pull/2)——権限承認の即時通知（`approval/asked` プロジェクション + クライアント側 3 系統フォールバック）；[PR #3](https://github.com/TelosmaYLX/dsh-session-notify/pull/3)——プッシュチャンネルを非フォーカス / フォーカスで分離し、両経路に「通知しない」を用意。

---

## コントリビューション

Issue と PR をお待ちしています：

1. リポジトリを Fork し、新しいブランチを作成（`feat/xxx`）
2. 変更後に `npm run build` を実行して構文チェック
3. PR を提出し、動機と検証方法を説明

提出前には [Cordis 開発チュートリアル](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) の規律を守ってください：

- Cordis 以外のリソース（タイマー、購読、watcher）は `ctx.effect()` でラップし、disposer を返す必要があります。
- 設定項目に明示的な `id` を付けて編集のズレを防止。
- プラグインは `dsh.bundle` manifest を宣言して初めて `dsh plugin add` で認識・インストールされます。

---

## 関連リンク

- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) —— DSH プラグインの精選リスト（投稿規約：`dsh.bundle` がインストールの唯一の証明）
- [Cordis 開発チュートリアル](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) —— プラグイン開発の全プロセス（01-07 章）
- [npm パッケージホームページ](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
- [GitHub リポジトリ](https://github.com/TelosmaYLX/dsh-session-notify)

---

## ライセンス

[MIT](./LICENSE) © dsh-session-notify contributors
