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
12 changes: 9 additions & 3 deletions common/tuios/.config/tuios/config.d/dotfiles.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ show_clock = true
[appearance.sidebar]
position = "left"
width = 32
sections = "sessions,agents"
sections = "sessions,agents,custom"
show_agents = true

# tmux sidebar の USAGE 相当 (ai-usage)。tmux と同じく最下部に置く。session 一覧は標準 sessions 欄だけ。
[appearance.sidebar.custom]
title = "usage"
command = "~/dotfiles/scripts/tui usage"
refresh = "30s"

# tmux.expose 相当。pane navigator を画面カード一覧で開く。
[panes]
navigator_layout = "cards"
Expand Down Expand Up @@ -113,7 +119,7 @@ prefix_workspace_switcher = ["w"]
prefix_workspace = []
prefix_split_horizontal = ["-"]
prefix_split_vertical = ["|", "\\"]
prefix_selection = ["v", "["]
prefix_selection = ["v", "[", "u", "ctrl+u"]
prefix_review = ["ctrl+v"]
prefix_rename_window = ["t"]
rename_workspace = ["T"]
Expand Down Expand Up @@ -148,7 +154,7 @@ prefix_session_switcher = ["s", "f"]
switch_workspace_9 = []
prefix_last_message = []
prefix_window = []
prefix_scrollback = ["u", "ctrl+u"]
prefix_scrollback = []
mark_pane = ["M"]
prefix_mail = []

Expand Down
7 changes: 4 additions & 3 deletions docs/tools/tuios.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TUIOS

> **由来:** **Upstream** [TUIOS](https://github.com/Gaurav-Gosain/tuios) / **Configuration** `common/tuios/`・`mac/tuios/` / **Custom** `scripts/tui`のproject picker・space設定と巡回・dock agents・theme / pane入替picker・dock status・Neovim navigation・ssh/fzf/tmuxへのキー委譲・`tui rcon` container session([区分](../provenance.md#区分))
> **由来:** **Upstream** [TUIOS](https://github.com/Gaurav-Gosain/tuios) / **Configuration** `common/tuios/`・`mac/tuios/` / **Custom** `scripts/tui`のproject picker・space設定と巡回・dock agents・sidebar usage・theme / pane入替picker・dock status・Neovim navigation・ssh/fzf/tmuxへのキー委譲・`tui rcon` container session([区分](../provenance.md#区分))

TUIOS 0.10.1の標準機能を中心に、local / remote session、layout、AI表示、復元を管理する。既存tmuxは引き続き利用できる。

Expand Down Expand Up @@ -44,7 +44,7 @@ prefixは`Ctrl+Space`。表はprefixの後に押すキー。
| `H` / `J` / `K` / `L` | paneを5セル単位でリサイズ |
| `z`、prefix不要の`Ctrl+z` | sidebarを残してzoom |
| `v` / `[` | vi copy mode。`y`で`clipboard-copy`へ渡す。`Esc`は選択中なら解除、それ以外は終了 |
| `u` / `Ctrl+u` | scrollbackブラウザー |
| `u` / `Ctrl+u` | copy modeへ入る。tmuxは入ると同時に半ページ上へ移動するが、TUIOS 0.10.1は1キーに2操作を割り当てられないため、続けて`Ctrl+u`で半ページ上へ移動する。scrollbackブラウザーのキー割り当ては外した |
| `e` | native hints |
| `s` / `f` | native session switcher。local / remoteのsessionを選択 |
| `Ctrl+f` / `/` | native pane navigator。全sessionのpaneを画面カードで一覧(tmux.expose相当) |
Expand Down Expand Up @@ -181,7 +181,8 @@ tmuxの`pane-border-status`に合わせる。

- pane上端に`番号: タイトル`を表示し、window buttonは隠す。TUIOS 0.10.1は`shared_borders`有効時にtitleを描画しないため無効にしている。
- focus外paneの`dim_unfocused`は使わない。TUIOS 0.10.1はtheme設定時に既定背景のセルへthemeの背景色を塗るため、背景透過が消える。代わりに枠線をtokyonightの青系で色分けする(focus `#7aa2f7`、focus外 `#3b4261`)。
- sidebarはTUIOS標準のsessions / agentsだけを使う。session一覧の重複を避けるためcustom sectionは置かない。session行の状態glyphとrepo / branchのgroupingは標準表示。
- sidebarのsession一覧はTUIOS標準のsessions欄だけを使う。session行の状態glyphとrepo / branchのgroupingは標準表示。
- sidebar最下部の`usage`はtmux sidebarのUSAGEと同じAI使用量で、`tui usage`が`ai-usage`(claude / codex / gemini / cursor / grok)を並列に呼んで30秒ごとに描く。リミット消化ペースの色(青=使い切り、緑=ペース良好、黄・橙=余り気味、赤=使い切れない見込み)は`tmux-agent-sidebar.sh`と同じ判定。
- dockの`󰚩 N ●●`は`tui dock agents`の出力で、表示中sessionのagent数と状態色(黄=作業中、赤=要応答/エラー、緑=完了、紫=hang、灰=待機)を3秒ごとに表示する。agentがいなければcellを隠す。
- hangはtmuxの`tmux-agent-hang-watch.sh`相当。`working`のまま`AGENT_HANG_THRESHOLD`秒(既定120)hookのreportがないpaneを紫で示す。hookはtool呼び出しごとに`working`を再reportし`evidence_age_ms`を更新するため、これをheartbeatとして使う。画面出力は生存扱いにしない。常駐watcherは置かず、dock cellの3秒周期で判定する。表示はdockのみで、TUIOS標準のrail glyph・Inbox・通知は`working`のまま(hook経由のagentの無応答検知はTUIOS 0.10.1未対応)。1つのtoolが長時間動く間やtool呼び出しのない長い思考中も同じくhangと推定される。
- dockの右側はtmuxの`status-right`相当。CPU・RAM(標準)、GPU、`/`の使用率、focus中paneのbranch、`月/日 時:分`の時計、ホスト名を並べる。GPU・ストレージ・branchは`tui dock gpu|storage|branch`の出力で、tmuxと同じ閾値色を使う。GPUはmacOSの`macmon`かLinuxの`nvidia-smi`があるときだけ表示する。dock commandにはfocus中paneのcwdが渡らないため、branchは`tuios list-agents -s $TUIOS_SESSION`のfocus中paneから引く。remote sessionのbranchはTUIOS 0.10.1未対応で表示しない。mode・prefix・copyの表示はdock標準のpillを使う。
Expand Down
22 changes: 22 additions & 0 deletions scripts/test-tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,28 @@ def test_swap_cancelled_source_swaps_nothing(self):
choose.assert_called_once()
self.assertEqual(run.call_count, 1)

def test_usage_draws_ai_usage_records_with_pace_like_tmux_sidebar(self):
outputs = {
"claude": "C\x1fcurrent\x1fG\x1f46%\x1f3h20m\nC\x1fweekly\x1fG\x1f30%\x1f3d12h\n",
"codex": "X\x1f--\n",
}
with patch.object(tui, "ai_usage", side_effect=lambda p: outputs.get(p, "")), patch("sys.stdout", new_callable=io.StringIO) as out:
tui.usage()
claude, codex = "\033[38;2;255;102;0m", "\033[38;2;125;211;252m"
self.assertEqual(out.getvalue().splitlines(), [
f"{claude}C\033[0m \033[2mcurrent\033[0m",
f" {claude}G 46%\033[0m \033[2m3h20m\033[0m",
" \033[2mweekly\033[0m",
f" {claude}G 30%\033[0m \033[38;5;208m3d12h\033[0m \033[38;5;208m●\033[0m",
f"{codex}X\033[0m \033[2m--\033[0m",
])

def test_pace_color_skips_early_window_and_marks_used_up(self):
self.assertEqual(tui.pace_color(10, "weekly", "6d23h"), "")
self.assertEqual(tui.pace_color(100, "current", "1h"), "\033[38;5;39m")
self.assertEqual(tui.pace_color(10, "current", "1h"), "\033[38;5;203m")
self.assertEqual(tui.pace_color(50, "unknown", "1h"), "")

def test_dock_branch_follows_focused_pane_of_client_session(self):
with tempfile.TemporaryDirectory() as repo, tempfile.TemporaryDirectory() as plain:
subprocess.run(["git", "init", "-q", "-b", "live", repo], check=True)
Expand Down
47 changes: 46 additions & 1 deletion scripts/tui
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import shlex
import shutil
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor


def run(*args, **kwargs):
Expand Down Expand Up @@ -215,6 +216,48 @@ def gpu_percent():
return None


USAGE_COLORS = {"claude": (255, 102, 0), "codex": (125, 211, 252), "gemini": (66, 133, 244), "cursor": (255, 255, 255), "grok": (94, 234, 212)}
USAGE_WINDOWS = {"current": 18000, "weekly": 604800, "monthly": 2592000, "cursor": 2592000, "other": 2592000}


def pace_color(pct, label, rem):
# tmux-agent-sidebar.sh の sb_pace_color と同じ消化ペースの推定色。
window = USAGE_WINDOWS.get(label) or (int(label[:-1]) * 86400 if re.fullmatch(r"\d+d", label) else None)
left = sum(int(n) * unit for n, unit in ((m.group(1), {"d": 86400, "h": 3600, "m": 60}[m.group(2)]) for m in re.finditer(r"(\d+)([dhm])", rem)))
if window is None or left <= 0 or (window - left) * 20 < window:
return ""
if pct >= 100:
return "\033[38;5;39m"
projected = pct * window // (window - left)
return next(f"\033[38;5;{c}m" for floor, c in ((95, 114), (80, 220), (50, 208), (0, 203)) if projected >= floor)


def ai_usage(provider):
# ai-usage は ~/.local/bin。各 provider は内部でキャッシュと backoff を持つ。
binary = shutil.which("ai-usage") or str(Path.home() / ".local/bin/ai-usage")
try:
return subprocess.run([binary, provider], capture_output=True, text=True, timeout=2.5).stdout
except (OSError, subprocess.TimeoutExpired):
return ""


def usage():
# Rows for the rail's custom section, as tmux-agent-sidebar.sh draws them.
with ThreadPoolExecutor() as pool:
outputs = list(pool.map(ai_usage, USAGE_COLORS))
for (provider, (r, g, b)), out in zip(USAGE_COLORS.items(), outputs):
color = f"\033[38;2;{r};{g};{b}m"
for i, record in enumerate(line.split("\x1f") for line in out.splitlines() if line):
head = f"{color}{record[0]}\033[0m " if i == 0 else " "
if len(record) < 5:
print(f"{head}\033[2m--\033[0m")
continue
_, label, gauge, pct, rem = record[:5]
pace = "" if f"{provider} {label}" in ("claude current", "grok extra") else pace_color(int(pct.rstrip("%") or 0), label, rem)
print(f"{head}\033[2m{label}\033[0m")
print(f" {color}{gauge} {pct}\033[0m {pace or chr(27) + '[2m'}{rem}\033[0m" + (f" {pace}●\033[0m" if pace else ""))


def dock(item):
# Cells for [dock.custom.*]. GPUや表示対象がない環境では何も出さず、cellを隠す。
if item == "gpu":
Expand Down Expand Up @@ -255,7 +298,7 @@ def dev():
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command")
for name in ("pick", "space", "root", "dev", "theme", "swap"):
for name in ("pick", "space", "root", "dev", "theme", "swap", "usage"):
sub.add_parser(name)
nav = sub.add_parser("space-nav")
nav.add_argument("direction", choices=("next", "prev"))
Expand Down Expand Up @@ -284,6 +327,8 @@ def main():
navigate_space(args.direction, args.client_pid)
elif args.command == "rcon":
rcon(args.target)
elif args.command == "usage":
usage()
elif args.command == "theme":
theme()
elif args.command == "swap":
Expand Down
Loading