@baimingtao/lbs-agent 1.3.0 → 1.5.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/lbs.js CHANGED
@@ -46,21 +46,21 @@ const INIT_SCRIPT = path.join(PKG_ROOT, 'scripts', 'init.js');
46
46
 
47
47
  // ── 颜色与图标 (对标 Hermes banner/colors) ─────────────────────
48
48
 
49
- // ── Sakura × Mint 配色 (对标 Web 端 tailwind.config.js) ───────
49
+ // ── Aurora Pink 配色 ───────────────────────────────────────────
50
50
 
51
51
  const C = {
52
52
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m',
53
- // 品牌色
54
- sakura: '\x1b[38;2;255;183;197m', // #FFB7C5
55
- sakuraDark: '\x1b[38;2;232;154;168m', // #E89AA8
56
- mint: '\x1b[38;2;152;216;200m', // #98D8C8
57
- mintDark: '\x1b[38;2;111;196;176m', // #6FC4B0
58
- // 功能色
59
- violet: '\x1b[38;2;110;90;168m', // #6E5AA8
60
- honey: '\x1b[38;2;255;184;107m', // #FFB86B
61
- green: '\x1b[38;2;69;214;163m', // #45D6A3
62
- red: '\x1b[38;2;255;107;129m', // #FF6B81
63
- blue: '\x1b[38;2;101;168;255m', // #65A8FF
53
+ pink: '\x1b[38;2;255;79;164m', // #FF4FA4
54
+ pinkL: '\x1b[38;2;255;167;213m', // #FFA7D5
55
+ purple: '\x1b[38;2;194;107;255m', // #C26BFF
56
+ blue: '\x1b[38;2;110;139;255m', // #6E8BFF
57
+ mint: '\x1b[38;2;152;216;200m', // #98D8C8
58
+ mintD: '\x1b[38;2;111;196;176m', // #6FC4B0
59
+ ok: '\x1b[38;2;62;229;138m', // #3EE58A
60
+ warn: '\x1b[38;2;255;179;71m', // #FFB347
61
+ err: '\x1b[38;2;255;107;129m', // #FF6B81
62
+ subtle: '\x1b[38;2;138;138;150m', // #8A8A96
63
+ faint: '\x1b[38;2;90;90;102m', // #5A5A66
64
64
  };
65
65
 
66
66
  function supportsColor() {
@@ -74,23 +74,36 @@ function paint(text, ...colors) {
74
74
  return colors.join('') + text + C.reset;
75
75
  }
76
76
 
77
- // ── ASCII Logo — Sakura 主题 ───────────────────────────────────
77
+ // 逐字符渐变
78
+ function gradient(text) {
79
+ if (!supportsColor()) return text;
80
+ const stops = [
81
+ '\x1b[38;2;255;79;164m', // pink
82
+ '\x1b[38;2;255;127;184m', // pink-pu
83
+ '\x1b[38;2;224;119;220m', // mid
84
+ '\x1b[38;2;194;107;255m', // purple
85
+ '\x1b[38;2;160;123;240m', // pu-blue
86
+ '\x1b[38;2;130;131;248m', // blue-ish
87
+ '\x1b[38;2;110;139;255m', // blue
88
+ ];
89
+ const n = text.length;
90
+ let out = '';
91
+ for (let i = 0; i < n; i++) {
92
+ if (text[i] === ' ') { out += ' '; continue; }
93
+ const idx = Math.min(Math.floor(i / Math.max(n, 1) * stops.length), stops.length - 1);
94
+ out += stops[idx] + text[i] + C.reset;
95
+ }
96
+ return out;
97
+ }
78
98
 
79
- const LOGO = [
80
- '',
81
- ' ' + paint('╔══╗', C.sakura) + ' ' + paint('╔═══╗', C.mint) + ' ' + paint('╔═════╗', C.mint),
82
- ' ' + paint('║ ║', C.sakura) + ' ' + paint('║ ║', C.mint) + ' ' + paint('║ ╔═╝', C.mint),
83
- ' ' + paint('╠══╣', C.sakura) + ' ' + paint('╠═══╣', C.mint) + ' ' + paint('║ ╚═╗', C.mint),
84
- ' ' + paint('║ ║', C.sakura) + ' ' + paint('║ ║', C.mint) + ' ' + paint('╚═════╝', C.mint),
85
- ' ' + paint('╚══╝', C.sakura) + ' ' + paint('╚═══╝', C.mint) + ' ' + paint('AGENT', C.dim),
86
- ].join('\n');
99
+ // ── Banner — 极简留白 ─────────────────────────────────────────
87
100
 
88
101
  function printBanner() {
89
- const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8'));
90
- console.log(LOGO);
91
- console.log(paint(' v' + pkg.version, C.bold, C.sakuraDark) +
92
- paint(' · ', C.sakura) +
93
- paint('Leaving Blank Space Agent', C.dim));
102
+ console.log();
103
+ console.log(' ' + paint('✦', C.pink));
104
+ console.log();
105
+ console.log(' ' + gradient('LBS Agent'));
106
+ console.log(' ' + paint('Autonomous AI Runtime', C.subtle));
94
107
  console.log();
95
108
  }
96
109
  // ── 初始化检查 ─────────────────────────────────────────────────
@@ -102,13 +115,13 @@ function isInitialized() {
102
115
  function ensureInitialized() {
103
116
  if (isInitialized()) return true;
104
117
 
105
- console.log(paint('⚙ ', C.sakura) + paint('首次运行,正在初始化 LBS Agent...', C.bold));
118
+ console.log(paint('⚙ ', C.pink) + paint('首次运行,正在初始化 LBS Agent...', C.bold));
106
119
  console.log(paint(' 状态目录: ', C.dim) + paint(STATE_ROOT, C.mint));
107
120
  console.log(paint(' 创建 Python 虚拟环境 + 安装依赖(约 1-2 分钟)', C.dim));
108
121
  console.log();
109
122
 
110
123
  if (!fs.existsSync(INIT_SCRIPT)) {
111
- console.error(paint('✖ ', C.red) + '初始化脚本不存在: ' + INIT_SCRIPT);
124
+ console.error(paint('✖ ', C.err) + '初始化脚本不存在: ' + INIT_SCRIPT);
112
125
  return false;
113
126
  }
114
127
 
@@ -119,11 +132,11 @@ function ensureInitialized() {
119
132
  });
120
133
 
121
134
  if (result.status !== 0) {
122
- console.error(paint('\n✖ ', C.red) + '初始化失败');
135
+ console.error(paint('\n✖ ', C.err) + '初始化失败');
123
136
  return false;
124
137
  }
125
138
 
126
- console.log(paint('\n✓ ', C.green) + paint('初始化完成!', C.bold, C.green));
139
+ console.log(paint('\n✓ ', C.ok) + paint('初始化完成!', C.bold, C.ok));
127
140
  console.log();
128
141
  return true;
129
142
  }
@@ -144,7 +157,7 @@ if (subcmd === 'init' || subcmd === '--init') {
144
157
  });
145
158
  process.exit(result.status || 0);
146
159
  }
147
- console.error(paint('✖ ', C.red) + '初始化脚本不存在');
160
+ console.error(paint('✖ ', C.err) + '初始化脚本不存在');
148
161
  process.exit(1);
149
162
  }
150
163
 
@@ -1,12 +1,11 @@
1
- """LBS CLI 主题系统Sakura × Mint (对标 Web 端 tailwind.config.js)。
2
-
3
- Web 端配色 → ANSI True Color 映射:
4
- sakura #FFB7C5 → 粉色 (品牌主色)
5
- mint #98D8C8 薄荷绿 (辅助色)
6
- berry #2B1828 深莓 (正文)
7
- violet #6E5AA8 → 紫色 (Agent)
8
- honey #FFB86B 蜂蜜 (警告)
9
- green #45D6A3 → 翠绿 (成功)
1
+ """LBS CLI ThemeAurora Pink 现代终端设计系统。
2
+
3
+ 设计原则:
4
+ 留白 > 密集
5
+ 符号 > 字符画
6
+ 渐变 > 纯色
7
+
8
+ 对标: Claude Code, Gemini CLI, Warp Terminal, Charm BubbleTea
10
9
  """
11
10
 
12
11
  from __future__ import annotations
@@ -15,51 +14,37 @@ import os
15
14
  import sys
16
15
 
17
16
 
18
- # ── ANSI True Color 码 (直接用 Web 端 hex) ─────────────────────
19
-
20
- class Theme:
21
- """LBS CLI 色彩主题,映射 Web 端 tailwind config。"""
22
-
23
- # 基础
24
- RESET = "\033[0m"
25
- BOLD = "\033[1m"
26
- DIM = "\033[2m"
27
- ITALIC = "\033[3m"
28
- UNDERLINE = "\033[4m"
29
-
30
- # 品牌色 (对标 tailwind)
31
- SAKURA = "\033[38;2;255;183;197m" # #FFB7C5
32
- SAKURA_LIGHT = "\033[38;2;255;212;222m" # #FFD4DE
33
- SAKURA_DARK = "\033[38;2;232;154;168m" # #E89AA8
34
- MINT = "\033[38;2;152;216;200m" # #98D8C8
35
- MINT_LIGHT = "\033[38;2;184;232;220m" # #B8E8DC
36
- MINT_DARK = "\033[38;2;111;196;176m" # #6FC4B0
37
-
38
- # 功能色
39
- BERRY = "\033[38;2;43;24;40m" # #2B1828 (深莓正文)
40
- BERRY_LIGHT = "\033[38;2;74;47;69m" # #4A2F45
41
- VIOLET = "\033[38;2;110;90;168m" # #6E5AA8
42
- VIOLET_LIGHT = "\033[38;2;144;128;200m" # #9080C8
43
- HONEY = "\033[38;2;255;184;107m" # #FFB86B
44
- GREEN = "\033[38;2;69;214;163m" # #45D6A3
45
- RED = "\033[38;2;255;107;129m" # #FF6B81
46
- BLUE = "\033[38;2;101;168;255m" # #65A8FF
47
-
48
- # 状态色 (终端兼容)
49
- OK = "\033[1;38;2;69;214;163m" # green bold
50
- ERR = "\033[1;38;2;255;107;129m" # red bold
51
- WARN = "\033[1;38;2;255;184;107m" # honey bold
52
- INFO = "\033[38;2;101;168;255m" # blue
53
- DIM_TEXT = "\033[2m"
54
-
55
- # 渐变 (用多段 ANSI 近似模拟)
56
- GRADIENT_PINK = "\033[1;38;2;255;183;197m"
57
- GRADIENT_MINT = "\033[1;38;2;152;216;200m"
58
- GRADIENT_VIOLET = "\033[1;38;2;110;90;168m"
59
-
60
-
61
- def _use_color() -> bool:
62
- """检测是否使用彩色输出。"""
17
+ # ── 色彩 ───────────────────────────────────────────────────────
18
+
19
+ class C:
20
+ RESET = "\033[0m"
21
+ BOLD = "\033[1m"
22
+ DIM = "\033[2m"
23
+ ITALIC = "\033[3m"
24
+
25
+ # Aurora Pink 渐变 (Pink → Purple → Blue)
26
+ PINK = "\033[38;2;255;79;164m" # #FF4FA4
27
+ PINK_L = "\033[38;2;255;167;213m" # #FFA7D5
28
+ PURPLE = "\033[38;2;194;107;255m" # #C26BFF
29
+ BLUE = "\033[38;2;110;139;255m" # #6E8BFF
30
+
31
+ # Mint (Web 端品牌色)
32
+ MINT = "\033[38;2;152;216;200m" # #98D8C8
33
+ MINT_D = "\033[38;2;111;196;176m" # #6FC4B0
34
+
35
+ # 状态
36
+ OK = "\033[38;2;62;229;138m" # #3EE58A
37
+ WARN = "\033[38;2;255;179;71m" # #FFB347
38
+ ERR = "\033[38;2;255;107;129m" # #FF6B81
39
+ INFO = "\033[38;2;101;168;255m" # #65A8FF
40
+
41
+ # 文字
42
+ TEXT = "\033[38;2;235;235;245m" # #EBEBF5 (正文)
43
+ SUBTLE = "\033[38;2;138;138;150m" # #8A8A96 (辅助)
44
+ FAINT = "\033[38;2;90;90;102m" # #5A5A66 (最弱)
45
+
46
+
47
+ def _ok() -> bool:
63
48
  if os.environ.get("NO_COLOR"):
64
49
  return False
65
50
  if os.environ.get("TERM") == "dumb":
@@ -67,134 +52,182 @@ def _use_color() -> bool:
67
52
  return sys.stdout.isatty()
68
53
 
69
54
 
70
- def paint(text: str, *codes: str) -> str:
71
- """给文本上色 (无 TTY 时自动降级)。"""
72
- if not _use_color():
55
+ def p(text: str, *codes: str) -> str:
56
+ """上色"""
57
+ if not _ok():
73
58
  return text
74
- return "".join(codes) + text + Theme.RESET
59
+ return "".join(codes) + text + C.RESET
75
60
 
76
61
 
77
- # ── 高级组件 ───────────────────────────────────────────────────
62
+ def dim(text: str) -> str:
63
+ return p(text, C.SUBTLE)
78
64
 
79
- def sakura_label(text: str) -> str:
80
- """粉色标签 (用于品牌名)。"""
81
- return paint(text, Theme.BOLD, Theme.SAKURA_DARK)
82
65
 
66
+ def faint(text: str) -> str:
67
+ return p(text, C.FAINT)
83
68
 
84
- def mint_label(text: str) -> str:
85
- """薄荷标签 (用于成功状态)。"""
86
- return paint(text, Theme.BOLD, Theme.MINT_DARK)
87
69
 
70
+ def pink(text: str) -> str:
71
+ return p(text, C.PINK)
88
72
 
89
- def dim(text: str) -> str:
90
- """灰色辅助文字。"""
91
- return paint(text, Theme.DIM)
92
73
 
74
+ def purple(text: str) -> str:
75
+ return p(text, C.PURPLE)
93
76
 
94
- def agent_badge(name: str) -> str:
95
- """Agent 名称紫色胶囊。"""
96
- return paint(f" {name} ", Theme.BOLD, Theme.VIOLET)
97
77
 
78
+ def mint(text: str) -> str:
79
+ return p(text, C.MINT_D)
98
80
 
99
- def model_pill(model: str) -> str:
100
- """模型名粉色文字。"""
101
- return paint(model, Theme.SAKURA_DARK)
102
81
 
82
+ def ok(text: str) -> str:
83
+ return p(text, C.OK)
103
84
 
104
- def status_dot(ok: bool = True) -> str:
105
- """状态圆点。"""
106
- color = Theme.GREEN if ok else Theme.RED
107
- return paint("●", color)
108
85
 
86
+ def err(text: str) -> str:
87
+ return p(text, C.ERR)
109
88
 
110
- def divider(width: int = 56) -> str:
111
- """分隔线 — 樱花粉渐变薄荷。"""
112
- return paint("─" * width, Theme.SAKURA, Theme.DIM)
113
89
 
90
+ # ── 组件 ───────────────────────────────────────────────────────
114
91
 
115
- def header_box(title: str, subtitle: str = "") -> str:
116
- """带圆角的标题框 (对标 Web glass card)。"""
117
- w = max(len(title) + 4, 44)
118
- top = paint("╭" + "─" * (w - 2) + "╮", Theme.SAKURA)
119
- bottom = paint("╰" + "─" * (w - 2) + "╯", Theme.SAKURA)
120
- mid_title = paint(f"│ {title}", Theme.BOLD, Theme.SAKURA_DARK)
121
- mid_title += " " * (w - 2 - len(title) - 1) + paint("│", Theme.SAKURA)
122
- lines = [top, mid_title]
123
- if subtitle:
124
- mid_sub = paint(f"│ ", Theme.SAKURA) + paint(subtitle, Theme.DIM)
125
- mid_sub += " " * (w - 2 - len(subtitle) - 1) + paint("│", Theme.SAKURA)
126
- lines.append(mid_sub)
127
- lines.append(bottom)
92
+ def gradient_text(text: str) -> str:
93
+ """Pink Purple Blue 逐字符渐变。"""
94
+ if not _ok():
95
+ return text
96
+ colors = [C.PINK, C.PINK_L, "\033[38;2;255;127;184m",
97
+ "\033[38;2;224;119;220m", C.PURPLE,
98
+ "\033[38;2;160;123;240m", C.BLUE]
99
+ n = len(text)
100
+ result = []
101
+ for i, ch in enumerate(text):
102
+ if ch == " ":
103
+ result.append(ch)
104
+ continue
105
+ idx = min(int(i / max(n, 1) * len(colors)), len(colors) - 1)
106
+ result.append(colors[idx] + ch + C.RESET)
107
+ return "".join(result)
108
+
109
+
110
+ def header() -> str:
111
+ """启动 header — 极简留白风格。"""
112
+ lines = [
113
+ "",
114
+ f" {p('✦', C.PINK)}",
115
+ "",
116
+ f" {gradient_text('LBS Agent')}",
117
+ f" {dim('Autonomous AI Runtime')}",
118
+ "",
119
+ ]
128
120
  return "\n".join(lines)
129
121
 
130
122
 
131
- def kv_row(label: str, value: str, label_width: int = 9) -> str:
132
- """键值行 (对标 Web status dock)。"""
133
- return f" {paint(label.ljust(label_width), Theme.DIM)} {paint('│', Theme.SAKURA, Theme.DIM)} {value}"
123
+ def session_card(rows: list[tuple[str, str]]) -> str:
124
+ """Session 信息卡 上下布局,图标 + label + value。"""
125
+ icons = {
126
+ "session": "◇",
127
+ "workspace": "▸",
128
+ "agent": "◐",
129
+ "model": "✦",
130
+ "provider": "☁",
131
+ "memory": "◈",
132
+ "runtime": "⚙",
133
+ "turns": "⟩",
134
+ "elapsed": "⏱",
135
+ }
136
+ lines = []
137
+ for label, value in rows:
138
+ icon = icons.get(label, "·")
139
+ lines.append(f" {p(icon, C.PURPLE)} {dim(f'{label:11}')} {value}")
140
+ return "\n".join(lines)
134
141
 
135
142
 
136
- def turn_prompt(agent: str, turn: int) -> str:
137
- """Chat turn 提示符。"""
138
- return f"{agent_badge(agent)} {dim(f'turn {turn}')} {paint('›', Theme.SAKURA)} "
143
+ def separator(width: int = 52) -> str:
144
+ """分隔线 最弱色"""
145
+ return p("─" * width, C.FAINT)
139
146
 
140
147
 
141
- def usage_bar(seconds: float, iterations: int, tools: int) -> str:
142
- """用量状态条 (对标 Web 端 status dock)。"""
143
- parts = [
144
- paint(f"{seconds:.1f}s", Theme.MINT_DARK),
145
- paint(f"iter {iterations}", Theme.DIM),
146
- paint(f"tools {tools}", Theme.DIM),
148
+ def prompt_symbol() -> str:
149
+ """输入提示符"""
150
+ return p("❯", C.PINK)
151
+
152
+
153
+ def footer() -> str:
154
+ """底部快捷键栏"""
155
+ keys = [
156
+ ("↵", "Send"),
157
+ ("↑↓", "History"),
158
+ ("Tab", "Complete"),
159
+ ("/help", "Commands"),
160
+ ("Ctrl+C", "Exit"),
147
161
  ]
148
- return " " + paint(" │ ", Theme.SAKURA, Theme.DIM).join(parts)
162
+ parts = []
163
+ for key, label in keys:
164
+ parts.append(f"{p(key, C.PURPLE)} {faint(label)}")
165
+ return faint(" ") + faint(" · ".join(parts))
149
166
 
150
167
 
151
- def react_header(agent_display: str, model_chain: list[str], tools: list[str]) -> str:
152
- """ReAct 迭代头 (对标 Web 端 ChatFlow 的任务头)。"""
153
- model_str = paint(" → ", Theme.SAKURA).join(
154
- paint(m, Theme.SAKURA_DARK) for m in model_chain
155
- )
156
- tools_str = paint(", ", Theme.DIM).join(
157
- paint(t, Theme.MINT_DARK) for t in tools
158
- )
159
- lines = [
160
- paint("━" * 56, Theme.SAKURA, Theme.DIM),
161
- f" {paint('Agent', Theme.DIM)} {agent_badge(agent_display)}",
162
- f" {paint('Model', Theme.DIM)} {model_str}",
163
- f" {paint('Tools', Theme.DIM)} {tools_str}",
164
- paint("━" * 56, Theme.SAKURA, Theme.DIM),
168
+ def usage_line(seconds: float, iterations: int, tools: int) -> str:
169
+ """用量条"""
170
+ parts = [
171
+ p(f"{seconds:.1f}s", C.MINT_D),
172
+ faint(f"iter {iterations}"),
173
+ faint(f"tools {tools}"),
165
174
  ]
166
- return "\n".join(lines)
175
+ return " " + faint(" · ").join(parts)
167
176
 
168
177
 
169
- def react_iteration(iteration: int, max_iterations: int, phase: str = "") -> str:
170
- """ReAct 迭代标记。"""
171
- phase_str = f" {paint(phase, Theme.VIOLET)}" if phase else ""
172
- return f"\n {paint('⟡', Theme.SAKURA)} {paint(f'iter {iteration}', Theme.BOLD, Theme.SAKURA_DARK)}{paint(f'/{max_iterations}', Theme.DIM)}{phase_str}"
178
+ def result_text(content: str) -> str:
179
+ """最终结果"""
180
+ return p(content, C.TEXT)
173
181
 
174
182
 
175
- def thinking_block(content: str) -> str:
176
- """思考过程块 (薄荷绿斜体)。"""
177
- return paint(content, Theme.ITALIC, Theme.MINT_DARK)
183
+ def error_text(content: str) -> str:
184
+ """错误"""
185
+ return p(f"✕ {content}", C.ERR)
178
186
 
179
187
 
180
- def result_block(content: str) -> str:
181
- """最终结果块。"""
182
- return paint(content, Theme.BOLD)
188
+ def agent_tag(name: str) -> str:
189
+ """Agent 标签"""
190
+ return p(f"◐ {name}", C.PURPLE)
183
191
 
184
192
 
185
- def error_block(content: str) -> str:
186
- """错误块。"""
187
- return paint(f" {content}", Theme.BOLD, Theme.RED)
193
+ def model_tag(model: str) -> str:
194
+ """模型标签"""
195
+ return p(f" {model}", C.PINK)
188
196
 
189
197
 
190
- def logo() -> str:
191
- """LBS Agent ASCII Logo Sakura 主题。"""
192
- lines = [
193
- "",
194
- f" {paint('╔══╗', Theme.SAKURA)} {paint('╔═══╗', Theme.MINT)} {paint('╔═════╗', Theme.MINT)}",
195
- f" {paint('║ ║', Theme.SAKURA)} {paint('║ ║', Theme.MINT)} {paint('║ ╔═╝', Theme.MINT)}",
196
- f" {paint('╠══╣', Theme.SAKURA)} {paint('╠═══╣', Theme.MINT)} {paint('║ ╚═╗', Theme.MINT)}",
197
- f" {paint('║ ║', Theme.SAKURA)} {paint('║ ║', Theme.MINT)} {paint('╚═════╝', Theme.MINT)}",
198
- f" {paint('╚══╝', Theme.SAKURA)} {paint('╚═══╝', Theme.MINT)} {paint('AGENT', Theme.DIM)}",
199
- ]
200
- return "\n".join(lines)
198
+ def react_header(agent_display: str, model_chain: list[str], tools: list[str]) -> str:
199
+ """ReAct 任务头极简留白"""
200
+ model_str = faint(" → ").join(pink(m) for m in model_chain)
201
+ tools_str = faint(", ").join(mint(t) for t in tools)
202
+ return "\n".join([
203
+ separator(),
204
+ f" {agent_tag(agent_display)}",
205
+ f" {model_str}",
206
+ f" {faint('tools')} {tools_str}" if tools_str else "",
207
+ separator(),
208
+ ])
209
+
210
+
211
+ def react_iter(iteration: int) -> str:
212
+ """迭代标记"""
213
+ return f"\n {p('⟡', C.PINK)} {p(f'iter {iteration}', C.BOLD, C.PINK)}"
214
+
215
+
216
+ def thinking_block(content: str) -> str:
217
+ """思考块"""
218
+ return p(content, C.ITALIC, C.MINT_D)
219
+
220
+
221
+ def tool_card(name: str, status: str, duration: str = "") -> str:
222
+ """工具调用卡片"""
223
+ status_color = C.OK if status == "ok" else C.ERR
224
+ icon = "✓" if status == "ok" else "✕"
225
+ dur = f" {faint(duration)}" if duration else ""
226
+ return f" {p(icon, status_color)} {faint(name)}{dur}"
227
+
228
+
229
+ def timeline_item(timestamp: str, label: str, success: bool = True) -> str:
230
+ """时间线条目"""
231
+ icon = "✓" if success else "✕"
232
+ color = C.OK if success else C.ERR
233
+ return f" {faint(timestamp)} {p(icon, color)} {dim(label)}"