SLOPSHOPPER

pike

Reports the account, input-wait notices and rate limits of this Claude Code session to the Pike terminal it runs in.

newprocess
★ 11v0.1.0MITupdated 2026-10-09kan/pike/src-tauri/claude-mod/pike
A shopper browsing a rack in a slop shop
README

Pike

License: MIT Tauri v2 Vue 3 Rust

「AI エージェント × ターミナル」に特化した、軽量な開発環境です。 ターミナル中心で作業する開発者が VS Code の代わりに使えることを目指しています。

Tauri v2(Rust + Vue / TypeScript)製。Windows を主対象としています。macOS(Apple Silicon)版も署名・公証して配布しており、自動更新も同じように働きます。macOS ではホストのログインシェルで開発でき、Windows 固有の機能(WSL、ジャンプリスト、管理者ターミナル、ウィンドウ背景の透過)だけが対象外です。

<img alt="エディタとファイルツリー" src="docs/manual/img/screenshot-editor.png">

<img alt="Git パネルとターミナルのエージェント" src="docs/manual/img/screenshot-git.png">

主な機能

  • マルチターミナル:xterm.js + PTY(WSL / cmd / PowerShell / PowerShell 7 / Git Bash、macOS ではログインシェル)、タスクバーとトレイのメニューからのシェル選択
  • AI エージェント:Claude Code / Codex / Copilot CLI / opencode をターミナルで直接動かす前提の導線。インストール済みのものだけを出す起動ボタン、過去セッションの再開、定型プロンプトの挿入、エージェントが入力を待ったときの知らせ
  • エージェント状態:アカウント・利用率・トークン使用量とコストを、使っているエージェントごとに確認。複数アカウント(CLAUDE_CONFIG_DIR)にも追従し、Claude Code の hook から実際のアカウントを受け取れる
  • ターミナルの AI 補助:出力中のパスをクリックしてファイル・画像・ディレクトリを開く(行番号の有無を問わない)、エディタの選択範囲や行の参照、診断、issue の着手指示をターミナルへ送る
  • エディタ:CodeMirror 6(30+ 言語)、ミニマップ、検索・置換、git diff ガター、コンフリクトの解消、定義ジャンプ、タブごとの折り返し切替、文字コード/改行コード対応
  • Markdown の入力支援:記法を挿入するツールバー、表と脚注のテンプレート、画像の挿入(選択・貼り付け・ドロップ)、貼り付けた URL をページのタイトル付きリンクに変換
  • ブラウザのタブ:Jira や GitHub などの外部のページをタブで開く。ログイン状態を保ち、再起動後もタブを復元する
  • プレビュー:Markdown / reStructuredText / Mermaid / CSV / JSON / SVG / HTML / PDF、Vue コンポーネント(vue-preview を入れたとき)、外部画像はドメイン単位で許可、表示専用の画像ビューア
  • Git:ステージング、コミット、push/pull、diff、コミットグラフ、コンフリクトの解消、止まった rebase / merge の再開、ブランチ切替、worktree 切替
  • サイドバーパネル:ファイルツリー、検索(ripgrep 同梱)、Docker(モノレポの compose も検出)、タスクランナー(npm / pnpm / just / cargo ほか)、アウトライン、Problems、GitHub の issue と pull request、CI(GitHub Actions / CircleCI)の実行の一覧と再実行
  • プロジェクト管理:WSL / Windows / macOS ローカルのプロジェクト、グループ整理(絞り込み・ドラッグでの並べ替え・絵文字アイコン)、マルチウィンドウ、セッション復元、プロジェクトごとのウィンドウ位置とサイズ、手元に無いプロジェクトの clone
  • 作業領域の分割:タブを左右 2 つのペインに分けて、エディタとターミナルを並べて使えます。幅はプロジェクトごとに覚えます
  • 設定:項目名と説明文からの絞り込み、UI / ターミナル / エディタの個別フォント設定、ダーク/ライト/システム追従、日英 i18n、設定同期、自動更新
  • pike CLI:pike file.rs:42 でファイルを開く、pike <dir> でプロジェクト切替、--wait で GIT_EDITOR 連携

使い方

詳しい使い方は ユーザーマニュアル を参照してください。

インストール

GitHub Releases から最新のインストーラをダウンロードして実行します。

ファイル種類
Pike_x.x.x_x64-setup.exeWindows インストーラ(NSIS、推奨)
Pike_x.x.x_x64_en-US.msiWindows インストーラ(MSI)
Pike_x.x.x_aarch64.dmgmacOS(Apple Silicon)

インストール後に自動でアップデートを確認します。手動で確認するときは歯車メニュー →「更新を確認」から行えます。Windows 版と macOS 版のどちらも対象です。

macOS 版について

配布しているのは Apple Silicon(arm64)版だけです。Intel Mac では動きません。

配布物は Apple Developer ID で署名して公証(notarization)しているため、ダウンロードしてそのまま開けます。

ソースからビルド

必要なもの

  • Windows 11 または macOS(Apple Silicon)
  • Node.js >= 20
  • Rust >= 1.90
  • Tauri v2 の前提条件
  • just(タスクランナー。winget install Casey.Just)
  • WSL2(任意。WSL シェルや Docker 連携を使う場合に必要)

ビルドと実行

開発タスクは justfile にまとめてあります。just だけで一覧が出ます。 レシピは Git Bash で走るため、Windows では Git for Windows が必要です。

# 依存をインストール
npm install

# 同梱用の ripgrep バイナリをダウンロード
just fetch-rg

# 開発版(インストール版と共存して起動できます)
just dev

# 本番ビルド
just build

# コミット前チェック一式(Biome / vue-tsc / ドキュメント整合 / ショートカット照合 / clippy / cargo test)
just check

開発・コントリビュート

アーキテクチャ、実装ルール、コミット規約などの開発者向け情報は CLAUDE.md にまとまっています。

ライセンス

MIT

Source 1 files
hooks/register.ts 229 lines
1// Pike が同梱する Claude Code の mod(#437)。Pike のターミナルで動く claude から、
2// アカウントの申告・入力待ちの知らせ・レート制限・ターミナルごとの状態を Pike へ渡す。
3//
4// **渡し方は `pike agent-hook` を起こすだけ**(`settings.json` に登録する hook と同じ受け口。
5// 受け側の正本は `src-tauri/src/agent_hook.rs`)。この mod が持つのは「いつ、何を付けて
6// 起こすか」だけで、申告の置き場も配送も Rust の側にある。
7//
8// **Pike のターミナルの外では何もしない**(`PIKE_PTY_ID` と `PIKE_HOOK_EXE` が無い)。
9// 利用者がこのフォルダを別の場所から読み込ませても、起こす相手が居ない。
10//
11// **セッションを待たせない。** `pike` の起動は待たずに `next(e)` へ進む(失敗も握り潰す)。
12// 例外は 2 つ。目印の環境変数を立てるところ(同じイベントの settings hook より先に
13// 立っていなければ意味が無い)と、セッションの終わり(待たないと、起こす前にプロセスが
14// 終わる)。
15import type { EngineInterface, Register } from 'claude-code'
16
17/** レートの報告の最短間隔。ターンのたびにプロセスを起こさない。 */
18const USAGE_INTERVAL_MS = 60_000
19/** 枠が動いたときの最短間隔。動くたびに送ると、応答のたびにプロセスが立つ。 */
20const USAGE_CHANGED_MS = 15_000
21
22/** `Notification` の種類 → Pike の契機(`agent_hook.rs` の `NoticeKind`)。載っていない種類は知らせない。 */
23const NOTICE_EVENTS: Record<string, string> = {
24  permission_prompt: 'waiting',
25  agent_needs_input: 'waiting',
26  elicitation_dialog: 'waiting',
27  idle_prompt: 'idle',
28}
29
30type Pike = {
31  exe: string
32  installKey: string
33  /** 走っている Pike へ届ける配送があるか(Pike が `PIKE_HOOK_LIVE` で知らせる。無い OS がある)。 */
34  live: boolean
35}
36
37/**
38 * `session.measure` は記録の場所(どのアカウントかの手がかり)を持たないので、classic の
39 * イベントで見たものを覚えておく。
40 *
41 * **`SessionStart` だけで覚えない。** mod は読み直されることがあり(Pike の更新が
42 * このファイルを書き換える)、そのときモジュールの変数は初期化される。`Stop` は
43 * ターンのたびに `session.measure` の直前に来るので、そこで拾い直せば報告は止まらない。
44 */
45let transcriptPath: string | undefined
46let lastUsageAt = 0
47
48/** 読めた結果は覚える。どれも Pike が PTY の環境に置いた値で、セッションの途中で変わらない。 */
49let pikeCache: Pike | undefined
50
51async function pikeOf($: EngineInterface): Promise<Pike | undefined> {
52  if (pikeCache) return pikeCache
53  const exe = await $.env.get('PIKE_HOOK_EXE')
54  const pty = await $.env.get('PIKE_PTY_ID')
55  if (!exe || !pty) return undefined
56  pikeCache = {
57    exe,
58    installKey: (await $.env.get('PIKE_INSTALL_KEY')) ?? '',
59    live: (await $.env.get('PIKE_HOOK_LIVE')) === '1',
60  }
61  return pikeCache
62}
63
64/**
65 * 知らせと状態の報告の宛先。**走っている Pike へ届ける配送が無い環境では `undefined`**
66 * (`Pike.live`)で、そこではプロセスを起こさない。申告とレートの報告は `pikeOf` を使う
67 * (ファイルに書くものなので、配送に依らない)。
68 */
69async function liveOf($: EngineInterface): Promise<Pike | undefined> {
70  const pike = await pikeOf($)
71  return pike?.live ? pike : undefined
72}
73
74/** `pike agent-hook` を起こす。**待たない**(呼び出し側は `void` で捨てる)。 */
75async function report($: EngineInterface, pike: Pike, flags: string[], payload: unknown): Promise<void> {
76  const argv = [pike.exe, 'agent-hook', '--via=mod', `--install-key=${pike.installKey}`, '--agent=claude', ...flags]
77  try {
78    await $.process.run(argv, { stdin: JSON.stringify(payload), timeoutMs: 10_000 })
79  } catch {
80    // Pike が居ない・起こせない。セッションには関係が無い。
81  }
82}
83
84/**
85 * 状態の報告(`--state=`)を、起きた順に 1 本ずつ送るための列。
86 *
87 * **報告は 1 件ごとに別のプロセス**なので、並べて起こすと Pike に届く順が入れ替わる
88 * (終わったセッションに「待機中」が後から届いて一覧に残る、中断したターンの「実行中」が
89 * 「待機中」の後に届いて印が回り続ける)。前の 1 本が終わってから次を起こす。
90 */
91let stateQueue: Promise<void> = Promise.resolve()
92
93function enqueue(task: () => Promise<void>): Promise<void> {
94  stateQueue = stateQueue.then(task)
95  return stateQueue
96}
97
98/** 許可の確認を出したまま答えを待っているか。答えのあとに「実行中」へ戻すために覚える。 */
99let awaitingPermission = false
100
101/**
102 * 許可の確認に答えが出たら「実行中」へ戻す。**待っていなければ何もしない**(ツールの
103 * たびに呼ばれるので、プロセスを起こすのは確認を出していたときだけ)。
104 */
105async function reportResumed($: EngineInterface): Promise<void> {
106  if (awaitingPermission) await reportRunning($)
107}
108
109/** ターンが走っていることを知らせる(始まったときと、確認に答えが出たとき)。 */
110async function reportRunning($: EngineInterface): Promise<void> {
111  awaitingPermission = false
112  const pike = await liveOf($)
113  if (pike) void enqueue(() => report($, pike, ['--state=running'], {}))
114}
115
116/**
117 * ターンが終わったことを、コンテキストの埋まり具合を添えて知らせる。
118 *
119 * **`session.measure` を待たずにここで読む。** あちらは値が動いたときにしか来ないので、
120 * 応答の前に中断したターンでは来ず、タブが「実行中」のまま残る。
121 */
122async function reportIdle($: EngineInterface, pike: Pike): Promise<void> {
123  const flags = ['--state=idle']
124  try {
125    const { context } = await $.session.usage()
126    // 最初の応答より前(始めたばかり・圧縮の直後)は `tokens` が無い。
127    if (context.tokens !== undefined) flags.push(`--context=${context.tokens}/${context.window}`)
128  } catch {
129    // 読めなくても、終わったことは知らせる。
130  }
131  try {
132    // セッションを再開できる場所(`agent_hook.rs` の `AgentState::root`)。worktree へ
133    // 移ると動くので、ターンのたびに読む。
134    flags.push(`--root=${await $.session.root()}`)
135  } catch {
136    // 無ければ、Pike はタブの場所で再開する。
137  }
138  await report($, pike, flags, {})
139}
140
141export const register: Register = (on) => {
142  on('classic.SessionStart', async ($, e, next) => {
143    const pike = await pikeOf($)
144    if (!pike) return next(e)
145    // **この mod が動いている目印。** `settings.json` に登録済みの同じ hook は、これを見て
146    // 何もせずに終わる(二重の通知を防ぐ)。`next(e)` がその hook を走らせるので、先に立てる。
147    await $.env.set('PIKE_AGENT_MOD', '1')
148    transcriptPath = e.transcript_path
149    // 申告(stdin)と、このターミナルでセッションが始まったことを 1 回で渡す。
150    // 配送が無い環境では申告だけ(フラグ無しが申告。`agent_hook.rs` の `run_hook`)。
151    const flags = pike.live ? ['--state=session', `--session=${e.session_id}`] : []
152    void enqueue(() => report($, pike, flags, { transcript_path: e.transcript_path, cwd: e.cwd }))
153    return next(e)
154  })
155
156  on('classic.Notification', async ($, e, next) => {
157    const event = NOTICE_EVENTS[e.notification_type]
158    // 許可の確認は `PermissionRequest` で知らせ済み。答えを待っているあいだに来る
159    // 同じ確認の `permission_prompt` では、プロセスを起こさない。
160    const known = awaitingPermission && e.notification_type === 'permission_prompt'
161    const pike = event && !known ? await liveOf($) : undefined
162    if (pike) void report($, pike, [`--event=${event}`], {})
163    return next(e)
164  })
165
166  // 許可の確認が出た時点で知らせる。`Notification` の `permission_prompt` は同じ確認の
167  // 約 6 秒後に来る(実測)ので、そちらだけだと知らせが遅れる。
168  //
169  // 状態(`--state=waiting`)も同じ 1 回で渡す。知らせは「まだ見ていない」あいだだけの
170  // 印で、状態は答えるまで続く(`agent_hook.rs` の `StateKind::Waiting`)。
171  on('classic.PermissionRequest', async ($, e, next) => {
172    const pike = await liveOf($)
173    if (pike) {
174      awaitingPermission = true
175      void enqueue(() => report($, pike, ['--state=waiting', '--event=waiting'], {}))
176    }
177    return next(e)
178  })
179
180  // 確認に答えが出た(ツールが走った・失敗した・断られた)。ターンはまだ続いている。
181  on('classic.PostToolUse', async ($, e, next) => (await reportResumed($), next(e)))
182  on('classic.PostToolUseFailure', async ($, e, next) => (await reportResumed($), next(e)))
183  on('classic.PermissionDenied', async ($, e, next) => (await reportResumed($), next(e)))
184
185  on('classic.Stop', async ($, e, next) => {
186    transcriptPath = e.transcript_path
187    const pike = await liveOf($)
188    if (pike) void report($, pike, ['--event=done'], {})
189    return next(e)
190  })
191
192  // 実行中かどうか。サブエージェントは `turn.start` を出さないので、来るのは本線だけ。
193  on('turn.start', async ($, e, next) => (await reportRunning($), next(e)))
194
195  on('turn.complete', async ($, e, next) => {
196    // サブエージェントの終わりは本線の終わりではない。
197    if (e.agentId !== undefined) return next(e)
198    const pike = await liveOf($)
199    awaitingPermission = false
200    if (pike) void enqueue(() => reportIdle($, pike))
201    return next(e)
202  })
203
204  on('session.end', async ($, e, next) => {
205    const pike = await liveOf($)
206    // 列の後ろに並べて待つ。先に送ると、まだ列にいるターンの終わりが後から届く。
207    // 利用者が自分で抜けたときは印を付ける(`agent_hook.rs` の `AgentState::left`)。
208    // `clear` と `resume` は付けない: 同じプロセスが次のセッションを始めて名乗り直す。
209    const left = e.reason === 'prompt_input_exit' || e.reason === 'logout'
210    const flags = left ? ['--state=ended', '--left'] : ['--state=ended']
211    if (pike) await enqueue(() => report($, pike, flags, {}))
212    return next(e)
213  })
214
215  on('session.measure', async ($, e, next) => {
216    // 枠が動いたときは早めに、そうでなければ間隔を空けて送る(「まだこの値」という鮮度のため)。
217    const since = Date.now() - lastUsageAt
218    const due = since >= (e.changed.includes('rateLimits') ? USAGE_CHANGED_MS : USAGE_INTERVAL_MS)
219    if (due && transcriptPath && e.rateLimits.length > 0) {
220      const pike = await pikeOf($)
221      if (pike) {
222        lastUsageAt = Date.now()
223        void report($, pike, ['--usage'], { transcript_path: transcriptPath, rate_limits: e.rateLimits })
224      }
225    }
226    return next(e)
227  })
228}
229