@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.
- package/README.md +35 -8
- package/README.zh-CN.md +33 -8
- package/assets/bytefly.schema.json +17 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
- package/extensions/bytefly/README.md +306 -68
- package/extensions/bytefly/block/component.ts +282 -0
- package/extensions/bytefly/block/config.ts +24 -0
- package/extensions/bytefly/block/fallback.ts +120 -0
- package/extensions/bytefly/block/register.ts +164 -0
- package/extensions/bytefly/chrome.ts +97 -0
- package/extensions/bytefly/doctor/format.ts +36 -10
- package/extensions/bytefly/doctor/tools.ts +10 -6
- package/extensions/bytefly/footer/config.ts +1 -1
- package/extensions/bytefly/footer/render/format.ts +5 -18
- package/extensions/bytefly/footer/render/layout.ts +2 -1
- package/extensions/bytefly/footer/render/segments.ts +51 -78
- package/extensions/bytefly/header/component.ts +49 -21
- package/extensions/bytefly/header/config.ts +28 -21
- package/extensions/bytefly/header/register.ts +6 -1
- package/extensions/bytefly/index.ts +32 -12
- package/extensions/bytefly/message/config.ts +15 -0
- package/extensions/bytefly/message/format.ts +42 -0
- package/extensions/bytefly/message/register.ts +15 -0
- package/extensions/bytefly/notice/component.ts +107 -0
- package/extensions/bytefly/notice/config.ts +41 -0
- package/extensions/bytefly/notice/register.ts +27 -0
- package/extensions/bytefly/pad.ts +42 -0
- package/extensions/bytefly/panel/commands.ts +69 -35
- package/extensions/bytefly/panel/format.ts +8 -8
- package/extensions/bytefly/panel/tui.ts +45 -70
- package/extensions/bytefly/separator/component.ts +2 -1
- package/extensions/bytefly/separator/config.ts +16 -8
- package/extensions/bytefly/separator/format.ts +18 -38
- package/extensions/bytefly/separator/register.ts +3 -2
- package/extensions/bytefly/separator/types.ts +0 -10
- package/extensions/bytefly/working/config.ts +20 -0
- package/extensions/bytefly/working/format.ts +25 -0
- package/extensions/bytefly/working/register.ts +88 -0
- package/extensions/document/README.md +12 -5
- package/extensions/document/paging.ts +10 -10
- package/extensions/document/render.ts +17 -16
- package/extensions/document/tool.ts +1 -1
- package/extensions/file-browse/README.md +54 -10
- package/extensions/file-browse/client.ts +110 -2
- package/extensions/file-browse/config.ts +12 -0
- package/extensions/file-browse/diff.ts +187 -0
- package/extensions/file-browse/git.ts +180 -8
- package/extensions/file-browse/html.ts +401 -25
- package/extensions/file-browse/index.ts +15 -4
- package/extensions/file-browse/markdown.ts +33 -0
- package/extensions/file-browse/router.ts +94 -2
- package/extensions/file-browse/server.ts +4 -1
- package/extensions/file-open/README.md +6 -0
- package/extensions/goal/README.md +16 -10
- package/extensions/goal/format.ts +13 -25
- package/extensions/goal/tools.ts +11 -22
- package/extensions/goal/tui.ts +12 -40
- package/extensions/job/README.md +22 -13
- package/extensions/job/format.ts +36 -13
- package/extensions/job/index.ts +26 -21
- package/extensions/job/tools.ts +15 -12
- package/extensions/job/tui.ts +148 -222
- package/extensions/job/ui.ts +24 -49
- package/extensions/memory/README.md +7 -1
- package/extensions/memory/format.ts +5 -3
- package/extensions/memory/tools.ts +4 -3
- package/extensions/memory/tui.ts +32 -36
- package/extensions/plan/README.md +86 -36
- package/extensions/plan/blocks.ts +44 -2
- package/extensions/plan/feedback.ts +20 -5
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/runtime.ts +3 -2
- package/extensions/plan/tools.ts +43 -27
- package/extensions/plan/tui.ts +21 -27
- package/extensions/plan/web-client.ts +386 -40
- package/extensions/plan/web-page.ts +138 -33
- package/extensions/plan/web-router.ts +46 -13
- package/extensions/plan/web.ts +63 -12
- package/extensions/question/README.md +14 -2
- package/extensions/question/answers.ts +25 -18
- package/extensions/question/tools.ts +22 -16
- package/extensions/question/tui.ts +132 -76
- package/extensions/rewind/README.md +3 -1
- package/extensions/rewind/format.ts +35 -2
- package/extensions/rewind/index.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +66 -62
- package/extensions/rewind/snapshot-run.ts +1 -1
- package/extensions/rewind/status.ts +3 -2
- package/extensions/subagent/README.md +6 -0
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +108 -66
- package/extensions/todo/README.md +20 -13
- package/extensions/todo/format.ts +47 -34
- package/extensions/todo/tools.ts +6 -4
- package/extensions/todo/tui.ts +46 -26
- package/extensions/watch/README.md +16 -8
- package/extensions/watch/format.ts +33 -13
- package/extensions/watch/index.ts +29 -24
- package/extensions/watch/tools.ts +16 -9
- package/extensions/watch/tui.ts +142 -196
- package/extensions/watch/ui.ts +24 -46
- package/extensions/web-access/README.md +16 -7
- package/extensions/web-access/fetch/tool.ts +12 -11
- package/extensions/web-access/search/tool.ts +16 -13
- package/extensions/wechat/README.md +8 -2
- package/extensions/wechat/index.ts +4 -3
- package/extensions/wechat/login-screen.ts +8 -5
- package/extensions/wechat/status-screen.ts +41 -22
- package/extensions/wechat/tools.ts +6 -5
- package/extensions/worktree/README.md +6 -0
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/index.ts +1 -1
- package/extensions/worktree/runtime.ts +1 -1
- package/lib/README.md +11 -7
- package/lib/format.ts +17 -0
- package/lib/notice.ts +87 -0
- package/lib/requirements.ts +2 -2
- package/lib/{tui.ts → tui/chrome.ts} +17 -34
- package/lib/tui/columns.ts +96 -0
- package/lib/{list-cursor.ts → tui/cursor.ts} +58 -4
- package/lib/tui/gauge.ts +78 -0
- package/lib/{ui.ts → tui/glyphs.ts} +10 -11
- package/lib/tui/indent.ts +166 -0
- package/lib/tui/index.ts +27 -0
- package/lib/tui/line-pane.ts +173 -0
- package/lib/tui/rows.ts +143 -0
- package/lib/tui/rules.ts +94 -0
- package/lib/tui/status-controller.ts +107 -0
- package/lib/tui/tokens.ts +72 -0
- package/lib/web/base.ts +2 -1
- package/lib/web/components.ts +3 -1
- package/lib/web/layout.ts +5 -1
- package/package.json +7 -3
- package/themes/bytefly-dark.json +7 -8
- package/themes/bytefly-light.json +10 -11
- 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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
96
|
+
const scope = typeof input.extension === "string" && input.extension ? sanitizeLine(input.extension) : "";
|
|
95
97
|
const text = reuseText(context?.lastComponent);
|
|
96
|
-
text.setText(
|
|
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 ? "
|
|
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
|
|
110
|
+
text.setText(first?.type === "text" ? first.text : paint(theme, "meta", "doctor: no result"));
|
|
107
111
|
return text;
|
|
108
112
|
}
|
|
109
|
-
text.setText(theme
|
|
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/
|
|
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
|
-
/**
|
|
69
|
-
export function
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
58
|
-
if (short !== name) forms.push(theme
|
|
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:
|
|
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:
|
|
78
|
-
forms: [theme
|
|
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
|
|
98
|
-
if (short !== label) forms.push(theme
|
|
99
|
-
forms.push(theme
|
|
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 =
|
|
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
|
|
131
|
-
`${theme
|
|
132
|
-
theme
|
|
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(
|
|
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:
|
|
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
|
|
172
|
-
const percentText = theme
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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: [
|
|
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:
|
|
209
|
-
forms: [theme
|
|
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:
|
|
224
|
-
forms: [
|
|
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:
|
|
244
|
-
forms: [
|
|
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
|
|
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
|
|
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:
|
|
251
|
+
separator: faint(theme, CONFIG.separators.item),
|
|
279
252
|
forms: [theme.fg(color, level)],
|
|
280
253
|
};
|
|
281
254
|
}
|