pi-chassis 0.0.0-stage → 0.7.1

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 (169) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/LICENSE +21 -0
  3. package/README.md +51 -2
  4. package/THIRD_PARTY_NOTICES +30 -0
  5. package/dist/chassis/alert.d.ts +50 -0
  6. package/dist/chassis/alert.js +107 -0
  7. package/dist/chassis/api.d.ts +411 -0
  8. package/dist/chassis/api.js +676 -0
  9. package/dist/chassis/bash-output.d.ts +89 -0
  10. package/dist/chassis/bash-output.js +157 -0
  11. package/dist/chassis/brand.d.ts +99 -0
  12. package/dist/chassis/brand.js +79 -0
  13. package/dist/chassis/client.d.ts +87 -0
  14. package/dist/chassis/client.js +271 -0
  15. package/dist/chassis/commands.d.ts +60 -0
  16. package/dist/chassis/commands.js +211 -0
  17. package/dist/chassis/editor.d.ts +70 -0
  18. package/dist/chassis/editor.js +213 -0
  19. package/dist/chassis/footer.d.ts +14 -0
  20. package/dist/chassis/footer.js +136 -0
  21. package/dist/chassis/header.d.ts +12 -0
  22. package/dist/chassis/header.js +38 -0
  23. package/dist/chassis/home.d.ts +48 -0
  24. package/dist/chassis/home.js +184 -0
  25. package/dist/chassis/icons.d.ts +203 -0
  26. package/dist/chassis/icons.js +190 -0
  27. package/dist/chassis/index.d.ts +21 -0
  28. package/dist/chassis/index.js +582 -0
  29. package/dist/chassis/layout.d.ts +40 -0
  30. package/dist/chassis/layout.js +264 -0
  31. package/dist/chassis/message-modes.d.ts +21 -0
  32. package/dist/chassis/message-modes.js +49 -0
  33. package/dist/chassis/messages.d.ts +75 -0
  34. package/dist/chassis/messages.js +392 -0
  35. package/dist/chassis/modes.d.ts +21 -0
  36. package/dist/chassis/modes.js +99 -0
  37. package/dist/chassis/palette.d.ts +84 -0
  38. package/dist/chassis/palette.js +235 -0
  39. package/dist/chassis/peek.d.ts +61 -0
  40. package/dist/chassis/peek.js +156 -0
  41. package/dist/chassis/prompt-row.d.ts +42 -0
  42. package/dist/chassis/prompt-row.js +68 -0
  43. package/dist/chassis/provider-view.d.ts +34 -0
  44. package/dist/chassis/provider-view.js +107 -0
  45. package/dist/chassis/provider.d.ts +69 -0
  46. package/dist/chassis/provider.js +73 -0
  47. package/dist/chassis/readonly-command.d.ts +19 -0
  48. package/dist/chassis/readonly-command.js +454 -0
  49. package/dist/chassis/sanitize.d.ts +42 -0
  50. package/dist/chassis/sanitize.js +103 -0
  51. package/dist/chassis/session-info.d.ts +39 -0
  52. package/dist/chassis/session-info.js +107 -0
  53. package/dist/chassis/sidebar.d.ts +48 -0
  54. package/dist/chassis/sidebar.js +273 -0
  55. package/dist/chassis/state.d.ts +38 -0
  56. package/dist/chassis/state.js +19 -0
  57. package/dist/chassis/telemetry.d.ts +60 -0
  58. package/dist/chassis/telemetry.js +191 -0
  59. package/dist/chassis/tools.d.ts +112 -0
  60. package/dist/chassis/tools.js +588 -0
  61. package/dist/chassis/version.d.ts +2 -0
  62. package/dist/chassis/version.js +20 -0
  63. package/dist/diff/git-diff.d.ts +71 -0
  64. package/dist/diff/git-diff.js +325 -0
  65. package/dist/diff/host.d.ts +34 -0
  66. package/dist/diff/host.js +50 -0
  67. package/dist/diff/index.d.ts +13 -0
  68. package/dist/diff/index.js +86 -0
  69. package/dist/diff/render.d.ts +67 -0
  70. package/dist/diff/render.js +196 -0
  71. package/dist/diff/types.d.ts +60 -0
  72. package/dist/diff/types.js +4 -0
  73. package/dist/diff/view.d.ts +118 -0
  74. package/dist/diff/view.js +528 -0
  75. package/dist/git/flows.d.ts +92 -0
  76. package/dist/git/flows.js +415 -0
  77. package/dist/git/forge.d.ts +26 -0
  78. package/dist/git/forge.js +41 -0
  79. package/dist/git/index.d.ts +20 -0
  80. package/dist/git/index.js +121 -0
  81. package/dist/git/repo.d.ts +64 -0
  82. package/dist/git/repo.js +186 -0
  83. package/dist/git/run.d.ts +38 -0
  84. package/dist/git/run.js +127 -0
  85. package/dist/git/settings.d.ts +8 -0
  86. package/dist/git/settings.js +38 -0
  87. package/dist/git/text.d.ts +51 -0
  88. package/dist/git/text.js +109 -0
  89. package/dist/widgets/container/detect.d.ts +23 -0
  90. package/dist/widgets/container/detect.js +67 -0
  91. package/dist/widgets/container/index.d.ts +9 -0
  92. package/dist/widgets/container/index.js +26 -0
  93. package/dist/widgets/mcp/index.d.ts +13 -0
  94. package/dist/widgets/mcp/index.js +50 -0
  95. package/dist/widgets/mcp/status.d.ts +50 -0
  96. package/dist/widgets/mcp/status.js +70 -0
  97. package/dist/widgets/session-todos/index.d.ts +12 -0
  98. package/dist/widgets/session-todos/index.js +52 -0
  99. package/dist/widgets/subagents/index.d.ts +16 -0
  100. package/dist/widgets/subagents/index.js +173 -0
  101. package/dist/widgets/subagents/status.d.ts +61 -0
  102. package/dist/widgets/subagents/status.js +198 -0
  103. package/dist/widgets/todo/index.d.ts +13 -0
  104. package/dist/widgets/todo/index.js +65 -0
  105. package/dist/widgets/todo/parse.d.ts +29 -0
  106. package/dist/widgets/todo/parse.js +122 -0
  107. package/docs/branding.md +25 -0
  108. package/docs/development.md +22 -0
  109. package/docs/diff-api.md +18 -0
  110. package/docs/git.md +23 -0
  111. package/docs/internals.md +13 -0
  112. package/docs/provider-status.md +23 -0
  113. package/docs/status-widgets.md +157 -0
  114. package/docs/tool-rows.md +50 -0
  115. package/docs/usage.md +62 -0
  116. package/docs/widgets.md +113 -0
  117. package/extensions/chassis/alert.ts +127 -0
  118. package/extensions/chassis/api.ts +967 -0
  119. package/extensions/chassis/bash-output.ts +186 -0
  120. package/extensions/chassis/brand.ts +151 -0
  121. package/extensions/chassis/client.ts +284 -0
  122. package/extensions/chassis/commands.ts +244 -0
  123. package/extensions/chassis/editor.ts +253 -0
  124. package/extensions/chassis/footer.ts +131 -0
  125. package/extensions/chassis/header.ts +43 -0
  126. package/extensions/chassis/home.ts +186 -0
  127. package/extensions/chassis/icons.ts +220 -0
  128. package/extensions/chassis/index.ts +605 -0
  129. package/extensions/chassis/layout.ts +298 -0
  130. package/extensions/chassis/message-modes.ts +48 -0
  131. package/extensions/chassis/messages.ts +403 -0
  132. package/extensions/chassis/modes.ts +108 -0
  133. package/extensions/chassis/palette.ts +258 -0
  134. package/extensions/chassis/peek.ts +167 -0
  135. package/extensions/chassis/prompt-row.ts +86 -0
  136. package/extensions/chassis/provider-view.ts +118 -0
  137. package/extensions/chassis/provider.ts +115 -0
  138. package/extensions/chassis/readonly-command.ts +440 -0
  139. package/extensions/chassis/sanitize.ts +108 -0
  140. package/extensions/chassis/session-info.ts +127 -0
  141. package/extensions/chassis/sidebar.ts +285 -0
  142. package/extensions/chassis/state.ts +50 -0
  143. package/extensions/chassis/telemetry.ts +222 -0
  144. package/extensions/chassis/tools.ts +618 -0
  145. package/extensions/chassis/version.ts +20 -0
  146. package/extensions/diff/git-diff.ts +365 -0
  147. package/extensions/diff/host.ts +58 -0
  148. package/extensions/diff/index.ts +101 -0
  149. package/extensions/diff/render.ts +217 -0
  150. package/extensions/diff/types.ts +67 -0
  151. package/extensions/diff/view.ts +523 -0
  152. package/extensions/git/flows.ts +485 -0
  153. package/extensions/git/forge.ts +54 -0
  154. package/extensions/git/index.ts +117 -0
  155. package/extensions/git/repo.ts +200 -0
  156. package/extensions/git/run.ts +143 -0
  157. package/extensions/git/settings.ts +38 -0
  158. package/extensions/git/text.ts +133 -0
  159. package/extensions/widgets/container/detect.ts +81 -0
  160. package/extensions/widgets/container/index.ts +29 -0
  161. package/extensions/widgets/mcp/index.ts +53 -0
  162. package/extensions/widgets/mcp/status.ts +94 -0
  163. package/extensions/widgets/session-todos/index.ts +55 -0
  164. package/extensions/widgets/subagents/index.ts +170 -0
  165. package/extensions/widgets/subagents/status.ts +237 -0
  166. package/extensions/widgets/todo/index.ts +66 -0
  167. package/extensions/widgets/todo/parse.ts +119 -0
  168. package/package.json +91 -4
  169. package/scripts/link-pi.mjs +76 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Icons: every glyph chassis draws from a Nerd Font lives here, each with a plain twin that needs
3
+ * no special font. The plain twin takes the same number of cells as the Nerd glyph (one, or none
4
+ * where the words beside it already say it), so no layout changes between the two sets.
5
+ *
6
+ * Which set is used, first match wins:
7
+ * 1. the PI_CHASSIS_ICONS environment variable ("nerd" or "plain")
8
+ * 2. the user's setting, `/chassis icons nerd|plain`, saved in chassis.json
9
+ * 3. the brand's default (`icons` in its spec: a brand that ships a font can ask for "nerd")
10
+ * 4. plain, so a terminal without a Nerd Font never shows empty boxes
11
+ *
12
+ * The plain glyphs are ASCII or characters that exist in the common monospace fonts (DejaVu Sans
13
+ * Mono, Noto Sans Mono, Consolas and Lucida Console were checked): • · ↑ ↓ → ● ○ ▲ × ▐ ▌.
14
+ *
15
+ * This file is the only place with Nerd Font code points. test/icons.test.ts fails on one anywhere else.
16
+ */
17
+ export type IconSet = "nerd" | "plain";
18
+ /** An icon a widget supplies: one string for both sets, or one per set. */
19
+ export type IconLike = string | {
20
+ nerd: string;
21
+ plain?: string;
22
+ };
23
+ /** nf-md-* unless noted. "" means no icon in that set. */
24
+ export declare const ICONS: {
25
+ readonly provider: {
26
+ readonly nerd: "󰚩";
27
+ readonly plain: "■";
28
+ };
29
+ readonly usage: {
30
+ readonly nerd: "󰝦";
31
+ readonly plain: " ";
32
+ };
33
+ readonly branch: {
34
+ readonly nerd: "";
35
+ readonly plain: "";
36
+ };
37
+ readonly worktree: {
38
+ readonly nerd: "󰓠";
39
+ readonly plain: "wt";
40
+ };
41
+ readonly cache: {
42
+ readonly nerd: "⟳";
43
+ readonly plain: "cache";
44
+ };
45
+ readonly resume: {
46
+ readonly nerd: "⟲";
47
+ readonly plain: "";
48
+ };
49
+ readonly levelOk: {
50
+ readonly nerd: "󰗡";
51
+ readonly plain: "●";
52
+ };
53
+ readonly levelWarn: {
54
+ readonly nerd: "󰀪";
55
+ readonly plain: "▲";
56
+ };
57
+ readonly levelError: {
58
+ readonly nerd: "󰅚";
59
+ readonly plain: "×";
60
+ };
61
+ readonly levelOff: {
62
+ readonly nerd: "󰍷";
63
+ readonly plain: "○";
64
+ };
65
+ readonly levelInfo: {
66
+ readonly nerd: "󰋽";
67
+ readonly plain: "•";
68
+ };
69
+ readonly statusLsp: {
70
+ readonly nerd: "󰅩";
71
+ readonly plain: "•";
72
+ };
73
+ readonly statusGraphify: {
74
+ readonly nerd: "󱁊";
75
+ readonly plain: "•";
76
+ };
77
+ readonly modeBuild: {
78
+ readonly nerd: "󰚩";
79
+ readonly plain: "";
80
+ };
81
+ readonly modePlan: {
82
+ readonly nerd: "󰈈";
83
+ readonly plain: "";
84
+ };
85
+ readonly capLeft: {
86
+ readonly nerd: "";
87
+ readonly plain: "▐";
88
+ };
89
+ readonly capRight: {
90
+ readonly nerd: "";
91
+ readonly plain: "▌";
92
+ };
93
+ readonly toolRead: {
94
+ readonly nerd: "󰧮";
95
+ readonly plain: "→";
96
+ };
97
+ readonly toolWrite: {
98
+ readonly nerd: "󰻭";
99
+ readonly plain: "+";
100
+ };
101
+ readonly toolEdit: {
102
+ readonly nerd: "󰏫";
103
+ readonly plain: "~";
104
+ };
105
+ readonly toolBash: {
106
+ readonly nerd: "󰆍";
107
+ readonly plain: "$";
108
+ };
109
+ readonly toolGrep: {
110
+ readonly nerd: "󰍉";
111
+ readonly plain: "?";
112
+ };
113
+ readonly toolFind: {
114
+ readonly nerd: "󰱽";
115
+ readonly plain: "*";
116
+ };
117
+ readonly toolLs: {
118
+ readonly nerd: "󰉖";
119
+ readonly plain: "/";
120
+ };
121
+ readonly toolOther: {
122
+ readonly nerd: "";
123
+ readonly plain: "•";
124
+ };
125
+ readonly telSpeed: {
126
+ readonly nerd: "󰓅";
127
+ readonly plain: "";
128
+ };
129
+ readonly telTtft: {
130
+ readonly nerd: "󰔟";
131
+ readonly plain: "";
132
+ };
133
+ readonly telTotal: {
134
+ readonly nerd: "󰅐";
135
+ readonly plain: "";
136
+ };
137
+ readonly telStall: {
138
+ readonly nerd: "󰀪";
139
+ readonly plain: "";
140
+ };
141
+ readonly diff: {
142
+ readonly nerd: "󰊢";
143
+ readonly plain: "";
144
+ };
145
+ readonly widgetContainer: {
146
+ readonly nerd: "󰡨";
147
+ readonly plain: "";
148
+ };
149
+ readonly widgetK8s: {
150
+ readonly nerd: "󱃾";
151
+ readonly plain: "";
152
+ };
153
+ readonly widgetMcp: {
154
+ readonly nerd: "󰚥";
155
+ readonly plain: "";
156
+ };
157
+ readonly widgetSessionTodos: {
158
+ readonly nerd: "󰢨";
159
+ readonly plain: "";
160
+ };
161
+ readonly widgetSubagents: {
162
+ readonly nerd: "󰭘";
163
+ readonly plain: "";
164
+ };
165
+ readonly widgetTodo: {
166
+ readonly nerd: "󰝖";
167
+ readonly plain: "";
168
+ };
169
+ };
170
+ export type IconName = keyof typeof ICONS;
171
+ /** The saved setting from the state file (`icons`), or undefined. */
172
+ export declare function loadIconSetting(path?: string): IconSet | undefined;
173
+ /** Set the user's setting for this run (undefined forgets it). Saving to the file is `saveIconSetting`. */
174
+ export declare function setIconSetting(set: IconSet | undefined): void;
175
+ /** Forget what was read, so the next call reads the state file again (tests, and /reload). */
176
+ export declare function resetIconSetting(): void;
177
+ /** Where the current set comes from, for `/chassis icons`. */
178
+ export declare function iconSource(): "env" | "setting" | "brand" | "default";
179
+ /** The icon set in use. */
180
+ export declare function iconSet(): IconSet;
181
+ /** The glyph for `name` in the current set. May be "" (no icon). */
182
+ export declare function icon(name: IconName): string;
183
+ /** `glyph`, a space, then `text`; just `text` when there is no glyph. */
184
+ export declare const withIcon: (glyph: string, text: string) => string;
185
+ /** "<glyph> " painted with `paint`, or nothing when the set has no glyph here. */
186
+ export declare const leadIcon: (glyph: string, paint: (s: string) => string) => string;
187
+ export declare const hasPrivateUse: (s: string) => boolean;
188
+ /**
189
+ * A widget's or brand's icon in the current set, or undefined when it has none there. An object
190
+ * gives its `nerd` or `plain` string. A bare string is used as given in the nerd set; in the plain
191
+ * set it loses any private-use characters (a font the terminal may not have), and is undefined if
192
+ * nothing but blanks is left.
193
+ */
194
+ export declare function resolveIcon(like: IconLike | undefined): string | undefined;
195
+ /**
196
+ * A single-cell mark that is safe to draw: in the plain set a private-use glyph (a brand's own
197
+ * font) is replaced by `fallback`.
198
+ */
199
+ export declare function safeGlyph(glyph: string, fallback?: string): string;
200
+ /** The usage circle for 0 to 100 percent, filled in eighths; a blank slot in the plain set. */
201
+ export declare function usageIcon(percent: number | undefined): string;
202
+ /** Save the user's setting into the state file, keeping its other keys. Best effort. */
203
+ export declare function saveIconSetting(set: IconSet | undefined, path?: string): void;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Icons: every glyph chassis draws from a Nerd Font lives here, each with a plain twin that needs
3
+ * no special font. The plain twin takes the same number of cells as the Nerd glyph (one, or none
4
+ * where the words beside it already say it), so no layout changes between the two sets.
5
+ *
6
+ * Which set is used, first match wins:
7
+ * 1. the PI_CHASSIS_ICONS environment variable ("nerd" or "plain")
8
+ * 2. the user's setting, `/chassis icons nerd|plain`, saved in chassis.json
9
+ * 3. the brand's default (`icons` in its spec: a brand that ships a font can ask for "nerd")
10
+ * 4. plain, so a terminal without a Nerd Font never shows empty boxes
11
+ *
12
+ * The plain glyphs are ASCII or characters that exist in the common monospace fonts (DejaVu Sans
13
+ * Mono, Noto Sans Mono, Consolas and Lucida Console were checked): • · ↑ ↓ → ● ○ ▲ × ▐ ▌.
14
+ *
15
+ * This file is the only place with Nerd Font code points. test/icons.test.ts fails on one anywhere else.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { dirname } from "node:path";
19
+ import { getBrand } from "./brand.js";
20
+ import { statePath } from "./api.js";
21
+ /** nf-md-* unless noted. "" means no icon in that set. */
22
+ export const ICONS = {
23
+ // sidebar: provider section
24
+ provider: { nerd: "\u{F06A9}", plain: "\u25A0" }, // robot; a filled square
25
+ usage: { nerd: "\u{F0766}", plain: " " }, // circle-outline; the filled slices are computed from it
26
+ branch: { nerd: "\uF418", plain: "" }, // octicons git-branch
27
+ worktree: { nerd: "\u{F04E0}", plain: "wt" }, // source-fork: a linked git worktree
28
+ cache: { nerd: "\u27F3", plain: "cache" },
29
+ resume: { nerd: "\u27F2", plain: "" },
30
+ // a widget row's level marker
31
+ levelOk: { nerd: "\u{F05E1}", plain: "\u25CF" }, // check-circle-outline
32
+ levelWarn: { nerd: "\u{F002A}", plain: "\u25B2" }, // alert-outline
33
+ levelError: { nerd: "\u{F015A}", plain: "\u00D7" }, // close-circle-outline
34
+ levelOff: { nerd: "\u{F0377}", plain: "\u25CB" }, // minus-circle-outline
35
+ levelInfo: { nerd: "\u{F02FD}", plain: "\u2022" }, // information-outline
36
+ // marks for plain setStatus() lines
37
+ statusLsp: { nerd: "\u{F0169}", plain: "\u2022" }, // code-braces
38
+ statusGraphify: { nerd: "\u{F104A}", plain: "\u2022" }, // graph-outline
39
+ // prompt row
40
+ modeBuild: { nerd: "\u{F06A9}", plain: "" }, // robot
41
+ modePlan: { nerd: "\u{F0208}", plain: "" }, // eye
42
+ capLeft: { nerd: "\uE0B6", plain: "\u2590" }, // powerline round cap; right half block
43
+ capRight: { nerd: "\uE0B4", plain: "\u258C" }, // powerline round cap; left half block
44
+ // tool rows
45
+ toolRead: { nerd: "\u{F09EE}", plain: "\u2192" }, // file-document-outline
46
+ toolWrite: { nerd: "\u{F0EED}", plain: "+" }, // file-plus-outline
47
+ toolEdit: { nerd: "\u{F03EB}", plain: "~" }, // pencil
48
+ toolBash: { nerd: "\u{F018D}", plain: "$" }, // console
49
+ toolGrep: { nerd: "\u{F0349}", plain: "?" }, // magnify
50
+ toolFind: { nerd: "\u{F0C7D}", plain: "*" }, // file-search-outline
51
+ toolLs: { nerd: "\u{F0256}", plain: "/" }, // folder-outline
52
+ toolOther: { nerd: "\uF013", plain: "\u2022" }, // fontawesome cog: a tool with no spec of its own
53
+ // turn telemetry: the words beside them say it
54
+ telSpeed: { nerd: "\u{F04C5}", plain: "" }, // speedometer
55
+ telTtft: { nerd: "\u{F051F}", plain: "" }, // timer-sand
56
+ telTotal: { nerd: "\u{F0150}", plain: "" }, // clock-outline
57
+ telStall: { nerd: "\u{F002A}", plain: "" }, // alert-outline
58
+ // diff view
59
+ diff: { nerd: "\u{F02A2}", plain: "" }, // source-branch
60
+ // widgets. A section heading or a row marker with no plain glyph falls back to the row bullet.
61
+ widgetContainer: { nerd: "\u{F0868}", plain: "" }, // docker
62
+ widgetK8s: { nerd: "\u{F10FE}", plain: "" }, // kubernetes
63
+ widgetMcp: { nerd: "\u{F06A5}", plain: "" }, // server-network
64
+ widgetSessionTodos: { nerd: "\u{F08A8}", plain: "" }, // clipboard-check-outline
65
+ widgetSubagents: { nerd: "\u{F0B58}", plain: "" }, // account-group-outline
66
+ widgetTodo: { nerd: "\u{F0756}", plain: "" }, // format-list-checks
67
+ };
68
+ /** The slice glyphs of the usage circle come after this one: U+F0A9E is one eighth. */
69
+ const USAGE_SLICE_BASE = 0xf0a9d;
70
+ const KEY = Symbol.for("pi-chassis/icons");
71
+ function slot() {
72
+ const g = globalThis;
73
+ g[KEY] ??= { loaded: false };
74
+ return g[KEY];
75
+ }
76
+ const asSet = (v) => (v === "nerd" || v === "plain" ? v : undefined);
77
+ /** The saved setting from the state file (`icons`), or undefined. */
78
+ export function loadIconSetting(path = statePath()) {
79
+ try {
80
+ return asSet(JSON.parse(readFileSync(path, "utf8"))?.icons);
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /** Set the user's setting for this run (undefined forgets it). Saving to the file is `saveIconSetting`. */
87
+ export function setIconSetting(set) {
88
+ const s = slot();
89
+ s.setting = set;
90
+ s.loaded = true;
91
+ }
92
+ /** Forget what was read, so the next call reads the state file again (tests, and /reload). */
93
+ export function resetIconSetting() {
94
+ const s = slot();
95
+ s.setting = undefined;
96
+ s.loaded = false;
97
+ }
98
+ /** Where the current set comes from, for `/chassis icons`. */
99
+ export function iconSource() {
100
+ if (asSet(process.env.PI_CHASSIS_ICONS))
101
+ return "env";
102
+ const s = slot();
103
+ if (!s.loaded) {
104
+ s.setting = loadIconSetting();
105
+ s.loaded = true;
106
+ }
107
+ if (s.setting)
108
+ return "setting";
109
+ return asSet(getBrand().icons) ? "brand" : "default";
110
+ }
111
+ /** The icon set in use. */
112
+ export function iconSet() {
113
+ const env = asSet(process.env.PI_CHASSIS_ICONS);
114
+ if (env)
115
+ return env;
116
+ const s = slot();
117
+ if (!s.loaded) {
118
+ s.setting = loadIconSetting();
119
+ s.loaded = true;
120
+ }
121
+ return s.setting ?? asSet(getBrand().icons) ?? "plain";
122
+ }
123
+ // ---- resolving --------------------------------------------------------------------------------
124
+ /** The glyph for `name` in the current set. May be "" (no icon). */
125
+ export function icon(name) {
126
+ return ICONS[name][iconSet()];
127
+ }
128
+ /** `glyph`, a space, then `text`; just `text` when there is no glyph. */
129
+ export const withIcon = (glyph, text) => (glyph ? `${glyph} ${text}` : text);
130
+ /** "<glyph> " painted with `paint`, or nothing when the set has no glyph here. */
131
+ export const leadIcon = (glyph, paint) => (glyph ? `${paint(glyph)} ` : "");
132
+ /** Private-use code points: where Nerd Font (and any custom icon font) glyphs live. */
133
+ const PRIVATE_USE = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/gu;
134
+ export const hasPrivateUse = (s) => new RegExp(PRIVATE_USE.source, "u").test(s);
135
+ /**
136
+ * A widget's or brand's icon in the current set, or undefined when it has none there. An object
137
+ * gives its `nerd` or `plain` string. A bare string is used as given in the nerd set; in the plain
138
+ * set it loses any private-use characters (a font the terminal may not have), and is undefined if
139
+ * nothing but blanks is left.
140
+ */
141
+ export function resolveIcon(like) {
142
+ if (like === undefined || like === "")
143
+ return undefined;
144
+ if (typeof like === "object") {
145
+ const s = iconSet() === "nerd" ? like.nerd : like.plain;
146
+ return s ? s : undefined;
147
+ }
148
+ if (iconSet() === "nerd")
149
+ return like;
150
+ const kept = like.replace(PRIVATE_USE, "");
151
+ return kept.trim() ? kept : undefined;
152
+ }
153
+ /**
154
+ * A single-cell mark that is safe to draw: in the plain set a private-use glyph (a brand's own
155
+ * font) is replaced by `fallback`.
156
+ */
157
+ export function safeGlyph(glyph, fallback = "\u25CF") {
158
+ return iconSet() === "plain" && hasPrivateUse(glyph) ? fallback : glyph;
159
+ }
160
+ /** The usage circle for 0 to 100 percent, filled in eighths; a blank slot in the plain set. */
161
+ export function usageIcon(percent) {
162
+ if (iconSet() === "plain")
163
+ return ICONS.usage.plain;
164
+ const eighths = percent === undefined || !Number.isFinite(percent) ? 0 : Math.round((Math.max(0, Math.min(100, percent)) / 100) * 8);
165
+ return eighths === 0 ? ICONS.usage.nerd : String.fromCodePoint(USAGE_SLICE_BASE + eighths);
166
+ }
167
+ // ---- saving -----------------------------------------------------------------------------------
168
+ /** Save the user's setting into the state file, keeping its other keys. Best effort. */
169
+ export function saveIconSetting(set, path = statePath()) {
170
+ try {
171
+ let data = {};
172
+ if (existsSync(path)) {
173
+ try {
174
+ data = JSON.parse(readFileSync(path, "utf8")) ?? {};
175
+ }
176
+ catch {
177
+ data = {};
178
+ }
179
+ }
180
+ if (set)
181
+ data.icons = set;
182
+ else
183
+ delete data.icons;
184
+ mkdirSync(dirname(path), { recursive: true });
185
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
186
+ }
187
+ catch {
188
+ // the in-memory setting still applies for this run
189
+ }
190
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * chassis: an OpenCode-style TUI for pi, with a brand that another package can replace (brand.ts).
3
+ *
4
+ * Pieces (each in its own file):
5
+ * layout.ts right sidebar (fullscreen split, overlay fallback) ctrl+x b
6
+ * home.ts OpenCode-style home screen (logo, centred prompt, tip, bottom bar)
7
+ * sidebar.ts session title, context/cost, status widgets, files, todo
8
+ * footer.ts one-line status bar under the prompt
9
+ * editor.ts left-rail prompt with mode badge + model, leader key, Tab mode switch
10
+ * commands.ts ctrl+x leader actions and the ctrl+p command palette
11
+ * modes.ts Build / Plan (read-only) agent modes
12
+ * tools.ts one-line tool rows ("→ Read x.ts 120 lines")
13
+ * messages.ts user prompts with a coloured left bar
14
+ * peek.ts thinking peek in the hidden-thinking label
15
+ * telemetry.ts per-run TPS/TTFT/tokens/cost summary
16
+ * api.ts the widget registry other extensions use (registerWidget)
17
+ *
18
+ * Commands: /chassis [on|off|sidebar|status]
19
+ */
20
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+ export default function (pi: ExtensionAPI): void;