Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ ceeker
| `c` | フィルタ全クリア |
| `q` | 終了 |

### Session List JSON

`--list-sessions` を付けると、ceeker は TUI を起動せず現在の session list を JSON で標準出力します。LLM や外部ツール連携向けのモードで、各 session には tmux pane を特定できる `pane_id` も含まれます。

```bash
ceeker --list-sessions
```

出力例:

```json
[
{
"session_id": "sess-123",
"agent_type": "codex",
"agent_status": "running",
"cwd": "/path/to/worktree",
"pane_id": "%42",
"last_message": "planning changes",
"last_updated": "2026-04-02T12:34:56Z"
}
]
```

出力前に ceeker は 1 回だけ同期的に pane 生存確認と capture ベースの状態更新を行います。tmux 更新に失敗した場合でも、保存済みの session list はそのまま返します。

### ジャンプ後に自動終了

`--exit-on-jump` を指定すると、ジャンプ成功後に ceeker が自動的に終了します。ポップアップで一時的に起動し、セッション選択→ジャンプ→自動クローズする運用に便利です。
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ Displays a list of all active sessions.
| `c` | Clear all filters |
| `q` | Quit |

### Session List JSON

With `--list-sessions`, ceeker skips the TUI and prints the current session list as JSON. This is intended for LLM or tool integration. Each session includes `pane_id` so callers can identify the tmux pane directly.

```bash
ceeker --list-sessions
```

Example output:

```json
[
{
"session_id": "sess-123",
"agent_type": "codex",
"agent_status": "running",
"cwd": "/path/to/worktree",
"pane_id": "%42",
"last_message": "planning changes",
"last_updated": "2026-04-02T12:34:56Z"
}
]
```

Before printing, ceeker performs one synchronous pane liveness and capture-based state refresh. If tmux refresh fails, ceeker still returns the stored session list.

### Exit on Jump

With `--exit-on-jump`, ceeker exits automatically after a successful jump. This is useful when running ceeker as a one-shot popup — select a session, jump, and the popup closes by itself.
Expand Down
31 changes: 31 additions & 0 deletions docs/project_docs/llm-session-list-option/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# llm-session-list-option Plan

## Summary

`ceeker` に TUI を起動せず現在の session list を JSON で返す
`--list-sessions` オプションを追加する。
LLM が対象 pane を特定できるよう、既存の session 情報に加えて
`pane_id` を必ず出力する。

## Key Changes

- CLI に `--list-sessions` を追加
- `ceeker --list-sessions` は TUI を起動せず JSON を stdout に出力
- 出力前に `tmux` pane の stale close と capture ベース状態更新を 1 回実行
- 更新失敗時は fail-open で保存済み state を返す
- 出力 JSON のキーは snake_case に統一し、`agent_type` と
`agent_status` は文字列化する

## Tests

- `--list-sessions` の CLI parse
- list mode で TUI が起動しないこと
- JSON に `pane_id` が含まれること
- refresh 失敗時も JSON 出力が継続すること
- session 並び順が TUI と一致すること

## Assumptions

- 「今の session list」は TUI が参照している state 全件を指す
- 出力形式は今回は JSON 固定とする
- tmux 外セッションでも `pane_id` キーは出し、値は空文字にする
101 changes: 67 additions & 34 deletions src/ceeker/core.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
(ns ceeker.core
"Entry point for ceeker CLI."
(:require [ceeker.hook.handler :as hook]
[ceeker.session-list :as session-list]
[ceeker.tui.app :as tui]
[cheshire.core :as json]
[clojure.core.async :as async]
[clojure.java.io :as io]
[clojure.string :as str]
Expand All @@ -20,6 +22,8 @@
:parse-fn keyword
:validate [#{:auto :table :card}
"Must be one of: auto, table, card"]]
[nil "--list-sessions"
"Print the current session list as JSON and exit"]
[nil "--exit-on-jump" "Exit after a successful jump"]
[nil "--startup-profile"
"Log startup profiling to stderr"]])
Expand All @@ -33,6 +37,8 @@
""
"Usage:"
" ceeker Start the TUI"
" ceeker --list-sessions"
" Print sessions as JSON"
" ceeker hook <agent> <event> [<payload>]"
" Handle a hook event"
" ceeker hook <agent> <json-payload>"
Expand Down Expand Up @@ -77,28 +83,29 @@
[args]
(let [agent-type (first args)
raw-second (second args)]
(when (or (nil? agent-type) (nil? raw-second))
(binding [*out* *err*]
(println "Usage: ceeker hook <agent> <event>")
(println " agent: claude | codex"))
(System/exit 1))
(let [{:keys [event-type payload]}
(resolve-hook-args args raw-second)
result (hook/handle-hook!
agent-type event-type payload)]
(binding [*out* *err*]
(println (str "ceeker: recorded "
(:agent-type result) " "
(or event-type "notify")
" for pane "
(:pane-id result)))))))
(if (or (nil? agent-type) (nil? raw-second))
(do
(binding [*out* *err*]
(println "Usage: ceeker hook <agent> <event>")
(println " agent: claude | codex"))
1)
(let [{:keys [event-type payload]}
(resolve-hook-args args raw-second)
result (hook/handle-hook!
agent-type event-type payload)]
(binding [*out* *err*]
(println (str "ceeker: recorded "
(:agent-type result) " "
(or event-type "notify")
" for pane "
(:pane-id result))))
0))))

(defn- print-errors!
"Prints CLI errors to stderr and exits."
"Prints CLI errors to stderr."
[errors]
(binding [*out* *err*]
(doseq [e errors] (println e)))
(System/exit 1))
(doseq [e errors] (println e))))

(defn- tui-opts
"Builds TUI opts from parsed CLI options."
Expand All @@ -107,6 +114,42 @@
:startup-profile (:startup-profile options)
:initial-display-mode (:view options)})

(defn list-sessions-for-cli
"Returns the current session list for CLI output."
[state-dir]
(map session-list/session->external
(session-list/refresh-and-read-session-list state-dir)))

(defn- print-session-list!
"Prints the current session list as JSON."
[state-dir]
(println (json/generate-string
(list-sessions-for-cli state-dir))))

(defn- run-parsed-cli!
"Executes the parsed CLI command and returns an exit code."
[{:keys [options arguments summary errors]}]
(cond
errors
(do (print-errors! errors)
1)
(:version options)
(do (println (str "ceeker " version))
0)
(:help options)
(do (println (usage summary))
0)
(:list-sessions options)
(do (print-session-list! nil)
0)
(= "hook" (first arguments))
(handle-hook-command (rest arguments))
:else
(do (tui/start-tui!
nil
(tui-opts options))
0)))

;; musl? is evaluated at AOT compile time (macro expansion time).
;; For musl static builds, set CEEKER_STATIC=true CEEKER_MUSL=true
;; when running `clojure -T:build uber` to produce a musl-compatible binary.
Expand All @@ -132,19 +175,9 @@
(run
(let [{:keys [options arguments summary errors]}
(cli/parse-opts args cli-options :in-order true)]
(cond
errors
(print-errors! errors)
(:version options)
(do (println (str "ceeker " version))
(System/exit 0))
(:help options)
(do (println (usage summary))
(System/exit 0))
(= "hook" (first arguments))
(handle-hook-command (rest arguments))
:else
(do (tui/start-tui!
nil
(tui-opts options))
(System/exit 0))))))
(System/exit
(run-parsed-cli!
{:options options
:arguments arguments
:summary summary
:errors errors})))))
53 changes: 53 additions & 0 deletions src/ceeker/session_list.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
(ns ceeker.session-list
"Shared session list access for TUI and CLI."
(:require [ceeker.state.store :as store]
[ceeker.tmux.pane :as pane]))

(defn sort-sessions
"Sorts sessions the same way as the TUI list."
[sessions]
(sort-by
(fn [session]
[(if (= :running (:agent-status session)) 0 1)
(or (:last-updated session) "")])
sessions))

(defn read-session-list
"Reads all sessions from the state store and sorts them."
([] (read-session-list nil))
([state-dir]
(let [state (if state-dir
(store/read-sessions state-dir)
(store/read-sessions))]
(sort-sessions (vals (:sessions state))))))

(defn refresh-session-state!
"Refreshes pane liveness and capture-based session states once."
[state-dir]
(pane/close-stale-sessions! state-dir)
(pane/refresh-session-states! state-dir))

(defn refresh-and-read-session-list
"Refreshes session state once, then reads the current session list.
Refresh failures are logged and do not prevent returning stored state."
([] (refresh-and-read-session-list nil))
([state-dir]
(try
(refresh-session-state! state-dir)
(catch Exception e
(binding [*out* *err*]
(println
(str "ceeker: session list refresh failed: "
(.getMessage e))))))
(sort-sessions (read-session-list state-dir))))

(defn session->external
"Converts an internal session map to a JSON-ready map."
[session]
{:session_id (:session-id session)
:agent_type (some-> (:agent-type session) name)
:agent_status (some-> (:agent-status session) name)
:cwd (:cwd session)
:pane_id (or (:pane-id session) "")
:last_message (:last-message session)
:last_updated (:last-updated session)})
18 changes: 3 additions & 15 deletions src/ceeker/tui/app.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns ceeker.tui.app
"TUI application main loop."
(:require [ceeker.state.store :as store]
(:require [ceeker.session-list :as session-list]
[ceeker.tmux.pane :as pane]
[ceeker.tui.filter :as f]
[ceeker.tui.input :as input]
Expand Down Expand Up @@ -47,15 +47,6 @@
(recur)))))
stop-ch)))

(defn- get-session-list
"Gets session list from state store."
([] (get-session-list nil))
([state-dir]
(let [state (if state-dir
(store/read-sessions state-dir)
(store/read-sessions))]
(vals (:sessions state)))))

(defn- find-tmux-pane
"Finds a tmux pane matching the given cwd."
[cwd]
Expand Down Expand Up @@ -116,10 +107,7 @@
(defn- filtered-sorted
"Applies filters then sorts sessions."
[sessions filter-state]
(sort-by
(fn [s]
[(if (= :running (:agent-status s)) 0 1)
(or (:last-updated s) "")])
(session-list/sort-sessions
(f/apply-filters filter-state sessions)))

(defn- get-terminal-width
Expand Down Expand Up @@ -349,7 +337,7 @@
(defn- render-and-read
"Renders current state and reads one key event."
[terminal w state-dir sel msg fs sm? sb display-mode]
(let [sessions (get-session-list state-dir)
(let [sessions (session-list/read-session-list state-dir)
visible (filtered-sorted sessions fs)
mx (max 0 (dec (count visible)))
cl (clamp sel 0 mx)
Expand Down
Loading
Loading