docs(tickets): PermissionのチケットとTUIのmd表示
This commit is contained in:
parent
ba2c8ae687
commit
fc5cfefb62
1
TODO.md
1
TODO.md
|
|
@ -19,6 +19,7 @@
|
|||
- 巻き戻されたターンの入力テキストを編集領域に復元 → [tickets/tui-empty-turn-restore.md](tickets/tui-empty-turn-restore.md)
|
||||
- セッションコンテキスト長 / ウィンドウ占有率の常時表示 → [tickets/tui-context-usage-indicator.md](tickets/tui-context-usage-indicator.md)
|
||||
- Compaction 進行中のライブ表示 → [tickets/tui-compact-progress.md](tickets/tui-compact-progress.md)
|
||||
- Assistant 応答の Markdown スタイル表示 → [tickets/tui-assistant-markdown.md](tickets/tui-assistant-markdown.md)
|
||||
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
|
||||
- Prune: 保護境界を turn 数から末尾 token budget に置き換え → [tickets/prune-token-budget.md](tickets/prune-token-budget.md)
|
||||
- メモリ機構
|
||||
|
|
|
|||
285
docs/manifest.toml
Normal file
285
docs/manifest.toml
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
# ============================================================================
|
||||
# Pod Manifest リファレンス
|
||||
# ============================================================================
|
||||
# Pod の宣言的設定 (`PodManifest` / `PodManifestConfig`)。
|
||||
#
|
||||
# カスケード層は下から順に
|
||||
# 1. builtin defaults (`manifest::defaults`)
|
||||
# 2. user manifest (`<config_dir>/manifest.toml`)
|
||||
# 3. project manifest (cwd から上方向に探す `.insomnia/manifest.toml`)
|
||||
# 4. programmatic overlay (呼び出し側が差し込む)
|
||||
# 上の層が同名フィールドを上書き、scope rule と skills.directories は
|
||||
# 累積マージ、tool_output.per_tool は key 単位でマージ。
|
||||
#
|
||||
# パス解決: 相対パスは「その層の manifest ファイルが置かれているディレクトリ」
|
||||
# を base に絶対パスへ解決される (overlay 層は cwd)。マージは絶対化済みの
|
||||
# 値同士で行われる。
|
||||
#
|
||||
# 凡例:
|
||||
# - 必須 … 値が無いと resolve エラー
|
||||
# - 任意 … 省略可
|
||||
# - デフォルト … 省略時に採用される値 (None なら "なし")
|
||||
# - 値 … enum 等で取り得る値
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ===== [pod] ================================================================
|
||||
# Pod メタデータ。
|
||||
[pod]
|
||||
|
||||
# 必須。Pod の表示名 (ResolveError::MissingField("pod.name") の対象)。
|
||||
name = "example-agent"
|
||||
|
||||
# 任意。デフォルト: なし。
|
||||
# PromptCatalog の 4 つ目の overlay 層として読み込む TOML pack のパス。
|
||||
# 相対パスは manifest base 起点で解決。`worker.instruction` (`$prefix/...`)
|
||||
# とは別系統の単なるファイルパス。
|
||||
# prompt_pack = "./prompts.local.toml"
|
||||
|
||||
|
||||
# ===== [model] ==============================================================
|
||||
# LLM モデル設定。次の 3 形態を受ける:
|
||||
# (a) `ref` 単独 — カタログから全部解決
|
||||
# (b) `ref` + 一部 override — auth など個別差し替え
|
||||
# (c) `scheme` + `model_id` 直書き — カタログを使わない inline 指定
|
||||
# (b) / (c) では `ref` 未指定なら `scheme` / `model_id` / `auth` が必須。
|
||||
# (実際の必須判定は `crates/provider` の resolve 側で行う)
|
||||
[model]
|
||||
|
||||
# 任意。形式: "<provider_id>/<model_id_in_ref>"。
|
||||
# 最初の `/` だけで split されるので、`openrouter/anthropic/claude-sonnet-4`
|
||||
# のように内部 model_id に `/` が含まれる場合もそのまま書ける。
|
||||
ref = "anthropic/claude-sonnet-4-6"
|
||||
|
||||
# 任意 (ただし ref 未指定時は実質必須)。デフォルト: なし。
|
||||
# 値: "anthropic" | "openai_chat" | "openai_responses" | "gemini"
|
||||
# scheme = "anthropic"
|
||||
|
||||
# 任意 (ref 未指定時は実質必須)。デフォルト: なし。
|
||||
# プロバイダが受け付けるモデル ID 文字列。
|
||||
# model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
# 任意。デフォルト: scheme ごとの組み込み既定 URL。
|
||||
# base_url = "https://api.anthropic.com"
|
||||
|
||||
# 任意 (ref 未指定時は実質必須)。デフォルト: なし。
|
||||
# kind の値: "none" | "api_key" | "codex_oauth"
|
||||
# - "none" … 認証不要 (ローカル Ollama 等)
|
||||
# - "api_key" … env / file のいずれかで key を渡す。両方指定なら env 優先。
|
||||
# env 未指定時は scheme ごとの既定環境変数:
|
||||
# Anthropic -> INSOMNIA_API_KEY_ANTHROPIC
|
||||
# OpenaiChat / OpenaiResponses -> INSOMNIA_API_KEY_OPENAI
|
||||
# Gemini -> INSOMNIA_API_KEY_GEMINI
|
||||
# file 指定時、相対パスは manifest base 起点で解決。
|
||||
# - "codex_oauth" … ChatGPT OAuth (`~/.codex/auth.json`)。追加フィールドなし。
|
||||
# auth = { kind = "none" }
|
||||
# auth = { kind = "api_key" } # env のみ既定使用
|
||||
# auth = { kind = "api_key", env = "MY_ANTHROPIC_KEY" }
|
||||
# auth = { kind = "api_key", file = "./sk-ant.local" }
|
||||
# auth = { kind = "codex_oauth" }
|
||||
|
||||
# 任意。デフォルト: モデルカタログ → provider.default_capability → scheme 既定
|
||||
# の順で解決される。明示 override したいときだけ書く。
|
||||
# [model.capability]
|
||||
# # 値: "none" | "sequential" | "parallel"
|
||||
# tool_calling = "parallel"
|
||||
# # 値: "none" | "json_object" | "json_schema"
|
||||
# structured_output = "json_schema"
|
||||
# # 任意。値: 省略 (= None) | "effort" | "budget_tokens" | "both"
|
||||
# reasoning = "both"
|
||||
# # 任意。デフォルト: false
|
||||
# vision = false
|
||||
# # 値: { kind = "explicit", max_breakpoints = <u8> } | { kind = "auto" }
|
||||
# prompt_caching = { kind = "explicit", max_breakpoints = 4 }
|
||||
|
||||
|
||||
# ===== [worker] =============================================================
|
||||
# ワーカーの生成パラメータ等。セクション自体省略可 (全フィールド任意)。
|
||||
[worker]
|
||||
|
||||
# 任意。デフォルト: "$insomnia/default" (`defaults::DEFAULT_INSTRUCTION`)。
|
||||
# システムプロンプト本体の `PromptLoader` 参照。
|
||||
# プレフィクス: "$insomnia/..." | "$user/..." | "$workspace/..."
|
||||
# instruction = "$insomnia/default"
|
||||
|
||||
# 任意。デフォルト: なし (プロバイダ任せ)。
|
||||
# 1 レスポンスあたりの出力 token 上限。
|
||||
# max_tokens = 4096
|
||||
|
||||
# 任意。デフォルト: なし (無制限)。
|
||||
# 1 セッションの最大ターン数。NonZeroU32 — 0 は parse エラー。
|
||||
# max_turns = 50
|
||||
|
||||
# 任意。デフォルト: なし (プロバイダ既定)。
|
||||
# temperature = 0.3
|
||||
|
||||
# 任意。デフォルト: なし (プロバイダ既定)。
|
||||
# top_p = 0.9
|
||||
|
||||
# 任意。デフォルト: なし (プロバイダ既定)。
|
||||
# top_k = 40
|
||||
|
||||
# 任意。デフォルト: 空配列。
|
||||
# stop_sequences = ["\n\n", "</stop>"]
|
||||
|
||||
# 任意。デフォルト: なし。
|
||||
# 値:
|
||||
# - 文字列 effort: "minimal" | "low" | "medium" | "high" | "xhigh"
|
||||
# あるいは provider-native な任意ラベル文字列
|
||||
# - 整数: Anthropic 系 thinking.budget_tokens (-1 で dynamic)
|
||||
# reasoning = "medium"
|
||||
# reasoning = -1
|
||||
|
||||
# 任意。tool 実行 content の byte 長キャップ。
|
||||
# セクション省略時は default_max_bytes = 16 * 1024、per_tool 空。
|
||||
# [worker.tool_output]
|
||||
# # 任意。デフォルト: 16384 (`defaults::TOOL_OUTPUT_MAX_BYTES` = 16 KiB)。
|
||||
# default_max_bytes = 16384
|
||||
#
|
||||
# # 任意。デフォルト: 空マップ。tool 名キーで個別キャップ上書き。
|
||||
# # キーは tool の登録名 ("Read", "Grep", "Glob", ...)。
|
||||
# [worker.tool_output.per_tool]
|
||||
# Read = 32768
|
||||
# Grep = 4096
|
||||
|
||||
|
||||
# ===== [scope] ==============================================================
|
||||
# Pod がアクセスできるディレクトリ/ファイル範囲。
|
||||
# - allow: 最低 1 件必要 (空だと ResolveError / ScopeError::EmptyAllow)。
|
||||
# 複数 allow がマッチした場合は最大の permission が採用される。
|
||||
# - deny : 任意。マッチした deny の最小 permission *未満* に effective を
|
||||
# 押し下げる (deny.read で完全遮断、deny.write で Read 止まり)。
|
||||
# `target` は最終的に絶対パスでなければならない。manifest 内では相対 OK
|
||||
# (manifest base 起点で resolve)。
|
||||
|
||||
# 必須: 最低 1 件の allow ルール。
|
||||
[[scope.allow]]
|
||||
# 必須。manifest 内では相対 OK (base 起点で絶対化)。
|
||||
target = "./"
|
||||
# 必須。値: "read" | "write"
|
||||
permission = "write"
|
||||
# 任意。デフォルト: true (再帰的にマッチ)。
|
||||
# false の場合、ルール自身および直下の child のみマッチ。
|
||||
# recursive = true
|
||||
|
||||
# allow は何件でも書ける。
|
||||
# [[scope.allow]]
|
||||
# target = "/abs/docs"
|
||||
# permission = "read"
|
||||
# recursive = false
|
||||
|
||||
# 任意。アクセスを *ルール内 permission 未満* に押し下げる。
|
||||
# [[scope.deny]]
|
||||
# target = "./secrets"
|
||||
# permission = "write" # write を禁止 → 該当パスは Read までに降格
|
||||
# recursive = true
|
||||
#
|
||||
# [[scope.deny]]
|
||||
# target = "./secrets/key"
|
||||
# permission = "read" # read 自体を禁止 → アクセス完全遮断
|
||||
# recursive = true
|
||||
|
||||
|
||||
# ===== [compaction] =========================================================
|
||||
# コンテキスト圧縮 (Prune / Compact)。セクション省略で両方無効。
|
||||
# セクションを書いた時点で Prune は有効化、Compact は閾値が None なら無効。
|
||||
# [compaction]
|
||||
#
|
||||
# # 任意。デフォルト: 3 (`defaults::PRUNE_PROTECTED_TURNS`)。
|
||||
# # pruning から保護する末尾ターン数。
|
||||
# prune_protected_turns = 3
|
||||
#
|
||||
# # 任意。デフォルト: 4096 (`defaults::PRUNE_MIN_SAVINGS`)。
|
||||
# # prune が発火するための最低節約 token 推定値。
|
||||
# prune_min_savings = 4096
|
||||
#
|
||||
# # 任意。デフォルト: なし (proactive compact 無効)。
|
||||
# # ターン間チェック (Controller post-run)。占有 token > これ で次ターン前に compact。
|
||||
# compact_threshold = 80000
|
||||
#
|
||||
# # 任意。デフォルト: なし (safety-net compact 無効)。
|
||||
# # ターン中チェック (PodInterceptor::pre_llm_request)。期待される関係:
|
||||
# # compact_threshold < compact_request_threshold (proactive を先に発火)。
|
||||
# # 逆順設定は許容するが warn ログを出す。
|
||||
# compact_request_threshold = 90000
|
||||
#
|
||||
# # 任意。デフォルト: 8000 (`defaults::COMPACT_RETAINED_TOKENS`)。
|
||||
# # compaction 後の history 末尾に verbatim で残す token budget。
|
||||
# compact_retained_tokens = 8000
|
||||
#
|
||||
# # 任意。デフォルト: 8000 (`defaults::COMPACT_AUTO_READ_BUDGET`)。
|
||||
# # compact worker が `mark_read_required` で取り込める累計 token。
|
||||
# compact_auto_read_budget = 8000
|
||||
#
|
||||
# # 任意。デフォルト: 50000 (`defaults::COMPACT_WORKER_MAX_INPUT_TOKENS`)。
|
||||
# # compact worker 自身の累積入力 token cap。超過で abort (circuit breaker)。
|
||||
# compact_worker_max_input_tokens = 50000
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # compact 専用モデルを使う場合のみ書く ([model] と同じ形式)。
|
||||
# # [compaction.model]
|
||||
# # ref = "anthropic/claude-haiku-4-5"
|
||||
|
||||
|
||||
# ===== [memory] =============================================================
|
||||
# Memory subsystem の opt-in。
|
||||
# - セクションが *ある* … memory tools (MemoryRead/Write/Edit) を登録、
|
||||
# `<workspace>/memory/` と `<workspace>/knowledge/`
|
||||
# の通常 write を Pod 自体に対して deny する。
|
||||
# - セクションが *無い* … 何も起きない (legacy 動作)。
|
||||
# `[memory]` だけ書いて中身を省略するのも有効 (全フィールド既定値で有効化)。
|
||||
# [memory]
|
||||
#
|
||||
# # 任意。デフォルト: Pod の pwd (構築時)。
|
||||
# # 必ず絶対パス (相対なら manifest base 起点で resolve)。
|
||||
# workspace_root = "/abs/path/to/workspace"
|
||||
#
|
||||
# # 任意。デフォルト: tool 側既定 = 20。
|
||||
# # MemoryQuery / KnowledgeQuery が 1 回に返す最大件数。
|
||||
# query_result_limit = 20
|
||||
#
|
||||
# # 任意。デフォルト: tool 側既定 = 3。
|
||||
# # 各マッチ前後に表示するコンテキスト行数。`query` 省略時は無視。
|
||||
# query_excerpt_lines = 3
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # Phase 1 (extract) ワーカーのモデル ([model] と同じ形式)。
|
||||
# # Haiku / 4o-mini / Flash クラスの軽量 reasoning モデル推奨。
|
||||
# # [memory.extract_model]
|
||||
# # ref = "anthropic/claude-haiku-4-5"
|
||||
#
|
||||
# # 任意。デフォルト: なし (Phase 1 自動発火を完全停止)。
|
||||
# # 前回 extract pointer 以降の累積入力 token がこの値を超えると Phase 1 起動。
|
||||
# # ※ memory tools と resident injection は extract_threshold が None でも動く。
|
||||
# extract_threshold = 30000
|
||||
#
|
||||
# # 任意。デフォルト: 30000 (`defaults::MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS`)。
|
||||
# # extract worker 自身の累積入力 token cap (超過で abort)。
|
||||
# extract_worker_max_input_tokens = 30000
|
||||
#
|
||||
# # 任意。デフォルト: メインモデルを `clone_boxed()` で複製。
|
||||
# # Phase 2 (consolidation) ワーカーのモデル。reasoning クラス推奨。
|
||||
# # [memory.consolidation_model]
|
||||
# # ref = "anthropic/claude-sonnet-4-6"
|
||||
#
|
||||
# # 任意。デフォルト: なし。
|
||||
# # `_staging/` のエントリ数がこの値以上で Phase 2 発火 (files / bytes は OR)。
|
||||
# consolidation_threshold_files = 50
|
||||
#
|
||||
# # 任意。デフォルト: なし。
|
||||
# # `_staging/` の総バイト数がこの値以上で Phase 2 発火 (files / bytes は OR)。
|
||||
# # files / bytes の両方が None だと Phase 2 完全無効。
|
||||
# consolidation_threshold_bytes = 1048576
|
||||
|
||||
|
||||
# ===== [skills] =============================================================
|
||||
# 外部 Agent Skills (`SKILL.md`) を Workflow として読み込むディレクトリ群。
|
||||
# セクション省略 = 何もロードしない (implicit な `$config_dir/skills/` 検索や
|
||||
# builtin probe は存在しない)。
|
||||
# [skills]
|
||||
#
|
||||
# # 任意。デフォルト: 空配列。
|
||||
# # 各エントリは skills *root* (root の child 各々が `<name>/SKILL.md` を持つ
|
||||
# # skill バンドル)。root 自身は skill ではない。
|
||||
# # 相対パスは manifest base 起点で解決。マージ時は層を跨いで concat される。
|
||||
# directories = [".claude/skills", ".cursor/skills"]
|
||||
|
|
@ -10,44 +10,82 @@ OpenCode はパターンベースのルール(tool × pattern → allow/deny/a
|
|||
|
||||
## 方針
|
||||
|
||||
`PreToolCall` Hook として実装する。マニフェストにルールを宣言し、
|
||||
insomnia 層の Hook 実装がツール呼び出し時に評価する。
|
||||
Permission の評価点は `PreToolCall` Hook とする。マニフェストにルールを宣言し、
|
||||
insomnia 層が built-in の `PreToolCall` Hook として登録してツール呼び出し時に評価する。
|
||||
|
||||
`deny` はターン全体の Cancel/Abort ではない。対象 tool call を実行せず、
|
||||
permission denied を表す `is_error = true` の synthetic tool result を履歴に追加してターンを継続する。
|
||||
これにより provider が要求する `tool_use` / `tool_result` の対応を壊さず、LLM は拒否結果を見て別手段の検討やユーザーへの説明に進める。
|
||||
|
||||
`ask` は `deny` の代替ではなく、ユーザー承認待ちを明示する action として扱う。承認されれば元の tool call を実行し、拒否されれば `deny` と同じ synthetic tool result に落とす。
|
||||
|
||||
```toml
|
||||
[[permission]]
|
||||
[permissions]
|
||||
default_action = "allow" # allow | deny | ask
|
||||
|
||||
[[permissions.rule]]
|
||||
tool = "bash"
|
||||
pattern = "rm *"
|
||||
action = "deny"
|
||||
|
||||
[[permission]]
|
||||
[[permissions.rule]]
|
||||
tool = "file_write"
|
||||
pattern = "*.env"
|
||||
action = "deny"
|
||||
```
|
||||
|
||||
[[permission]]
|
||||
tool = "*"
|
||||
allowlist 型にしたい場合:
|
||||
|
||||
```toml
|
||||
[permissions]
|
||||
default_action = "deny"
|
||||
|
||||
[[permissions.rule]]
|
||||
tool = "read"
|
||||
pattern = "*"
|
||||
action = "allow"
|
||||
|
||||
[[permissions.rule]]
|
||||
tool = "grep"
|
||||
pattern = "*"
|
||||
action = "allow"
|
||||
```
|
||||
|
||||
評価順序(OpenCode に倣う):
|
||||
1. 最初にマッチした `deny` → 拒否
|
||||
2. すべてマッチする `allow` → 許可
|
||||
3. それ以外 → `ask`(ユーザー確認)
|
||||
確認待ちを基本にしたい場合:
|
||||
|
||||
```toml
|
||||
[permissions]
|
||||
default_action = "ask"
|
||||
|
||||
[[permissions.rule]]
|
||||
tool = "bash"
|
||||
pattern = "rm *"
|
||||
action = "deny"
|
||||
```
|
||||
|
||||
評価順序:
|
||||
1. `[permissions]` が無い場合、Permission 層は無効。従来通り実行する
|
||||
2. `[permissions]` がある場合、`default_action` は必須
|
||||
3. `[[permissions.rule]]` は宣言順に評価し、最初に `tool` と `pattern` が一致した rule の `action` を採用する
|
||||
4. 一致する rule が無ければ `permissions.default_action` を採用する
|
||||
|
||||
## 設計ポイント
|
||||
|
||||
- 設計原則3: 新しい trait は作らない。`PreToolCall` Hook として実装
|
||||
- 設計原則2: マニフェストに宣言した以上、insomnia 層が解決する
|
||||
- `ask` アクションは Pod Protocol の拡張が必要(Method に `PermissionReply` を追加)
|
||||
- Permission Hook は Pod が自動登録する built-in Hook とし、ユーザー追加 Hook より先に評価する
|
||||
- `deny` は `PreToolAction::Abort` / 既存 `Skip` では表現しない。tool call 単位の拒否結果を履歴へ返すため、Worker 側に synthetic tool result を返せる action が必要
|
||||
- `ask` アクションは Pod Protocol の拡張が必要(Event に `PermissionRequest`、Method に `PermissionReply` を追加)
|
||||
- `ask` を処理できない実行環境では暗黙に待機しない。設定時に validation error とするか、fail closed で `deny` 相当の synthetic tool result に落とす
|
||||
- `Scope` との関係: Scope は書き込みの物理的境界、Permission はツール実行のポリシー。補完関係
|
||||
- ルール評価はパターンマッチのみ。コンテキスト依存の判断はしない(シンプルに保つ)
|
||||
|
||||
## 段階的実装
|
||||
|
||||
1. **拡張ポイントの記録**(今): docs/pod.md の拡張ポイント表に追加
|
||||
2. **deny/allow の実装**(ツール実装時): PreToolCall Hook でパターン評価
|
||||
3. **ask の実装**(Protocol 拡張時): Method/Event に Permission 関連メッセージを追加
|
||||
2. **deny/allow の実装**(ツール実装時): `default_action` と rule 評価を manifest に追加し、built-in `PreToolCall` Hook でパターン評価
|
||||
3. **拒否 tool result の実装**: `deny` が turn Abort ではなく synthetic error tool result として履歴に入るよう Worker の pre-tool action を拡張
|
||||
4. **ask の実装**(Protocol 拡張時): Method/Event に Permission 関連メッセージを追加し、承認後に元 tool call を実行、拒否時は synthetic error tool result を返す
|
||||
|
||||
## 受け皿になる外部仕様
|
||||
|
||||
|
|
@ -55,7 +93,7 @@ action = "allow"
|
|||
|
||||
`tickets/agent-skills.md` で ingest した SKILL.md の frontmatter には agent-skills 仕様の experimental field である `allowed-tools` (例: `["Read", "Bash"]`) が含まれる場合がある。`crates/memory/src/skill.rs::parse_skill_md` 時点では `tracing::warn!` で受け流しているだけで、実効化していない。
|
||||
|
||||
本チケットの Permission 層が固まった時点で、Skill 由来 Workflow を実行中のみ当該 skill の `allowed-tools` リストに含まれるツールしか走れない形で反映する。スコープは「Workflow 実行中」相当 (Workflow の system message が context に乗っているターン) に限定する想定。skill 単位で local な permission 集合を持つので、グローバルな `[[permission]]` ルールとは独立に評価する。
|
||||
本チケットの Permission 層が固まった時点で、Skill 由来 Workflow を実行中のみ当該 skill の `allowed-tools` リストに含まれるツールしか走れない形で反映する。スコープは「Workflow 実行中」相当 (Workflow の system message が context に乗っているターン) に限定する想定。skill 単位で local な permission 集合を持つので、グローバルな `[[permissions.rule]]` ルールとは独立に評価する。
|
||||
|
||||
実装上の足がかり:
|
||||
- `WorkflowRecord` の出所は `WorkflowSource::Skill { dir }` で識別済み (`crates/memory/src/workflow.rs`)。`dir` は manifest `[skills] directories` に書かれた skill ルートそのもの
|
||||
|
|
|
|||
82
tickets/tui-assistant-markdown.md
Normal file
82
tickets/tui-assistant-markdown.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# TUI: Assistant 応答の Markdown スタイル表示
|
||||
|
||||
## 背景
|
||||
|
||||
LLM の出力は実質 Markdown だが、TUI は `Block::AssistantText { text }` を
|
||||
`push_padded_lines` で 1 行ずつ素のテキストとして
|
||||
`Style(MessageKind::Assistant)` に流しているだけで、`**強調**` /
|
||||
`` `code` `` / `# 見出し` / `- list` 等の記号がそのまま見える状態になっている
|
||||
(`crates/tui/src/ui.rs:592-595, 640-648`)。スタイルが付かないため、
|
||||
構造のあるアシスタント応答は読みにくい。
|
||||
|
||||
ratatui 0.30 の `Vec<Line<'static>>` で表現できる範囲のスタイル付けで
|
||||
十分目的を満たせる。既存の `wrap_line_into`(`crates/tui/src/ui.rs:473-`)が
|
||||
span 単位のラップを既に実装しているため、Markdown レンダラは
|
||||
スタイル付きの `Vec<Line>` を返すだけでよく、ラップ/スクロール/overview
|
||||
畳み込みの仕組みを変える必要はない。
|
||||
|
||||
## 方針
|
||||
|
||||
- `pulldown-cmark` を `tui` クレートの依存に追加し、Event ストリームを
|
||||
既存の `MessageKind` / `ratatui::style::Style` 体系へ畳み込む小さな
|
||||
自前レンダラを `crates/tui/src/markdown.rs` に置く。
|
||||
- レンダラの公開面は `render(text: &str, base: Style) -> Vec<Line<'static>>`
|
||||
程度の 1 関数。`Block::AssistantText` の `Mode::Detail` / `Mode::Normal`
|
||||
描画から呼ぶ。`Mode::Overview` は現行通り 1 行畳み込み(Markdown 記号
|
||||
含めて表示しても情報量はほぼ同じなので素のテキストでよい)。
|
||||
- ストリーミング中の不完全要素(未閉鎖の `**` や開きっぱなしのフェンス)
|
||||
は CommonMark の流儀(テキスト扱い/EOF で閉じる)に任せる。挙動が
|
||||
破綻する場合だけ末尾要素を素のテキストにフォールバックする小さな
|
||||
後処理を入れる余地を残す。
|
||||
- `tui-markdown` クレートは採用しない。syntect 依存でビルドが肥大する
|
||||
割にカスタマイズが効かず、本クレートの色味(`MessageKind`
|
||||
パレット)との整合を握りにくいため。
|
||||
|
||||
## 対応する Markdown 要素
|
||||
|
||||
最小限の "対応できる範囲" を以下に限定する。CommonMark + GFM の一部。
|
||||
|
||||
- 強調: `**bold**` / `*italic*` / `~~strike~~`(GFM)
|
||||
- インラインコード: `` `code` ``
|
||||
- フェンスコードブロック: ` ```lang ` / ` ``` `(言語タグは無視、
|
||||
ブロック全体を等幅・低彩度の背景/前景で塗る)
|
||||
- 見出し: H1〜H4(H5/H6 は H4 と同等)
|
||||
- 箇条書きリスト: `-` / `*` / `+`、ネスト可(深さ分インデント)
|
||||
- 順序リスト: `1.` / `1)`、ネスト可(番号は元の値で表示)
|
||||
- 引用: `> ...`(ネスト可)
|
||||
- 水平線: `---` / `***`
|
||||
- リンク: `[text](url)` の `text` をリンク色で着色(URL は表示しない)
|
||||
|
||||
## 範囲外
|
||||
|
||||
- 表(GFM table)
|
||||
- 画像 ``(テキストとしても表示しない)
|
||||
- HTML パススルー(タグはそのまま生テキストで出る)
|
||||
- 数式(`$...$` / `$$...$$`)
|
||||
- コードブロックの syntax highlighting
|
||||
- リンクのターミナルクリック(OSC 8)/URL の自動表示
|
||||
- `Thinking` 本文 / `SystemMessage` への適用
|
||||
(同じ `markdown::render` を後で差せばよい。本チケットは
|
||||
`Block::AssistantText` のみ)
|
||||
- ライブストリーム最中の "途中要素のフォールバック" の作り込み
|
||||
(CommonMark のデフォルト挙動で破綻が見えたら別チケット)
|
||||
|
||||
## 完了条件
|
||||
|
||||
- アシスタント応答に含まれる上記要素が、それぞれ視認可能な
|
||||
スタイルで描画される。
|
||||
- ストリーミング中、テキストが追記されるたびに描画が更新され、
|
||||
フェンスコードブロックの開きが先に着いて中身が後から流れる
|
||||
ようなケースでも、テキスト全体の見た目が大きく崩れない。
|
||||
- `Mode::Detail` / `Mode::Normal` で Markdown スタイルが、
|
||||
`Mode::Overview` では従来通りの 1 行畳み込みが出る。
|
||||
- 既存の `wrap_line_into` によるラップ・右パディング・スクロール
|
||||
が引き続き機能する(行幅計算が乱れない)。
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `crates/tui/Cargo.toml`: `pulldown-cmark` を追加(`cargo add` 経由)。
|
||||
- `crates/tui/src/markdown.rs`: 新設。`render(&str, Style) -> Vec<Line<'static>>`。
|
||||
- `crates/tui/src/ui.rs`: `Block::AssistantText` 分岐で Markdown
|
||||
レンダラを呼ぶ。`Mode::Overview` は現行のまま。
|
||||
- `crates/tui/src/main.rs` または `lib.rs`: 新モジュールの宣言。
|
||||
Loading…
Reference in New Issue
Block a user