@gavin-hu/my-pi-agent 0.8.1 → 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.
Files changed (174) hide show
  1. package/README.md +35 -8
  2. package/README.zh-CN.md +33 -8
  3. package/assets/bytefly.schema.json +17 -1
  4. package/assets/chrome.png +0 -0
  5. package/assets/file-browse.png +0 -0
  6. package/assets/job.png +0 -0
  7. package/assets/plan-review.png +0 -0
  8. package/assets/question.png +0 -0
  9. package/assets/rewind.png +0 -0
  10. package/assets/todo.png +0 -0
  11. package/assets/tool-renderings.png +0 -0
  12. package/assets/tools/ask_user_question.png +0 -0
  13. package/assets/tools/bash.png +0 -0
  14. package/assets/tools/edit.png +0 -0
  15. package/assets/tools/enter_plan_mode.png +0 -0
  16. package/assets/tools/enter_worktree.png +0 -0
  17. package/assets/tools/exit_plan_mode.png +0 -0
  18. package/assets/tools/exit_worktree.png +0 -0
  19. package/assets/tools/find.png +0 -0
  20. package/assets/tools/goal.png +0 -0
  21. package/assets/tools/grep.png +0 -0
  22. package/assets/tools/job.png +0 -0
  23. package/assets/tools/list_worktrees.png +0 -0
  24. package/assets/tools/ls.png +0 -0
  25. package/assets/tools/memory.png +0 -0
  26. package/assets/tools/notice.png +0 -0
  27. package/assets/tools/open_path.png +0 -0
  28. package/assets/tools/prune_worktrees.png +0 -0
  29. package/assets/tools/read.png +0 -0
  30. package/assets/tools/read_doc.png +0 -0
  31. package/assets/tools/run_doctor.png +0 -0
  32. package/assets/tools/send_wechat.png +0 -0
  33. package/assets/tools/set_plan_steps.png +0 -0
  34. package/assets/tools/subagent.png +0 -0
  35. package/assets/tools/todo.png +0 -0
  36. package/assets/tools/turn-rule.png +0 -0
  37. package/assets/tools/user-message.png +0 -0
  38. package/assets/tools/watch.png +0 -0
  39. package/assets/tools/web_fetch.png +0 -0
  40. package/assets/tools/web_search.png +0 -0
  41. package/assets/tools/write.png +0 -0
  42. package/assets/tools/write_plan.png +0 -0
  43. package/extensions/bytefly/README.md +306 -68
  44. package/extensions/bytefly/block/component.ts +282 -0
  45. package/extensions/bytefly/block/config.ts +24 -0
  46. package/extensions/bytefly/block/fallback.ts +120 -0
  47. package/extensions/bytefly/block/register.ts +164 -0
  48. package/extensions/bytefly/chrome.ts +97 -0
  49. package/extensions/bytefly/doctor/format.ts +36 -10
  50. package/extensions/bytefly/doctor/tools.ts +10 -6
  51. package/extensions/bytefly/footer/config.ts +1 -1
  52. package/extensions/bytefly/footer/render/format.ts +5 -18
  53. package/extensions/bytefly/footer/render/layout.ts +2 -1
  54. package/extensions/bytefly/footer/render/segments.ts +51 -78
  55. package/extensions/bytefly/header/component.ts +49 -21
  56. package/extensions/bytefly/header/config.ts +28 -21
  57. package/extensions/bytefly/header/register.ts +6 -1
  58. package/extensions/bytefly/index.ts +32 -12
  59. package/extensions/bytefly/message/config.ts +15 -0
  60. package/extensions/bytefly/message/format.ts +42 -0
  61. package/extensions/bytefly/message/register.ts +15 -0
  62. package/extensions/bytefly/notice/component.ts +107 -0
  63. package/extensions/bytefly/notice/config.ts +41 -0
  64. package/extensions/bytefly/notice/register.ts +27 -0
  65. package/extensions/bytefly/pad.ts +42 -0
  66. package/extensions/bytefly/panel/commands.ts +69 -35
  67. package/extensions/bytefly/panel/format.ts +8 -8
  68. package/extensions/bytefly/panel/tui.ts +45 -70
  69. package/extensions/bytefly/separator/component.ts +2 -1
  70. package/extensions/bytefly/separator/config.ts +16 -8
  71. package/extensions/bytefly/separator/format.ts +18 -38
  72. package/extensions/bytefly/separator/register.ts +3 -2
  73. package/extensions/bytefly/separator/types.ts +0 -10
  74. package/extensions/bytefly/working/config.ts +20 -0
  75. package/extensions/bytefly/working/format.ts +25 -0
  76. package/extensions/bytefly/working/register.ts +88 -0
  77. package/extensions/document/README.md +12 -5
  78. package/extensions/document/paging.ts +10 -10
  79. package/extensions/document/render.ts +17 -16
  80. package/extensions/document/tool.ts +1 -1
  81. package/extensions/file-browse/README.md +54 -10
  82. package/extensions/file-browse/client.ts +110 -2
  83. package/extensions/file-browse/config.ts +12 -0
  84. package/extensions/file-browse/diff.ts +187 -0
  85. package/extensions/file-browse/git.ts +180 -8
  86. package/extensions/file-browse/html.ts +401 -25
  87. package/extensions/file-browse/index.ts +15 -4
  88. package/extensions/file-browse/markdown.ts +33 -0
  89. package/extensions/file-browse/router.ts +94 -2
  90. package/extensions/file-browse/server.ts +4 -1
  91. package/extensions/file-open/README.md +6 -0
  92. package/extensions/goal/README.md +16 -10
  93. package/extensions/goal/format.ts +13 -25
  94. package/extensions/goal/tools.ts +11 -22
  95. package/extensions/goal/tui.ts +12 -40
  96. package/extensions/job/README.md +22 -13
  97. package/extensions/job/format.ts +36 -13
  98. package/extensions/job/index.ts +26 -21
  99. package/extensions/job/tools.ts +15 -12
  100. package/extensions/job/tui.ts +148 -222
  101. package/extensions/job/ui.ts +24 -49
  102. package/extensions/memory/README.md +7 -1
  103. package/extensions/memory/format.ts +5 -3
  104. package/extensions/memory/tools.ts +4 -3
  105. package/extensions/memory/tui.ts +32 -36
  106. package/extensions/plan/README.md +86 -36
  107. package/extensions/plan/blocks.ts +44 -2
  108. package/extensions/plan/feedback.ts +20 -5
  109. package/extensions/plan/index.ts +1 -1
  110. package/extensions/plan/runtime.ts +3 -2
  111. package/extensions/plan/tools.ts +43 -27
  112. package/extensions/plan/tui.ts +21 -27
  113. package/extensions/plan/web-client.ts +386 -40
  114. package/extensions/plan/web-page.ts +138 -33
  115. package/extensions/plan/web-router.ts +46 -13
  116. package/extensions/plan/web.ts +63 -12
  117. package/extensions/question/README.md +14 -2
  118. package/extensions/question/answers.ts +25 -18
  119. package/extensions/question/tools.ts +22 -16
  120. package/extensions/question/tui.ts +132 -76
  121. package/extensions/rewind/README.md +3 -1
  122. package/extensions/rewind/format.ts +35 -2
  123. package/extensions/rewind/index.ts +3 -1
  124. package/extensions/rewind/rewind-tui.ts +66 -62
  125. package/extensions/rewind/snapshot-run.ts +1 -1
  126. package/extensions/rewind/status.ts +3 -2
  127. package/extensions/subagent/README.md +6 -0
  128. package/extensions/subagent/index.ts +1 -1
  129. package/extensions/subagent/render.ts +108 -66
  130. package/extensions/todo/README.md +20 -13
  131. package/extensions/todo/format.ts +47 -34
  132. package/extensions/todo/tools.ts +6 -4
  133. package/extensions/todo/tui.ts +46 -26
  134. package/extensions/watch/README.md +16 -8
  135. package/extensions/watch/format.ts +33 -13
  136. package/extensions/watch/index.ts +29 -24
  137. package/extensions/watch/tools.ts +16 -9
  138. package/extensions/watch/tui.ts +142 -196
  139. package/extensions/watch/ui.ts +24 -46
  140. package/extensions/web-access/README.md +16 -7
  141. package/extensions/web-access/fetch/tool.ts +12 -11
  142. package/extensions/web-access/search/tool.ts +16 -13
  143. package/extensions/wechat/README.md +8 -2
  144. package/extensions/wechat/index.ts +4 -3
  145. package/extensions/wechat/login-screen.ts +8 -5
  146. package/extensions/wechat/status-screen.ts +41 -22
  147. package/extensions/wechat/tools.ts +6 -5
  148. package/extensions/worktree/README.md +6 -0
  149. package/extensions/worktree/enter.ts +1 -1
  150. package/extensions/worktree/index.ts +1 -1
  151. package/extensions/worktree/runtime.ts +1 -1
  152. package/lib/README.md +11 -7
  153. package/lib/format.ts +17 -0
  154. package/lib/notice.ts +87 -0
  155. package/lib/requirements.ts +2 -2
  156. package/lib/{tui.ts → tui/chrome.ts} +17 -34
  157. package/lib/tui/columns.ts +96 -0
  158. package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
  159. package/lib/tui/gauge.ts +78 -0
  160. package/lib/{ui.ts → tui/glyphs.ts} +10 -11
  161. package/lib/tui/indent.ts +166 -0
  162. package/lib/tui/index.ts +27 -0
  163. package/lib/tui/line-pane.ts +173 -0
  164. package/lib/tui/rows.ts +143 -0
  165. package/lib/tui/rules.ts +94 -0
  166. package/lib/tui/status-controller.ts +107 -0
  167. package/lib/tui/tokens.ts +72 -0
  168. package/lib/web/base.ts +2 -1
  169. package/lib/web/components.ts +3 -1
  170. package/lib/web/layout.ts +5 -1
  171. package/package.json +7 -3
  172. package/themes/bytefly-dark.json +7 -8
  173. package/themes/bytefly-light.json +10 -11
  174. package/assets/bytefly-chrome.png +0 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The transcript rail and the shared `chrome` section of `bytefly.json`.
3
+ *
4
+ * The rail is the vertical stroke that leads a block's body — a tool result, a
5
+ * notice row — while the headline stays at the margin. Its glyph is the one
6
+ * configurable part; the geometry is fixed by `lib/tui/indent.ts`. A glyph must
7
+ * be exactly one visible column, or it would push the body right and drift the
8
+ * width math, so `normalizeChromeConfig` rejects anything else and falls back.
9
+ *
10
+ * The value is read at `session_start`/`session_tree` (nothing watches the
11
+ * document), held in a `RailSource` closure created once per load, and read by
12
+ * the renderers at render time, so no module state lives here.
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
16
+ import { visibleWidth } from "@earendil-works/pi-tui";
17
+ import { cleanString, loadConfigSection } from "../../lib/config.ts";
18
+ import { GLYPHS } from "../../lib/tui/glyphs.ts";
19
+ import { paint, type TuiRole } from "../../lib/tui/tokens.ts";
20
+
21
+ /** How a railed block reads at a glance. */
22
+ export type RailStatus = "info" | "ok" | "warn" | "bad";
23
+
24
+ /**
25
+ * Status to design role: a rail is neutral by default, so a finished or merely
26
+ * informational block draws no colour and only `warn` and `bad` stand out. A
27
+ * surface whose `info` means *active* rather than *informational* brightens it
28
+ * itself — a running tool result takes the accent (see `block/config.ts`).
29
+ */
30
+ export const RAIL_ROLES: Readonly<Record<RailStatus, TuiRole>> = {
31
+ info: "info",
32
+ ok: "faint",
33
+ warn: "warn",
34
+ bad: "bad",
35
+ };
36
+
37
+ /** The rail glyph, painted in the role. `lib/tui/indent.ts` adds the gap. */
38
+ export function railGlyph(theme: Theme, role: TuiRole, glyph: string): string {
39
+ return paint(theme, role, glyph);
40
+ }
41
+
42
+ /** Transcript chrome presentation. */
43
+ export interface ChromeConfig {
44
+ /** The rail glyph: exactly one visible column. */
45
+ rail: string;
46
+ }
47
+
48
+ /** The default rail glyph, from the shared vocabulary. */
49
+ export const DEFAULT_RAIL_GLYPH: string = GLYPHS.divider;
50
+
51
+ export const DEFAULT_CHROME_CONFIG: ChromeConfig = { rail: DEFAULT_RAIL_GLYPH };
52
+
53
+ /** Whether `value` is a single, unstyled terminal column. */
54
+ function isRailGlyph(value: string): boolean {
55
+ // Control characters first: a bare ESC confuses a width measurement.
56
+ if (/[\u0000-\u001f\u007f]/.test(value)) return false;
57
+ return [...value].length === 1 && visibleWidth(value) === 1;
58
+ }
59
+
60
+ /** Validate the `chrome` section over `base`. Total: never throws, never guesses. */
61
+ export function normalizeChromeConfig(raw: Record<string, unknown> | undefined, base: ChromeConfig): ChromeConfig {
62
+ if (!raw) return base;
63
+ const rail = cleanString(raw.rail, "");
64
+ return { rail: isRailGlyph(rail) ? rail : base.rail };
65
+ }
66
+
67
+ /** Effective chrome config for `cwd` (defaults, then the shared `chrome` section). */
68
+ export function loadChromeConfig(cwd: string): ChromeConfig {
69
+ return loadConfigSection(cwd, "chrome", DEFAULT_CHROME_CONFIG, normalizeChromeConfig);
70
+ }
71
+
72
+ /** Where a render-time surface reads the current rail glyph. */
73
+ export interface RailSource {
74
+ /** The configured glyph. */
75
+ resolve(): string;
76
+ /** Replace the glyph; a value that is not one column keeps the previous one. */
77
+ set(glyph: string): void;
78
+ }
79
+
80
+ /** A store holding the configured rail glyph, created once per extension load. */
81
+ export function createRailSource(initial: string = DEFAULT_RAIL_GLYPH): RailSource {
82
+ let glyph = isRailGlyph(initial) ? initial : DEFAULT_RAIL_GLYPH;
83
+ return {
84
+ resolve: () => glyph,
85
+ set(next: string) {
86
+ if (isRailGlyph(next)) glyph = next;
87
+ },
88
+ };
89
+ }
90
+
91
+ /** Register the chrome lifecycle and hand back the rail source it keeps current. */
92
+ export function registerChrome(pi: ExtensionAPI, rail: RailSource = createRailSource()): RailSource {
93
+ const load = (ctx: ExtensionContext): void => rail.set(loadChromeConfig(ctx.cwd).rail);
94
+ pi.on("session_start", (_event, ctx) => load(ctx));
95
+ pi.on("session_tree", (_event, ctx) => load(ctx));
96
+ return rail;
97
+ }
@@ -12,7 +12,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
13
13
  import { sanitizeLine } from "../../../lib/format.ts";
14
14
  import type { ExtensionReport, Finding, FindingSeverity } from "../../../lib/requirements.ts";
15
- import { GLYPHS } from "../../../lib/ui.ts";
15
+ import { GLYPHS } from "../../../lib/tui/glyphs.ts";
16
+ import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
16
17
 
17
18
  /** Counts the report headline uses. */
18
19
  export interface ReportCounts {
@@ -102,15 +103,40 @@ export function formatWarning(findings: readonly Finding[]): string {
102
103
  byExtension.set(finding.extension, list);
103
104
  }
104
105
  const parts = [...byExtension].map(([extension, checks]) => `${sanitizeLine(extension)} (${checks.join(", ")})`);
105
- return `doctor: missing ${parts.join("; ")} — run /doctor`;
106
+ return `doctor: missing ${parts.join("; ")} — run /bytefly doctor`;
106
107
  }
107
108
 
108
- /** The severity glyph, from the shared vocabulary. */
109
+ /** The design role a severity is drawn in. */
110
+ export function severityRole(severity: FindingSeverity): TuiRole {
111
+ switch (severity) {
112
+ case "missing":
113
+ return "bad";
114
+ case "degraded":
115
+ return "warn";
116
+ case "info":
117
+ return "faint";
118
+ default:
119
+ return "ok";
120
+ }
121
+ }
122
+
123
+ /** The bare severity glyph, from the shared vocabulary. */
124
+ export function severityGlyphChar(severity: FindingSeverity): string {
125
+ switch (severity) {
126
+ case "missing":
127
+ return GLYPHS.cross;
128
+ case "degraded":
129
+ return GLYPHS.warn;
130
+ case "info":
131
+ return GLYPHS.disabled;
132
+ default:
133
+ return GLYPHS.check;
134
+ }
135
+ }
136
+
137
+ /** The severity glyph, themed by role. */
109
138
  export function severityGlyph(severity: FindingSeverity, theme: Theme): string {
110
- if (severity === "missing") return theme.fg("error", GLYPHS.cross);
111
- if (severity === "degraded") return theme.fg("warning", GLYPHS.warn);
112
- if (severity === "info") return theme.fg("dim", GLYPHS.disabled);
113
- return theme.fg("success", GLYPHS.check);
139
+ return paint(theme, severityRole(severity), severityGlyphChar(severity));
114
140
  }
115
141
 
116
142
  /** The severity that represents an extension: worst finding wins. */
@@ -147,7 +173,7 @@ export function detailLines(
147
173
  const budget = Math.max(1, rows);
148
174
  const shown = includeInfo ? report.findings : report.findings.filter((finding) => finding.severity !== "info");
149
175
  if (shown.length === 0) {
150
- return [` ${theme.fg("dim", report.enabled ? "no problems" : "disabled")}`];
176
+ return [` ${paint(theme, "faint", report.enabled ? "no problems" : "disabled")}`];
151
177
  }
152
178
 
153
179
  const lines: string[] = [];
@@ -160,10 +186,10 @@ export function detailLines(
160
186
  }
161
187
  if (finding.hint) {
162
188
  for (const line of wrapTextWithAnsi(`→ ${sanitizeLine(finding.hint)}`, Math.max(1, width - 4))) {
163
- lines.push(` ${theme.fg("dim", line)}`);
189
+ lines.push(` ${paint(theme, "faint", line)}`);
164
190
  }
165
191
  }
166
192
  }
167
193
  if (lines.length <= budget) return lines;
168
- return [...lines.slice(0, budget - 1), ` ${theme.fg("dim", "…")}`];
194
+ return [...lines.slice(0, budget - 1), ` ${paint(theme, "faint", "…")}`];
169
195
  }
@@ -8,8 +8,10 @@
8
8
  */
9
9
 
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { sanitizeLine } from "../../../lib/format.ts";
11
12
  import { missingFindings } from "../../../lib/requirements.ts";
12
- import { reuseText, toolDetails } from "../../../lib/tui.ts";
13
+ import { reuseText, toolDetails } from "../../../lib/tui/chrome.ts";
14
+ import { paint, type TuiRole } from "../../../lib/tui/tokens.ts";
13
15
  import { formatExtension, formatReport, isProblemSeverity } from "./format.ts";
14
16
  import type { DoctorRuntime } from "./runtime.ts";
15
17
  import { DoctorOutput, DoctorParams, TOOL_NAME, type DoctorArgs, type DoctorStructured } from "./schema.ts";
@@ -91,22 +93,24 @@ export function registerTools(pi: ExtensionAPI, runtime: DoctorRuntime): void {
91
93
 
92
94
  renderCall(args, theme, context) {
93
95
  const input = args as DoctorArgs;
94
- const scope = `${input.extension ? ` ${input.extension}` : ""}${input.refresh ? " (refresh)" : ""}`;
96
+ const scope = typeof input.extension === "string" && input.extension ? sanitizeLine(input.extension) : "";
95
97
  const text = reuseText(context?.lastComponent);
96
- text.setText(`${theme.fg("toolTitle", "doctor")}${theme.fg("dim", scope)}`);
98
+ text.setText(
99
+ `${theme.fg("toolTitle", theme.bold(TOOL_NAME))}${scope ? ` ${paint(theme, "label", scope)}` : ""}${input.refresh ? paint(theme, "faint", " (refresh)") : ""}`,
100
+ );
97
101
  return text;
98
102
  },
99
103
 
100
104
  renderResult(result, _options, theme, context) {
101
105
  const structured = toolDetails<DoctorStructured>(result.details);
102
- const tone = (structured?.missing ?? 0) > 0 ? "error" : (structured?.degraded ?? 0) > 0 ? "warning" : "success";
106
+ const tone: TuiRole = (structured?.missing ?? 0) > 0 ? "bad" : (structured?.degraded ?? 0) > 0 ? "warn" : "ok";
103
107
  const text = reuseText(context?.lastComponent);
104
108
  if (!structured) {
105
109
  const first = result.content[0];
106
- text.setText(first?.type === "text" ? first.text : theme.fg("muted", "doctor: no result"));
110
+ text.setText(first?.type === "text" ? first.text : paint(theme, "meta", "doctor: no result"));
107
111
  return text;
108
112
  }
109
- text.setText(theme.fg(tone, summaryLines(structured).join("\n")));
113
+ text.setText(paint(theme, tone, summaryLines(structured).join("\n")));
110
114
  return text;
111
115
  },
112
116
  });
@@ -7,7 +7,7 @@
7
7
  * configuration is `bytefly.json`, read by `../price/config.ts`.
8
8
  */
9
9
 
10
- import { GLYPHS, SEPARATORS, STATUS_KEYS } from "../../../lib/ui.ts";
10
+ import { GLYPHS, SEPARATORS, STATUS_KEYS } from "../../../lib/tui/glyphs.ts";
11
11
 
12
12
  export const CONFIG = {
13
13
  /** Context percentage thresholds. */
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
9
- import type { ThemeColor } from "@earendil-works/pi-coding-agent";
10
9
  import { stripTerminalSequences, truncateToWidth } from "@earendil-works/pi-tui";
11
10
  import { shortenPath } from "../../../../lib/format.ts";
11
+ import { gaugeRole } from "../../../../lib/tui/gauge.ts";
12
+ import type { TuiRole } from "../../../../lib/tui/tokens.ts";
12
13
  import { CONFIG } from "../config.ts";
13
14
  import { DEFAULT_CURRENCY } from "../price/config.ts";
14
15
  import type { Currency } from "../price/types.ts";
@@ -65,23 +66,9 @@ export function formatCwd(cwd: string, home: string | undefined, level: 0 | 1 |
65
66
  return parts.length <= 2 ? shortened : `…/${parts.slice(-2).join("/")}`;
66
67
  }
67
68
 
68
- /** Filled/empty block counts for a percentage across `blocks` cells. */
69
- export function computeGauge(percent: number | null, blocks: number): { filled: number; empty: number } {
70
- if (blocks <= 0) return { filled: 0, empty: 0 };
71
- if (percent === null) return { filled: 0, empty: blocks };
72
- const clamped = Math.max(0, Math.min(100, percent));
73
- // Floor so the bar is only full at 100%; force one block for any non-zero
74
- // usage so a small percentage is still visible.
75
- const filled = Math.max(clamped > 0 ? 1 : 0, Math.floor((clamped / 100) * blocks));
76
- return { filled, empty: blocks - filled };
77
- }
78
-
79
- /** Gauge color for a context percentage. Neutral until it crosses a threshold. */
80
- export function contextColor(percent: number | null): ThemeColor {
81
- if (percent === null) return "muted";
82
- if (percent > CONFIG.thresholds.danger) return "error";
83
- if (percent > CONFIG.thresholds.warn) return "warning";
84
- return "muted";
69
+ /** The design role a context percentage is drawn in. Neutral until it crosses a threshold. */
70
+ export function contextRole(percent: number | null): TuiRole {
71
+ return gaugeRole(percent, CONFIG.thresholds);
85
72
  }
86
73
 
87
74
  const THINKING_TOKENS: Record<ThinkingLevel, ThinkingColor> = {
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
13
+ import { paint } from "../../../../lib/tui/tokens.ts";
13
14
  import { CONFIG } from "../config.ts";
14
15
  import type { LineSpec, Segment } from "../types.ts";
15
16
 
@@ -135,5 +136,5 @@ export function renderLine(spec: LineSpec, width: number, theme: Theme): string
135
136
  if (!collapseSeparator(states)) break;
136
137
  }
137
138
 
138
- return truncateToWidth(assemble(states, target), target, theme.fg("dim", "…"));
139
+ return truncateToWidth(assemble(states, target), target, paint(theme, "faint", "…"));
139
140
  }
@@ -3,26 +3,23 @@
3
3
  *
4
4
  * Each segment carries progressively shorter `forms` and a `weight`; the layout
5
5
  * engine advances forms and drops segments from the highest weight down, so the
6
- * bar degrades predictably instead of clipping arbitrary text.
6
+ * bar degrades predictably instead of clipping arbitrary text. Colors come from
7
+ * the design-system roles, so the bar follows the active theme.
7
8
  */
8
9
 
9
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
11
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
11
12
  import { formatTokens, sanitize, sanitizeLine } from "../../../../lib/format.ts";
13
+ import { gaugeBar } from "../../../../lib/tui/gauge.ts";
14
+ import { chip } from "../../../../lib/tui/rows.ts";
15
+ import { paint } from "../../../../lib/tui/tokens.ts";
12
16
  import { CONFIG } from "../config.ts";
13
- import {
14
- computeGauge,
15
- contextColor,
16
- formatCost,
17
- formatCwd,
18
- formatPercent,
19
- thinkingColor,
20
- truncateLabel,
21
- } from "./format.ts";
17
+ import { contextRole, formatCost, formatCwd, formatPercent, thinkingColor, truncateLabel } from "./format.ts";
22
18
  import { compactStatus, dropStatusDetail } from "./status.ts";
23
19
  import type { Segment, StatusSnapshot } from "../types.ts";
24
20
 
25
- const dim = (theme: Theme, text: string): string => theme.fg("dim", text);
21
+ /** Tertiary text: paths and de-emphasized meters. */
22
+ const faint = (theme: Theme, text: string): string => paint(theme, "faint", text);
26
23
 
27
24
  /**
28
25
  * The label part of a themed status chip: strip its escape sequences, sanitize
@@ -41,9 +38,9 @@ export function pwdSegment(snapshot: StatusSnapshot, theme: Theme, home: string
41
38
  droppable: false,
42
39
  separator: "",
43
40
  forms: [
44
- dim(theme, formatCwd(snapshot.cwd, home, 0)),
45
- dim(theme, formatCwd(snapshot.cwd, home, 1)),
46
- dim(theme, formatCwd(snapshot.cwd, home, 2)),
41
+ faint(theme, formatCwd(snapshot.cwd, home, 0)),
42
+ faint(theme, formatCwd(snapshot.cwd, home, 1)),
43
+ faint(theme, formatCwd(snapshot.cwd, home, 2)),
47
44
  ],
48
45
  };
49
46
  }
@@ -54,13 +51,13 @@ export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment
54
51
  const short = truncateLabel(name, CONFIG.sessionLabelMax);
55
52
  // Brackets and a brighter token keep the session name from reading as a
56
53
  // trailing path segment.
57
- const forms = [theme.fg("muted", `[${name}]`)];
58
- if (short !== name) forms.push(theme.fg("muted", `[${short}]`));
54
+ const forms = [paint(theme, "meta", `[${name}]`)];
55
+ if (short !== name) forms.push(paint(theme, "meta", `[${short}]`));
59
56
  return {
60
57
  id: "session",
61
58
  weight: 3,
62
59
  droppable: true,
63
- separator: dim(theme, CONFIG.separators.item),
60
+ separator: faint(theme, CONFIG.separators.item),
64
61
  forms,
65
62
  };
66
63
  }
@@ -68,14 +65,13 @@ export function sessionSegment(snapshot: StatusSnapshot, theme: Theme): Segment
68
65
  export function branchSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
69
66
  if (!snapshot.branch) return null;
70
67
  const detached = snapshot.branch === CONFIG.labels.detached;
71
- const color = detached ? "warning" : "muted";
72
68
  const icon = detached ? CONFIG.icons.detached : CONFIG.icons.branch;
73
69
  return {
74
70
  id: "branch",
75
71
  weight: 1,
76
72
  droppable: true,
77
- separator: dim(theme, CONFIG.separators.item),
78
- forms: [theme.fg(color, `${icon} ${snapshot.branch}`)],
73
+ separator: faint(theme, CONFIG.separators.item),
74
+ forms: [chip(theme, icon, snapshot.branch, detached ? "warn" : "meta")],
79
75
  };
80
76
  }
81
77
 
@@ -83,27 +79,16 @@ export function worktreeSegment(snapshot: StatusSnapshot, theme: Theme): Segment
83
79
  const raw = snapshot.statuses.get(CONFIG.worktreeStatusKey);
84
80
  if (!raw) return null;
85
81
  const icon = CONFIG.icons.worktree;
82
+ const separator = faint(theme, CONFIG.separators.item);
86
83
  const label = chipLabel(raw);
87
84
  if (!label) {
88
- return {
89
- id: "worktree",
90
- weight: 3,
91
- droppable: true,
92
- separator: dim(theme, CONFIG.separators.item),
93
- forms: [theme.fg("success", icon)],
94
- };
85
+ return { id: "worktree", weight: 3, droppable: true, separator, forms: [paint(theme, "ok", icon)] };
95
86
  }
96
87
  const short = truncateLabel(label, CONFIG.worktreeLabelMax);
97
- const forms = [theme.fg("success", `${icon} ${label}`)];
98
- if (short !== label) forms.push(theme.fg("success", `${icon} ${short}`));
99
- forms.push(theme.fg("success", icon));
100
- return {
101
- id: "worktree",
102
- weight: 3,
103
- droppable: true,
104
- separator: dim(theme, CONFIG.separators.item),
105
- forms,
106
- };
88
+ const forms = [chip(theme, icon, label, "ok")];
89
+ if (short !== label) forms.push(chip(theme, icon, short, "ok"));
90
+ forms.push(paint(theme, "ok", icon));
91
+ return { id: "worktree", weight: 3, droppable: true, separator, forms };
107
92
  }
108
93
 
109
94
  export function browseSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
@@ -111,25 +96,20 @@ export function browseSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
111
96
  if (!raw) return null;
112
97
  const icon = CONFIG.icons.browse;
113
98
  const label = chipLabel(raw);
114
- const separator = dim(theme, CONFIG.separators.item);
99
+ const separator = faint(theme, CONFIG.separators.item);
115
100
  if (!label) {
116
- return {
117
- id: "browse",
118
- weight: 3,
119
- droppable: true,
120
- separator,
121
- forms: [theme.fg("success", icon)],
122
- };
101
+ return { id: "browse", weight: 3, droppable: true, separator, forms: [paint(theme, "ok", icon)] };
123
102
  }
103
+ // A two-tone chip: the `◉` glyph stays green while the port reads as accent.
124
104
  return {
125
105
  id: "browse",
126
106
  weight: 3,
127
107
  droppable: true,
128
108
  separator,
129
109
  forms: [
130
- `${theme.fg("success", icon)} ${theme.fg("accent", label)}`,
131
- `${theme.fg("success", icon)}${theme.fg("accent", label)}`,
132
- theme.fg("success", icon),
110
+ `${paint(theme, "ok", icon)} ${paint(theme, "accent", label)}`,
111
+ `${paint(theme, "ok", icon)}${paint(theme, "accent", label)}`,
112
+ paint(theme, "ok", icon),
133
113
  ],
134
114
  };
135
115
  }
@@ -150,7 +130,7 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
150
130
  })
151
131
  .filter(Boolean);
152
132
  if (others.length === 0) return null;
153
- const full = others.join(dim(theme, CONFIG.separators.item));
133
+ const full = others.join(faint(theme, CONFIG.separators.item));
154
134
  // The compact form keeps each status's own colour (see `compactStatus`); a
155
135
  // two-token badge keeps its count (`↺ 2` -> `↺2`) instead of collapsing to a
156
136
  // bare icon.
@@ -161,31 +141,22 @@ export function modesSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
161
141
  // than truncating the model, which sits at the end of the line.
162
142
  weight: 1,
163
143
  droppable: true,
164
- separator: dim(theme, CONFIG.separators.group),
144
+ separator: faint(theme, CONFIG.separators.group),
165
145
  forms: [full, icons],
166
146
  };
167
147
  }
168
148
 
169
149
  export function contextSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
170
150
  const percent = snapshot.context.percent;
171
- const color = contextColor(percent);
172
- const percentText = theme.fg(color, formatPercent(percent));
173
- const gaugeWidths: readonly number[] = percent === null ? [0] : CONFIG.gauge.widths;
174
- const forms = gaugeWidths.map((blocks) => {
175
- const { filled, empty } = computeGauge(percent, blocks);
176
- const gauge =
177
- blocks > 0
178
- ? theme.fg(color, CONFIG.gauge.full.repeat(filled)) + theme.fg("dim", CONFIG.gauge.empty.repeat(empty)) + " "
179
- : "";
180
- return `${gauge}${percentText}`;
181
- });
182
- return {
183
- id: "context",
184
- weight: 1,
185
- droppable: false,
186
- separator: "",
187
- forms,
188
- };
151
+ const role = contextRole(percent);
152
+ const percentText = paint(theme, role, formatPercent(percent));
153
+ // Unknown usage draws no track, so `?` sits where the gauge would be rather
154
+ // than a full row of empty blocks claiming a known zero.
155
+ const widths: readonly number[] = percent === null ? [0] : CONFIG.gauge.widths;
156
+ const forms = widths.map((blocks) =>
157
+ blocks > 0 ? `${gaugeBar(theme, { percent, blocks, role, emptyRole: "faint" })} ${percentText}` : percentText,
158
+ );
159
+ return { id: "context", weight: 1, droppable: false, separator: "", forms };
189
160
  }
190
161
 
191
162
  export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
@@ -195,7 +166,7 @@ export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
195
166
  weight: 3,
196
167
  droppable: true,
197
168
  separator: "",
198
- forms: [dim(theme, `/${formatTokens(snapshot.context.contextWindow)}`)],
169
+ forms: [faint(theme, `/${formatTokens(snapshot.context.contextWindow)}`)],
199
170
  };
200
171
  }
201
172
 
@@ -205,8 +176,8 @@ export function costSegment(snapshot: StatusSnapshot, theme: Theme): Segment | n
205
176
  id: "cost",
206
177
  weight: 2,
207
178
  droppable: true,
208
- separator: dim(theme, CONFIG.separators.item),
209
- forms: [theme.fg("muted", formatCost(snapshot.usage.cost, snapshot.currency))],
179
+ separator: faint(theme, CONFIG.separators.item),
180
+ forms: [paint(theme, "meta", formatCost(snapshot.usage.cost, snapshot.currency))],
210
181
  };
211
182
  }
212
183
 
@@ -220,8 +191,8 @@ export function tokensSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
220
191
  id: "tokens",
221
192
  weight: 4,
222
193
  droppable: true,
223
- separator: dim(theme, CONFIG.separators.item),
224
- forms: [dim(theme, parts.join(" "))],
194
+ separator: faint(theme, CONFIG.separators.item),
195
+ forms: [faint(theme, parts.join(" "))],
225
196
  };
226
197
  }
227
198
 
@@ -240,8 +211,8 @@ export function cacheSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
240
211
  id: "cache",
241
212
  weight: 5,
242
213
  droppable: true,
243
- separator: dim(theme, CONFIG.separators.item),
244
- forms: [dim(theme, parts.join(" "))],
214
+ separator: faint(theme, CONFIG.separators.item),
215
+ forms: [faint(theme, parts.join(" "))],
245
216
  };
246
217
  }
247
218
 
@@ -254,7 +225,7 @@ export function modelSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
254
225
  weight: 1,
255
226
  droppable: false,
256
227
  separator: "",
257
- forms: [theme.fg("muted", CONFIG.labels.noModel)],
228
+ forms: [paint(theme, "meta", CONFIG.labels.noModel)],
258
229
  };
259
230
  }
260
231
  const provider = snapshot.providerCount > 1 ? `(${model.provider}) ` : "";
@@ -263,19 +234,21 @@ export function modelSegment(snapshot: StatusSnapshot, theme: Theme): Segment {
263
234
  weight: 1,
264
235
  droppable: false,
265
236
  separator: "",
266
- forms: [theme.fg("accent", `${provider}${model.id}`), theme.fg("accent", model.id)],
237
+ forms: [paint(theme, "accent", `${provider}${model.id}`), paint(theme, "accent", model.id)],
267
238
  };
268
239
  }
269
240
 
270
241
  export function thinkingSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
271
242
  if (!snapshot.model?.reasoning) return null;
272
243
  const level = snapshot.thinkingLevel ?? "off";
244
+ // The reasoning scale is Pi's own seven-token ramp, not a design role: a
245
+ // level colors itself from `thinkingColor`.
273
246
  const color = thinkingColor(level);
274
247
  return {
275
248
  id: "thinking",
276
249
  weight: 2,
277
250
  droppable: true,
278
- separator: dim(theme, CONFIG.separators.item),
251
+ separator: faint(theme, CONFIG.separators.item),
279
252
  forms: [theme.fg(color, level)],
280
253
  };
281
254
  }