handoff-cli 0.3.5__tar.gz → 0.3.7__tar.gz
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.
- handoff_cli-0.3.7/Makefile +33 -0
- handoff_cli-0.3.5/README.md → handoff_cli-0.3.7/PKG-INFO +18 -0
- handoff_cli-0.3.5/PKG-INFO → handoff_cli-0.3.7/README.md +9 -9
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/list.py +2 -1
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/new.py +15 -3
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/resume.py +20 -2
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/run.py +13 -4
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/tail.py +2 -1
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/config.py +45 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/jsonl_parser.py +42 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/jsonl_viewer.py +217 -105
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/main.py +5 -3
- handoff_cli-0.3.7/cli/skills/handoff-codex/SKILL.md +52 -0
- handoff_cli-0.3.7/cli/skills/handoff-ds/SKILL.md +52 -0
- handoff_cli-0.3.7/cli/skills/handoff-ds.toml +63 -0
- handoff_cli-0.3.7/cli/skills/handoff-opus/SKILL.md +52 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/tui.py +51 -6
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/configuration.zh-CN.md +15 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/pyproject.toml +1 -1
- handoff_cli-0.3.7/tests/test_markdown_parser.py +158 -0
- handoff_cli-0.3.5/Makefile +0 -7
- handoff_cli-0.3.5/cli/skills/handoff-codex/SKILL.md +0 -83
- handoff_cli-0.3.5/cli/skills/handoff-ds/SKILL.md +0 -83
- handoff_cli-0.3.5/cli/skills/handoff-ds.toml +0 -52
- handoff_cli-0.3.5/cli/skills/handoff-opus/SKILL.md +0 -83
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/.github/workflows/publish.yml +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/.gitignore +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/CLAUDE.md +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/README.zh-CN.md +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/__init__.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/backend.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/backend_types.yaml +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/__init__.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/env.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/commands/init.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/core.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/stream.py +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/cli/user_config_template.yaml +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/TODO.md +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/claude-code.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/codex.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/handoff-hero.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/list-tui.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/parallel.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/shell.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/assets/tail.jpg +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/cli-reference.zh-CN.md +0 -0
- {handoff_cli-0.3.5 → handoff_cli-0.3.7}/docs/design.zh-CN.md +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.PHONY: render skills
|
|
2
|
+
|
|
3
|
+
render: ## Preview PyPI long description rendering
|
|
4
|
+
pip install -q "readme_renderer[md]" 2>/dev/null
|
|
5
|
+
python -m readme_renderer README.md > /tmp/handoff-pypi-preview.html
|
|
6
|
+
@echo "✅ /tmp/handoff-pypi-preview.html ($(shell wc -c < /tmp/handoff-pypi-preview.html | tr -d ' ') bytes)"
|
|
7
|
+
open /tmp/handoff-pypi-preview.html
|
|
8
|
+
|
|
9
|
+
# ── skill 文档同步 ──────────────────────────────────────────────────
|
|
10
|
+
# handoff-ds/SKILL.md 是主文档(master),只改它。
|
|
11
|
+
# 它的 frontmatter(顶部的 `---...---` 块)不同步;其下的正文会被复制到
|
|
12
|
+
# 其它 backend 的 SKILL.md,并把 backend 名/缩写替换成对应值。占位符在
|
|
13
|
+
# 「构建时」由 sed 替换,落盘的都是具体值——LLM 永远读不到占位符。
|
|
14
|
+
SKILLS := cli/skills
|
|
15
|
+
MASTER := $(SKILLS)/handoff-ds/SKILL.md
|
|
16
|
+
|
|
17
|
+
skills: ## 把 handoff-ds/SKILL.md 正文同步到其它 backend 的 SKILL.md
|
|
18
|
+
@$(call sync_skill,handoff-codex,codex,cx)
|
|
19
|
+
@$(call sync_skill,handoff-opus,opus,op)
|
|
20
|
+
@echo "done."
|
|
21
|
+
|
|
22
|
+
# $(1)=目标目录 $(2)=backend 名 $(3)=run_id 缩写
|
|
23
|
+
define sync_skill
|
|
24
|
+
target="$(SKILLS)/$(1)/SKILL.md"; \
|
|
25
|
+
tmp=$$(mktemp); \
|
|
26
|
+
awk '{print} /^---[[:space:]]*$$/{n++; if(n==2) exit}' "$$target" > "$$tmp"; \
|
|
27
|
+
awk 'body{print} /^---[[:space:]]*$$/{n++; if(n==2) body=1}' "$(MASTER)" \
|
|
28
|
+
| sed -e 's/deepseek/$(2)/g' \
|
|
29
|
+
-e 's/handoff-ds/handoff-$(2)/g' \
|
|
30
|
+
-e 's/0613-ds-/0613-$(3)-/g' >> "$$tmp"; \
|
|
31
|
+
mv "$$tmp" "$$target"; \
|
|
32
|
+
echo "synced $$target (backend=$(2))";
|
|
33
|
+
endef
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: handoff-cli
|
|
3
|
+
Version: 0.3.7
|
|
4
|
+
Summary: Multi coding-agent task dispatcher — a CLI proxy for claude that sends coding tasks to configurable AI backends
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: pyyaml<7,>=6
|
|
7
|
+
Requires-Dist: textual<3,>=2
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
1
10
|
<div align="center">
|
|
2
11
|
<img src="https://raw.githubusercontent.com/dazuiba/handoff/main/docs/assets/handoff-hero.jpg" width="100%" alt="hero">
|
|
3
12
|
|
|
@@ -139,6 +148,15 @@ Dispatching and resuming are the AI's job (`handoff run` / `handoff resume` unde
|
|
|
139
148
|
|
|
140
149
|
</details>
|
|
141
150
|
|
|
151
|
+
<details>
|
|
152
|
+
<summary><b>Can I change the TUI theme?</b></summary>
|
|
153
|
+
|
|
154
|
+
<br>
|
|
155
|
+
|
|
156
|
+
Yes. Inside `handoff list` and `handoff tail`, press `D` to toggle between `textual-dark` and `textual-light`. Your choice is saved automatically to `~/.handoff/tui_state.json` and restored next time you run the TUI.
|
|
157
|
+
|
|
158
|
+
</details>
|
|
159
|
+
|
|
142
160
|
<details>
|
|
143
161
|
<summary><b>Can I dispatch several tasks at once?</b></summary>
|
|
144
162
|
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: handoff-cli
|
|
3
|
-
Version: 0.3.5
|
|
4
|
-
Summary: Multi coding-agent task dispatcher — a CLI proxy for claude that sends coding tasks to configurable AI backends
|
|
5
|
-
Requires-Python: >=3.9
|
|
6
|
-
Requires-Dist: pyyaml<7,>=6
|
|
7
|
-
Requires-Dist: textual<3,>=2
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
1
|
<div align="center">
|
|
11
2
|
<img src="https://raw.githubusercontent.com/dazuiba/handoff/main/docs/assets/handoff-hero.jpg" width="100%" alt="hero">
|
|
12
3
|
|
|
@@ -148,6 +139,15 @@ Dispatching and resuming are the AI's job (`handoff run` / `handoff resume` unde
|
|
|
148
139
|
|
|
149
140
|
</details>
|
|
150
141
|
|
|
142
|
+
<details>
|
|
143
|
+
<summary><b>Can I change the TUI theme?</b></summary>
|
|
144
|
+
|
|
145
|
+
<br>
|
|
146
|
+
|
|
147
|
+
Yes. Inside `handoff list` and `handoff tail`, press `D` to toggle between `textual-dark` and `textual-light`. Your choice is saved automatically to `~/.handoff/tui_state.json` and restored next time you run the TUI.
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
151
|
<details>
|
|
152
152
|
<summary><b>Can I dispatch several tasks at once?</b></summary>
|
|
153
153
|
|
|
@@ -47,7 +47,8 @@ def cmd_list(argv: list[str], config: Config):
|
|
|
47
47
|
"FROM runs ORDER BY created_at DESC LIMIT 50"
|
|
48
48
|
).fetchall()
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
from ..config import read_tui_theme
|
|
51
|
+
app = RunListApp(rows, full_cwd, refresh_fn=_refresh_rows, theme_name=read_tui_theme())
|
|
51
52
|
app.run(mouse=False)
|
|
52
53
|
conn.close()
|
|
53
54
|
|
|
@@ -4,9 +4,10 @@ Pre-allocates a run_id and returns the canonical .prompt.md path so the caller
|
|
|
4
4
|
can write the prompt directly to its final archive location before dispatching.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
|
-
handoff new --backend <name> [--slug <slug>]
|
|
7
|
+
handoff new --backend <name> [--slug <slug>] [--write]
|
|
8
8
|
|
|
9
|
-
Stdout: one line — absolute path to the .prompt.md file
|
|
9
|
+
Stdout: one line — absolute path to the .prompt.md file.
|
|
10
|
+
By default the file is not created. With --write, stdin is written to the file.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -26,9 +27,10 @@ from ..config import Config
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def cmd_new(argv: list[str], config: Config):
|
|
29
|
-
"""handoff new --backend <name> [--slug <slug>]"""
|
|
30
|
+
"""handoff new --backend <name> [--slug <slug>] [--write]"""
|
|
30
31
|
backend_arg = ""
|
|
31
32
|
slug_arg = ""
|
|
33
|
+
write_prompt = False
|
|
32
34
|
|
|
33
35
|
i = 0
|
|
34
36
|
while i < len(argv):
|
|
@@ -49,6 +51,8 @@ def cmd_new(argv: list[str], config: Config):
|
|
|
49
51
|
slug_arg = argv[i]
|
|
50
52
|
elif a.startswith("--slug="):
|
|
51
53
|
slug_arg = a.split("=", 1)[1]
|
|
54
|
+
elif a == "--write":
|
|
55
|
+
write_prompt = True
|
|
52
56
|
elif a in ("-h", "--help"):
|
|
53
57
|
from ..main import usage
|
|
54
58
|
usage()
|
|
@@ -85,4 +89,12 @@ def cmd_new(argv: list[str], config: Config):
|
|
|
85
89
|
run_id = f"{mmdd}-{b2}-{seq_code}-{clean_slug}"
|
|
86
90
|
prompt_path = os.path.join(TASKS_DIR, f"{run_id}.prompt.md")
|
|
87
91
|
|
|
92
|
+
if write_prompt:
|
|
93
|
+
if sys.stdin.isatty():
|
|
94
|
+
print("handoff new: --write requires prompt text on stdin", file=sys.stderr)
|
|
95
|
+
sys.exit(2)
|
|
96
|
+
os.makedirs(os.path.dirname(prompt_path), exist_ok=True)
|
|
97
|
+
with open(prompt_path, "w") as f:
|
|
98
|
+
f.write(sys.stdin.read())
|
|
99
|
+
|
|
88
100
|
print(prompt_path)
|
|
@@ -27,7 +27,7 @@ from ..config import Config
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def cmd_resume(argv: list[str], config: Config):
|
|
30
|
-
"""handoff resume [<run-id|seq>] [--backend <name>] [--pro] [--cwd <dir>]
|
|
30
|
+
"""handoff resume [<run-id|seq>] [--backend <name>] [--slug <slug>] [--pro] [--cwd <dir>]
|
|
31
31
|
[--verbose] [(<input-file|-> | --text <prompt...>)]."""
|
|
32
32
|
# Pre-scan --verbose so it works regardless of position (e.g. after --text).
|
|
33
33
|
verbose = "--verbose" in argv
|
|
@@ -36,6 +36,7 @@ def cmd_resume(argv: list[str], config: Config):
|
|
|
36
36
|
pro = False
|
|
37
37
|
cwd = ""
|
|
38
38
|
backend_arg = ""
|
|
39
|
+
slug_arg = ""
|
|
39
40
|
selector = ""
|
|
40
41
|
input_src = ""
|
|
41
42
|
text_mode = False
|
|
@@ -61,6 +62,14 @@ def cmd_resume(argv: list[str], config: Config):
|
|
|
61
62
|
backend_arg = filtered[i]
|
|
62
63
|
elif a.startswith("--backend="):
|
|
63
64
|
backend_arg = a.split("=", 1)[1]
|
|
65
|
+
elif a == "--slug":
|
|
66
|
+
i += 1
|
|
67
|
+
if i >= len(filtered):
|
|
68
|
+
print("handoff resume: --slug requires a value", file=sys.stderr)
|
|
69
|
+
sys.exit(2)
|
|
70
|
+
slug_arg = filtered[i]
|
|
71
|
+
elif a.startswith("--slug="):
|
|
72
|
+
slug_arg = a.split("=", 1)[1]
|
|
64
73
|
elif a == "--text":
|
|
65
74
|
text_mode = True
|
|
66
75
|
if input_src:
|
|
@@ -159,7 +168,16 @@ def cmd_resume(argv: list[str], config: Config):
|
|
|
159
168
|
# Non-interactive: dispatch a new turn through the run pipeline.
|
|
160
169
|
conn.close()
|
|
161
170
|
from .run import _execute
|
|
162
|
-
_execute(
|
|
171
|
+
_execute(
|
|
172
|
+
cwd,
|
|
173
|
+
prompt_text,
|
|
174
|
+
backend_name,
|
|
175
|
+
pro,
|
|
176
|
+
config,
|
|
177
|
+
resume_session_id=session_id,
|
|
178
|
+
slug=slug_arg or "resume",
|
|
179
|
+
verbose=verbose,
|
|
180
|
+
)
|
|
163
181
|
|
|
164
182
|
|
|
165
183
|
def _resume_interactive(config: Config, backend_name: str, session_id: str, cwd: str, pro: bool, verbose: bool = False):
|
|
@@ -40,7 +40,7 @@ def _is_adopted_path(input_src: str) -> bool:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def cmd_run(argv: list[str], config: Config):
|
|
43
|
-
"""handoff run [--backend <name>] [--cwd <dir>] [--pro] [--verbose] (<input-file|-> | --text <prompt...>)."""
|
|
43
|
+
"""handoff run [--backend <name>] [--cwd <dir>] [--slug <slug>] [--pro] [--verbose] (<input-file|-> | --text <prompt...>)."""
|
|
44
44
|
# Pre-scan --verbose so it works regardless of position (e.g. after --text).
|
|
45
45
|
verbose = "--verbose" in argv
|
|
46
46
|
filtered = [a for a in argv if a != "--verbose"]
|
|
@@ -48,6 +48,7 @@ def cmd_run(argv: list[str], config: Config):
|
|
|
48
48
|
pro = False
|
|
49
49
|
cwd = ""
|
|
50
50
|
backend_arg = ""
|
|
51
|
+
slug_arg = ""
|
|
51
52
|
input_src = ""
|
|
52
53
|
text_mode = False
|
|
53
54
|
text_parts = []
|
|
@@ -71,6 +72,14 @@ def cmd_run(argv: list[str], config: Config):
|
|
|
71
72
|
backend_arg = filtered[i]
|
|
72
73
|
elif a.startswith("--backend="):
|
|
73
74
|
backend_arg = a.split("=", 1)[1]
|
|
75
|
+
elif a == "--slug":
|
|
76
|
+
i += 1
|
|
77
|
+
if i >= len(filtered):
|
|
78
|
+
print("handoff run: --slug requires a value", file=sys.stderr)
|
|
79
|
+
sys.exit(2)
|
|
80
|
+
slug_arg = filtered[i]
|
|
81
|
+
elif a.startswith("--slug="):
|
|
82
|
+
slug_arg = a.split("=", 1)[1]
|
|
74
83
|
elif a == "--text":
|
|
75
84
|
text_mode = True
|
|
76
85
|
if input_src:
|
|
@@ -130,10 +139,10 @@ def cmd_run(argv: list[str], config: Config):
|
|
|
130
139
|
if not prompt_text:
|
|
131
140
|
print("handoff run: --text requires a non-empty value", file=sys.stderr)
|
|
132
141
|
sys.exit(2)
|
|
133
|
-
slug = "from-text"
|
|
142
|
+
slug = slug_arg or "from-text"
|
|
134
143
|
elif input_src == "-" or (not input_src and not sys.stdin.isatty()):
|
|
135
144
|
prompt_text = sys.stdin.read()
|
|
136
|
-
slug = "from-stdin"
|
|
145
|
+
slug = slug_arg or "from-stdin"
|
|
137
146
|
elif input_src:
|
|
138
147
|
if not os.path.isfile(input_src):
|
|
139
148
|
print(f"handoff run: input file not found: {input_src}", file=sys.stderr)
|
|
@@ -164,7 +173,7 @@ def cmd_run(argv: list[str], config: Config):
|
|
|
164
173
|
else:
|
|
165
174
|
with open(input_src) as f:
|
|
166
175
|
prompt_text = f.read()
|
|
167
|
-
slug = "from-file"
|
|
176
|
+
slug = slug_arg or "from-file"
|
|
168
177
|
else:
|
|
169
178
|
print("handoff run: input file required, or use --text <prompt...> / pipe via '-'", file=sys.stderr)
|
|
170
179
|
sys.exit(2)
|
|
@@ -44,5 +44,6 @@ def cmd_tail(argv: list[str], config=None):
|
|
|
44
44
|
"out_path": out_path,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
from ..config import read_tui_theme
|
|
47
48
|
from ..jsonl_viewer import run_tail
|
|
48
|
-
run_tail(jsonl_path, prompt_path, result_path, run_info)
|
|
49
|
+
run_tail(jsonl_path, prompt_path, result_path, run_info, theme_name=read_tui_theme())
|
|
@@ -18,6 +18,7 @@ Backend resolution:
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import json
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
23
24
|
import sys
|
|
@@ -35,6 +36,50 @@ except ImportError:
|
|
|
35
36
|
|
|
36
37
|
_BACKEND_TYPES_PATH = os.path.join(os.path.dirname(__file__), "backend_types.yaml")
|
|
37
38
|
_USER_CONFIG_TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "user_config_template.yaml")
|
|
39
|
+
DEFAULT_DARK_THEME = "textual-dark"
|
|
40
|
+
DEFAULT_LIGHT_THEME = "textual-light"
|
|
41
|
+
|
|
42
|
+
# ── TUI state persistence (theme, independent from config.yaml) ──────────
|
|
43
|
+
|
|
44
|
+
_TUI_STATE_FILENAME = "tui_state.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tui_state_path() -> str:
|
|
48
|
+
return os.path.join(user_config_dir(), _TUI_STATE_FILENAME)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def read_tui_theme() -> str:
|
|
52
|
+
"""Read the persisted TUI theme name from ``~/.handoff/tui_state.json``.
|
|
53
|
+
|
|
54
|
+
Falls back to ``textual-dark`` when the file is missing, corrupt, or
|
|
55
|
+
the stored theme name is empty / invalid. Never raises.
|
|
56
|
+
"""
|
|
57
|
+
path = _tui_state_path()
|
|
58
|
+
try:
|
|
59
|
+
if os.path.isfile(path):
|
|
60
|
+
with open(path, "r") as f:
|
|
61
|
+
data = json.load(f)
|
|
62
|
+
theme = data.get("theme", "")
|
|
63
|
+
if isinstance(theme, str) and theme.strip():
|
|
64
|
+
return theme.strip()
|
|
65
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
66
|
+
pass
|
|
67
|
+
return DEFAULT_DARK_THEME
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_tui_theme(theme: str) -> None:
|
|
71
|
+
"""Persist the TUI theme name to ``~/.handoff/tui_state.json``.
|
|
72
|
+
|
|
73
|
+
Best-effort: failures are silently ignored so the app never crashes
|
|
74
|
+
on a write.
|
|
75
|
+
"""
|
|
76
|
+
path = _tui_state_path()
|
|
77
|
+
try:
|
|
78
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
79
|
+
with open(path, "w") as f:
|
|
80
|
+
json.dump({"theme": theme}, f)
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
38
83
|
|
|
39
84
|
# Top-level config keys that belong to the mechanism layer or are otherwise
|
|
40
85
|
# removed. Warn once and ignore. system_prompt is deliberately absent from
|
|
@@ -7,6 +7,8 @@ import json
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import TextIO
|
|
9
9
|
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@dataclass
|
|
12
14
|
class ParsedEvent:
|
|
@@ -160,6 +162,46 @@ def format_event_for_viewer(event: ParsedEvent) -> str | None:
|
|
|
160
162
|
return f"`{ts}` {kind_mark} {_truncate(event.text)}"
|
|
161
163
|
|
|
162
164
|
|
|
165
|
+
def format_event_as_rich(event: ParsedEvent) -> Text | None:
|
|
166
|
+
"""Format one parsed event as a rich Text with styled spans.
|
|
167
|
+
|
|
168
|
+
Returns None for result_text/error_text events (handled separately by
|
|
169
|
+
the viewer as Markdown content). The returned Text uses colour/emphasis
|
|
170
|
+
per kind: tool=cyan, text=default, result=green, error=red, task=yellow,
|
|
171
|
+
info=dim.
|
|
172
|
+
"""
|
|
173
|
+
if event.kind in ("result_text", "error_text"):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
ts = event.ts or " " * 8
|
|
177
|
+
mark_map = {
|
|
178
|
+
"tool": "▷",
|
|
179
|
+
"text": "✎",
|
|
180
|
+
"result": "✓",
|
|
181
|
+
"error": "✗",
|
|
182
|
+
"task": "▶",
|
|
183
|
+
"info": "·",
|
|
184
|
+
}
|
|
185
|
+
colour_map = {
|
|
186
|
+
"tool": "cyan",
|
|
187
|
+
"text": "",
|
|
188
|
+
"result": "green",
|
|
189
|
+
"error": "red",
|
|
190
|
+
"task": "yellow",
|
|
191
|
+
"info": "dim",
|
|
192
|
+
}
|
|
193
|
+
mark = mark_map.get(event.kind, " ")
|
|
194
|
+
colour = colour_map.get(event.kind, "")
|
|
195
|
+
|
|
196
|
+
text = Text()
|
|
197
|
+
text.append(f"{ts:8}", style="dim")
|
|
198
|
+
text.append(" │ ", style="dim")
|
|
199
|
+
text.append(mark, style=colour)
|
|
200
|
+
text.append(" ")
|
|
201
|
+
text.append(_truncate(event.text))
|
|
202
|
+
return text
|
|
203
|
+
|
|
204
|
+
|
|
163
205
|
def format_event_for_stream(event: ParsedEvent) -> str | None:
|
|
164
206
|
"""Return the single-line text stream shown during `handoff run`."""
|
|
165
207
|
if event.kind != "text":
|