Claude の最新の回答を latest.md に書き出し、プロンプト上のボタンでエディタで開く

<img width="634" height="263" alt="Image" src="https://github.com/user-attachments/assets/695fd5d3-02a7-428b-85c9-b6ddf4392f84" />
Claude Code の最新の回答を ~/.claude/latest.md に書き出し、プロンプトの上に出る open ボタンからエディタで開けるようにするプラグインです。
長い回答をターミナルで読むのがつらいとき、使い慣れたエディタでじっくり読めます。
%USERPROFILE%\.claude\latest.md に上書き保存しますlatest.md があるとき、プロンプトの上に open と copy の 2 つのボタンが出ます.md に関連付けられたアプリで latest.md を開きますrundll32 で関連付けられたアプリを起動するため).md ファイルが好きなエディタに関連付けられていることこのリポジトリを「プラグインの入手元」として登録します。これだけではまだプラグインは動きません。最初の 1 回だけ必要です。入手元の指定方法は 3 通りあります。
GitHub のリポジトリ(owner/repo):
claude plugin marketplace add stakiran/open-latest
git の URL:
claude plugin marketplace add https://github.com/stakiran/open-latest.git
ローカルのディレクトリ(clone 済みのフォルダなど):
claude plugin marketplace add D:\path\to\open-latest
3 通りあります。
~/.claude/settings.json:
claude plugin install open-latest@open-latest --scope user
そのディレクトリの .claude/settings.json:
claude plugin install open-latest@open-latest --scope project
そのディレクトリの .claude/settings.local.json:
claude plugin install open-latest@open-latest --scope local
アンインストールするときは /plugin のメニューから open-latest を削除してください。
インストールせずに、そのセッションだけ読み込んで試せます。Claude Code を閉じれば元に戻ります。
git clone https://github.com/stakiran/open-latest.git
claude --plugin-dir ./open-latest
起動したら何か質問して回答を待ち、プロンプトの上に open と copy のボタンが出ることを確かめてください。
hooks/register.js 72 lines1// latest.md が存在するか(ボタンを出すかどうか)
2let hasLatest = false
3
4async function latestPath($) {
5 const home = await $.env.get('USERPROFILE')
6 return home + '\\.claude\\latest.md'
7}
8
9export function register(on) {
10 on('session.start', async ($, e, next) => {
11 hasLatest = await $.fs.exists(await latestPath($))
12 return next(e)
13 })
14
15 // ターン終了時に最終回答を latest.md に上書き保存
16 on('turn.complete', async ($, e, next) => {
17 // サブエージェントのターン、中断されたターン、空の回答は無視
18 if (e.agentId || e.isAborted || !e.answer) return next(e)
19 await $.fs.write(await latestPath($), e.answer)
20 // コピー用。$.fs には読み込みがないので、セッションをまたいで残る $.store にも持っておく
21 await $.store.set('latest', e.answer).catch(() => $.store.delete('latest'))
22 hasLatest = true
23 $.ui.invalidate('ui.render')
24 return next(e)
25 })
26
27 // プロンプト上の帯にボタンを描く
28 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
29 // 他の mod が帯に描いたものは残す
30 const others = await next(e)
31 if (!hasLatest) return others
32
33 const { Box, Button } = $.ui.resolve(e)
34 const openButton = Button({
35 key: 'open-latest',
36 label: 'open',
37 plain: true,
38 onPress: async () => {
39 try {
40 // 関連付けられたアプリ(ShellExecute)で開く。rundll32 は起動後すぐ終わるので待たされない
41 const file = await latestPath($)
42 const r = await $.process.run(['rundll32', 'url.dll,FileProtocolHandler', file])
43 if (r.exitCode !== 0) $.ui.toast('エディタを開けませんでした: ' + (r.stderr || r.exitCode))
44 } catch (err) {
45 $.ui.toast('エディタを開けませんでした: ' + err)
46 }
47 },
48 })
49 const copyButton = Button({
50 key: 'copy-latest',
51 label: 'copy',
52 plain: true,
53 onPress: async press => {
54 const text = await $.store.get('latest')
55 if (typeof text !== 'string') return $.ui.toast('コピーできる回答がありません')
56 const r = await $.ui.copy({ text, surface: press.surface })
57 $.ui.toast(r.isCopied ? '回答をコピーしました' : 'コピーできませんでした: ' + r.reason)
58 },
59 })
60 const buttons = Box({
61 flexDirection: 'row',
62 columnGap: 2,
63 children: [openButton, copyButton],
64 })
65
66 return Box({
67 flexDirection: 'column',
68 children: others ? [buttons, others] : [buttons],
69 })
70 })
71}
72