@thoth-agents/pi-thoth-theme 0.4.0 → 0.6.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 +20 -0
  2. package/dist/index.ts +236 -26
  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
package/dist/index.ts CHANGED
@@ -139,6 +139,9 @@ var init_render_kit = __esm({
139
139
  completed: "\u2713",
140
140
  deleted: "\u2713",
141
141
  failed: "\u2717",
142
+ timedOut: "\u25F7",
143
+ resolved: "\u21BB",
144
+ expectedExit: "\u2713",
142
145
  cancelled: "\u2717",
143
146
  interrupted: "\u2717",
144
147
  blocked: "\u2717"
@@ -159,6 +162,9 @@ var init_panel_frame = __esm({
159
162
  }
160
163
  });
161
164
 
165
+ // src/index.ts
166
+ import { visibleWidth as visibleWidth8 } from "@earendil-works/pi-tui";
167
+
162
168
  // ../pi-core/src/validation.ts
163
169
  function isRecord(value) {
164
170
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -521,6 +527,7 @@ function cachedComponent(renderFn) {
521
527
 
522
528
  // src/shared/icons.ts
523
529
  var ICONS = {
530
+ user: { nerd: "\uF007", unicode: "\u25CF", ascii: "@" },
524
531
  branch: { nerd: "\uE0A0", unicode: "\u2442", ascii: "git" },
525
532
  folder: { nerd: "\uF07C", unicode: "dir", ascii: "dir" },
526
533
  model: { nerd: "\u{F06A9}", unicode: "\u25CF", ascii: "*" },
@@ -592,7 +599,10 @@ var STATUS_ICONS = {
592
599
  running: { nerd: "\u25D0", unicode: "\u25D0", ascii: "*" },
593
600
  completed: { nerd: "\uF00C", unicode: "\u2713", ascii: "+" },
594
601
  failed: { nerd: "\uF00D", unicode: "\u2717", ascii: "x" },
595
- 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: "#" },
596
606
  interrupted: STOP,
597
607
  stopping: STOP,
598
608
  deleted: BLOCKED,
@@ -967,6 +977,9 @@ var STATUS_ROLE = {
967
977
  running: "accent",
968
978
  completed: "success",
969
979
  failed: "error",
980
+ timedOut: "warning",
981
+ resolved: "muted",
982
+ expectedExit: "dim",
970
983
  cancelled: "muted",
971
984
  interrupted: "warning",
972
985
  stopping: "warning",
@@ -978,13 +991,16 @@ var TOOL_FOOTER_STATUS = {
978
991
  completed: "completed",
979
992
  deleted: "completed",
980
993
  failed: "failed",
994
+ timedOut: "timedOut",
995
+ resolved: "resolved",
996
+ expectedExit: "expectedExit",
981
997
  cancelled: "failed",
982
998
  interrupted: "failed",
983
999
  blocked: "failed"
984
1000
  };
985
1001
  function startsWithFooterGlyph(footer, mode) {
986
1002
  const plain = stripTerminalSequences(footer);
987
- return ["completed", "failed"].some((status) => {
1003
+ return ["completed", "failed", "timedOut", "resolved", "expectedExit"].some((status) => {
988
1004
  const glyph = statusIcon(status, mode);
989
1005
  return plain.startsWith(glyph) && (plain.length === glyph.length || /^\s/u.test(plain.slice(glyph.length)));
990
1006
  });
@@ -1729,43 +1745,71 @@ function decorateEditor(editor, deps) {
1729
1745
  import { truncateToWidth as truncateToWidth7, visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
1730
1746
  function createWorkingState(requestRender) {
1731
1747
  let startedAt;
1748
+ let pendingBackground = 0;
1732
1749
  let ticker;
1733
1750
  let disposed = false;
1734
1751
  function stop() {
1735
1752
  if (ticker !== void 0) clearInterval(ticker);
1736
1753
  ticker = void 0;
1737
- startedAt = void 0;
1754
+ }
1755
+ function syncTicker() {
1756
+ if (startedAt === void 0 && pendingBackground === 0) {
1757
+ stop();
1758
+ } else if (ticker === void 0) {
1759
+ ticker = setInterval(requestRender, 50);
1760
+ ticker.unref?.();
1761
+ }
1762
+ requestRender();
1738
1763
  }
1739
1764
  return {
1740
1765
  get isWorking() {
1741
- return startedAt !== void 0;
1766
+ return startedAt !== void 0 || pendingBackground > 0;
1742
1767
  },
1743
1768
  start() {
1744
1769
  if (disposed) return;
1745
- stop();
1746
1770
  startedAt = Date.now();
1747
- ticker = setInterval(requestRender, 50);
1748
- ticker.unref?.();
1749
- requestRender();
1771
+ syncTicker();
1750
1772
  },
1751
1773
  end() {
1752
1774
  if (disposed) return;
1753
- stop();
1754
- requestRender();
1775
+ startedAt = void 0;
1776
+ syncTicker();
1777
+ },
1778
+ setPendingBackground(count) {
1779
+ if (disposed || pendingBackground === count) return;
1780
+ pendingBackground = count;
1781
+ syncTicker();
1755
1782
  },
1756
1783
  status(indicator, width, styleMuted = (text) => text, frame, mode = "nerd") {
1757
1784
  const ready = `${icon("ready", mode)} ready`;
1758
1785
  const separator2 = icon("separator", mode);
1786
+ const now = frame?.now ?? Date.now();
1787
+ const fallback = () => {
1788
+ if (pendingBackground === 0)
1789
+ return styleMuted(truncateToWidth7(ready, width, ""));
1790
+ const glyphs = frames("workingFrames", mode);
1791
+ const glyph = glyphs[Math.floor(now / 200) % glyphs.length];
1792
+ const label2 = renderWorkingIndicator(
1793
+ `${glyph} working${icon("ellipsis", mode)}`,
1794
+ frame ?? createBreathingFrame(now),
1795
+ styleMuted,
1796
+ glyphs
1797
+ );
1798
+ return truncateToWidth7(
1799
+ `${label2}${styleMuted(` ${separator2} ${pendingBackground} bg`)}`,
1800
+ width,
1801
+ ""
1802
+ );
1803
+ };
1759
1804
  if (!indicator || typeof indicator !== "object" || !("renderInBorder" in indicator) || typeof indicator.renderInBorder !== "function") {
1760
- return styleMuted(truncateToWidth7(ready, width, ""));
1805
+ return fallback();
1761
1806
  }
1762
1807
  const working = "kind" in indicator && indicator.kind === "working";
1763
- const now = frame?.now ?? Date.now();
1764
1808
  const elapsed = working && startedAt !== void 0 ? ` ${separator2} ${formatDuration(Math.floor((now - startedAt) / 1e3) * 1e3)}` : "";
1765
1809
  const nativeWidth = Math.max(1, width - visibleWidth6(elapsed));
1766
1810
  const native = indicator.renderInBorder(nativeWidth);
1767
1811
  if (typeof native !== "string" || visibleWidth6(native) === 0) {
1768
- return styleMuted(truncateToWidth7(ready, width, ""));
1812
+ return fallback();
1769
1813
  }
1770
1814
  const fitted = truncateToWidth7(native, nativeWidth, "");
1771
1815
  const label = working && startedAt !== void 0 ? renderWorkingIndicator(
@@ -1782,11 +1826,107 @@ function createWorkingState(requestRender) {
1782
1826
  },
1783
1827
  dispose() {
1784
1828
  disposed = true;
1829
+ startedAt = void 0;
1830
+ pendingBackground = 0;
1785
1831
  stop();
1786
1832
  }
1787
1833
  };
1788
1834
  }
1789
1835
 
1836
+ // src/status-line/orca-child-hold.ts
1837
+ var memoryKey = /* @__PURE__ */ Symbol.for("@thoth-agents/pi-thoth-theme/orca-child-holds");
1838
+ function createOrcaChildHold(events) {
1839
+ const processState = globalThis;
1840
+ processState[memoryKey] ??= /* @__PURE__ */ new Map();
1841
+ const sessions = processState[memoryKey];
1842
+ let announced = /* @__PURE__ */ new Set();
1843
+ let subagents = [];
1844
+ let background = [];
1845
+ let timer;
1846
+ let disposed = false;
1847
+ function reconcile(prefix, tasks) {
1848
+ const live = /* @__PURE__ */ new Set();
1849
+ const byId = /* @__PURE__ */ new Map();
1850
+ for (const task of tasks) {
1851
+ if (disposed) return;
1852
+ const id = `${prefix}${task.id}`;
1853
+ if (!byId.has(id)) byId.set(id, task);
1854
+ if (!task.live) continue;
1855
+ live.add(id);
1856
+ if (announced.has(id)) continue;
1857
+ announced.add(id);
1858
+ events.emit("task:subagent:lifecycle", {
1859
+ id,
1860
+ status: "started",
1861
+ agent: task.agent,
1862
+ ...task.description ? { description: task.description } : {}
1863
+ });
1864
+ }
1865
+ for (const id of announced) {
1866
+ if (disposed) return;
1867
+ if (!id.startsWith(prefix) || live.has(id)) continue;
1868
+ announced.delete(id);
1869
+ const status = byId.get(id)?.status;
1870
+ events.emit("task:subagent:lifecycle", {
1871
+ id,
1872
+ status: status === "failed" || status === "timed_out" ? "failed" : status === "cancelled" || status === "interrupted" ? "aborted" : "completed"
1873
+ });
1874
+ }
1875
+ }
1876
+ return {
1877
+ startSession(sessionId) {
1878
+ if (disposed) return;
1879
+ if (timer !== void 0) clearTimeout(timer);
1880
+ announced = sessions.get(sessionId) ?? /* @__PURE__ */ new Set();
1881
+ sessions.set(sessionId, announced);
1882
+ subagents = [];
1883
+ background = [];
1884
+ timer = setTimeout(() => {
1885
+ timer = void 0;
1886
+ if (disposed) return;
1887
+ reconcile("thoth:subagent:", subagents);
1888
+ reconcile("thoth:bg:", background);
1889
+ }, 0);
1890
+ timer.unref?.();
1891
+ },
1892
+ updateSubagents(data) {
1893
+ if (disposed) return;
1894
+ subagents = [
1895
+ ...data.tasks.map((task) => ({
1896
+ id: task.id,
1897
+ live: task.mode === "background" && (task.status === "queued" || task.status === "running" || task.status === "stopping"),
1898
+ status: task.status,
1899
+ agent: task.agent,
1900
+ description: task.displayName
1901
+ })),
1902
+ ...data.history.map((task) => ({
1903
+ id: task.id,
1904
+ live: false,
1905
+ status: task.status,
1906
+ agent: task.agent
1907
+ }))
1908
+ ];
1909
+ if (timer === void 0) reconcile("thoth:subagent:", subagents);
1910
+ },
1911
+ updateBackground(data) {
1912
+ if (disposed) return;
1913
+ background = data.tasks.map((task) => ({
1914
+ id: task.id,
1915
+ live: task.status === "running",
1916
+ status: task.status,
1917
+ agent: task.kind === "command_watch" ? "watch" : "process",
1918
+ description: task.name
1919
+ }));
1920
+ if (timer === void 0) reconcile("thoth:bg:", background);
1921
+ },
1922
+ dispose() {
1923
+ disposed = true;
1924
+ if (timer !== void 0) clearTimeout(timer);
1925
+ timer = void 0;
1926
+ }
1927
+ };
1928
+ }
1929
+
1790
1930
  // src/status-line/throughput.ts
1791
1931
  function createThroughputTracker() {
1792
1932
  let pending;
@@ -1930,6 +2070,7 @@ function registerStatusLine(pi, ctx, config) {
1930
2070
  requestRender();
1931
2071
  };
1932
2072
  const working = createWorkingState(invalidateRender);
2073
+ const orcaChildHold = createOrcaChildHold(pi.events);
1933
2074
  const editorDecorations = /* @__PURE__ */ new Set();
1934
2075
  const refreshSession = () => {
1935
2076
  if (disposed) return;
@@ -1937,7 +2078,12 @@ function registerStatusLine(pi, ctx, config) {
1937
2078
  invalidateRender();
1938
2079
  };
1939
2080
  let offSubagentUsage;
2081
+ let offBackgroundWork = [];
1940
2082
  unsubs.push(() => offSubagentUsage?.());
2083
+ unsubs.push(() => {
2084
+ for (const off of offBackgroundWork) off();
2085
+ offBackgroundWork = [];
2086
+ });
1941
2087
  if (footerData?.onBranchChange) {
1942
2088
  unsubs.push(footerData.onBranchChange(invalidateRender));
1943
2089
  }
@@ -1958,7 +2104,50 @@ function registerStatusLine(pi, ctx, config) {
1958
2104
  data: {}
1959
2105
  });
1960
2106
  };
2107
+ const subscribeAndRequestBackgroundWork = () => {
2108
+ const sessionId = ctx.sessionManager.getSessionId();
2109
+ for (const off of offBackgroundWork) off();
2110
+ orcaChildHold.startSession(sessionId);
2111
+ let subagentPending = 0;
2112
+ let backgroundPending = 0;
2113
+ if (inputBoxEnabled) working.setPendingBackground(0);
2114
+ offBackgroundWork = [
2115
+ subscribe(pi.events, SUBAGENTS_STATE_CHANNEL, {
2116
+ sessionId,
2117
+ onSnapshot: ({ data }) => {
2118
+ if (disposed) return;
2119
+ orcaChildHold.updateSubagents(data);
2120
+ subagentPending = data.tasks.filter(
2121
+ (task) => task.mode === "background" && (task.status === "queued" || task.status === "running" || task.status === "stopping")
2122
+ ).length;
2123
+ if (inputBoxEnabled)
2124
+ working.setPendingBackground(subagentPending + backgroundPending);
2125
+ }
2126
+ }),
2127
+ subscribe(pi.events, BACKGROUND_STATE_CHANNEL, {
2128
+ sessionId,
2129
+ onSnapshot: ({ data }) => {
2130
+ if (disposed) return;
2131
+ orcaChildHold.updateBackground(data);
2132
+ backgroundPending = data.counts.running;
2133
+ if (inputBoxEnabled)
2134
+ working.setPendingBackground(subagentPending + backgroundPending);
2135
+ }
2136
+ })
2137
+ ];
2138
+ for (const channel of [
2139
+ SUBAGENTS_STATE_REQUEST,
2140
+ BACKGROUND_STATE_REQUEST
2141
+ ]) {
2142
+ request(pi.events, channel, {
2143
+ source: "@thoth-agents/pi-thoth-theme",
2144
+ sessionId,
2145
+ data: {}
2146
+ });
2147
+ }
2148
+ };
1961
2149
  if (pi?.on) {
2150
+ unsubs.push(pi.on("session_shutdown", dispose));
1962
2151
  unsubs.push(
1963
2152
  pi.on("session_start", () => {
1964
2153
  if (disposed) return;
@@ -1967,6 +2156,7 @@ function registerStatusLine(pi, ctx, config) {
1967
2156
  if (inputBoxEnabled) working.end();
1968
2157
  refreshSession();
1969
2158
  subscribeAndRequestSubagentUsage();
2159
+ subscribeAndRequestBackgroundWork();
1970
2160
  })
1971
2161
  );
1972
2162
  if (inputBoxEnabled) {
@@ -1992,6 +2182,22 @@ function registerStatusLine(pi, ctx, config) {
1992
2182
  unsubs.push(pi.on("thinking_level_select", invalidateRender));
1993
2183
  }
1994
2184
  subscribeAndRequestSubagentUsage();
2185
+ subscribeAndRequestBackgroundWork();
2186
+ function dispose() {
2187
+ if (disposed) return;
2188
+ disposed = true;
2189
+ working.dispose();
2190
+ orcaChildHold.dispose();
2191
+ for (const decoration of editorDecorations) decoration.dispose();
2192
+ editorDecorations.clear();
2193
+ for (const unsub of unsubs) {
2194
+ try {
2195
+ unsub();
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ unsubs.length = 0;
2200
+ }
1995
2201
  return {
1996
2202
  getStatusSnapshot,
1997
2203
  render(width) {
@@ -2025,19 +2231,7 @@ function registerStatusLine(pi, ctx, config) {
2025
2231
  return cachedLines;
2026
2232
  },
2027
2233
  invalidate: invalidateRender,
2028
- dispose() {
2029
- disposed = true;
2030
- working.dispose();
2031
- for (const decoration of editorDecorations) decoration.dispose();
2032
- editorDecorations.clear();
2033
- for (const unsub of unsubs) {
2034
- try {
2035
- unsub();
2036
- } catch {
2037
- }
2038
- }
2039
- unsubs.length = 0;
2040
- }
2234
+ dispose
2041
2235
  };
2042
2236
  });
2043
2237
  }
@@ -4115,6 +4309,22 @@ function registerWelcome(pi, ctx, config, options) {
4115
4309
  // src/index.ts
4116
4310
  function thothTheme(pi) {
4117
4311
  const config = loadConfig();
4312
+ pi.registerMarkdownTransformer((markdown, context) => {
4313
+ if (context.messageType !== "user" || !markdown.trim()) return markdown;
4314
+ if (/^╭── [\uF007●@] user ─+╮\r?\n\r?\n[\s\S]*\r?\n\r?\n╰─+╯$/.test(markdown))
4315
+ return markdown;
4316
+ const width = context.availableWidth;
4317
+ const title = `\u256D\u2500\u2500 ${icon("user", config.icons)} user `;
4318
+ const titleWidth = visibleWidth8(title);
4319
+ if (!Number.isInteger(width) || width < titleWidth + 2) return markdown;
4320
+ const top = `${title}${"\u2500".repeat(width - titleWidth - 1)}\u256E`;
4321
+ const bottom = `\u2570${"\u2500".repeat(width - 2)}\u256F`;
4322
+ return `${top}
4323
+
4324
+ ${markdown}
4325
+
4326
+ ${bottom}`;
4327
+ });
4118
4328
  const owner = {};
4119
4329
  const cwd = process.cwd();
4120
4330
  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.4.0",
3
+ "version": "0.6.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.5.0"
54
54
  },
55
55
  "upstream": {
56
56
  "repository": "https://github.com/QuangThai/pi-omp-theme",