pi-quiver 5.1.0 → 5.2.1
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 +8 -0
- package/README.md +4 -1
- package/extensions/session-name.ts +107 -11
- package/extensions/sword-header.ts +3 -2
- package/lib/herdr-tab.ts +93 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
|
|
|
8
8
|
via OIDC trusted publishing. The release helper at
|
|
9
9
|
`.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
|
|
10
10
|
|
|
11
|
+
## v5.2.1 - 2026-09-02
|
|
12
|
+
|
|
13
|
+
- sword-header: clip each header line to the terminal width in `render(width)` (ANSI-aware `truncateToWidth`, hard clip). Fixes a hard crash (`Rendered line N exceeds terminal width`) in terminals/panes narrower than 80 columns.
|
|
14
|
+
|
|
15
|
+
## v5.2.0 - 2026-09-01
|
|
16
|
+
|
|
17
|
+
- session-name: sync the session name to the Herdr tab label (`herdrTab`, default true within the opt-in `quiver.sessionAutoName`). Claim-once - only a tab still on its default numeric label is adopted; manual renames always win. Restores the default label on shutdown; crash leaves the last label (rename by hand to recover).
|
|
18
|
+
|
|
11
19
|
## v5.1.0 - 2026-09-01
|
|
12
20
|
|
|
13
21
|
- `slack`: optional `policyPath` config injects a repo policy file into the system prompt every turn (`<slack-policy source="...">`); a missing/unreadable/empty file degrades to a `status=` block plus one deduped warning, tools stay fully usable either way (#9). `slack_post`/`slack_update` now resolve `@name` mentions to `<@U...>` (cache-first, one batched `users.list` live pass), leaving unresolvable names literal and reported via `unresolved mentions: ...` plus `details.unresolvedMentions`. The name cache gained an optional per-user `email` field and a file-level `snapshot_at` marker (set only by a full `slack_cache_refresh`, gating whether an alias match can be trusted straight from cache); `slack_cache_refresh`'s result line now reports an email/user ratio with a missing-scope hint. `slack_post` gained `unfurl_links`/`unfurl_media` params, applied to the headline and inline detail leg (never the upload stub), omitted when unset so Slack's default stands; `slack_update` has no equivalent (`chat.update` has no unfurl argument). See [doc/slack.md](doc/slack.md).
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ A 300 KB changelog page never touches your context window - you get a preview an
|
|
|
64
64
|
| --- | --- | --- |
|
|
65
65
|
| `extensions/fetch.ts` | `fetch` | Retrieve URLs over HTTP(S). HTML -> Markdown (Readability extraction, Turndown conversion). Binary saved untouched to a temp file. GitHub issue/PR/repo/actions-run/actions-job URLs auto-route through `gh` (falls back to HTTP); failed runs/jobs include failed-step logs (best-effort, summary-only otherwise). Same size gate as `doc_to_md`. Behavior lives in `lib/fetch-core.ts`; also exposed as the `pi-quiver fetch` CLI (see [Claude Code support](#claude-code-support)). |
|
|
66
66
|
| `extensions/doc_to_md.ts` | `doc_to_md` | Convert a local PDF/DOCX/PPTX to Markdown. High-fidelity via `pymupdf4llm`, resolved per process (`uv` -> system Python >= 3.12 with the package -> one-time managed venv in the user cache dir); degraded pure-JS fallback (`unpdf`) otherwise. DOCX/PPTX convert via LibreOffice first. Behavior lives in `lib/doc-to-md-core.ts`; also exposed as the `pi-quiver doc-to-md` CLI (see [Claude Code support](#claude-code-support)). |
|
|
67
|
-
| `extensions/session-name.ts` | `/session-name` | Manual + opt-in automatic session naming, naming rules and deny list, long-session revisits, and Ghostty tab rename. OFF by default. |
|
|
67
|
+
| `extensions/session-name.ts` | `/session-name` | Manual + opt-in automatic session naming, naming rules and deny list, long-session revisits, and Ghostty/Herdr tab rename. OFF by default. |
|
|
68
68
|
| `extensions/sword-header.ts` | `/builtin-header` | Themed ASCII startup header replacing pi's default logo. OFF by default. |
|
|
69
69
|
| `extensions/fast-mode.ts` | `/fast` | Inject Anthropic fast-mode (`speed: "fast"` + `anthropic-beta: fast-mode-2026-02-01`) into every Claude Opus 4.8 / Opus 5 request, any thinking level. `--fast` flag + `/fast [on\|off\|status]`. OFF by default. |
|
|
70
70
|
| `extensions/provider-stall-watchdog.ts` | - | Opt-in provider-stall recovery, in two tiers: a pre-first-event deadline (`firstEventMs`, 20s) on every provider request in every mode, and the mid-stream pair (warn at 2 min, recover at 4 min) in TUI runs only. Policy D offers each stall to Pi's retry loop until the stall retry budget (`maxStallRetries`, default = `retry.maxRetries`) is exhausted. OFF by default. |
|
|
@@ -146,6 +146,7 @@ These extensions are opt-in via `settings.json` (project `.pi/settings.json` ove
|
|
|
146
146
|
"sessionAutoName": {
|
|
147
147
|
"enabled": false,
|
|
148
148
|
"ghosttyTab": true,
|
|
149
|
+
"herdrTab": true,
|
|
149
150
|
"rules": [],
|
|
150
151
|
"deny": [],
|
|
151
152
|
"revisitFirstTurn": 0,
|
|
@@ -181,6 +182,8 @@ survive untouched.
|
|
|
181
182
|
|
|
182
183
|
`sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. `rules` appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive `deny` phrases are stripped from every name; whitespace inside a phrase is loose, so `"acme corp"` also catches `AcmeCorp`. `revisitFirstTurn` re-evaluates the name once that many model round trips have completed, while `revisitEveryTurns` does so at every multiple; both default to `0` (off) because each revisit costs another short LLM call. For example, `10` and `100` mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Counts come from the persisted transcript, so they survive resume.
|
|
183
184
|
|
|
185
|
+
`herdrTab` (default `true`) mirrors the same curated label to the Herdr tab bar over Herdr's unix socket, independent of `ghosttyTab` - either sink can be toggled off without affecting the other. It's claim-once: the extension adopts a tab only while it still shows its default numeric label (its live 1-based position among the workspace's tabs); a manually renamed tab, or one renamed mid-session by hand, is never touched again for that session - the human's label always wins. On every shutdown (quit, reload, `/new`, resume, fork) a claimed tab's label is restored to its then-current default position, so a successor session in the same pane can claim cleanly. It only ever runs in TUI mode, on an attached TTY, under Herdr (`HERDR_ENV`/`HERDR_TAB_ID`/`HERDR_SOCKET_PATH` all set) - `pi -p`/json/rpc runs and background subagents never touch the tab. Caveats: a hard crash (`kill -9`) skips the restore and leaves the stale label - rename the tab by hand to recover; and the restored label is internally a custom name (Herdr has no clear-to-auto API), so that tab keeps a number-looking label but stops renumbering on later tab closes/reorders.
|
|
186
|
+
|
|
184
187
|
`fastMode` only affects `claude-opus-4-8` and `claude-opus-5` requests on Anthropic's `anthropic-messages` API; enabling it opts into premium fast-mode pricing. `--fast` forces it on for one launch; `/fast on|off` toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. `fastMode`'s header injection needs the `before_provider_headers` hook (pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5); on older pi the beta header is silently not sent. See [doc/fetch.md](doc/fetch.md) and [doc/doc-to-md.md](doc/doc-to-md.md) for the ingestion tools' full reference; session-name/sword-header behavior above is complete.
|
|
185
188
|
|
|
186
189
|
`pi-ai` prices every fast request at standard rates - it has no `usage.speed` support and no request-level pricing modifier - so `fastMode` corrects the reported cost itself: a `message_end` handler scales all four `usage.cost` components by `FAST_MODE_COST_MULTIPLIER` (2x) and returns the corrected message. Persisted session JSONL and pi's own native cost display are always exact, since they're written from this corrected message. pi-cohort's live `Σ$` reflects the correction only when pi-quiver's `message_end` handler runs before pi-cohort's - best-effort, depending on extension load order - and is reconciled on pi-cohort's next `session_start` regardless. The upstream fix (teaching `pi-ai`'s `Usage`/`calculateCost` about `usage.speed`) is the better long-term path and is tracked separately.
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
37
37
|
import { resolveConfig } from "../lib/extension-config.ts";
|
|
38
38
|
|
|
39
|
+
// ExtensionMode ("tui" | "rpc" | "json" | "print") isn't among the package's
|
|
40
|
+
// public exports; derive it from ExtensionContext, which is, instead of
|
|
41
|
+
// duplicating the union here.
|
|
42
|
+
type Mode = ExtensionContext["mode"];
|
|
43
|
+
import { getTab, isHerdrActive, listTabs, renameTab } from "../lib/herdr-tab.ts";
|
|
44
|
+
|
|
39
45
|
// `complete` moved between pi-ai layouts: older builds re-export it from the
|
|
40
46
|
// package index, newer ones expose it only via the `/compat` subpath. A static
|
|
41
47
|
// import that targets one breaks at load time on the other (and a missing
|
|
@@ -52,6 +58,7 @@ async function loadComplete(): Promise<CompleteFn> {
|
|
|
52
58
|
type Config = {
|
|
53
59
|
enabled: boolean;
|
|
54
60
|
ghosttyTab: boolean;
|
|
61
|
+
herdrTab: boolean;
|
|
55
62
|
rules: string[];
|
|
56
63
|
deny: string[];
|
|
57
64
|
revisitFirstTurn: number;
|
|
@@ -60,6 +67,7 @@ type Config = {
|
|
|
60
67
|
const DEFAULT_CONFIG: Config = {
|
|
61
68
|
enabled: false,
|
|
62
69
|
ghosttyTab: true,
|
|
70
|
+
herdrTab: true,
|
|
63
71
|
rules: [],
|
|
64
72
|
deny: [],
|
|
65
73
|
revisitFirstTurn: 0,
|
|
@@ -76,12 +84,13 @@ const turnCount = (v: unknown): number | undefined =>
|
|
|
76
84
|
|
|
77
85
|
export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
78
86
|
if (raw === undefined) return undefined;
|
|
79
|
-
if (typeof raw === "boolean") return { enabled: raw, ghosttyTab: raw };
|
|
87
|
+
if (typeof raw === "boolean") return { enabled: raw, ghosttyTab: raw, herdrTab: raw };
|
|
80
88
|
if (raw && typeof raw === "object") {
|
|
81
89
|
const o = raw as Record<string, unknown>;
|
|
82
90
|
const out: Partial<Config> = {};
|
|
83
91
|
if (typeof o.enabled === "boolean") out.enabled = o.enabled;
|
|
84
92
|
if (typeof o.ghosttyTab === "boolean") out.ghosttyTab = o.ghosttyTab;
|
|
93
|
+
if (typeof o.herdrTab === "boolean") out.herdrTab = o.herdrTab;
|
|
85
94
|
const rules = stringList(o.rules);
|
|
86
95
|
if (rules) out.rules = rules;
|
|
87
96
|
const deny = stringList(o.deny);
|
|
@@ -425,6 +434,8 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
425
434
|
// per-turn re-assert wins the race and keeps the tab in sync with the name.
|
|
426
435
|
let lastSyncedName: string | null = null;
|
|
427
436
|
let currentTabLabel: string | null = null;
|
|
437
|
+
let herdrClaim: { lastWritten: string } | "backed-off" | null = null;
|
|
438
|
+
let herdrChain: Promise<void> = Promise.resolve();
|
|
428
439
|
|
|
429
440
|
// Adopt a name we set ourselves, keeping any curated tab label (auto-naming
|
|
430
441
|
// produces a separate TAB line that need not match the first words of the
|
|
@@ -451,12 +462,13 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
451
462
|
return "human";
|
|
452
463
|
};
|
|
453
464
|
|
|
454
|
-
const setName = (
|
|
465
|
+
const setName = async (
|
|
455
466
|
cfg: Config,
|
|
456
467
|
name: string,
|
|
457
468
|
tabLabel?: string,
|
|
458
469
|
author?: NameAuthor,
|
|
459
|
-
|
|
470
|
+
mode?: Mode,
|
|
471
|
+
): Promise<void> => {
|
|
460
472
|
const clean = applyDenyList(name, cfg.deny);
|
|
461
473
|
if (author) recordNameAuthor(clean, author);
|
|
462
474
|
expectedInternalName = clean;
|
|
@@ -464,13 +476,17 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
464
476
|
lastSyncedName = clean;
|
|
465
477
|
currentTabLabel = toTabLabel(applyDenyList(tabLabel ?? clean, cfg.deny));
|
|
466
478
|
renameGhosttyTab(currentTabLabel, cfg.ghosttyTab);
|
|
479
|
+
await syncHerdrTab(cfg, currentTabLabel, mode);
|
|
467
480
|
};
|
|
468
481
|
|
|
469
482
|
// Re-assert the tab from the current session name. Self-heals when the name
|
|
470
483
|
// changed outside setName (builtin/manual rename we didn't author): the
|
|
471
|
-
// curated label no longer applies, so re-derive from the name.
|
|
484
|
+
// curated label no longer applies, so re-derive from the name. The Ghostty
|
|
485
|
+
// write is already internally gated by renameGhosttyTab's own cfg.ghosttyTab
|
|
486
|
+
// check, which lets turn_start re-derive currentTabLabel for the Herdr sink
|
|
487
|
+
// even with the Ghostty sink off.
|
|
472
488
|
const syncTab = (cfg: Config): void => {
|
|
473
|
-
if (!cfg.enabled
|
|
489
|
+
if (!cfg.enabled) return;
|
|
474
490
|
const name = pi.getSessionName();
|
|
475
491
|
if (!name) return;
|
|
476
492
|
if (name !== lastSyncedName) {
|
|
@@ -480,13 +496,83 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
480
496
|
if (currentTabLabel) renameGhosttyTab(currentTabLabel, cfg.ghosttyTab);
|
|
481
497
|
};
|
|
482
498
|
|
|
499
|
+
// Herdr sink. Claim-once: adopt the tab only while it shows its default
|
|
500
|
+
// (position) label; a human rename - before or during the session - wins
|
|
501
|
+
// permanently. All syncs serialize on one chain so overlapping hooks never
|
|
502
|
+
// interleave a read with a rename.
|
|
503
|
+
// Used both for turn_start syncs (bounds a wedged-but-accepting Herdr so it
|
|
504
|
+
// can't stall the turn) and for the session_end restore.
|
|
505
|
+
const HERDR_TIMEOUT_MS = 500;
|
|
506
|
+
|
|
507
|
+
const positionOf = (tabs: { tab_id: string; workspace_id: string }[], tabId: string): number => {
|
|
508
|
+
const own = tabs.find((t) => t.tab_id === tabId);
|
|
509
|
+
if (!own) return -1;
|
|
510
|
+
const siblings = tabs.filter((t) => t.workspace_id === own.workspace_id);
|
|
511
|
+
return siblings.findIndex((t) => t.tab_id === tabId) + 1;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const syncHerdrTab = (cfg: Config, label: string | null, mode: Mode | undefined): Promise<void> => {
|
|
515
|
+
const run = async (): Promise<void> => {
|
|
516
|
+
if (!cfg.herdrTab || mode !== "tui" || !label) return;
|
|
517
|
+
if (herdrClaim === "backed-off") return;
|
|
518
|
+
if (!isHerdrActive()) return;
|
|
519
|
+
const sock = process.env.HERDR_SOCKET_PATH as string;
|
|
520
|
+
const tabId = process.env.HERDR_TAB_ID as string;
|
|
521
|
+
if (herdrClaim === null) {
|
|
522
|
+
const tabs = await listTabs(sock, HERDR_TIMEOUT_MS);
|
|
523
|
+
if (!tabs) return; // transient; retry next sync
|
|
524
|
+
const position = positionOf(tabs, tabId);
|
|
525
|
+
if (position === -1) return; // stale tab id (pane moved); retry harmlessly
|
|
526
|
+
const own = tabs.find((t) => t.tab_id === tabId)!;
|
|
527
|
+
if (own.label !== String(position)) {
|
|
528
|
+
herdrClaim = "backed-off"; // human (or crashed predecessor) owns it
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (await renameTab(sock, tabId, label, HERDR_TIMEOUT_MS)) {
|
|
532
|
+
herdrClaim = { lastWritten: label };
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const live = await getTab(sock, tabId, HERDR_TIMEOUT_MS);
|
|
537
|
+
if (!live) return; // failed read is not a human rename; stay claimed
|
|
538
|
+
if (live.label !== herdrClaim.lastWritten) {
|
|
539
|
+
herdrClaim = "backed-off";
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (label === herdrClaim.lastWritten) return;
|
|
543
|
+
if (await renameTab(sock, tabId, label, HERDR_TIMEOUT_MS)) herdrClaim.lastWritten = label;
|
|
544
|
+
};
|
|
545
|
+
herdrChain = herdrChain.then(run, run);
|
|
546
|
+
return herdrChain;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// No mode gate needed here: herdrClaim can only become an object via
|
|
550
|
+
// syncHerdrTab, which itself gates on mode === "tui".
|
|
551
|
+
const restoreHerdrTab = (): Promise<void> => {
|
|
552
|
+
const run = async (): Promise<void> => {
|
|
553
|
+
if (typeof herdrClaim !== "object" || herdrClaim === null) return;
|
|
554
|
+
if (!isHerdrActive()) return;
|
|
555
|
+
const sock = process.env.HERDR_SOCKET_PATH as string;
|
|
556
|
+
const tabId = process.env.HERDR_TAB_ID as string;
|
|
557
|
+
const live = await getTab(sock, tabId, HERDR_TIMEOUT_MS);
|
|
558
|
+
if (!live || live.label !== herdrClaim.lastWritten) return; // human's label wins
|
|
559
|
+
const tabs = await listTabs(sock, HERDR_TIMEOUT_MS);
|
|
560
|
+
if (!tabs) return;
|
|
561
|
+
const position = positionOf(tabs, tabId);
|
|
562
|
+
if (position === -1) return;
|
|
563
|
+
await renameTab(sock, tabId, String(position), HERDR_TIMEOUT_MS);
|
|
564
|
+
};
|
|
565
|
+
herdrChain = herdrChain.then(run, run);
|
|
566
|
+
return herdrChain;
|
|
567
|
+
};
|
|
568
|
+
|
|
483
569
|
pi.registerCommand("session-name", {
|
|
484
570
|
description: "Set or show session name (usage: /session-name [new name])",
|
|
485
571
|
handler: async (args, ctx) => {
|
|
486
572
|
const name = args.trim();
|
|
487
573
|
if (name) {
|
|
488
574
|
autoNameTried = true; // manual name wins; don't auto-overwrite later
|
|
489
|
-
setName(loadConfig(ctx), name, undefined, "human");
|
|
575
|
+
await setName(loadConfig(ctx), name, undefined, "human", ctx.mode);
|
|
490
576
|
ctx.ui.notify(`Session named: ${pi.getSessionName()}`, "info");
|
|
491
577
|
} else {
|
|
492
578
|
const current = pi.getSessionName();
|
|
@@ -504,7 +590,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
504
590
|
// The curated tab label is not persisted, so derive from the name.
|
|
505
591
|
autoNameTried = true;
|
|
506
592
|
nameAuthor = restoredNameAuthor(ctx, current);
|
|
507
|
-
setName(cfg, current);
|
|
593
|
+
await setName(cfg, current, undefined, undefined, ctx.mode);
|
|
508
594
|
} else {
|
|
509
595
|
// Fresh session: clear carryover so a new name re-derives cleanly and
|
|
510
596
|
// auto-naming can run again.
|
|
@@ -534,14 +620,24 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
534
620
|
recordNameAuthor(current, "human");
|
|
535
621
|
if (cfg.deny.length === 0) return;
|
|
536
622
|
const clean = applyDenyList(current, cfg.deny);
|
|
537
|
-
if (clean !== current) setName(cfg, clean, undefined, "human");
|
|
623
|
+
if (clean !== current) await setName(cfg, clean, undefined, "human", ctx.mode);
|
|
538
624
|
});
|
|
539
625
|
|
|
540
626
|
// Re-assert at the start of every turn. This is the only signal we get that
|
|
541
627
|
// fires after pi's own OS-title writer on session replacement, so it keeps
|
|
542
628
|
// the tab pinned to the session name and picks up external renames.
|
|
543
629
|
pi.on("turn_start", async (_event, ctx) => {
|
|
544
|
-
|
|
630
|
+
const cfg = loadConfig(ctx);
|
|
631
|
+
syncTab(cfg);
|
|
632
|
+
if (cfg.enabled) await syncHerdrTab(cfg, currentTabLabel, ctx.mode);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Restore the numeric label on every teardown reason (quit/reload/new/
|
|
636
|
+
// resume/fork): a successor session in this pane must find the default so
|
|
637
|
+
// claim-once stays sound. The restored label is internally a custom name -
|
|
638
|
+
// Herdr has no clear-to-auto API - so it looks right but won't renumber.
|
|
639
|
+
pi.on("session_shutdown", async () => {
|
|
640
|
+
await restoreHerdrTab();
|
|
545
641
|
});
|
|
546
642
|
|
|
547
643
|
pi.on("agent_end", async (_event, ctx) => {
|
|
@@ -552,7 +648,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
552
648
|
try {
|
|
553
649
|
const generated = await generate(ctx, { rules: cfg.rules });
|
|
554
650
|
if (generated && generated !== KEEP && !pi.getSessionName()) {
|
|
555
|
-
setName(cfg, generated.sessionName, generated.tabLabel, "auto");
|
|
651
|
+
await setName(cfg, generated.sessionName, generated.tabLabel, "auto", ctx.mode);
|
|
556
652
|
if (ctx.hasUI) ctx.ui.notify(`Auto-named session: ${pi.getSessionName()}`, "info");
|
|
557
653
|
}
|
|
558
654
|
} catch {
|
|
@@ -588,7 +684,7 @@ export function installSessionName(pi: ExtensionAPI, generate: NameGenerator = g
|
|
|
588
684
|
if (!result || result === KEEP) return;
|
|
589
685
|
if (pi.getSessionName() !== current) return; // renamed under us mid-call
|
|
590
686
|
if (nameAuthor === "auto") {
|
|
591
|
-
setName(cfg, result.sessionName, result.tabLabel, "auto");
|
|
687
|
+
await setName(cfg, result.sessionName, result.tabLabel, "auto", ctx.mode);
|
|
592
688
|
if (ctx.hasUI) ctx.ui.notify(`Renamed session: ${pi.getSessionName()}`, "info");
|
|
593
689
|
} else if (ctx.hasUI) {
|
|
594
690
|
// Human wording is theirs to change; surface the drift and stop.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { ExtensionAPI, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
18
|
import { resolveConfig } from "../lib/extension-config.ts";
|
|
18
19
|
|
|
19
20
|
type Config = { enabled: boolean };
|
|
@@ -69,8 +70,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
69
70
|
const cfg = resolveConfig(ctx.cwd, "swordHeader", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
|
|
70
71
|
if (!cfg.enabled) return;
|
|
71
72
|
ctx.ui.setHeader((_tui, theme) => ({
|
|
72
|
-
render(
|
|
73
|
-
return renderSwordLines(theme);
|
|
73
|
+
render(width: number): string[] {
|
|
74
|
+
return renderSwordLines(theme).map((line) => truncateToWidth(line, width, ""));
|
|
74
75
|
},
|
|
75
76
|
invalidate() {},
|
|
76
77
|
}));
|
package/lib/herdr-tab.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Pi-free Herdr socket client for the session-name extension's tab sink.
|
|
2
|
+
// One-shot newline-delimited JSON RPC against Herdr's unix socket (protocol
|
|
3
|
+
// 20, herdr 0.8.2). Every failure mode resolves null: a dead, wedged, or
|
|
4
|
+
// absent Herdr must never break a session or stall shutdown.
|
|
5
|
+
// Responses are trusted structurally: result.tab/result.tabs shapes are not
|
|
6
|
+
// validated beyond presence, and the consumer treats their fields as
|
|
7
|
+
// Herdr-provided truth.
|
|
8
|
+
import net from "node:net";
|
|
9
|
+
|
|
10
|
+
export interface HerdrTab {
|
|
11
|
+
tab_id: string;
|
|
12
|
+
workspace_id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
number: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isHerdrActive(
|
|
18
|
+
env: Record<string, string | undefined> = process.env,
|
|
19
|
+
isTTY: boolean = process.stdout.isTTY === true,
|
|
20
|
+
): boolean {
|
|
21
|
+
return env.HERDR_ENV === "1" && !!env.HERDR_TAB_ID && !!env.HERDR_SOCKET_PATH && isTTY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let nextRequestId = 1;
|
|
25
|
+
|
|
26
|
+
export function herdrRequest(
|
|
27
|
+
socketPath: string,
|
|
28
|
+
method: string,
|
|
29
|
+
params: Record<string, unknown>,
|
|
30
|
+
timeoutMs = 1500,
|
|
31
|
+
): Promise<Record<string, unknown> | null> {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const path = process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
34
|
+
let settled = false;
|
|
35
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
36
|
+
const socket = net.createConnection({ path });
|
|
37
|
+
const done = (value: Record<string, unknown> | null) => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
socket.destroy();
|
|
42
|
+
resolve(value);
|
|
43
|
+
};
|
|
44
|
+
socket.on("error", () => done(null));
|
|
45
|
+
socket.on("close", () => done(null));
|
|
46
|
+
socket.on("connect", () => {
|
|
47
|
+
socket.write(`${JSON.stringify({ id: String(nextRequestId++), method, params })}\n`);
|
|
48
|
+
});
|
|
49
|
+
let buffer = "";
|
|
50
|
+
socket.on("data", (chunk) => {
|
|
51
|
+
buffer += chunk.toString("utf8");
|
|
52
|
+
const newline = buffer.indexOf("\n");
|
|
53
|
+
if (newline === -1) return;
|
|
54
|
+
try {
|
|
55
|
+
const message = JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>;
|
|
56
|
+
const result = message.result;
|
|
57
|
+
if (!("error" in message) && result && typeof result === "object") {
|
|
58
|
+
done(result as Record<string, unknown>);
|
|
59
|
+
} else {
|
|
60
|
+
done(null);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
done(null);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getTab(
|
|
70
|
+
socketPath: string,
|
|
71
|
+
tabId: string,
|
|
72
|
+
timeoutMs = 1500,
|
|
73
|
+
): Promise<HerdrTab | null> {
|
|
74
|
+
const result = await herdrRequest(socketPath, "tab.get", { tab_id: tabId }, timeoutMs);
|
|
75
|
+
const tab = result?.tab;
|
|
76
|
+
return tab && typeof tab === "object" ? (tab as unknown as HerdrTab) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function listTabs(socketPath: string, timeoutMs = 1500): Promise<HerdrTab[] | null> {
|
|
80
|
+
const result = await herdrRequest(socketPath, "tab.list", {}, timeoutMs);
|
|
81
|
+
const tabs = result?.tabs;
|
|
82
|
+
return Array.isArray(tabs) ? (tabs as unknown as HerdrTab[]) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function renameTab(
|
|
86
|
+
socketPath: string,
|
|
87
|
+
tabId: string,
|
|
88
|
+
label: string,
|
|
89
|
+
timeoutMs = 1500,
|
|
90
|
+
): Promise<true | null> {
|
|
91
|
+
const result = await herdrRequest(socketPath, "tab.rename", { tab_id: tabId, label }, timeoutMs);
|
|
92
|
+
return result ? true : null;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|