@thoth-agents/pi-thoth-theme 0.3.0 → 0.5.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 +24 -2
  2. package/dist/index.ts +221 -2
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -27,6 +27,26 @@ from the repository root. Restart Pi after installation.
27
27
  Select the `thoth` theme in Pi's theme settings. Theme selection is independent
28
28
  of the module toggles below.
29
29
 
30
+ ## User messages
31
+
32
+ The `thoth` theme renders user messages in `#E8DCB5` on `#120E0A`.
33
+ The extension surrounds the original Markdown with a top `╭── <icon> user ─…╮`
34
+ and bottom `╰─…╯` frame, separated from the content by blank lines. Both borders
35
+ fill the content width after Pi's horizontal padding and adapt on terminal resize.
36
+ Framing is skipped when the width is missing, invalid, or too narrow to fit the
37
+ title, one fill cell and the closing corner (13 columns with the current icons).
38
+ This is display-only: stored messages, assistant replies and thinking are unchanged.
39
+ Blank content and already-framed messages are left alone. The frame is independent
40
+ of the optional modules below.
41
+
42
+ The frame reuses `icons`: `"nerd"` (the existing default) selects nf-fa-user
43
+ (U+F007), `"unicode"` selects `●` (U+25CF), and `"ascii"` selects `@`.
44
+ Set `"icons": "nerd"` in the package configuration and use a Nerd Font in your
45
+ terminal for the Nerd glyph; use `"icons": "unicode"` for its Unicode fallback.
46
+
47
+ Markdown accents and syntax colors remain shared with assistant messages; only
48
+ ordinary user text and the frame use `userMessageText` (`#E8DCB5`).
49
+
30
50
  ## Modules
31
51
 
32
52
  - **Status line**: a responsive footer below the input showing cumulative
@@ -37,8 +57,10 @@ of the module toggles below.
37
57
  drop speed, then cache, then tokens before truncating cost. Editor-border
38
58
  metadata is decorated in place without claiming the custom editor slot.
39
59
  - **Tools**: boxed calls and results for `read`, `bash`, `powershell`, `ls`,
40
- `grep`, `find`, `edit` and `write`, with Nerd Font icons, native Unicode
41
- glyphs or ASCII alternatives. `bash` and `powershell` show a live elapsed time
60
+ `grep`, `find`, `edit`, `write` and `codemode`, with Nerd Font icons, native Unicode
61
+ glyphs or ASCII alternatives. `codemode` previews up to eight nested calls
62
+ with status and duration; expanded results show the script, output and errors
63
+ without replacing Pi's builtin codemode extension. `bash` and `powershell` show a live elapsed time
42
64
  while running. Execution and parameters delegate to Pi's built-in tools;
43
65
  Pi's native output toggle still
44
66
  collapses and expands results. Registered non-built-in tools keep their own
package/dist/index.ts CHANGED
@@ -54,6 +54,15 @@ function getToolElapsedMs(options) {
54
54
  state.completedElapsedMs = elapsedMs;
55
55
  return elapsedMs;
56
56
  }
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
+ }
57
66
  function registration() {
58
67
  try {
59
68
  const record = shared[registryKey];
@@ -71,6 +80,41 @@ function registerRenderKit(kit, owner) {
71
80
  function withdrawRenderKit(token) {
72
81
  if (registration()?.token === token) delete shared[registryKey];
73
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
+ }
74
118
  var nativeFrames, terminalFooterGlyphs, RENDER_KIT_VERSION, registryKey, shared;
75
119
  var init_render_kit = __esm({
76
120
  "../pi-core/src/render-kit.ts"() {
@@ -95,6 +139,9 @@ var init_render_kit = __esm({
95
139
  completed: "\u2713",
96
140
  deleted: "\u2713",
97
141
  failed: "\u2717",
142
+ timedOut: "\u25F7",
143
+ resolved: "\u21BB",
144
+ expectedExit: "\u2713",
98
145
  cancelled: "\u2717",
99
146
  interrupted: "\u2717",
100
147
  blocked: "\u2717"
@@ -115,6 +162,9 @@ var init_panel_frame = __esm({
115
162
  }
116
163
  });
117
164
 
165
+ // src/index.ts
166
+ import { visibleWidth as visibleWidth8 } from "@earendil-works/pi-tui";
167
+
118
168
  // ../pi-core/src/validation.ts
119
169
  function isRecord(value) {
120
170
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -477,6 +527,7 @@ function cachedComponent(renderFn) {
477
527
 
478
528
  // src/shared/icons.ts
479
529
  var ICONS = {
530
+ user: { nerd: "\uF007", unicode: "\u25CF", ascii: "@" },
480
531
  branch: { nerd: "\uE0A0", unicode: "\u2442", ascii: "git" },
481
532
  folder: { nerd: "\uF07C", unicode: "dir", ascii: "dir" },
482
533
  model: { nerd: "\u{F06A9}", unicode: "\u25CF", ascii: "*" },
@@ -548,7 +599,10 @@ var STATUS_ICONS = {
548
599
  running: { nerd: "\u25D0", unicode: "\u25D0", ascii: "*" },
549
600
  completed: { nerd: "\uF00C", unicode: "\u2713", ascii: "+" },
550
601
  failed: { nerd: "\uF00D", unicode: "\u2717", ascii: "x" },
551
- cancelled: STOP,
602
+ timedOut: { nerd: "\uF017", unicode: "\u25F7", ascii: "T" },
603
+ resolved: { nerd: "\uF021", unicode: "\u21BB", ascii: "~" },
604
+ expectedExit: { nerd: "\uF00C", unicode: "\u2713", ascii: "+" },
605
+ cancelled: { ...STOP, ascii: "#" },
552
606
  interrupted: STOP,
553
607
  stopping: STOP,
554
608
  deleted: BLOCKED,
@@ -923,6 +977,9 @@ var STATUS_ROLE = {
923
977
  running: "accent",
924
978
  completed: "success",
925
979
  failed: "error",
980
+ timedOut: "warning",
981
+ resolved: "muted",
982
+ expectedExit: "dim",
926
983
  cancelled: "muted",
927
984
  interrupted: "warning",
928
985
  stopping: "warning",
@@ -934,13 +991,16 @@ var TOOL_FOOTER_STATUS = {
934
991
  completed: "completed",
935
992
  deleted: "completed",
936
993
  failed: "failed",
994
+ timedOut: "timedOut",
995
+ resolved: "resolved",
996
+ expectedExit: "expectedExit",
937
997
  cancelled: "failed",
938
998
  interrupted: "failed",
939
999
  blocked: "failed"
940
1000
  };
941
1001
  function startsWithFooterGlyph(footer, mode) {
942
1002
  const plain = stripTerminalSequences(footer);
943
- return ["completed", "failed"].some((status) => {
1003
+ return ["completed", "failed", "timedOut", "resolved", "expectedExit"].some((status) => {
944
1004
  const glyph = statusIcon(status, mode);
945
1005
  return plain.startsWith(glyph) && (plain.length === glyph.length || /^\s/u.test(plain.slice(glyph.length)));
946
1006
  });
@@ -2254,6 +2314,148 @@ function createCustomPowerShellTool(_cwd, config) {
2254
2314
  return createCustomShellTool(POWERSHELL_CONFIG, config);
2255
2315
  }
2256
2316
 
2317
+ // src/tools/codemode.ts
2318
+ var COLLAPSED_CALLS = 8;
2319
+ var RESULT_PRESENT = "codemodeResultPresent";
2320
+ var RESULT_BORDER = "codemodeResultBorder";
2321
+ var CALL_COMPONENT = "codemodeCallComponent";
2322
+ var CALL_STATUS = {
2323
+ running: "running",
2324
+ ok: "completed",
2325
+ error: "failed",
2326
+ cancelled: "cancelled"
2327
+ };
2328
+ function displayLines(text) {
2329
+ return text.split("\n").map(escapeControlCharacters);
2330
+ }
2331
+ function scriptText(args) {
2332
+ return args && typeof args === "object" && "code" in args && typeof args.code === "string" ? args.code : "(script unavailable)";
2333
+ }
2334
+ function displayCalls(details) {
2335
+ if (!details || typeof details !== "object" || !("calls" in details) || !Array.isArray(details.calls))
2336
+ return [];
2337
+ return details.calls.flatMap((call) => {
2338
+ 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")
2339
+ return [];
2340
+ return [
2341
+ {
2342
+ name: call.name,
2343
+ status: call.status,
2344
+ durationMs: "durationMs" in call && typeof call.durationMs === "number" && Number.isFinite(call.durationMs) && call.durationMs >= 0 ? call.durationMs : void 0,
2345
+ error: "error" in call && typeof call.error === "string" ? call.error : void 0
2346
+ }
2347
+ ];
2348
+ });
2349
+ }
2350
+ function createCodemodeRenderer(config) {
2351
+ const fallbackKit = createRenderKit({}, void 0, config.icons);
2352
+ return {
2353
+ renderShell: "self",
2354
+ renderCall(_args, theme, context) {
2355
+ if (context?.state) {
2356
+ context.state[RESULT_PRESENT] = false;
2357
+ context.state[RESULT_BORDER] = void 0;
2358
+ }
2359
+ const memo = createKitRenderMemo();
2360
+ const component = {
2361
+ invalidate: () => memo.invalidate(),
2362
+ render(width) {
2363
+ return memo.render(width, (registeredKit) => {
2364
+ const kit = registeredKit ?? fallbackKit;
2365
+ const hasResult = context?.state?.[RESULT_PRESENT] === true;
2366
+ const status = context?.executionStarted ? "running" : "pending";
2367
+ return kit.card(
2368
+ theme,
2369
+ {
2370
+ title: theme.fg("toolTitle", "codemode"),
2371
+ footer: hasResult ? "" : renderToolFooter(kit, theme, { status, context }),
2372
+ part: hasResult ? "start" : "full",
2373
+ ...context?.state?.[RESULT_BORDER]
2374
+ },
2375
+ width
2376
+ );
2377
+ });
2378
+ }
2379
+ };
2380
+ if (context?.state) context.state[CALL_COMPONENT] = component;
2381
+ return component;
2382
+ },
2383
+ renderResult(result, options, theme, context) {
2384
+ const status = options.isPartial ? "running" : context?.isError ? "failed" : "completed";
2385
+ const border = {
2386
+ isError: status === "failed",
2387
+ isSuccess: status === "completed" && context?.executionStarted !== false
2388
+ };
2389
+ if (context?.state) {
2390
+ context.state[RESULT_PRESENT] = true;
2391
+ context.state[RESULT_BORDER] = border;
2392
+ context.state[CALL_COMPONENT]?.invalidate?.();
2393
+ }
2394
+ const calls = displayCalls(result.details);
2395
+ const visibleCalls = options.expanded ? calls : calls.slice(0, COLLAPSED_CALLS);
2396
+ const output = displayLines(getResultText(result) || "(no output)");
2397
+ const errors = calls.flatMap(
2398
+ (call) => call.error ? displayLines(`${call.name}: ${call.error}`).map(
2399
+ (line) => theme.fg("error", line)
2400
+ ) : []
2401
+ );
2402
+ const memo = createKitRenderMemo();
2403
+ return {
2404
+ invalidate: () => memo.invalidate(),
2405
+ render(width) {
2406
+ return memo.render(width, (registeredKit) => {
2407
+ const kit = registeredKit ?? fallbackKit;
2408
+ return kit.card(
2409
+ theme,
2410
+ {
2411
+ part: context?.state?.[CALL_COMPONENT] ? "end" : "full",
2412
+ title: theme.fg("toolTitle", "codemode"),
2413
+ body: (bodyWidth) => {
2414
+ if (calls.length === 0) {
2415
+ return options.expanded ? [] : kit.collapse(theme, output, options);
2416
+ }
2417
+ const rows = visibleCalls.map((call, i) => {
2418
+ const duration = call.durationMs === void 0 ? "" : ` \xB7 ${formatDuration(call.durationMs)}`;
2419
+ return kit.treeRow(
2420
+ theme,
2421
+ {
2422
+ text: `${kit.statusGlyph(theme, CALL_STATUS[call.status])} ${escapeControlCharacters(call.name)}${duration}`,
2423
+ last: i === visibleCalls.length - 1
2424
+ },
2425
+ bodyWidth
2426
+ );
2427
+ });
2428
+ if (!options.expanded && calls.length > COLLAPSED_CALLS) {
2429
+ rows.push(
2430
+ theme.fg(
2431
+ "dim",
2432
+ `+${calls.length - COLLAPSED_CALLS} more \xB7 ctrl+o to expand`
2433
+ )
2434
+ );
2435
+ }
2436
+ return rows;
2437
+ },
2438
+ sections: options.expanded ? [
2439
+ {
2440
+ title: "Script",
2441
+ rows: displayLines(scriptText(context?.args))
2442
+ },
2443
+ { title: "Output", rows: output },
2444
+ ...errors.length ? [{ title: "Errors", rows: errors }] : []
2445
+ ] : void 0,
2446
+ wrap: options.expanded,
2447
+ footer: renderToolFooter(kit, theme, { status, context }),
2448
+ ...border
2449
+ },
2450
+ width
2451
+ );
2452
+ });
2453
+ }
2454
+ };
2455
+ }
2456
+ };
2457
+ }
2458
+
2257
2459
  // src/tools/edit.ts
2258
2460
  var COLLAPSED_DIFF_LINES = 6;
2259
2461
  function parseDiffStats(diffText) {
@@ -3345,6 +3547,7 @@ function createCustomWriteTool(cwd, config) {
3345
3547
  // src/tools/index.ts
3346
3548
  function createToolRendererResolver(pi, config, cwd = process.cwd()) {
3347
3549
  const renderers = /* @__PURE__ */ new Map([
3550
+ ["codemode", createCodemodeRenderer(config)],
3348
3551
  ["read", createCustomReadTool(cwd, config)],
3349
3552
  ["bash", createCustomBashTool(cwd, config)],
3350
3553
  ["powershell", createCustomPowerShellTool(cwd, config)],
@@ -3928,6 +4131,22 @@ function registerWelcome(pi, ctx, config, options) {
3928
4131
  // src/index.ts
3929
4132
  function thothTheme(pi) {
3930
4133
  const config = loadConfig();
4134
+ pi.registerMarkdownTransformer((markdown, context) => {
4135
+ if (context.messageType !== "user" || !markdown.trim()) return markdown;
4136
+ if (/^╭── [\uF007●@] user ─+╮\r?\n\r?\n[\s\S]*\r?\n\r?\n╰─+╯$/.test(markdown))
4137
+ return markdown;
4138
+ const width = context.availableWidth;
4139
+ const title = `\u256D\u2500\u2500 ${icon("user", config.icons)} user `;
4140
+ const titleWidth = visibleWidth8(title);
4141
+ if (!Number.isInteger(width) || width < titleWidth + 2) return markdown;
4142
+ const top = `${title}${"\u2500".repeat(width - titleWidth - 1)}\u256E`;
4143
+ const bottom = `\u2570${"\u2500".repeat(width - 2)}\u256F`;
4144
+ return `${top}
4145
+
4146
+ ${markdown}
4147
+
4148
+ ${bottom}`;
4149
+ });
3931
4150
  const owner = {};
3932
4151
  const cwd = process.cwd();
3933
4152
  const resolveToolRenderers = config.tools.enabled ? createToolRendererResolver(pi, config, cwd) : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thoth-agents/pi-thoth-theme",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.3.0"
53
+ "@thoth-agents/pi-core": "^0.4.0"
54
54
  },
55
55
  "upstream": {
56
56
  "repository": "https://github.com/QuangThai/pi-omp-theme",