aiwcli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +41 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Status line for Claude Code sessions.
|
|
3
|
+
|
|
4
|
+
Renders context window usage and git status with ANSI colors.
|
|
5
|
+
Optionally persists context_window data to the session's state.json.
|
|
6
|
+
|
|
7
|
+
Ported from PAI statusline.ts — context and git sections only.
|
|
8
|
+
|
|
9
|
+
Usage: echo '{"session_id":"...","model":{"display_name":"Opus"},...}' | python status_line.py
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Path setup (matches save_handoff.py pattern)
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
24
|
+
SHARED_ROOT = SCRIPT_DIR.parent # _shared/
|
|
25
|
+
sys.path.insert(0, str(SHARED_ROOT))
|
|
26
|
+
|
|
27
|
+
from lib.base.atomic_write import atomic_write
|
|
28
|
+
from lib.base.hook_utils import CONTEXT_BASELINE_TOKENS
|
|
29
|
+
|
|
30
|
+
# Cache file for session_id → context_id mapping
|
|
31
|
+
OUTPUT_DIR = Path(".") / "_output"
|
|
32
|
+
STATUSLINE_CACHE = OUTPUT_DIR / ".statusline-cache.json"
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# NO_COLOR support (https://no-color.org)
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
NO_COLOR = bool(os.environ.get("NO_COLOR"))
|
|
38
|
+
|
|
39
|
+
RESET = "" if NO_COLOR else "\x1b[0m"
|
|
40
|
+
|
|
41
|
+
# Structural
|
|
42
|
+
SLATE_300 = "" if NO_COLOR else "\x1b[38;2;203;213;225m"
|
|
43
|
+
SLATE_400 = "" if NO_COLOR else "\x1b[38;2;148;163;184m"
|
|
44
|
+
SLATE_500 = "" if NO_COLOR else "\x1b[38;2;100;116;139m"
|
|
45
|
+
SLATE_600 = "" if NO_COLOR else "\x1b[38;2;71;85;105m"
|
|
46
|
+
|
|
47
|
+
# Semantic
|
|
48
|
+
EMERALD = "" if NO_COLOR else "\x1b[38;2;74;222;128m"
|
|
49
|
+
ROSE = "" if NO_COLOR else "\x1b[38;2;251;113;133m"
|
|
50
|
+
AMBER = "" if NO_COLOR else "\x1b[38;2;251;191;36m"
|
|
51
|
+
|
|
52
|
+
# Context colors
|
|
53
|
+
CTX_PRIMARY = "" if NO_COLOR else "\x1b[38;2;129;140;248m"
|
|
54
|
+
CTX_SECONDARY = "" if NO_COLOR else "\x1b[38;2;165;180;252m"
|
|
55
|
+
CTX_ACCENT = "" if NO_COLOR else "\x1b[38;2;139;92;246m"
|
|
56
|
+
CTX_BUCKET_EMPTY = "" if NO_COLOR else "\x1b[38;2;75;82;95m"
|
|
57
|
+
|
|
58
|
+
# Git colors
|
|
59
|
+
GIT_PRIMARY = "" if NO_COLOR else "\x1b[38;2;56;189;248m"
|
|
60
|
+
GIT_VALUE = "" if NO_COLOR else "\x1b[38;2;186;230;253m"
|
|
61
|
+
GIT_DIR = "" if NO_COLOR else "\x1b[38;2;147;197;253m"
|
|
62
|
+
GIT_CLEAN = "" if NO_COLOR else "\x1b[38;2;125;211;252m"
|
|
63
|
+
GIT_MODIFIED = "" if NO_COLOR else "\x1b[38;2;96;165;250m"
|
|
64
|
+
GIT_ADDED = "" if NO_COLOR else "\x1b[38;2;59;130;246m"
|
|
65
|
+
GIT_STASH = "" if NO_COLOR else "\x1b[38;2;165;180;252m"
|
|
66
|
+
GIT_AGE_FRESH = "" if NO_COLOR else "\x1b[38;2;125;211;252m"
|
|
67
|
+
GIT_AGE_RECENT = "" if NO_COLOR else "\x1b[38;2;96;165;250m"
|
|
68
|
+
GIT_AGE_STALE = "" if NO_COLOR else "\x1b[38;2;59;130;246m"
|
|
69
|
+
GIT_AGE_OLD = "" if NO_COLOR else "\x1b[38;2;99;102;241m"
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Display modes
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def get_terminal_width() -> int:
|
|
76
|
+
"""Detect terminal width with fallbacks."""
|
|
77
|
+
# Try COLUMNS env var first
|
|
78
|
+
cols_env = os.environ.get("COLUMNS")
|
|
79
|
+
if cols_env:
|
|
80
|
+
try:
|
|
81
|
+
cols = int(cols_env)
|
|
82
|
+
if cols > 0:
|
|
83
|
+
return cols
|
|
84
|
+
except ValueError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# Try os.get_terminal_size
|
|
88
|
+
try:
|
|
89
|
+
return os.get_terminal_size().columns
|
|
90
|
+
except (OSError, ValueError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
return 80
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_display_mode(width: int) -> str:
|
|
97
|
+
"""Map terminal width to display mode."""
|
|
98
|
+
if width < 35:
|
|
99
|
+
return "nano"
|
|
100
|
+
if width < 55:
|
|
101
|
+
return "micro"
|
|
102
|
+
if width < 80:
|
|
103
|
+
return "mini"
|
|
104
|
+
return "normal"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Color helpers
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def get_bucket_color(pos: int, max_pos: int) -> str:
|
|
112
|
+
"""Get gradient color for context bar bucket at position."""
|
|
113
|
+
if NO_COLOR:
|
|
114
|
+
return ""
|
|
115
|
+
pct = (pos * 100) // max_pos
|
|
116
|
+
|
|
117
|
+
if pct <= 33:
|
|
118
|
+
r = 74 + ((250 - 74) * pct) // 33
|
|
119
|
+
g = 222 + ((204 - 222) * pct) // 33
|
|
120
|
+
b = 128 + ((21 - 128) * pct) // 33
|
|
121
|
+
elif pct <= 66:
|
|
122
|
+
t = pct - 33
|
|
123
|
+
r = 250 + ((251 - 250) * t) // 33
|
|
124
|
+
g = 204 + ((146 - 204) * t) // 33
|
|
125
|
+
b = 21 + ((60 - 21) * t) // 33
|
|
126
|
+
else:
|
|
127
|
+
t = pct - 66
|
|
128
|
+
r = 251 + ((239 - 251) * t) // 34
|
|
129
|
+
g = 146 + ((68 - 146) * t) // 34
|
|
130
|
+
b = 60 + ((68 - 60) * t) // 34
|
|
131
|
+
|
|
132
|
+
return f"\x1b[38;2;{r};{g};{b}m"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Context bar rendering
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def render_context_bar(width: int, pct: int) -> Tuple[str, str]:
|
|
140
|
+
"""Render the context usage bar with gradient colors.
|
|
141
|
+
|
|
142
|
+
Returns (bar_string, last_filled_color).
|
|
143
|
+
"""
|
|
144
|
+
pct = max(0, min(100, pct))
|
|
145
|
+
filled = (pct * width) // 100
|
|
146
|
+
last_color = EMERALD
|
|
147
|
+
parts = []
|
|
148
|
+
|
|
149
|
+
for i in range(1, width + 1):
|
|
150
|
+
if i <= filled:
|
|
151
|
+
color = get_bucket_color(i, width)
|
|
152
|
+
last_color = color
|
|
153
|
+
parts.append(f"{color}\u26C1{RESET}")
|
|
154
|
+
else:
|
|
155
|
+
parts.append(f"{CTX_BUCKET_EMPTY}\u26C1{RESET}")
|
|
156
|
+
if width > 8:
|
|
157
|
+
parts.append(" ")
|
|
158
|
+
|
|
159
|
+
return "".join(parts).rstrip(), last_color
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Separator
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
SEPARATOR = f"{SLATE_600}" + "\u2500" * 72 + f"{RESET}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Context section
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def shorten_model(name: str) -> str:
|
|
174
|
+
"""Shorten common model display names."""
|
|
175
|
+
replacements = [
|
|
176
|
+
("claude-opus-4-6", "opus-4.6"),
|
|
177
|
+
("claude-opus-4-5", "opus-4.5"),
|
|
178
|
+
("claude-sonnet-4", "sonnet-4"),
|
|
179
|
+
("claude-3-5-sonnet", "sonnet-3.5"),
|
|
180
|
+
("claude-3-5-haiku", "haiku-3.5"),
|
|
181
|
+
("claude-", ""),
|
|
182
|
+
]
|
|
183
|
+
result = name
|
|
184
|
+
for old, new in replacements:
|
|
185
|
+
result = result.replace(old, new)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def render_context(
|
|
190
|
+
mode: str,
|
|
191
|
+
context_pct: int,
|
|
192
|
+
context_k: int,
|
|
193
|
+
max_k: int,
|
|
194
|
+
time_display: str,
|
|
195
|
+
model_name: str,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Render the context usage section."""
|
|
198
|
+
if context_pct <= 33:
|
|
199
|
+
pct_color = EMERALD
|
|
200
|
+
elif context_pct <= 66:
|
|
201
|
+
pct_color = AMBER
|
|
202
|
+
else:
|
|
203
|
+
pct_color = ROSE
|
|
204
|
+
|
|
205
|
+
short_model = shorten_model(model_name)
|
|
206
|
+
|
|
207
|
+
if mode == "nano":
|
|
208
|
+
bar, _ = render_context_bar(5, context_pct)
|
|
209
|
+
print(
|
|
210
|
+
f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
|
|
211
|
+
f"{bar} {pct_color}{context_pct}%{RESET} "
|
|
212
|
+
f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
|
|
213
|
+
)
|
|
214
|
+
elif mode == "micro":
|
|
215
|
+
bar, _ = render_context_bar(6, context_pct)
|
|
216
|
+
print(
|
|
217
|
+
f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
|
|
218
|
+
f"{SLATE_600}\u2502{RESET} "
|
|
219
|
+
f"{bar} {pct_color}{context_pct}%{RESET} {SLATE_500}({context_k}k){RESET} "
|
|
220
|
+
f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
|
|
221
|
+
)
|
|
222
|
+
elif mode == "mini":
|
|
223
|
+
bar, _ = render_context_bar(8, context_pct)
|
|
224
|
+
print(
|
|
225
|
+
f"{CTX_PRIMARY}\u25C9{RESET} {CTX_ACCENT}{short_model}{RESET} "
|
|
226
|
+
f"{SLATE_600}\u2502{RESET} "
|
|
227
|
+
f"{CTX_SECONDARY}CTX:{RESET} {bar} "
|
|
228
|
+
f"{pct_color}{context_pct}%{RESET} {SLATE_500}({context_k}k/{max_k}k){RESET} "
|
|
229
|
+
f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
|
|
230
|
+
)
|
|
231
|
+
else: # normal
|
|
232
|
+
bar, last_color = render_context_bar(16, context_pct)
|
|
233
|
+
print(
|
|
234
|
+
f"{CTX_PRIMARY}\u25C9{RESET} {CTX_SECONDARY}Model:{RESET} {CTX_ACCENT}{short_model}{RESET} "
|
|
235
|
+
f"{SLATE_600}\u2502{RESET} "
|
|
236
|
+
f"{CTX_SECONDARY}Context:{RESET} {bar} "
|
|
237
|
+
f"{last_color}{context_pct}%{RESET} {SLATE_500}({context_k}k/{max_k}k){RESET} "
|
|
238
|
+
f"{SLATE_600}\u2502{RESET} "
|
|
239
|
+
f"{CTX_ACCENT}\u23F1{RESET} {SLATE_300}{time_display}{RESET}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
print(SEPARATOR)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# Git status
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def _run_git(args: list, cwd: str, timeout: int = 2) -> Optional[str]:
|
|
250
|
+
"""Run a git command and return stdout, or None on failure."""
|
|
251
|
+
try:
|
|
252
|
+
kwargs: Dict[str, Any] = {
|
|
253
|
+
"capture_output": True,
|
|
254
|
+
"text": True,
|
|
255
|
+
"timeout": timeout,
|
|
256
|
+
"cwd": cwd,
|
|
257
|
+
}
|
|
258
|
+
if sys.platform == "win32":
|
|
259
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
260
|
+
result = subprocess.run(["git"] + args, **kwargs)
|
|
261
|
+
if result.returncode == 0:
|
|
262
|
+
return result.stdout.strip()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_git_status(cwd: str) -> Optional[Dict[str, Any]]:
|
|
269
|
+
"""Gather git repository status."""
|
|
270
|
+
# Check if git repo
|
|
271
|
+
if _run_git(["rev-parse", "--git-dir"], cwd) is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
status: Dict[str, Any] = {
|
|
275
|
+
"branch": "detached",
|
|
276
|
+
"modified": 0,
|
|
277
|
+
"staged": 0,
|
|
278
|
+
"untracked": 0,
|
|
279
|
+
"stash_count": 0,
|
|
280
|
+
"ahead": 0,
|
|
281
|
+
"behind": 0,
|
|
282
|
+
"age_display": "",
|
|
283
|
+
"age_color": GIT_AGE_FRESH,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Branch
|
|
287
|
+
branch = _run_git(["branch", "--show-current"], cwd)
|
|
288
|
+
if branch:
|
|
289
|
+
status["branch"] = branch
|
|
290
|
+
|
|
291
|
+
# Modified files
|
|
292
|
+
diff = _run_git(["diff", "--name-only"], cwd)
|
|
293
|
+
if diff:
|
|
294
|
+
status["modified"] = len([l for l in diff.splitlines() if l])
|
|
295
|
+
|
|
296
|
+
# Staged files
|
|
297
|
+
staged = _run_git(["diff", "--cached", "--name-only"], cwd)
|
|
298
|
+
if staged:
|
|
299
|
+
status["staged"] = len([l for l in staged.splitlines() if l])
|
|
300
|
+
|
|
301
|
+
# Untracked files
|
|
302
|
+
untracked = _run_git(["ls-files", "--others", "--exclude-standard"], cwd)
|
|
303
|
+
if untracked:
|
|
304
|
+
status["untracked"] = len([l for l in untracked.splitlines() if l])
|
|
305
|
+
|
|
306
|
+
# Stash count
|
|
307
|
+
stash = _run_git(["stash", "list"], cwd)
|
|
308
|
+
if stash:
|
|
309
|
+
status["stash_count"] = len([l for l in stash.splitlines() if l])
|
|
310
|
+
|
|
311
|
+
# Ahead/behind
|
|
312
|
+
ab = _run_git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], cwd)
|
|
313
|
+
if ab:
|
|
314
|
+
parts = ab.split()
|
|
315
|
+
if len(parts) >= 2:
|
|
316
|
+
status["ahead"] = int(parts[0] or 0)
|
|
317
|
+
status["behind"] = int(parts[1] or 0)
|
|
318
|
+
|
|
319
|
+
# Commit age
|
|
320
|
+
log = _run_git(["log", "-1", "--format=%ct"], cwd)
|
|
321
|
+
if log:
|
|
322
|
+
try:
|
|
323
|
+
import time
|
|
324
|
+
last_epoch = int(log)
|
|
325
|
+
now_epoch = int(time.time())
|
|
326
|
+
age_sec = now_epoch - last_epoch
|
|
327
|
+
age_min = age_sec // 60
|
|
328
|
+
age_hrs = age_sec // 3600
|
|
329
|
+
age_days = age_sec // 86400
|
|
330
|
+
|
|
331
|
+
if age_min < 1:
|
|
332
|
+
status["age_display"] = "now"
|
|
333
|
+
status["age_color"] = GIT_AGE_FRESH
|
|
334
|
+
elif age_hrs < 1:
|
|
335
|
+
status["age_display"] = f"{age_min}m"
|
|
336
|
+
status["age_color"] = GIT_AGE_FRESH
|
|
337
|
+
elif age_hrs < 24:
|
|
338
|
+
status["age_display"] = f"{age_hrs}h"
|
|
339
|
+
status["age_color"] = GIT_AGE_RECENT
|
|
340
|
+
elif age_days < 7:
|
|
341
|
+
status["age_display"] = f"{age_days}d"
|
|
342
|
+
status["age_color"] = GIT_AGE_STALE
|
|
343
|
+
else:
|
|
344
|
+
status["age_display"] = f"{age_days}d"
|
|
345
|
+
status["age_color"] = GIT_AGE_OLD
|
|
346
|
+
except (ValueError, TypeError):
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
return status
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def render_git(mode: str, git: Dict[str, Any], dir_name: str) -> None:
|
|
353
|
+
"""Render the git status section."""
|
|
354
|
+
total_changed = git["modified"] + git["staged"]
|
|
355
|
+
status_icon = "*" if (total_changed > 0 or git["untracked"] > 0) else "\u2713"
|
|
356
|
+
|
|
357
|
+
if mode == "nano":
|
|
358
|
+
line = f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} {GIT_VALUE}{git['branch']}{RESET} "
|
|
359
|
+
if status_icon == "\u2713":
|
|
360
|
+
line += f"{GIT_CLEAN}\u2713{RESET}"
|
|
361
|
+
else:
|
|
362
|
+
line += f"{GIT_MODIFIED}*{total_changed}{RESET}"
|
|
363
|
+
print(line)
|
|
364
|
+
|
|
365
|
+
elif mode == "micro":
|
|
366
|
+
line = f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} {GIT_VALUE}{git['branch']}{RESET}"
|
|
367
|
+
if git["age_display"]:
|
|
368
|
+
line += f" {git['age_color']}{git['age_display']}{RESET}"
|
|
369
|
+
line += " "
|
|
370
|
+
if status_icon == "\u2713":
|
|
371
|
+
line += f"{GIT_CLEAN}{status_icon}{RESET}"
|
|
372
|
+
else:
|
|
373
|
+
line += f"{GIT_MODIFIED}{status_icon}{total_changed}{RESET}"
|
|
374
|
+
print(line)
|
|
375
|
+
|
|
376
|
+
elif mode == "mini":
|
|
377
|
+
line = (
|
|
378
|
+
f"{GIT_PRIMARY}\u25C8{RESET} {GIT_DIR}{dir_name}{RESET} "
|
|
379
|
+
f"{SLATE_600}\u2502{RESET} {GIT_VALUE}{git['branch']}{RESET}"
|
|
380
|
+
)
|
|
381
|
+
if git["age_display"]:
|
|
382
|
+
line += f" {SLATE_600}\u2502{RESET} {git['age_color']}{git['age_display']}{RESET}"
|
|
383
|
+
line += f" {SLATE_600}\u2502{RESET} "
|
|
384
|
+
if status_icon == "\u2713":
|
|
385
|
+
line += f"{GIT_CLEAN}{status_icon}{RESET}"
|
|
386
|
+
else:
|
|
387
|
+
line += f"{GIT_MODIFIED}{status_icon}{total_changed}{RESET}"
|
|
388
|
+
if git["untracked"] > 0:
|
|
389
|
+
line += f" {GIT_ADDED}+{git['untracked']}{RESET}"
|
|
390
|
+
print(line)
|
|
391
|
+
|
|
392
|
+
else: # normal
|
|
393
|
+
line = (
|
|
394
|
+
f"{GIT_PRIMARY}\u25C8{RESET} {GIT_PRIMARY}PWD:{RESET} {GIT_DIR}{dir_name}{RESET} "
|
|
395
|
+
f"{SLATE_600}\u2502{RESET} "
|
|
396
|
+
f"{GIT_PRIMARY}Branch:{RESET} {GIT_VALUE}{git['branch']}{RESET}"
|
|
397
|
+
)
|
|
398
|
+
if git["age_display"]:
|
|
399
|
+
line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Age:{RESET} {git['age_color']}{git['age_display']}{RESET}"
|
|
400
|
+
if git["stash_count"] > 0:
|
|
401
|
+
line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Stash:{RESET} {GIT_STASH}{git['stash_count']}{RESET}"
|
|
402
|
+
|
|
403
|
+
if total_changed > 0 or git["untracked"] > 0:
|
|
404
|
+
line += f" {SLATE_600}\u2502{RESET} "
|
|
405
|
+
if total_changed > 0:
|
|
406
|
+
line += f"{GIT_PRIMARY}Mod:{RESET} {GIT_MODIFIED}{total_changed}{RESET}"
|
|
407
|
+
if git["untracked"] > 0:
|
|
408
|
+
if total_changed > 0:
|
|
409
|
+
line += " "
|
|
410
|
+
line += f"{GIT_PRIMARY}New:{RESET} {GIT_ADDED}{git['untracked']}{RESET}"
|
|
411
|
+
else:
|
|
412
|
+
line += f" {SLATE_600}\u2502{RESET} {GIT_CLEAN}\u2713 clean{RESET}"
|
|
413
|
+
|
|
414
|
+
if git["ahead"] > 0 or git["behind"] > 0:
|
|
415
|
+
line += f" {SLATE_600}\u2502{RESET} {GIT_PRIMARY}Sync:{RESET} "
|
|
416
|
+
if git["ahead"] > 0:
|
|
417
|
+
line += f"{GIT_CLEAN}\u2191{git['ahead']}{RESET}"
|
|
418
|
+
if git["behind"] > 0:
|
|
419
|
+
line += f"{GIT_STASH}\u2193{git['behind']}{RESET}"
|
|
420
|
+
print(line)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
# Context manager line (line 3)
|
|
425
|
+
# ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
def render_context_manager(
|
|
428
|
+
mode: str,
|
|
429
|
+
context_id: str,
|
|
430
|
+
context_state=None,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Render the context manager line (line 3) showing context ID, mode, and plan."""
|
|
433
|
+
# Strip YYMMDD-HHMM- timestamp prefix from context ID for display
|
|
434
|
+
display_id = re.sub(r"^\d{6}-\d{4}-", "", context_id)
|
|
435
|
+
if not display_id:
|
|
436
|
+
display_id = context_id # fallback if regex strips everything
|
|
437
|
+
|
|
438
|
+
# Truncate display_id per mode
|
|
439
|
+
max_id_len = {"nano": 14, "micro": 18, "mini": 22, "normal": 30}.get(mode, 30)
|
|
440
|
+
truncated_id = display_id[:max_id_len]
|
|
441
|
+
if len(display_id) > max_id_len:
|
|
442
|
+
truncated_id += "\u2026"
|
|
443
|
+
|
|
444
|
+
# Read state fields (ContextState object from context_store)
|
|
445
|
+
state_mode = getattr(context_state, "mode", "idle") if context_state else "idle"
|
|
446
|
+
state_plan_path = getattr(context_state, "plan_path", None) if context_state else None
|
|
447
|
+
|
|
448
|
+
# Detect plan mode heuristic: if state is idle but a recent plan file exists
|
|
449
|
+
# in ~/.claude/plans/, we're likely in active planning (transient, not persisted)
|
|
450
|
+
active_plan_file = _find_active_plan_file()
|
|
451
|
+
is_planning = state_mode == "idle" and active_plan_file is not None
|
|
452
|
+
|
|
453
|
+
# Build mode badge
|
|
454
|
+
mode_badge = ""
|
|
455
|
+
if is_planning:
|
|
456
|
+
label = "Plan" if mode == "nano" else "Planning"
|
|
457
|
+
mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {AMBER}{label}{RESET}"
|
|
458
|
+
elif state_mode == "has_plan":
|
|
459
|
+
label = "Ready" if mode == "nano" else "Plan Ready"
|
|
460
|
+
mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {EMERALD}{label}{RESET}"
|
|
461
|
+
elif state_mode == "active":
|
|
462
|
+
label = "Active" if mode == "nano" else "Active"
|
|
463
|
+
mode_badge = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Mode:{RESET} {CTX_ACCENT}{label}{RESET}"
|
|
464
|
+
|
|
465
|
+
# Resolve plan file path for display
|
|
466
|
+
plan_file_path = None
|
|
467
|
+
if is_planning:
|
|
468
|
+
plan_file_path = active_plan_file
|
|
469
|
+
elif state_plan_path:
|
|
470
|
+
plan_file_path = state_plan_path
|
|
471
|
+
elif state_mode in ("has_plan", "active"):
|
|
472
|
+
# Fallback: check context's plans/ folder
|
|
473
|
+
try:
|
|
474
|
+
from lib.context.plan_manager import find_latest_plan
|
|
475
|
+
plan_file_path = find_latest_plan(context_id)
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
# Build plan name (mini/normal only)
|
|
480
|
+
plan_part = ""
|
|
481
|
+
if mode in ("mini", "normal") and plan_file_path:
|
|
482
|
+
plan_stem = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", Path(plan_file_path).stem)
|
|
483
|
+
max_plan_len = 20 if mode == "mini" else 30
|
|
484
|
+
truncated_plan = plan_stem[:max_plan_len]
|
|
485
|
+
if len(plan_stem) > max_plan_len:
|
|
486
|
+
truncated_plan += "\u2026"
|
|
487
|
+
plan_part = f" {SLATE_600}\u2502{RESET} {CTX_SECONDARY}Plan:{RESET} {SLATE_300}{truncated_plan}{RESET}"
|
|
488
|
+
|
|
489
|
+
if mode == "nano":
|
|
490
|
+
print(
|
|
491
|
+
f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
|
|
492
|
+
f"{mode_badge}"
|
|
493
|
+
)
|
|
494
|
+
elif mode == "micro":
|
|
495
|
+
print(
|
|
496
|
+
f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
|
|
497
|
+
f"{mode_badge}"
|
|
498
|
+
)
|
|
499
|
+
elif mode == "mini":
|
|
500
|
+
print(
|
|
501
|
+
f"{CTX_ACCENT}\u25C6{RESET} {SLATE_400}{truncated_id}{RESET}"
|
|
502
|
+
f"{mode_badge}{plan_part}"
|
|
503
|
+
)
|
|
504
|
+
else: # normal
|
|
505
|
+
print(
|
|
506
|
+
f"{CTX_ACCENT}\u25C6{RESET} {CTX_SECONDARY}Context:{RESET} {SLATE_300}{truncated_id}{RESET}"
|
|
507
|
+
f"{mode_badge}{plan_part}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
# Context persistence
|
|
513
|
+
# ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
def _load_cache() -> Dict[str, Any]:
|
|
516
|
+
"""Load the statusline cache file."""
|
|
517
|
+
try:
|
|
518
|
+
if STATUSLINE_CACHE.exists():
|
|
519
|
+
return json.loads(STATUSLINE_CACHE.read_text(encoding="utf-8"))
|
|
520
|
+
except Exception:
|
|
521
|
+
pass
|
|
522
|
+
return {}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _save_cache(cache: Dict[str, Any]) -> None:
|
|
526
|
+
"""Save the statusline cache file."""
|
|
527
|
+
try:
|
|
528
|
+
STATUSLINE_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
529
|
+
STATUSLINE_CACHE.write_text(
|
|
530
|
+
json.dumps(cache, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
531
|
+
)
|
|
532
|
+
except Exception:
|
|
533
|
+
pass
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _resolve_context_id(session_id: str) -> Optional[str]:
|
|
537
|
+
"""Resolve session_id to context_id, using cache when possible."""
|
|
538
|
+
if not session_id or session_id == "unknown":
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
# Check cache first
|
|
542
|
+
cache = _load_cache()
|
|
543
|
+
cached_entry = cache.get("sessions", {}).get(session_id)
|
|
544
|
+
if cached_entry and cached_entry.get("context_id") is not None:
|
|
545
|
+
return cached_entry["context_id"]
|
|
546
|
+
|
|
547
|
+
# Cache miss — look up via context manager
|
|
548
|
+
try:
|
|
549
|
+
from lib.context.context_store import get_context_by_session_id
|
|
550
|
+
context = get_context_by_session_id(session_id)
|
|
551
|
+
if context:
|
|
552
|
+
# Update cache
|
|
553
|
+
if "sessions" not in cache:
|
|
554
|
+
cache["sessions"] = {}
|
|
555
|
+
cache["sessions"][session_id] = {"context_id": context.id}
|
|
556
|
+
_save_cache(cache)
|
|
557
|
+
return context.id
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
# Mark as no-context in cache to avoid repeated lookups
|
|
562
|
+
if "sessions" not in cache:
|
|
563
|
+
cache["sessions"] = {}
|
|
564
|
+
cache["sessions"][session_id] = {"context_id": None}
|
|
565
|
+
_save_cache(cache)
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _load_context_state(context_id: str):
|
|
570
|
+
"""Load context state from state.json (with context.json fallback)."""
|
|
571
|
+
try:
|
|
572
|
+
from lib.context.context_store import load_state
|
|
573
|
+
return load_state(context_id)
|
|
574
|
+
except Exception:
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _find_active_plan_file() -> Optional[str]:
|
|
579
|
+
"""Find most recent plan file in ~/.claude/plans/."""
|
|
580
|
+
try:
|
|
581
|
+
plans_dir = Path.home() / ".claude" / "plans"
|
|
582
|
+
if not plans_dir.exists():
|
|
583
|
+
return None
|
|
584
|
+
plan_files = list(plans_dir.glob("*.md"))
|
|
585
|
+
if not plan_files:
|
|
586
|
+
return None
|
|
587
|
+
plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
588
|
+
return str(plan_files[0])
|
|
589
|
+
except Exception:
|
|
590
|
+
return None
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _write_context_window(context_id: str, context_window_data: Dict[str, Any]) -> None:
|
|
594
|
+
"""Write context_window data to state.json last_session."""
|
|
595
|
+
try:
|
|
596
|
+
from lib.context.context_store import get_context as get_ctx, save_state
|
|
597
|
+
state = get_ctx(context_id)
|
|
598
|
+
if state:
|
|
599
|
+
if state.last_session is None:
|
|
600
|
+
state.last_session = {}
|
|
601
|
+
state.last_session["context_remaining_pct"] = context_window_data.get("remaining_percentage")
|
|
602
|
+
save_state(state)
|
|
603
|
+
except Exception:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
# Main
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def main() -> None:
|
|
612
|
+
"""Read stdin JSON, render status line, optionally persist context data."""
|
|
613
|
+
# Force UTF-8 stdout on Windows to support Unicode symbols
|
|
614
|
+
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
|
615
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
616
|
+
|
|
617
|
+
# Read JSON from stdin
|
|
618
|
+
try:
|
|
619
|
+
input_data = json.loads(sys.stdin.read())
|
|
620
|
+
except Exception:
|
|
621
|
+
input_data = {}
|
|
622
|
+
|
|
623
|
+
# Terminal width and mode
|
|
624
|
+
term_width = get_terminal_width()
|
|
625
|
+
mode = get_display_mode(term_width)
|
|
626
|
+
|
|
627
|
+
# Extract input fields
|
|
628
|
+
session_id = input_data.get("session_id", "")
|
|
629
|
+
model_name = (input_data.get("model") or {}).get("display_name", "unknown")
|
|
630
|
+
cost = input_data.get("cost") or {}
|
|
631
|
+
duration_ms = cost.get("total_duration_ms", 0)
|
|
632
|
+
workspace = input_data.get("workspace") or {}
|
|
633
|
+
current_dir = workspace.get("project_dir", os.getcwd())
|
|
634
|
+
dir_name = os.path.basename(current_dir)
|
|
635
|
+
|
|
636
|
+
# Context window data
|
|
637
|
+
ctx_win = input_data.get("context_window") or {}
|
|
638
|
+
usage = ctx_win.get("current_usage") or {}
|
|
639
|
+
cache_read = usage.get("cache_read_input_tokens", 0)
|
|
640
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
641
|
+
cache_creation = usage.get("cache_creation_input_tokens", 0)
|
|
642
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
643
|
+
context_max = ctx_win.get("context_window_size", 200000)
|
|
644
|
+
|
|
645
|
+
# Calculate context percentage
|
|
646
|
+
# Use used_percentage if available (pre-calculated), else raw tokens + baseline
|
|
647
|
+
used_pct = ctx_win.get("used_percentage")
|
|
648
|
+
if used_pct is not None:
|
|
649
|
+
context_pct = int(used_pct)
|
|
650
|
+
total_input = cache_read + input_tokens + cache_creation
|
|
651
|
+
context_used = total_input + output_tokens + CONTEXT_BASELINE_TOKENS
|
|
652
|
+
else:
|
|
653
|
+
total_input = cache_read + input_tokens + cache_creation
|
|
654
|
+
context_used = total_input + output_tokens + CONTEXT_BASELINE_TOKENS
|
|
655
|
+
context_pct = (context_used * 100) // context_max if context_max > 0 else 0
|
|
656
|
+
|
|
657
|
+
context_k = context_used // 1000
|
|
658
|
+
max_k = context_max // 1000
|
|
659
|
+
|
|
660
|
+
# Format duration
|
|
661
|
+
duration_sec = duration_ms // 1000
|
|
662
|
+
if duration_sec >= 3600:
|
|
663
|
+
time_display = f"{duration_sec // 3600}h{(duration_sec % 3600) // 60}m"
|
|
664
|
+
elif duration_sec >= 60:
|
|
665
|
+
time_display = f"{duration_sec // 60}m{duration_sec % 60}s"
|
|
666
|
+
else:
|
|
667
|
+
time_display = f"{duration_sec}s"
|
|
668
|
+
|
|
669
|
+
# Resolve context ID for display and persistence
|
|
670
|
+
context_id = _resolve_context_id(session_id)
|
|
671
|
+
|
|
672
|
+
# Render context section
|
|
673
|
+
render_context(mode, context_pct, context_k, max_k, time_display, model_name)
|
|
674
|
+
|
|
675
|
+
# Render git section
|
|
676
|
+
git = get_git_status(current_dir)
|
|
677
|
+
if git:
|
|
678
|
+
render_git(mode, git, dir_name)
|
|
679
|
+
|
|
680
|
+
# Render context manager line (line 3) with separator
|
|
681
|
+
if context_id:
|
|
682
|
+
print(SEPARATOR)
|
|
683
|
+
context_state = _load_context_state(context_id)
|
|
684
|
+
render_context_manager(mode, context_id, context_state)
|
|
685
|
+
|
|
686
|
+
# Persist context_window to state.json
|
|
687
|
+
if context_id:
|
|
688
|
+
_write_context_window(context_id, {
|
|
689
|
+
"used_percentage": context_pct,
|
|
690
|
+
"remaining_percentage": 100 - context_pct,
|
|
691
|
+
"context_window_size": context_max,
|
|
692
|
+
"tokens_used": context_used,
|
|
693
|
+
"total_input_tokens": total_input,
|
|
694
|
+
"total_output_tokens": output_tokens,
|
|
695
|
+
"model": model_name,
|
|
696
|
+
"last_updated": datetime.now().isoformat(timespec="seconds"),
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
if __name__ == "__main__":
|
|
701
|
+
main()
|