@thoth-agents/pi-thoth-theme 0.2.0 → 0.3.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 (3) hide show
  1. package/README.md +40 -5
  2. package/dist/index.ts +383 -102
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -7,12 +7,23 @@ dependencies are pinned to `1.0.2` in the root workspace lockfile.
7
7
 
8
8
  ## Install
9
9
 
10
- From the thoth-agents repository root:
10
+ Thoth's complete Pi Install and applied Update install and individually verify
11
+ `npm:@thoth-agents/pi-thoth-theme@>=0.3.0`. An existing copy from any source at or
12
+ above that floor is preserved without reinstalling; an older copy remains
13
+ untouched and blocks completion with manual upgrade guidance. Ambiguous identity
14
+ fails closed. Upgrade a local/Git checkout in place, or review ownership before
15
+ switching its configured source with Pi's native remove/install commands; verify
16
+ the installed manifest and rerun setup. Dry-run performs no mutation.
17
+
18
+ For a standalone install:
11
19
 
12
20
  ```sh
13
- pi install ./pi-packages/pi-thoth-theme
21
+ pi install 'npm:@thoth-agents/pi-thoth-theme@>=0.3.0'
14
22
  ```
15
23
 
24
+ For workspace development, build first and run `pi install ./pi-packages/pi-thoth-theme`
25
+ from the repository root. Restart Pi after installation.
26
+
16
27
  Select the `thoth` theme in Pi's theme settings. Theme selection is independent
17
28
  of the module toggles below.
18
29
 
@@ -41,11 +52,31 @@ of the module toggles below.
41
52
  tool providers, and recent sessions. Resource and session details load
42
53
  best-effort through public APIs.
43
54
 
55
+ ## Subagent usage channel
56
+
57
+ The status line subscribes to pi-core's v1 `thoth:subagents:usage` channel and
58
+ requests a snapshot on `thoth:subagents:usage:request` at session start, after
59
+ subscribing, using `data: {}`. The producer is `@thoth-agents/pi-subagents`;
60
+ envelopes are `{ v, source, sessionId, at, data }`, with the parent session ID
61
+ in the envelope, Unix-millisecond `at`, and only cumulative `totalCost` and
62
+ `runCount` in data. Pre-readiness requests are answered once after usage restore.
63
+ Invalid, unsupported-version and foreign-session envelopes are ignored; no task
64
+ prompts, results, transcripts or logs are consumed.
65
+
66
+ The raw `thoth:subagent-usage` bus event/request is removed with no fallback or
67
+ dual publication. Upgrade pi-subagents and pi-thoth-theme to `>=0.3.0` together
68
+ and `/reload`; mixed old/new versions lose subagent cost display. The persisted
69
+ usage checkpoint custom-entry type remains `thoth:subagent-usage`, so checkpoint
70
+ restore and replay-duplicate suppression are unchanged. See
71
+ [pi-core's exact contract](../pi-core/README.md#task-summary-and-usage-channels).
72
+
44
73
  ## Render KIT
45
74
 
46
75
  Provides the Render KIT v1 contract defined by `@thoth-agents/pi-core` for
47
76
  first-party tools, custom messages and above-editor widgets. The kit shares this
48
77
  package's frames, theme roles, collapse hints and working/elapsed indicators.
78
+ The Render KIT v1 semantic icon table includes `elapsed` for work-panel metrics,
79
+ with nerd-clock, Unicode and ASCII alternatives.
49
80
  Producers discover it at render time, so extension load order does not matter;
50
81
  they do not depend on this package and keep native Pi rendering when it is absent.
51
82
 
@@ -65,7 +96,7 @@ Edit `~/.pi/agent/pi-thoth-theme.json` (or
65
96
  "icons": "nerd",
66
97
  "statusLine": {
67
98
  "enabled": true,
68
- "subscriptionProviders": ["claude-bridge"]
99
+ "subscriptionProviders": ["claude-bridge", "antigravity"]
69
100
  },
70
101
  "inputBox": { "enabled": true },
71
102
  "tools": {
@@ -82,14 +113,18 @@ or `"ascii"` (plain-text alternatives).
82
113
  Missing or malformed icon values use `"nerd"`. Each module's `enabled` key
83
114
  accepts a boolean and defaults to `true`. Set it to `false` to leave Pi's native
84
115
  behavior on that surface. `statusLine.subscriptionProviders` accepts an array of
85
- provider identifier strings (defaults to `["claude-bridge"]`) to mark
116
+ provider identifier strings (defaults to `["claude-bridge", "antigravity"]`) to mark
86
117
  subscription-backed usage with `(sub)` in the status line cost segment.
118
+ Classification uses the current provider for the displayed total, not a
119
+ per-message billing split; explicit user lists are preserved. Antigravity costs
120
+ are API-equivalent catalog estimates, not subscription charges.
87
121
 
88
122
  `inputBox.enabled` defaults to `true` and requires `statusLine.enabled` to be
89
123
  `true`. It frames the native editor in a rounded `muted` (sand) box with side borders,
90
124
  a dim `type or / for commands` placeholder, and top-left ready or native working
91
125
  status (including elapsed seconds). Git branch and working directory appear on
92
- the top border; model, thinking effort and context usage appear on the bottom.
126
+ the top border, using pi-core's root-exported `formatCwd` for home abbreviation
127
+ (shared with the sidebar); model, thinking effort and context usage appear on the bottom.
93
128
  Native scroll indicators are preserved. The status line always stays in its
94
129
  separate footer row below the box. Narrow widths use the native editor geometry.
95
130
  Disable `inputBox` to keep the native editor and the same footer.
package/dist/index.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  // @ts-nocheck
2
-
3
- // ../pi-core/src/validation.ts
4
- function isRecord(value) {
5
- return typeof value === "object" && value !== null && !Array.isArray(value);
6
- }
7
- function isNonNegativeInteger(value) {
8
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
9
- }
10
-
11
- // ../pi-core/src/channels.ts
12
- function defineChannel(definition) {
13
- return definition;
14
- }
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
15
11
 
16
12
  // ../pi-core/src/duration.ts
17
13
  function formatDuration(ms) {
@@ -26,31 +22,13 @@ function formatDuration(ms) {
26
22
  const hours = Math.floor(ms / 36e5);
27
23
  return `${hours}h ${String(totalMinutes % 60).padStart(2, "0")}m`;
28
24
  }
25
+ var init_duration = __esm({
26
+ "../pi-core/src/duration.ts"() {
27
+ "use strict";
28
+ }
29
+ });
29
30
 
30
31
  // ../pi-core/src/render-kit.ts
31
- var nativeFrames = {
32
- spinnerFrames: Object.freeze([
33
- "\u280B",
34
- "\u2819",
35
- "\u2839",
36
- "\u2838",
37
- "\u283C",
38
- "\u2834",
39
- "\u2826",
40
- "\u2827",
41
- "\u2807",
42
- "\u280F"
43
- ]),
44
- workingFrames: Object.freeze(["\u25B3", "\u25ED", "\u25B2", "\u25EE"])
45
- };
46
- var terminalFooterGlyphs = {
47
- completed: "\u2713",
48
- deleted: "\u2713",
49
- failed: "\u2717",
50
- cancelled: "\u2717",
51
- interrupted: "\u2717",
52
- blocked: "\u2717"
53
- };
54
32
  function getToolElapsedMs(options) {
55
33
  const context = options.context;
56
34
  const state = context?.state;
@@ -76,11 +54,6 @@ function getToolElapsedMs(options) {
76
54
  state.completedElapsedMs = elapsedMs;
77
55
  return elapsedMs;
78
56
  }
79
- var RENDER_KIT_VERSION = 1;
80
- var registryKey = /* @__PURE__ */ Symbol.for(
81
- `thoth-agents.pi-core.render-kit.v${RENDER_KIT_VERSION}`
82
- );
83
- var shared = globalThis;
84
57
  function registration() {
85
58
  try {
86
59
  const record = shared[registryKey];
@@ -98,6 +71,313 @@ function registerRenderKit(kit, owner) {
98
71
  function withdrawRenderKit(token) {
99
72
  if (registration()?.token === token) delete shared[registryKey];
100
73
  }
74
+ var nativeFrames, terminalFooterGlyphs, RENDER_KIT_VERSION, registryKey, shared;
75
+ var init_render_kit = __esm({
76
+ "../pi-core/src/render-kit.ts"() {
77
+ "use strict";
78
+ init_duration();
79
+ nativeFrames = {
80
+ spinnerFrames: Object.freeze([
81
+ "\u280B",
82
+ "\u2819",
83
+ "\u2839",
84
+ "\u2838",
85
+ "\u283C",
86
+ "\u2834",
87
+ "\u2826",
88
+ "\u2827",
89
+ "\u2807",
90
+ "\u280F"
91
+ ]),
92
+ workingFrames: Object.freeze(["\u25B3", "\u25ED", "\u25B2", "\u25EE"])
93
+ };
94
+ terminalFooterGlyphs = {
95
+ completed: "\u2713",
96
+ deleted: "\u2713",
97
+ failed: "\u2717",
98
+ cancelled: "\u2717",
99
+ interrupted: "\u2717",
100
+ blocked: "\u2717"
101
+ };
102
+ RENDER_KIT_VERSION = 1;
103
+ registryKey = /* @__PURE__ */ Symbol.for(
104
+ `thoth-agents.pi-core.render-kit.v${RENDER_KIT_VERSION}`
105
+ );
106
+ shared = globalThis;
107
+ }
108
+ });
109
+
110
+ // ../pi-core/src/panel-frame.ts
111
+ var init_panel_frame = __esm({
112
+ "../pi-core/src/panel-frame.ts"() {
113
+ "use strict";
114
+ init_render_kit();
115
+ }
116
+ });
117
+
118
+ // ../pi-core/src/validation.ts
119
+ function isRecord(value) {
120
+ return typeof value === "object" && value !== null && !Array.isArray(value);
121
+ }
122
+ function isNonEmptyString(value) {
123
+ return typeof value === "string" && value.length > 0;
124
+ }
125
+ function isNonNegativeInteger(value) {
126
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
127
+ }
128
+
129
+ // ../pi-core/src/channels.ts
130
+ function defineChannel(definition) {
131
+ return definition;
132
+ }
133
+ function publish(events, channel, snapshot) {
134
+ events.emit(channel.name, {
135
+ v: channel.version,
136
+ source: snapshot.source,
137
+ sessionId: snapshot.sessionId,
138
+ at: Date.now(),
139
+ data: snapshot.data
140
+ });
141
+ }
142
+ function request(events, requestChannel, payload) {
143
+ publish(events, requestChannel, payload);
144
+ }
145
+ function subscribe(events, channel, options) {
146
+ return events.on(channel.name, (value) => {
147
+ if (!isEnvelope(value, channel, options.sessionId)) return;
148
+ options.onSnapshot(value);
149
+ });
150
+ }
151
+ function isEnvelope(value, channel, sessionId) {
152
+ try {
153
+ return isRecord(value) && value.v === channel.version && isNonEmptyString(value.source) && isNonEmptyString(value.sessionId) && (sessionId === void 0 || value.sessionId === sessionId) && typeof value.at === "number" && Number.isFinite(value.at) && value.at >= 0 && channel.validate(value.data);
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ // ../pi-core/src/background.ts
160
+ var backgroundStatuses = [
161
+ "running",
162
+ "succeeded",
163
+ "failed",
164
+ "cancelled",
165
+ "timed_out"
166
+ ];
167
+ var BACKGROUND_PROGRESS_MAX_LENGTH = 200;
168
+ function isBackgroundSnapshot(value) {
169
+ try {
170
+ if (!isExactRecord(value, ["tasks", "counts"]) || !Array.isArray(value.tasks))
171
+ return false;
172
+ for (const task of value.tasks) {
173
+ if (!isBackgroundTask(task)) return false;
174
+ }
175
+ const { counts } = value;
176
+ return isExactRecord(counts, backgroundStatuses) && backgroundStatuses.every((status) => isNonNegativeInteger(counts[status]));
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+ function isExactRecord(value, allowedKeys) {
182
+ if (!isRecord(value)) return false;
183
+ const prototype = Object.getPrototypeOf(value);
184
+ return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every(
185
+ (key) => typeof key === "string" && allowedKeys.includes(key)
186
+ );
187
+ }
188
+ function isOptionalTimestamp(value) {
189
+ return value === void 0 || isNonNegativeInteger(value);
190
+ }
191
+ function isProgress(value) {
192
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= BACKGROUND_PROGRESS_MAX_LENGTH;
193
+ }
194
+ function isBackgroundTask(value) {
195
+ return isExactRecord(value, [
196
+ "id",
197
+ "name",
198
+ "kind",
199
+ "status",
200
+ "createdAt",
201
+ "startedAt",
202
+ "endedAt",
203
+ "deadlineAt",
204
+ "lastCheckedAt",
205
+ "lastProgressAt",
206
+ "stopRequestedAt",
207
+ "dismissedAt",
208
+ "exitCode",
209
+ "signal",
210
+ "progress",
211
+ "dismissed"
212
+ ]) && isNonEmptyString(value.id) && (value.name === void 0 || typeof value.name === "string") && (value.kind === "process" || value.kind === "command_watch") && backgroundStatuses.some((status) => status === value.status) && isNonNegativeInteger(value.createdAt) && isNonNegativeInteger(value.startedAt) && isOptionalTimestamp(value.endedAt) && isOptionalTimestamp(value.deadlineAt) && isOptionalTimestamp(value.lastCheckedAt) && isOptionalTimestamp(value.lastProgressAt) && isOptionalTimestamp(value.stopRequestedAt) && isOptionalTimestamp(value.dismissedAt) && (value.exitCode === void 0 || value.exitCode === null || typeof value.exitCode === "number" && Number.isSafeInteger(value.exitCode)) && (value.signal === void 0 || value.signal === null || isNonEmptyString(value.signal)) && (value.progress === void 0 || isProgress(value.progress)) && (value.dismissed === void 0 || typeof value.dismissed === "boolean");
213
+ }
214
+ var BACKGROUND_STATE_CHANNEL = defineChannel({
215
+ name: "thoth:background:state",
216
+ version: 1,
217
+ validate: isBackgroundSnapshot
218
+ });
219
+ function isBackgroundStateRequest(value) {
220
+ try {
221
+ return isExactRecord(value, []);
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+ var BACKGROUND_STATE_REQUEST = defineChannel({
227
+ name: "thoth:background:state:request",
228
+ version: 1,
229
+ validate: isBackgroundStateRequest
230
+ });
231
+
232
+ // ../pi-core/src/cwd.ts
233
+ import { isAbsolute, relative, resolve, sep } from "node:path";
234
+ function formatCwd(cwd, home) {
235
+ if (!home) return cwd;
236
+ const relativeToHome = relative(resolve(home), resolve(cwd));
237
+ if (relativeToHome === ".." || relativeToHome.startsWith(`..${sep}`) || isAbsolute(relativeToHome)) {
238
+ return cwd;
239
+ }
240
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
241
+ }
242
+
243
+ // ../pi-core/src/index.ts
244
+ init_duration();
245
+ init_render_kit();
246
+
247
+ // ../pi-core/src/session-cost.ts
248
+ function computeSessionCost(entries, { subscriptionProviders = [], providerOf } = {}) {
249
+ let cost = 0;
250
+ for (const entry of entries ?? []) {
251
+ if (!entry) continue;
252
+ if (entry.type === "message") {
253
+ const message = entry.message;
254
+ if (message?.role === "assistant" || message?.role === "toolResult")
255
+ cost += message.usage?.cost?.total || 0;
256
+ } else if (["branch_summary", "compaction", "usage"].includes(entry.type)) {
257
+ cost += entry.usage?.cost?.total || 0;
258
+ }
259
+ }
260
+ const provider = providerOf?.();
261
+ return {
262
+ cost,
263
+ isSubscription: provider !== void 0 && subscriptionProviders.includes(provider)
264
+ };
265
+ }
266
+ function combineSessionAndSubagentCost(sessionCost, latestSubagentCost = 0) {
267
+ return sessionCost + latestSubagentCost;
268
+ }
269
+
270
+ // ../pi-core/src/subagents.ts
271
+ var subagentStatuses = [
272
+ "queued",
273
+ "running",
274
+ "stopping",
275
+ "completed",
276
+ "failed",
277
+ "cancelled",
278
+ "interrupted"
279
+ ];
280
+ var subagentEfforts = [
281
+ "off",
282
+ "minimal",
283
+ "low",
284
+ "medium",
285
+ "high",
286
+ "xhigh",
287
+ "max"
288
+ ];
289
+ var SUBAGENT_PREVIEW_MAX_LENGTH = 800;
290
+ function isSubagentsSnapshot(value) {
291
+ try {
292
+ if (!isExactRecord2(value, ["tasks", "history", "counts", "totals"]) || !Array.isArray(value.tasks) || !Array.isArray(value.history) || value.history.length > 100)
293
+ return false;
294
+ for (const tasks of [value.tasks, value.history]) {
295
+ for (const task of tasks) {
296
+ if (!isSubagentTask(task)) return false;
297
+ }
298
+ }
299
+ const { counts, totals } = value;
300
+ return isCounts(counts) && isExactRecord2(totals, [...subagentStatuses, "total"]) && isNonNegativeInteger(totals.total) && subagentStatuses.every((status) => isNonNegativeInteger(totals[status]));
301
+ } catch {
302
+ return false;
303
+ }
304
+ }
305
+ function isExactRecord2(value, allowedKeys) {
306
+ if (!isRecord(value)) return false;
307
+ const prototype = Object.getPrototypeOf(value);
308
+ return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every(
309
+ (key) => typeof key === "string" && allowedKeys.includes(key)
310
+ );
311
+ }
312
+ function isCounts(value) {
313
+ return isExactRecord2(value, subagentStatuses) && subagentStatuses.every((status) => isNonNegativeInteger(value[status]));
314
+ }
315
+ function isOptionalString(value) {
316
+ return value === void 0 || typeof value === "string";
317
+ }
318
+ function isOptionalTimestamp2(value) {
319
+ return value === void 0 || isNonNegativeInteger(value);
320
+ }
321
+ function isNonNegativeNumber(value) {
322
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
323
+ }
324
+ function isTaskUsage(value) {
325
+ return isExactRecord2(value, ["input", "output", "cost"]) && (value.input === void 0 || isNonNegativeInteger(value.input)) && (value.output === void 0 || isNonNegativeInteger(value.output)) && (value.cost === void 0 || isNonNegativeNumber(value.cost));
326
+ }
327
+ function isSubagentTask(value) {
328
+ return isExactRecord2(value, [
329
+ "id",
330
+ "agent",
331
+ "displayName",
332
+ "mode",
333
+ "status",
334
+ "model",
335
+ "effort",
336
+ "createdAt",
337
+ "startedAt",
338
+ "endedAt",
339
+ "lastActivityAt",
340
+ "usage",
341
+ "preview"
342
+ ]) && isNonEmptyString(value.id) && isNonEmptyString(value.agent) && isOptionalString(value.displayName) && (value.mode === "task" || value.mode === "background") && subagentStatuses.some((status) => status === value.status) && isOptionalString(value.model) && (value.effort === void 0 || subagentEfforts.some((effort) => effort === value.effort)) && isNonNegativeInteger(value.createdAt) && isOptionalTimestamp2(value.startedAt) && isOptionalTimestamp2(value.endedAt) && isOptionalTimestamp2(value.lastActivityAt) && (value.usage === void 0 || isTaskUsage(value.usage)) && (value.preview === void 0 || typeof value.preview === "string" && value.preview.length <= SUBAGENT_PREVIEW_MAX_LENGTH);
343
+ }
344
+ var SUBAGENTS_STATE_CHANNEL = defineChannel({
345
+ name: "thoth:subagents:state",
346
+ version: 2,
347
+ validate: isSubagentsSnapshot
348
+ });
349
+ function isSubagentsStateRequest(value) {
350
+ try {
351
+ return isExactRecord2(value, []);
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ function isSubagentsUsageRequest(value) {
357
+ return isSubagentsStateRequest(value);
358
+ }
359
+ var SUBAGENTS_STATE_REQUEST = defineChannel({
360
+ name: "thoth:subagents:state:request",
361
+ version: 1,
362
+ validate: isSubagentsStateRequest
363
+ });
364
+ function isSubagentsUsageSnapshot(value) {
365
+ try {
366
+ return isExactRecord2(value, ["totalCost", "runCount"]) && isNonNegativeNumber(value.totalCost) && isNonNegativeInteger(value.runCount);
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ var SUBAGENTS_USAGE_CHANNEL = defineChannel({
372
+ name: "thoth:subagents:usage",
373
+ version: 1,
374
+ validate: isSubagentsUsageSnapshot
375
+ });
376
+ var SUBAGENTS_USAGE_REQUEST = defineChannel({
377
+ name: "thoth:subagents:usage:request",
378
+ version: 1,
379
+ validate: isSubagentsUsageRequest
380
+ });
101
381
 
102
382
  // ../pi-core/src/todo.ts
103
383
  var todoStatuses = [
@@ -123,11 +403,11 @@ function isTodoSnapshot(value) {
123
403
  function isTaskId(value) {
124
404
  return isNonNegativeInteger(value) && value > 0;
125
405
  }
126
- function isOptionalString(value) {
406
+ function isOptionalString2(value) {
127
407
  return value === void 0 || typeof value === "string";
128
408
  }
129
409
  function isTodoTask(value) {
130
- if (!isRecord(value) || !isTaskId(value.id) || typeof value.subject !== "string" || !todoStatuses.some((status) => status === value.status) || !Array.isArray(value.blockedBy) || !isOptionalString(value.description) || !isOptionalString(value.activeForm) || !isOptionalString(value.owner))
410
+ if (!isRecord(value) || !isTaskId(value.id) || typeof value.subject !== "string" || !todoStatuses.some((status) => status === value.status) || !Array.isArray(value.blockedBy) || !isOptionalString2(value.description) || !isOptionalString2(value.activeForm) || !isOptionalString2(value.owner))
131
411
  return false;
132
412
  for (const id of value.blockedBy) {
133
413
  if (!isTaskId(id)) return false;
@@ -160,6 +440,16 @@ var registryKey2 = /* @__PURE__ */ Symbol.for(
160
440
  `thoth-agents.pi-core.tool-definitions.v${TOOL_DEFINITION_REGISTRY_VERSION}`
161
441
  );
162
442
 
443
+ // ../pi-core/src/work-panel-host.ts
444
+ init_render_kit();
445
+
446
+ // ../pi-core/src/work-panel-detail.ts
447
+ init_panel_frame();
448
+ init_render_kit();
449
+
450
+ // ../pi-core/src/work-panel-render.ts
451
+ init_render_kit();
452
+
163
453
  // src/render-kit/index.ts
164
454
  import {
165
455
  stripTerminalSequences,
@@ -192,6 +482,7 @@ var ICONS = {
192
482
  model: { nerd: "\u{F06A9}", unicode: "\u25CF", ascii: "*" },
193
483
  effort: { nerd: "\u{F09D1}", unicode: "\u25D0", ascii: "o" },
194
484
  context: { nerd: "\uF2DB", unicode: "ctx", ascii: "ctx" },
485
+ elapsed: { nerd: "\uF017", unicode: "\u25F7", ascii: "elapsed" },
195
486
  cost: { nerd: "\uF155", unicode: "$", ascii: "$" },
196
487
  tokensIn: { nerd: "\uF062", unicode: "\u2191", ascii: "^" },
197
488
  tokensOut: { nerd: "\uF063", unicode: "\u2193", ascii: "v" },
@@ -453,7 +744,7 @@ function createComponent(renderFn) {
453
744
  }
454
745
 
455
746
  // src/tools/frame.ts
456
- import { relative, resolve } from "node:path";
747
+ import { relative as relative2, resolve as resolve2 } from "node:path";
457
748
  import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
458
749
  var FRAME_HORIZONTAL = "\u2500";
459
750
  var FRAME_VERTICAL = "\u2502";
@@ -467,9 +758,9 @@ function formatDisplayPath(targetPath, cwd) {
467
758
  if (!targetPath) return "";
468
759
  if (!cwd) return targetPath;
469
760
  try {
470
- const absTarget = resolve(cwd, targetPath);
471
- const absCwd = resolve(cwd);
472
- const rel = relative(absCwd, absTarget);
761
+ const absTarget = resolve2(cwd, targetPath);
762
+ const absCwd = resolve2(cwd);
763
+ const rel = relative2(absCwd, absTarget);
473
764
  if (!rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel)) {
474
765
  return rel.replace(/\\/g, "/") || ".";
475
766
  }
@@ -522,6 +813,14 @@ function renderFrameDivider(theme, title, width, borderColor = "accent") {
522
813
  if (safeWidth <= 4) {
523
814
  return [truncateToWidth2(title, safeWidth)];
524
815
  }
816
+ if (!title) {
817
+ return [
818
+ theme.fg(
819
+ borderColor,
820
+ `${FRAME_DIVIDER_LEFT}${FRAME_HORIZONTAL.repeat(safeWidth - 2)}${FRAME_DIVIDER_RIGHT}`
821
+ )
822
+ ];
823
+ }
525
824
  const innerWidth = Math.max(1, safeWidth - 2);
526
825
  const rawTitle = ` ${title} `;
527
826
  const maxTitleWidth = Math.max(1, innerWidth - 4);
@@ -837,7 +1136,7 @@ function parseSubscriptionProviders(value) {
837
1136
  (item) => typeof item === "string" && item.trim().length > 0
838
1137
  );
839
1138
  }
840
- return ["claude-bridge"];
1139
+ return ["claude-bridge", "antigravity"];
841
1140
  }
842
1141
  function parseRespectPackages(value) {
843
1142
  const packages = asObject(value).respectPackages;
@@ -876,19 +1175,10 @@ import {
876
1175
  } from "@earendil-works/pi-tui";
877
1176
 
878
1177
  // src/status-line/layout.ts
879
- import { isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
880
1178
  import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
881
1179
  function themeFg(theme, token, text) {
882
1180
  return theme?.fg ? theme.fg(token, text) : text;
883
1181
  }
884
- function formatCwd(cwd, home) {
885
- if (!home) return cwd;
886
- const relativeToHome = relative2(resolve2(home), resolve2(cwd));
887
- if (relativeToHome === ".." || relativeToHome.startsWith(`..${sep}`) || isAbsolute(relativeToHome)) {
888
- return cwd;
889
- }
890
- return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
891
- }
892
1182
  function formatTokens(count) {
893
1183
  if (count < 1e3) return count.toString();
894
1184
  if (count < 1e6) {
@@ -947,14 +1237,18 @@ function contextBar(percent, isAscii, theme) {
947
1237
  let token = "success";
948
1238
  if (percent >= 90) token = "error";
949
1239
  else if (percent >= 70) token = "warning";
950
- const filled = Math.min(10, Math.max(0, Math.round(percent / 100 * 10)));
951
- const fillChar = isAscii ? "#" : "\u2588";
952
- const emptyChar = isAscii ? "-" : "\u2591";
1240
+ const halves = Math.min(
1241
+ 16,
1242
+ Math.max(percent > 0 ? 1 : 0, Math.round(percent / 100 * 16))
1243
+ );
1244
+ const full = Math.floor(halves / 2);
1245
+ const half = halves % 2;
953
1246
  const dim = (text) => themeFg(theme, "dim", text);
1247
+ const filled = isAscii ? "#".repeat(full) + (half ? "=" : "") : "\u25C6".repeat(full) + (half ? "\u2B16" : "");
1248
+ const empty = (isAscii ? "-" : "\u25C7").repeat(8 - full - half);
1249
+ const bar = isAscii ? `${dim("[")}${themeFg(theme, token, filled)}${dim(empty)}${dim("]")}` : `${themeFg(theme, token, filled)}${dim(empty)}`;
954
1250
  return {
955
- bar: `${dim("[")}${themeFg(theme, token, fillChar.repeat(filled))}${dim(
956
- emptyChar.repeat(10 - filled)
957
- )}${dim("]")}`,
1251
+ bar,
958
1252
  pct: themeFg(theme, token, `${Math.round(percent)}%`)
959
1253
  };
960
1254
  }
@@ -993,7 +1287,7 @@ function renderStatusLine(data, options) {
993
1287
  const sep2 = ` ${themeFg(theme, "border", icon("separator", mode))} `;
994
1288
  const segments = [];
995
1289
  if (data.cost !== void 0) {
996
- const total = data.cost + (data.subagentCost ?? 0);
1290
+ const total = combineSessionAndSubagentCost(data.cost, data.subagentCost);
997
1291
  segments.push(
998
1292
  themeFg(
999
1293
  theme,
@@ -1449,25 +1743,6 @@ function createWorkingState(requestRender) {
1449
1743
  };
1450
1744
  }
1451
1745
 
1452
- // src/status-line/cost.ts
1453
- function calculateSessionCost(source) {
1454
- if (!source) return 0;
1455
- const entries = Array.isArray(source) ? source : typeof source.getEntries === "function" ? source.getEntries() : [];
1456
- let totalCost = 0;
1457
- for (const entry of entries) {
1458
- if (!entry) continue;
1459
- if (entry.type === "message") {
1460
- const msg = entry.message;
1461
- if ((msg?.role === "assistant" || msg?.role === "toolResult") && "usage" in msg && msg.usage?.cost?.total) {
1462
- totalCost += msg.usage.cost.total;
1463
- }
1464
- } else if ((entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "usage") && "usage" in entry && entry.usage?.cost?.total) {
1465
- totalCost += entry.usage.cost.total;
1466
- }
1467
- }
1468
- return totalCost;
1469
- }
1470
-
1471
1746
  // src/status-line/throughput.ts
1472
1747
  function createThroughputTracker() {
1473
1748
  let pending;
@@ -1546,7 +1821,8 @@ function calculateSessionTokens(source) {
1546
1821
  // src/status-line/index.ts
1547
1822
  function registerStatusLine(pi, ctx, config) {
1548
1823
  const subscriptionProviders = config.statusLine.subscriptionProviders ?? [
1549
- "claude-bridge"
1824
+ "claude-bridge",
1825
+ "antigravity"
1550
1826
  ];
1551
1827
  const inputBoxEnabled = config.inputBox?.enabled !== false;
1552
1828
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -1589,12 +1865,13 @@ function registerStatusLine(pi, ctx, config) {
1589
1865
  };
1590
1866
  function readSession() {
1591
1867
  const usage = ctx.getContextUsage?.();
1592
- const provider = ctx.model?.provider;
1593
1868
  const entries = ctx.sessionManager.getEntries();
1594
1869
  return {
1595
- cost: calculateSessionCost(entries),
1870
+ ...computeSessionCost(entries, {
1871
+ subscriptionProviders,
1872
+ providerOf: () => ctx.model?.provider
1873
+ }),
1596
1874
  tokenTotals: calculateSessionTokens(entries),
1597
- isSubscription: provider !== void 0 && subscriptionProviders.includes(provider),
1598
1875
  contextTokens: usage?.tokens ?? null,
1599
1876
  contextPercent: usage?.percent ?? null,
1600
1877
  contextWindow: usage?.contextWindow
@@ -1615,22 +1892,26 @@ function registerStatusLine(pi, ctx, config) {
1615
1892
  sessionDirty = true;
1616
1893
  invalidateRender();
1617
1894
  };
1618
- unsubs.push(
1619
- pi.events.on("thoth:subagent-usage", (data) => {
1620
- if (disposed || !data || typeof data !== "object") return;
1621
- const { parentSessionId, totalCost } = data;
1622
- if (parentSessionId !== ctx.sessionManager.getSessionId() || typeof totalCost !== "number" || !Number.isFinite(totalCost) || totalCost < 0)
1623
- return;
1624
- subagentCost = totalCost;
1625
- requestRender();
1626
- })
1627
- );
1895
+ let offSubagentUsage;
1896
+ unsubs.push(() => offSubagentUsage?.());
1628
1897
  if (footerData?.onBranchChange) {
1629
1898
  unsubs.push(footerData.onBranchChange(invalidateRender));
1630
1899
  }
1631
- const requestSubagentUsage = () => {
1632
- pi.events.emit("thoth:subagent-usage:request", {
1633
- parentSessionId: ctx.sessionManager.getSessionId()
1900
+ const subscribeAndRequestSubagentUsage = () => {
1901
+ const sessionId = ctx.sessionManager.getSessionId();
1902
+ offSubagentUsage?.();
1903
+ offSubagentUsage = subscribe(pi.events, SUBAGENTS_USAGE_CHANNEL, {
1904
+ sessionId,
1905
+ onSnapshot: ({ data }) => {
1906
+ if (disposed) return;
1907
+ subagentCost = data.totalCost;
1908
+ requestRender();
1909
+ }
1910
+ });
1911
+ request(pi.events, SUBAGENTS_USAGE_REQUEST, {
1912
+ source: "@thoth-agents/pi-thoth-theme",
1913
+ sessionId,
1914
+ data: {}
1634
1915
  });
1635
1916
  };
1636
1917
  if (pi?.on) {
@@ -1641,7 +1922,7 @@ function registerStatusLine(pi, ctx, config) {
1641
1922
  throughput.reset();
1642
1923
  if (inputBoxEnabled) working.end();
1643
1924
  refreshSession();
1644
- requestSubagentUsage();
1925
+ subscribeAndRequestSubagentUsage();
1645
1926
  })
1646
1927
  );
1647
1928
  if (inputBoxEnabled) {
@@ -1666,7 +1947,7 @@ function registerStatusLine(pi, ctx, config) {
1666
1947
  unsubs.push(pi.on("model_select", refreshSession));
1667
1948
  unsubs.push(pi.on("thinking_level_select", invalidateRender));
1668
1949
  }
1669
- requestSubagentUsage();
1950
+ subscribeAndRequestSubagentUsage();
1670
1951
  return {
1671
1952
  getStatusSnapshot,
1672
1953
  render(width) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thoth-agents/pi-thoth-theme",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Modular Thoth theme, status line, tool styling, and welcome header for Pi.",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "@earendil-works/pi-tui": "1.0.2",
51
51
  "@earendil-works/pi-ai": "1.0.2",
52
52
  "@rolldown/binding-wasm32-wasi": "^1.2.4",
53
- "@thoth-agents/pi-core": "^0.2.0"
53
+ "@thoth-agents/pi-core": "^0.3.0"
54
54
  },
55
55
  "upstream": {
56
56
  "repository": "https://github.com/QuangThai/pi-omp-theme",