pi-claude-code-ui 1.0.74 → 1.0.76

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  > [!IMPORTANT]
4
4
  > **1.0.69 — package rename (permanent).** Canonical npm name is now [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui). `pi-claude-style-tools` is legacy and will not receive further releases. Install with `pi install npm:pi-claude-code-ui` or `npm i pi-claude-code-ui`.
5
5
 
6
+ ## 1.0.75 — 2026-07-29
7
+
8
+ ### Fixed
9
+
10
+ - **Herdr progress with spinner verbs** — themed working messages (`Cooking…`, `Syncing…`, …) no longer break [herdr](https://herdr.dev) agent progress. Stock herdr pi screen detection only matches literal `Working...`; when `HERDR_ENV=1` the spinner now reports `working` / `idle` over herdr’s socket (same `herdr:pi` lifecycle authority as the official pi integration). Spinner UI and verbs are unchanged. Outside herdr the extension is a no-op for this path.
11
+
6
12
  ## 1.0.74 — 2026-07-18
7
13
 
8
14
  ### Fixed
@@ -1494,6 +1494,15 @@ function hasInlineMathMarkers(text: string): boolean {
1494
1494
  return text.includes("\\(") || text.includes("$");
1495
1495
  }
1496
1496
 
1497
+ // Magic Context prefixes live assistant text with §N§ while the response is
1498
+ // streaming and removes that metadata on message_end. Keep the transient tag
1499
+ // out of the display without mutating the message used by context management.
1500
+ const MAGIC_CONTEXT_TAG_LINE_PREFIX = /(^|\r?\n)[ \t]*(?:§\d+§[ \t]*)+/g;
1501
+
1502
+ function stripTransientMagicContextTags(text: string): string {
1503
+ return text.replace(MAGIC_CONTEXT_TAG_LINE_PREFIX, "$1");
1504
+ }
1505
+
1497
1506
  function replaceInlineMath(text: string): string {
1498
1507
  if (!hasInlineMathMarkers(text)) return text;
1499
1508
  const withParens = text.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
@@ -1702,7 +1711,7 @@ class DottedParagraph {
1702
1711
 
1703
1712
  constructor(text: string, markdownTheme: MarkdownThemeLike) {
1704
1713
  this.markdownTheme = copySafeMarkdownTheme(markdownTheme);
1705
- this.segments = buildParagraphSegments(text, this.markdownTheme);
1714
+ this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme);
1706
1715
  }
1707
1716
 
1708
1717
  invalidate(): void {
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import net from "node:net";
2
3
 
3
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
5
  import { Loader } from "@earendil-works/pi-tui";
@@ -509,6 +510,162 @@ function statusText(text: string): string {
509
510
  return `${STATUS_DIM}${text}${RESET}`;
510
511
  }
511
512
 
513
+ // ---------------------------------------------------------------------------
514
+ // Herdr lifecycle reporting
515
+ //
516
+ // Stock herdr pi screen detection only matches the literal "Working...".
517
+ // This extension replaces that text with themed spinner verbs (Cooking…,
518
+ // Syncing…, …), so herdr sidebars go idle/stale while pi is actually busy.
519
+ // When running inside herdr (HERDR_ENV=1), report working/idle over the
520
+ // socket the same way herdr's official pi integration does. UI is unchanged.
521
+ // ---------------------------------------------------------------------------
522
+
523
+ type HerdrAgentState = "working" | "idle";
524
+
525
+ const HERDR_ENV = process.env.HERDR_ENV;
526
+ const HERDR_SOCKET_PATH = process.env.HERDR_SOCKET_PATH;
527
+ const HERDR_SOCKET_ENDPOINT =
528
+ process.platform === "win32" && HERDR_SOCKET_PATH
529
+ ? `\\\\.\\pipe\\${HERDR_SOCKET_PATH}`
530
+ : HERDR_SOCKET_PATH;
531
+ const HERDR_PANE_ID = process.env.HERDR_PANE_ID;
532
+ // Must be exactly "herdr:pi" — herdr only grants full lifecycle authority
533
+ // (skip screen-manifest fallback) to that official source pair with agent "pi".
534
+ const HERDR_SOURCE = "herdr:pi";
535
+
536
+ function herdrEnabled(): boolean {
537
+ return HERDR_ENV === "1" && !!HERDR_SOCKET_PATH && !!HERDR_PANE_ID;
538
+ }
539
+
540
+ function herdrSendAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
541
+ if (!herdrEnabled()) return Promise.resolve(true);
542
+
543
+ return new Promise((resolve) => {
544
+ let done = false;
545
+ let timeout: ReturnType<typeof setTimeout> | undefined;
546
+ const finish = (delivered: boolean) => {
547
+ if (done) return;
548
+ done = true;
549
+ if (timeout) clearTimeout(timeout);
550
+ socket.destroy();
551
+ resolve(delivered);
552
+ };
553
+
554
+ const socket = net.createConnection(HERDR_SOCKET_ENDPOINT!);
555
+ socket.on("error", () => finish(false));
556
+ socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
557
+ socket.on("data", () => finish(true));
558
+ socket.on("end", () => finish(false));
559
+ timeout = setTimeout(() => finish(false), timeoutMs);
560
+ unrefTimer(timeout);
561
+ });
562
+ }
563
+
564
+ async function herdrSend(request: unknown): Promise<void> {
565
+ if (await herdrSendAttempt(request, 500)) return;
566
+ await herdrSendAttempt(request, 1500);
567
+ }
568
+
569
+ let herdrReportSeq = Date.now() * 1000;
570
+ let herdrSessionId: string | undefined;
571
+ let herdrSessionPath: string | undefined;
572
+ let herdrSendInFlight = false;
573
+ let herdrQueued: { state: HerdrAgentState; seq: number } | undefined;
574
+ let herdrLastState: HerdrAgentState | undefined;
575
+
576
+ function herdrNextSeq(): number {
577
+ herdrReportSeq += 1;
578
+ return herdrReportSeq;
579
+ }
580
+
581
+ function herdrUpdateSessionRef(ctx: any): void {
582
+ try {
583
+ const file = ctx?.sessionManager?.getSessionFile?.();
584
+ herdrSessionPath =
585
+ typeof file === "string" && file.startsWith("/") ? file : undefined;
586
+ } catch {
587
+ herdrSessionPath = undefined;
588
+ }
589
+
590
+ try {
591
+ const id = ctx?.sessionManager?.getSessionId?.();
592
+ herdrSessionId = typeof id === "string" && id.length > 0 ? id : undefined;
593
+ } catch {
594
+ herdrSessionId = undefined;
595
+ }
596
+ }
597
+
598
+ function herdrWithSessionRef(params: Record<string, unknown>): Record<string, unknown> {
599
+ if (herdrSessionPath) return { ...params, agent_session_path: herdrSessionPath };
600
+ if (herdrSessionId) return { ...params, agent_session_id: herdrSessionId };
601
+ return params;
602
+ }
603
+
604
+ function herdrCurrentSessionRef(): Record<string, unknown> | undefined {
605
+ if (herdrSessionPath) return { agent_session_path: herdrSessionPath };
606
+ if (herdrSessionId) return { agent_session_id: herdrSessionId };
607
+ return undefined;
608
+ }
609
+
610
+ function herdrReportSession(sessionStartSource?: string): Promise<void> {
611
+ const sessionRef = herdrCurrentSessionRef();
612
+ if (!sessionRef) return Promise.resolve();
613
+
614
+ return herdrSend({
615
+ id: `${HERDR_SOURCE}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`,
616
+ method: "pane.report_agent_session",
617
+ params: {
618
+ pane_id: HERDR_PANE_ID,
619
+ source: HERDR_SOURCE,
620
+ agent: "pi",
621
+ seq: herdrNextSeq(),
622
+ session_start_source: sessionStartSource,
623
+ ...sessionRef,
624
+ },
625
+ });
626
+ }
627
+
628
+ function herdrSendState(state: HerdrAgentState, seq = herdrNextSeq()): Promise<void> {
629
+ return herdrSend({
630
+ id: `${HERDR_SOURCE}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
631
+ method: "pane.report_agent",
632
+ params: herdrWithSessionRef({
633
+ pane_id: HERDR_PANE_ID,
634
+ source: HERDR_SOURCE,
635
+ agent: "pi",
636
+ state,
637
+ seq,
638
+ }),
639
+ });
640
+ }
641
+
642
+ function herdrQueueState(state: HerdrAgentState): void {
643
+ herdrQueued = { state, seq: herdrNextSeq() };
644
+ if (!herdrSendInFlight) void herdrDrainQueue();
645
+ }
646
+
647
+ async function herdrDrainQueue(): Promise<void> {
648
+ if (herdrSendInFlight) return;
649
+ herdrSendInFlight = true;
650
+ try {
651
+ while (herdrQueued) {
652
+ const next = herdrQueued;
653
+ herdrQueued = undefined;
654
+ await herdrSendState(next.state, next.seq);
655
+ }
656
+ } finally {
657
+ herdrSendInFlight = false;
658
+ if (herdrQueued) void herdrDrainQueue();
659
+ }
660
+ }
661
+
662
+ function herdrPublishState(state: HerdrAgentState, force = false): void {
663
+ if (!herdrEnabled()) return;
664
+ if (!force && state === herdrLastState) return;
665
+ herdrLastState = state;
666
+ herdrQueueState(state);
667
+ }
668
+
512
669
  // ---------------------------------------------------------------------------
513
670
  // Extension
514
671
  // ---------------------------------------------------------------------------
@@ -545,6 +702,9 @@ export default function (pi: ExtensionAPI) {
545
702
  let turnActive = false;
546
703
  let lastWorkingMessage: string | null = null;
547
704
  let activeCtx: { ui: any; hasUI: boolean } | null = null;
705
+ // Root interactive session only — nested/non-UI contexts must not fight
706
+ // herdr state authority for this pane.
707
+ let herdrRootSession = false;
548
708
 
549
709
  function getEffortSuffix(): string {
550
710
  try {
@@ -709,6 +869,31 @@ export default function (pi: ExtensionAPI) {
709
869
  restoreDefaultWorkingMessage();
710
870
  }
711
871
 
872
+ function herdrMarkWorking(ctx?: any): void {
873
+ if (!herdrRootSession) return;
874
+ if (ctx) herdrUpdateSessionRef(ctx);
875
+ void herdrReportSession();
876
+ herdrPublishState("working");
877
+ }
878
+
879
+ function herdrMarkIdle(ctx?: any, force = false): void {
880
+ if (!herdrRootSession) return;
881
+ if (ctx) herdrUpdateSessionRef(ctx);
882
+ herdrPublishState("idle", force);
883
+ }
884
+
885
+ // session_start: claim root UI session + announce native session identity.
886
+ pi.on("session_start", async (event, ctx) => {
887
+ if (!herdrEnabled()) return;
888
+ if (ctx?.hasUI !== true) return;
889
+ herdrRootSession = true;
890
+ herdrUpdateSessionRef(ctx);
891
+ await herdrReportSession((event as any)?.reason);
892
+ // Reload mid-run: isIdle() false means work is still in flight.
893
+ const working = ctx?.isIdle?.() === false;
894
+ herdrPublishState(working ? "working" : "idle", true);
895
+ });
896
+
712
897
  function onThinkingEnd(): void {
713
898
  if (thinkingStatus !== "thinking") return;
714
899
  const duration = Date.now() - thinkingStartTime;
@@ -728,8 +913,9 @@ export default function (pi: ExtensionAPI) {
728
913
  if (!agentStartTime) agentStartTime = Date.now();
729
914
  });
730
915
 
731
- pi.on("agent_start", async () => {
916
+ pi.on("agent_start", async (_event, ctx) => {
732
917
  if (!agentStartTime) agentStartTime = Date.now();
918
+ herdrMarkWorking(ctx);
733
919
  });
734
920
 
735
921
  pi.on("turn_start", async (_event, ctx) => {
@@ -749,6 +935,9 @@ export default function (pi: ExtensionAPI) {
749
935
  scheduleThoughtStatusClear();
750
936
  }
751
937
  startRefreshLoop();
938
+ // turn_start is the earliest UI-visible busy signal; cover cases where
939
+ // agent_start already fired or is ordering-sensitive across reloads.
940
+ herdrMarkWorking(ctx);
752
941
  });
753
942
 
754
943
  pi.on("message_update", async (event, ctx) => {
@@ -843,9 +1032,23 @@ export default function (pi: ExtensionAPI) {
843
1032
  clearDisplay();
844
1033
  });
845
1034
 
1035
+ // Prefer agent_settled over agent_end: pi may still auto-retry, compact, or
1036
+ // drain a follow-up queue after agent_end. Settled means truly idle.
1037
+ // Cast: agent_settled landed after some older @earendil-works/pi-coding-agent
1038
+ // type packages still used for local typecheck; runtime pi has the event.
1039
+ (pi as ExtensionAPI & {
1040
+ on(event: "agent_settled", handler: (event: unknown, ctx: any) => void | Promise<void>): void;
1041
+ }).on("agent_settled", async (_event, ctx) => {
1042
+ if (!herdrRootSession) return;
1043
+ if (ctx?.isIdle?.() !== true) return;
1044
+ herdrMarkIdle(ctx);
1045
+ });
1046
+
846
1047
  pi.on("session_shutdown", async () => {
847
1048
  turnActive = false;
848
1049
  clearDisplay();
1050
+ herdrMarkIdle(undefined, true);
1051
+ herdrRootSession = false;
849
1052
  activeCtx = null;
850
1053
  });
851
1054
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-code-ui",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
5
5
  "keywords": [
6
6
  "pi-package",