@thoth-agents/pi-thoth-theme 0.2.0 → 0.4.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 +44 -7
  2. package/dist/index.ts +570 -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
 
@@ -26,8 +37,10 @@ of the module toggles below.
26
37
  drop speed, then cache, then tokens before truncating cost. Editor-border
27
38
  metadata is decorated in place without claiming the custom editor slot.
28
39
  - **Tools**: boxed calls and results for `read`, `bash`, `powershell`, `ls`,
29
- `grep`, `find`, `edit` and `write`, with Nerd Font icons, native Unicode
30
- glyphs or ASCII alternatives. `bash` and `powershell` show a live elapsed time
40
+ `grep`, `find`, `edit`, `write` and `codemode`, with Nerd Font icons, native Unicode
41
+ glyphs or ASCII alternatives. `codemode` previews up to eight nested calls
42
+ with status and duration; expanded results show the script, output and errors
43
+ without replacing Pi's builtin codemode extension. `bash` and `powershell` show a live elapsed time
31
44
  while running. Execution and parameters delegate to Pi's built-in tools;
32
45
  Pi's native output toggle still
33
46
  collapses and expands results. Registered non-built-in tools keep their own
@@ -41,11 +54,31 @@ of the module toggles below.
41
54
  tool providers, and recent sessions. Resource and session details load
42
55
  best-effort through public APIs.
43
56
 
57
+ ## Subagent usage channel
58
+
59
+ The status line subscribes to pi-core's v1 `thoth:subagents:usage` channel and
60
+ requests a snapshot on `thoth:subagents:usage:request` at session start, after
61
+ subscribing, using `data: {}`. The producer is `@thoth-agents/pi-subagents`;
62
+ envelopes are `{ v, source, sessionId, at, data }`, with the parent session ID
63
+ in the envelope, Unix-millisecond `at`, and only cumulative `totalCost` and
64
+ `runCount` in data. Pre-readiness requests are answered once after usage restore.
65
+ Invalid, unsupported-version and foreign-session envelopes are ignored; no task
66
+ prompts, results, transcripts or logs are consumed.
67
+
68
+ The raw `thoth:subagent-usage` bus event/request is removed with no fallback or
69
+ dual publication. Upgrade pi-subagents and pi-thoth-theme to `>=0.3.0` together
70
+ and `/reload`; mixed old/new versions lose subagent cost display. The persisted
71
+ usage checkpoint custom-entry type remains `thoth:subagent-usage`, so checkpoint
72
+ restore and replay-duplicate suppression are unchanged. See
73
+ [pi-core's exact contract](../pi-core/README.md#task-summary-and-usage-channels).
74
+
44
75
  ## Render KIT
45
76
 
46
77
  Provides the Render KIT v1 contract defined by `@thoth-agents/pi-core` for
47
78
  first-party tools, custom messages and above-editor widgets. The kit shares this
48
79
  package's frames, theme roles, collapse hints and working/elapsed indicators.
80
+ The Render KIT v1 semantic icon table includes `elapsed` for work-panel metrics,
81
+ with nerd-clock, Unicode and ASCII alternatives.
49
82
  Producers discover it at render time, so extension load order does not matter;
50
83
  they do not depend on this package and keep native Pi rendering when it is absent.
51
84
 
@@ -65,7 +98,7 @@ Edit `~/.pi/agent/pi-thoth-theme.json` (or
65
98
  "icons": "nerd",
66
99
  "statusLine": {
67
100
  "enabled": true,
68
- "subscriptionProviders": ["claude-bridge"]
101
+ "subscriptionProviders": ["claude-bridge", "antigravity"]
69
102
  },
70
103
  "inputBox": { "enabled": true },
71
104
  "tools": {
@@ -82,14 +115,18 @@ or `"ascii"` (plain-text alternatives).
82
115
  Missing or malformed icon values use `"nerd"`. Each module's `enabled` key
83
116
  accepts a boolean and defaults to `true`. Set it to `false` to leave Pi's native
84
117
  behavior on that surface. `statusLine.subscriptionProviders` accepts an array of
85
- provider identifier strings (defaults to `["claude-bridge"]`) to mark
118
+ provider identifier strings (defaults to `["claude-bridge", "antigravity"]`) to mark
86
119
  subscription-backed usage with `(sub)` in the status line cost segment.
120
+ Classification uses the current provider for the displayed total, not a
121
+ per-message billing split; explicit user lists are preserved. Antigravity costs
122
+ are API-equivalent catalog estimates, not subscription charges.
87
123
 
88
124
  `inputBox.enabled` defaults to `true` and requires `statusLine.enabled` to be
89
125
  `true`. It frames the native editor in a rounded `muted` (sand) box with side borders,
90
126
  a dim `type or / for commands` placeholder, and top-left ready or native working
91
127
  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.
128
+ the top border, using pi-core's root-exported `formatCwd` for home abbreviation
129
+ (shared with the sidebar); model, thinking effort and context usage appear on the bottom.
93
130
  Native scroll indicators are preserved. The status line always stays in its
94
131
  separate footer row below the box. Narrow widths use the native editor geometry.
95
132
  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,15 @@ 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;
57
+ function renderToolFooter(kit, theme, options) {
58
+ if (kit?.toolFooter) return kit.toolFooter(theme, options);
59
+ const elapsedMs = getToolElapsedMs(options);
60
+ const elapsed = elapsedMs === void 0 ? "" : formatDuration(Math.floor(elapsedMs / 1e3) * 1e3);
61
+ const terminalGlyph = terminalFooterGlyphs[options.status];
62
+ const status = terminalGlyph ?? (options.status === "in_progress" ? "running" : options.status);
63
+ const summary = typeof options.summary === "string" ? [options.summary] : options.summary ?? [];
64
+ return [status, elapsed, ...terminalGlyph ? summary : []].filter(Boolean).join(" \xB7 ");
65
+ }
84
66
  function registration() {
85
67
  try {
86
68
  const record = shared[registryKey];
@@ -98,6 +80,348 @@ function registerRenderKit(kit, owner) {
98
80
  function withdrawRenderKit(token) {
99
81
  if (registration()?.token === token) delete shared[registryKey];
100
82
  }
83
+ function getRenderKit() {
84
+ try {
85
+ const kit = registration()?.kit;
86
+ if (kit?.version === RENDER_KIT_VERSION && (!("resolveToolRenderers" in kit) || typeof kit.resolveToolRenderers === "function") && (!("toolFooter" in kit) || typeof kit.toolFooter === "function") && (!("icon" in kit) || typeof kit.icon === "function") && [
87
+ "card",
88
+ "collapse",
89
+ "cachedComponent",
90
+ "indicator",
91
+ "statusGlyph",
92
+ "widgetHeading",
93
+ "treeRow",
94
+ "fg"
95
+ ].every(
96
+ (method2) => typeof kit[method2] === "function"
97
+ ))
98
+ return kit;
99
+ } catch {
100
+ }
101
+ return void 0;
102
+ }
103
+ function createKitRenderMemo() {
104
+ let cached;
105
+ return {
106
+ render(width, build) {
107
+ const kit = getRenderKit();
108
+ if (!cached || cached.width !== width || cached.kit !== kit) {
109
+ cached = { width, kit, lines: build(kit) };
110
+ }
111
+ return cached.lines;
112
+ },
113
+ invalidate() {
114
+ cached = void 0;
115
+ }
116
+ };
117
+ }
118
+ var nativeFrames, terminalFooterGlyphs, RENDER_KIT_VERSION, registryKey, shared;
119
+ var init_render_kit = __esm({
120
+ "../pi-core/src/render-kit.ts"() {
121
+ "use strict";
122
+ init_duration();
123
+ nativeFrames = {
124
+ spinnerFrames: Object.freeze([
125
+ "\u280B",
126
+ "\u2819",
127
+ "\u2839",
128
+ "\u2838",
129
+ "\u283C",
130
+ "\u2834",
131
+ "\u2826",
132
+ "\u2827",
133
+ "\u2807",
134
+ "\u280F"
135
+ ]),
136
+ workingFrames: Object.freeze(["\u25B3", "\u25ED", "\u25B2", "\u25EE"])
137
+ };
138
+ terminalFooterGlyphs = {
139
+ completed: "\u2713",
140
+ deleted: "\u2713",
141
+ failed: "\u2717",
142
+ cancelled: "\u2717",
143
+ interrupted: "\u2717",
144
+ blocked: "\u2717"
145
+ };
146
+ RENDER_KIT_VERSION = 1;
147
+ registryKey = /* @__PURE__ */ Symbol.for(
148
+ `thoth-agents.pi-core.render-kit.v${RENDER_KIT_VERSION}`
149
+ );
150
+ shared = globalThis;
151
+ }
152
+ });
153
+
154
+ // ../pi-core/src/panel-frame.ts
155
+ var init_panel_frame = __esm({
156
+ "../pi-core/src/panel-frame.ts"() {
157
+ "use strict";
158
+ init_render_kit();
159
+ }
160
+ });
161
+
162
+ // ../pi-core/src/validation.ts
163
+ function isRecord(value) {
164
+ return typeof value === "object" && value !== null && !Array.isArray(value);
165
+ }
166
+ function isNonEmptyString(value) {
167
+ return typeof value === "string" && value.length > 0;
168
+ }
169
+ function isNonNegativeInteger(value) {
170
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
171
+ }
172
+
173
+ // ../pi-core/src/channels.ts
174
+ function defineChannel(definition) {
175
+ return definition;
176
+ }
177
+ function publish(events, channel, snapshot) {
178
+ events.emit(channel.name, {
179
+ v: channel.version,
180
+ source: snapshot.source,
181
+ sessionId: snapshot.sessionId,
182
+ at: Date.now(),
183
+ data: snapshot.data
184
+ });
185
+ }
186
+ function request(events, requestChannel, payload) {
187
+ publish(events, requestChannel, payload);
188
+ }
189
+ function subscribe(events, channel, options) {
190
+ return events.on(channel.name, (value) => {
191
+ if (!isEnvelope(value, channel, options.sessionId)) return;
192
+ options.onSnapshot(value);
193
+ });
194
+ }
195
+ function isEnvelope(value, channel, sessionId) {
196
+ try {
197
+ 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);
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ // ../pi-core/src/background.ts
204
+ var backgroundStatuses = [
205
+ "running",
206
+ "succeeded",
207
+ "failed",
208
+ "cancelled",
209
+ "timed_out"
210
+ ];
211
+ var BACKGROUND_PROGRESS_MAX_LENGTH = 200;
212
+ function isBackgroundSnapshot(value) {
213
+ try {
214
+ if (!isExactRecord(value, ["tasks", "counts"]) || !Array.isArray(value.tasks))
215
+ return false;
216
+ for (const task of value.tasks) {
217
+ if (!isBackgroundTask(task)) return false;
218
+ }
219
+ const { counts } = value;
220
+ return isExactRecord(counts, backgroundStatuses) && backgroundStatuses.every((status) => isNonNegativeInteger(counts[status]));
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ function isExactRecord(value, allowedKeys) {
226
+ if (!isRecord(value)) return false;
227
+ const prototype = Object.getPrototypeOf(value);
228
+ return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every(
229
+ (key) => typeof key === "string" && allowedKeys.includes(key)
230
+ );
231
+ }
232
+ function isOptionalTimestamp(value) {
233
+ return value === void 0 || isNonNegativeInteger(value);
234
+ }
235
+ function isProgress(value) {
236
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= BACKGROUND_PROGRESS_MAX_LENGTH;
237
+ }
238
+ function isBackgroundTask(value) {
239
+ return isExactRecord(value, [
240
+ "id",
241
+ "name",
242
+ "kind",
243
+ "status",
244
+ "createdAt",
245
+ "startedAt",
246
+ "endedAt",
247
+ "deadlineAt",
248
+ "lastCheckedAt",
249
+ "lastProgressAt",
250
+ "stopRequestedAt",
251
+ "dismissedAt",
252
+ "exitCode",
253
+ "signal",
254
+ "progress",
255
+ "dismissed"
256
+ ]) && 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");
257
+ }
258
+ var BACKGROUND_STATE_CHANNEL = defineChannel({
259
+ name: "thoth:background:state",
260
+ version: 1,
261
+ validate: isBackgroundSnapshot
262
+ });
263
+ function isBackgroundStateRequest(value) {
264
+ try {
265
+ return isExactRecord(value, []);
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ var BACKGROUND_STATE_REQUEST = defineChannel({
271
+ name: "thoth:background:state:request",
272
+ version: 1,
273
+ validate: isBackgroundStateRequest
274
+ });
275
+
276
+ // ../pi-core/src/cwd.ts
277
+ import { isAbsolute, relative, resolve, sep } from "node:path";
278
+ function formatCwd(cwd, home) {
279
+ if (!home) return cwd;
280
+ const relativeToHome = relative(resolve(home), resolve(cwd));
281
+ if (relativeToHome === ".." || relativeToHome.startsWith(`..${sep}`) || isAbsolute(relativeToHome)) {
282
+ return cwd;
283
+ }
284
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
285
+ }
286
+
287
+ // ../pi-core/src/index.ts
288
+ init_duration();
289
+ init_render_kit();
290
+
291
+ // ../pi-core/src/session-cost.ts
292
+ function computeSessionCost(entries, { subscriptionProviders = [], providerOf } = {}) {
293
+ let cost = 0;
294
+ for (const entry of entries ?? []) {
295
+ if (!entry) continue;
296
+ if (entry.type === "message") {
297
+ const message = entry.message;
298
+ if (message?.role === "assistant" || message?.role === "toolResult")
299
+ cost += message.usage?.cost?.total || 0;
300
+ } else if (["branch_summary", "compaction", "usage"].includes(entry.type)) {
301
+ cost += entry.usage?.cost?.total || 0;
302
+ }
303
+ }
304
+ const provider = providerOf?.();
305
+ return {
306
+ cost,
307
+ isSubscription: provider !== void 0 && subscriptionProviders.includes(provider)
308
+ };
309
+ }
310
+ function combineSessionAndSubagentCost(sessionCost, latestSubagentCost = 0) {
311
+ return sessionCost + latestSubagentCost;
312
+ }
313
+
314
+ // ../pi-core/src/subagents.ts
315
+ var subagentStatuses = [
316
+ "queued",
317
+ "running",
318
+ "stopping",
319
+ "completed",
320
+ "failed",
321
+ "cancelled",
322
+ "interrupted"
323
+ ];
324
+ var subagentEfforts = [
325
+ "off",
326
+ "minimal",
327
+ "low",
328
+ "medium",
329
+ "high",
330
+ "xhigh",
331
+ "max"
332
+ ];
333
+ var SUBAGENT_PREVIEW_MAX_LENGTH = 800;
334
+ function isSubagentsSnapshot(value) {
335
+ try {
336
+ if (!isExactRecord2(value, ["tasks", "history", "counts", "totals"]) || !Array.isArray(value.tasks) || !Array.isArray(value.history) || value.history.length > 100)
337
+ return false;
338
+ for (const tasks of [value.tasks, value.history]) {
339
+ for (const task of tasks) {
340
+ if (!isSubagentTask(task)) return false;
341
+ }
342
+ }
343
+ const { counts, totals } = value;
344
+ return isCounts(counts) && isExactRecord2(totals, [...subagentStatuses, "total"]) && isNonNegativeInteger(totals.total) && subagentStatuses.every((status) => isNonNegativeInteger(totals[status]));
345
+ } catch {
346
+ return false;
347
+ }
348
+ }
349
+ function isExactRecord2(value, allowedKeys) {
350
+ if (!isRecord(value)) return false;
351
+ const prototype = Object.getPrototypeOf(value);
352
+ return (prototype === Object.prototype || prototype === null) && Reflect.ownKeys(value).every(
353
+ (key) => typeof key === "string" && allowedKeys.includes(key)
354
+ );
355
+ }
356
+ function isCounts(value) {
357
+ return isExactRecord2(value, subagentStatuses) && subagentStatuses.every((status) => isNonNegativeInteger(value[status]));
358
+ }
359
+ function isOptionalString(value) {
360
+ return value === void 0 || typeof value === "string";
361
+ }
362
+ function isOptionalTimestamp2(value) {
363
+ return value === void 0 || isNonNegativeInteger(value);
364
+ }
365
+ function isNonNegativeNumber(value) {
366
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
367
+ }
368
+ function isTaskUsage(value) {
369
+ 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));
370
+ }
371
+ function isSubagentTask(value) {
372
+ return isExactRecord2(value, [
373
+ "id",
374
+ "agent",
375
+ "displayName",
376
+ "mode",
377
+ "status",
378
+ "model",
379
+ "effort",
380
+ "createdAt",
381
+ "startedAt",
382
+ "endedAt",
383
+ "lastActivityAt",
384
+ "usage",
385
+ "preview"
386
+ ]) && 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);
387
+ }
388
+ var SUBAGENTS_STATE_CHANNEL = defineChannel({
389
+ name: "thoth:subagents:state",
390
+ version: 2,
391
+ validate: isSubagentsSnapshot
392
+ });
393
+ function isSubagentsStateRequest(value) {
394
+ try {
395
+ return isExactRecord2(value, []);
396
+ } catch {
397
+ return false;
398
+ }
399
+ }
400
+ function isSubagentsUsageRequest(value) {
401
+ return isSubagentsStateRequest(value);
402
+ }
403
+ var SUBAGENTS_STATE_REQUEST = defineChannel({
404
+ name: "thoth:subagents:state:request",
405
+ version: 1,
406
+ validate: isSubagentsStateRequest
407
+ });
408
+ function isSubagentsUsageSnapshot(value) {
409
+ try {
410
+ return isExactRecord2(value, ["totalCost", "runCount"]) && isNonNegativeNumber(value.totalCost) && isNonNegativeInteger(value.runCount);
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+ var SUBAGENTS_USAGE_CHANNEL = defineChannel({
416
+ name: "thoth:subagents:usage",
417
+ version: 1,
418
+ validate: isSubagentsUsageSnapshot
419
+ });
420
+ var SUBAGENTS_USAGE_REQUEST = defineChannel({
421
+ name: "thoth:subagents:usage:request",
422
+ version: 1,
423
+ validate: isSubagentsUsageRequest
424
+ });
101
425
 
102
426
  // ../pi-core/src/todo.ts
103
427
  var todoStatuses = [
@@ -123,11 +447,11 @@ function isTodoSnapshot(value) {
123
447
  function isTaskId(value) {
124
448
  return isNonNegativeInteger(value) && value > 0;
125
449
  }
126
- function isOptionalString(value) {
450
+ function isOptionalString2(value) {
127
451
  return value === void 0 || typeof value === "string";
128
452
  }
129
453
  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))
454
+ 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
455
  return false;
132
456
  for (const id of value.blockedBy) {
133
457
  if (!isTaskId(id)) return false;
@@ -160,6 +484,16 @@ var registryKey2 = /* @__PURE__ */ Symbol.for(
160
484
  `thoth-agents.pi-core.tool-definitions.v${TOOL_DEFINITION_REGISTRY_VERSION}`
161
485
  );
162
486
 
487
+ // ../pi-core/src/work-panel-host.ts
488
+ init_render_kit();
489
+
490
+ // ../pi-core/src/work-panel-detail.ts
491
+ init_panel_frame();
492
+ init_render_kit();
493
+
494
+ // ../pi-core/src/work-panel-render.ts
495
+ init_render_kit();
496
+
163
497
  // src/render-kit/index.ts
164
498
  import {
165
499
  stripTerminalSequences,
@@ -192,6 +526,7 @@ var ICONS = {
192
526
  model: { nerd: "\u{F06A9}", unicode: "\u25CF", ascii: "*" },
193
527
  effort: { nerd: "\u{F09D1}", unicode: "\u25D0", ascii: "o" },
194
528
  context: { nerd: "\uF2DB", unicode: "ctx", ascii: "ctx" },
529
+ elapsed: { nerd: "\uF017", unicode: "\u25F7", ascii: "elapsed" },
195
530
  cost: { nerd: "\uF155", unicode: "$", ascii: "$" },
196
531
  tokensIn: { nerd: "\uF062", unicode: "\u2191", ascii: "^" },
197
532
  tokensOut: { nerd: "\uF063", unicode: "\u2193", ascii: "v" },
@@ -453,7 +788,7 @@ function createComponent(renderFn) {
453
788
  }
454
789
 
455
790
  // src/tools/frame.ts
456
- import { relative, resolve } from "node:path";
791
+ import { relative as relative2, resolve as resolve2 } from "node:path";
457
792
  import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
458
793
  var FRAME_HORIZONTAL = "\u2500";
459
794
  var FRAME_VERTICAL = "\u2502";
@@ -467,9 +802,9 @@ function formatDisplayPath(targetPath, cwd) {
467
802
  if (!targetPath) return "";
468
803
  if (!cwd) return targetPath;
469
804
  try {
470
- const absTarget = resolve(cwd, targetPath);
471
- const absCwd = resolve(cwd);
472
- const rel = relative(absCwd, absTarget);
805
+ const absTarget = resolve2(cwd, targetPath);
806
+ const absCwd = resolve2(cwd);
807
+ const rel = relative2(absCwd, absTarget);
473
808
  if (!rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel)) {
474
809
  return rel.replace(/\\/g, "/") || ".";
475
810
  }
@@ -522,6 +857,14 @@ function renderFrameDivider(theme, title, width, borderColor = "accent") {
522
857
  if (safeWidth <= 4) {
523
858
  return [truncateToWidth2(title, safeWidth)];
524
859
  }
860
+ if (!title) {
861
+ return [
862
+ theme.fg(
863
+ borderColor,
864
+ `${FRAME_DIVIDER_LEFT}${FRAME_HORIZONTAL.repeat(safeWidth - 2)}${FRAME_DIVIDER_RIGHT}`
865
+ )
866
+ ];
867
+ }
525
868
  const innerWidth = Math.max(1, safeWidth - 2);
526
869
  const rawTitle = ` ${title} `;
527
870
  const maxTitleWidth = Math.max(1, innerWidth - 4);
@@ -837,7 +1180,7 @@ function parseSubscriptionProviders(value) {
837
1180
  (item) => typeof item === "string" && item.trim().length > 0
838
1181
  );
839
1182
  }
840
- return ["claude-bridge"];
1183
+ return ["claude-bridge", "antigravity"];
841
1184
  }
842
1185
  function parseRespectPackages(value) {
843
1186
  const packages = asObject(value).respectPackages;
@@ -876,19 +1219,10 @@ import {
876
1219
  } from "@earendil-works/pi-tui";
877
1220
 
878
1221
  // src/status-line/layout.ts
879
- import { isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
880
1222
  import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
881
1223
  function themeFg(theme, token, text) {
882
1224
  return theme?.fg ? theme.fg(token, text) : text;
883
1225
  }
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
1226
  function formatTokens(count) {
893
1227
  if (count < 1e3) return count.toString();
894
1228
  if (count < 1e6) {
@@ -947,14 +1281,18 @@ function contextBar(percent, isAscii, theme) {
947
1281
  let token = "success";
948
1282
  if (percent >= 90) token = "error";
949
1283
  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";
1284
+ const halves = Math.min(
1285
+ 16,
1286
+ Math.max(percent > 0 ? 1 : 0, Math.round(percent / 100 * 16))
1287
+ );
1288
+ const full = Math.floor(halves / 2);
1289
+ const half = halves % 2;
953
1290
  const dim = (text) => themeFg(theme, "dim", text);
1291
+ const filled = isAscii ? "#".repeat(full) + (half ? "=" : "") : "\u25C6".repeat(full) + (half ? "\u2B16" : "");
1292
+ const empty = (isAscii ? "-" : "\u25C7").repeat(8 - full - half);
1293
+ const bar = isAscii ? `${dim("[")}${themeFg(theme, token, filled)}${dim(empty)}${dim("]")}` : `${themeFg(theme, token, filled)}${dim(empty)}`;
954
1294
  return {
955
- bar: `${dim("[")}${themeFg(theme, token, fillChar.repeat(filled))}${dim(
956
- emptyChar.repeat(10 - filled)
957
- )}${dim("]")}`,
1295
+ bar,
958
1296
  pct: themeFg(theme, token, `${Math.round(percent)}%`)
959
1297
  };
960
1298
  }
@@ -993,7 +1331,7 @@ function renderStatusLine(data, options) {
993
1331
  const sep2 = ` ${themeFg(theme, "border", icon("separator", mode))} `;
994
1332
  const segments = [];
995
1333
  if (data.cost !== void 0) {
996
- const total = data.cost + (data.subagentCost ?? 0);
1334
+ const total = combineSessionAndSubagentCost(data.cost, data.subagentCost);
997
1335
  segments.push(
998
1336
  themeFg(
999
1337
  theme,
@@ -1449,25 +1787,6 @@ function createWorkingState(requestRender) {
1449
1787
  };
1450
1788
  }
1451
1789
 
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
1790
  // src/status-line/throughput.ts
1472
1791
  function createThroughputTracker() {
1473
1792
  let pending;
@@ -1546,7 +1865,8 @@ function calculateSessionTokens(source) {
1546
1865
  // src/status-line/index.ts
1547
1866
  function registerStatusLine(pi, ctx, config) {
1548
1867
  const subscriptionProviders = config.statusLine.subscriptionProviders ?? [
1549
- "claude-bridge"
1868
+ "claude-bridge",
1869
+ "antigravity"
1550
1870
  ];
1551
1871
  const inputBoxEnabled = config.inputBox?.enabled !== false;
1552
1872
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -1589,12 +1909,13 @@ function registerStatusLine(pi, ctx, config) {
1589
1909
  };
1590
1910
  function readSession() {
1591
1911
  const usage = ctx.getContextUsage?.();
1592
- const provider = ctx.model?.provider;
1593
1912
  const entries = ctx.sessionManager.getEntries();
1594
1913
  return {
1595
- cost: calculateSessionCost(entries),
1914
+ ...computeSessionCost(entries, {
1915
+ subscriptionProviders,
1916
+ providerOf: () => ctx.model?.provider
1917
+ }),
1596
1918
  tokenTotals: calculateSessionTokens(entries),
1597
- isSubscription: provider !== void 0 && subscriptionProviders.includes(provider),
1598
1919
  contextTokens: usage?.tokens ?? null,
1599
1920
  contextPercent: usage?.percent ?? null,
1600
1921
  contextWindow: usage?.contextWindow
@@ -1615,22 +1936,26 @@ function registerStatusLine(pi, ctx, config) {
1615
1936
  sessionDirty = true;
1616
1937
  invalidateRender();
1617
1938
  };
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
- );
1939
+ let offSubagentUsage;
1940
+ unsubs.push(() => offSubagentUsage?.());
1628
1941
  if (footerData?.onBranchChange) {
1629
1942
  unsubs.push(footerData.onBranchChange(invalidateRender));
1630
1943
  }
1631
- const requestSubagentUsage = () => {
1632
- pi.events.emit("thoth:subagent-usage:request", {
1633
- parentSessionId: ctx.sessionManager.getSessionId()
1944
+ const subscribeAndRequestSubagentUsage = () => {
1945
+ const sessionId = ctx.sessionManager.getSessionId();
1946
+ offSubagentUsage?.();
1947
+ offSubagentUsage = subscribe(pi.events, SUBAGENTS_USAGE_CHANNEL, {
1948
+ sessionId,
1949
+ onSnapshot: ({ data }) => {
1950
+ if (disposed) return;
1951
+ subagentCost = data.totalCost;
1952
+ requestRender();
1953
+ }
1954
+ });
1955
+ request(pi.events, SUBAGENTS_USAGE_REQUEST, {
1956
+ source: "@thoth-agents/pi-thoth-theme",
1957
+ sessionId,
1958
+ data: {}
1634
1959
  });
1635
1960
  };
1636
1961
  if (pi?.on) {
@@ -1641,7 +1966,7 @@ function registerStatusLine(pi, ctx, config) {
1641
1966
  throughput.reset();
1642
1967
  if (inputBoxEnabled) working.end();
1643
1968
  refreshSession();
1644
- requestSubagentUsage();
1969
+ subscribeAndRequestSubagentUsage();
1645
1970
  })
1646
1971
  );
1647
1972
  if (inputBoxEnabled) {
@@ -1666,7 +1991,7 @@ function registerStatusLine(pi, ctx, config) {
1666
1991
  unsubs.push(pi.on("model_select", refreshSession));
1667
1992
  unsubs.push(pi.on("thinking_level_select", invalidateRender));
1668
1993
  }
1669
- requestSubagentUsage();
1994
+ subscribeAndRequestSubagentUsage();
1670
1995
  return {
1671
1996
  getStatusSnapshot,
1672
1997
  render(width) {
@@ -1973,6 +2298,148 @@ function createCustomPowerShellTool(_cwd, config) {
1973
2298
  return createCustomShellTool(POWERSHELL_CONFIG, config);
1974
2299
  }
1975
2300
 
2301
+ // src/tools/codemode.ts
2302
+ var COLLAPSED_CALLS = 8;
2303
+ var RESULT_PRESENT = "codemodeResultPresent";
2304
+ var RESULT_BORDER = "codemodeResultBorder";
2305
+ var CALL_COMPONENT = "codemodeCallComponent";
2306
+ var CALL_STATUS = {
2307
+ running: "running",
2308
+ ok: "completed",
2309
+ error: "failed",
2310
+ cancelled: "cancelled"
2311
+ };
2312
+ function displayLines(text) {
2313
+ return text.split("\n").map(escapeControlCharacters);
2314
+ }
2315
+ function scriptText(args) {
2316
+ return args && typeof args === "object" && "code" in args && typeof args.code === "string" ? args.code : "(script unavailable)";
2317
+ }
2318
+ function displayCalls(details) {
2319
+ if (!details || typeof details !== "object" || !("calls" in details) || !Array.isArray(details.calls))
2320
+ return [];
2321
+ return details.calls.flatMap((call) => {
2322
+ if (!call || typeof call !== "object" || !("name" in call) || typeof call.name !== "string" || !("status" in call) || call.status !== "running" && call.status !== "ok" && call.status !== "error" && call.status !== "cancelled")
2323
+ return [];
2324
+ return [
2325
+ {
2326
+ name: call.name,
2327
+ status: call.status,
2328
+ durationMs: "durationMs" in call && typeof call.durationMs === "number" && Number.isFinite(call.durationMs) && call.durationMs >= 0 ? call.durationMs : void 0,
2329
+ error: "error" in call && typeof call.error === "string" ? call.error : void 0
2330
+ }
2331
+ ];
2332
+ });
2333
+ }
2334
+ function createCodemodeRenderer(config) {
2335
+ const fallbackKit = createRenderKit({}, void 0, config.icons);
2336
+ return {
2337
+ renderShell: "self",
2338
+ renderCall(_args, theme, context) {
2339
+ if (context?.state) {
2340
+ context.state[RESULT_PRESENT] = false;
2341
+ context.state[RESULT_BORDER] = void 0;
2342
+ }
2343
+ const memo = createKitRenderMemo();
2344
+ const component = {
2345
+ invalidate: () => memo.invalidate(),
2346
+ render(width) {
2347
+ return memo.render(width, (registeredKit) => {
2348
+ const kit = registeredKit ?? fallbackKit;
2349
+ const hasResult = context?.state?.[RESULT_PRESENT] === true;
2350
+ const status = context?.executionStarted ? "running" : "pending";
2351
+ return kit.card(
2352
+ theme,
2353
+ {
2354
+ title: theme.fg("toolTitle", "codemode"),
2355
+ footer: hasResult ? "" : renderToolFooter(kit, theme, { status, context }),
2356
+ part: hasResult ? "start" : "full",
2357
+ ...context?.state?.[RESULT_BORDER]
2358
+ },
2359
+ width
2360
+ );
2361
+ });
2362
+ }
2363
+ };
2364
+ if (context?.state) context.state[CALL_COMPONENT] = component;
2365
+ return component;
2366
+ },
2367
+ renderResult(result, options, theme, context) {
2368
+ const status = options.isPartial ? "running" : context?.isError ? "failed" : "completed";
2369
+ const border = {
2370
+ isError: status === "failed",
2371
+ isSuccess: status === "completed" && context?.executionStarted !== false
2372
+ };
2373
+ if (context?.state) {
2374
+ context.state[RESULT_PRESENT] = true;
2375
+ context.state[RESULT_BORDER] = border;
2376
+ context.state[CALL_COMPONENT]?.invalidate?.();
2377
+ }
2378
+ const calls = displayCalls(result.details);
2379
+ const visibleCalls = options.expanded ? calls : calls.slice(0, COLLAPSED_CALLS);
2380
+ const output = displayLines(getResultText(result) || "(no output)");
2381
+ const errors = calls.flatMap(
2382
+ (call) => call.error ? displayLines(`${call.name}: ${call.error}`).map(
2383
+ (line) => theme.fg("error", line)
2384
+ ) : []
2385
+ );
2386
+ const memo = createKitRenderMemo();
2387
+ return {
2388
+ invalidate: () => memo.invalidate(),
2389
+ render(width) {
2390
+ return memo.render(width, (registeredKit) => {
2391
+ const kit = registeredKit ?? fallbackKit;
2392
+ return kit.card(
2393
+ theme,
2394
+ {
2395
+ part: context?.state?.[CALL_COMPONENT] ? "end" : "full",
2396
+ title: theme.fg("toolTitle", "codemode"),
2397
+ body: (bodyWidth) => {
2398
+ if (calls.length === 0) {
2399
+ return options.expanded ? [] : kit.collapse(theme, output, options);
2400
+ }
2401
+ const rows = visibleCalls.map((call, i) => {
2402
+ const duration = call.durationMs === void 0 ? "" : ` \xB7 ${formatDuration(call.durationMs)}`;
2403
+ return kit.treeRow(
2404
+ theme,
2405
+ {
2406
+ text: `${kit.statusGlyph(theme, CALL_STATUS[call.status])} ${escapeControlCharacters(call.name)}${duration}`,
2407
+ last: i === visibleCalls.length - 1
2408
+ },
2409
+ bodyWidth
2410
+ );
2411
+ });
2412
+ if (!options.expanded && calls.length > COLLAPSED_CALLS) {
2413
+ rows.push(
2414
+ theme.fg(
2415
+ "dim",
2416
+ `+${calls.length - COLLAPSED_CALLS} more \xB7 ctrl+o to expand`
2417
+ )
2418
+ );
2419
+ }
2420
+ return rows;
2421
+ },
2422
+ sections: options.expanded ? [
2423
+ {
2424
+ title: "Script",
2425
+ rows: displayLines(scriptText(context?.args))
2426
+ },
2427
+ { title: "Output", rows: output },
2428
+ ...errors.length ? [{ title: "Errors", rows: errors }] : []
2429
+ ] : void 0,
2430
+ wrap: options.expanded,
2431
+ footer: renderToolFooter(kit, theme, { status, context }),
2432
+ ...border
2433
+ },
2434
+ width
2435
+ );
2436
+ });
2437
+ }
2438
+ };
2439
+ }
2440
+ };
2441
+ }
2442
+
1976
2443
  // src/tools/edit.ts
1977
2444
  var COLLAPSED_DIFF_LINES = 6;
1978
2445
  function parseDiffStats(diffText) {
@@ -3064,6 +3531,7 @@ function createCustomWriteTool(cwd, config) {
3064
3531
  // src/tools/index.ts
3065
3532
  function createToolRendererResolver(pi, config, cwd = process.cwd()) {
3066
3533
  const renderers = /* @__PURE__ */ new Map([
3534
+ ["codemode", createCodemodeRenderer(config)],
3067
3535
  ["read", createCustomReadTool(cwd, config)],
3068
3536
  ["bash", createCustomBashTool(cwd, config)],
3069
3537
  ["powershell", createCustomPowerShellTool(cwd, config)],
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.4.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",