@henryqw/pi-herdr-tools 1.4.0 → 1.4.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/README.md CHANGED
@@ -152,7 +152,7 @@ Normal cleanup does these steps. It checks whether a tab in another Herdr worksp
152
152
  - The consumer-owned `pi-herdr-btw/btw` task defaults to `fast`.
153
153
  - Before the pane starts, `/btw` selects the first authenticated viable effective profile route.
154
154
 
155
- `~/.pi/agent/config/pi-task-models/config.json` is shared, and `@henryqw/pi-task-models` owns it. A task entry is an explicit user override. Routes resolve before the pane starts. If this file is missing, BTW shows a warning one time per session.
155
+ `~/.pi/agent/config/pi-task-models/config.json` is shared, and `@henryqw/pi-task-models` owns it. A task entry is an explicit user override. Routes resolve before the pane starts. If this file is missing or unreadable, or a task profile is not configured, the package shows one warning one time per session.
156
156
 
157
157
  #### Merge delivery
158
158
 
@@ -253,7 +253,7 @@ A large Main context can be more than the context limit of the side thread. Make
253
253
 
254
254
  Display titles are natural task phrases. They usually have two or three words, and they always have a maximum of four words and 20 characters. Models that answer in another language still make English display titles.
255
255
 
256
- If the shared task-model config is missing, the extension shows a warning one time at session start. Run `/task-models` to configure rename routing. If no route is viable, display titles do not change. `/rename` shows a warning if the session has no user text to rename.
256
+ If the shared task-model config is missing, the package shows one warning one time per session at session start. Run `/task-models` to configure rename routing. If no route is viable, display titles do not change. `/rename` shows a warning if the session has no user text to rename.
257
257
 
258
258
  Herdr and Git synchronization failures show as warnings. Correct the reported problem. Then run `/rename` to try again. When a newer rename cancels an older rename, the extension shows no message. Older titles get no migration.
259
259
 
@@ -1,11 +1,13 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import registerBtwExtension from "../internal/btw.ts";
2
+ import registerBtwExtension, { BTW_TASK } from "../internal/btw.ts";
3
3
  import herdrCloneExtension from "../internal/clone-tab.ts";
4
4
  import herdrDoneExtension from "../internal/done.ts";
5
5
  import herdrHandoffExtension from "../internal/handoff.ts";
6
- import herdrRenameExtension from "../internal/rename.ts";
6
+ import herdrRenameExtension, { RENAME_TASK } from "../internal/rename.ts";
7
+ import { registerTaskModelsWarning } from "../internal/task-models.ts";
7
8
 
8
9
  export default async function herdrToolsExtension(pi: ExtensionAPI): Promise<void> {
10
+ registerTaskModelsWarning(pi, [RENAME_TASK, BTW_TASK]);
9
11
  herdrRenameExtension(pi);
10
12
  await registerBtwExtension(pi);
11
13
  herdrCloneExtension(pi);
package/internal/btw.ts CHANGED
@@ -1,14 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { createHerdrClient, hasHerdrErrorCode, startPiAgent } from "@henryqw/pi-herdr";
4
- import {
5
- loadTaskModelsConfig,
6
- registerModelTask,
7
- resolveConfiguredTaskRoutes,
8
- type ModelTask,
9
- type ResolvedTaskRoute,
10
- type TaskRouteError,
11
- } from "@henryqw/pi-task-models";
4
+ import { registerModelTask, type ModelTask, type ResolvedTaskRoute } from "@henryqw/pi-task-models";
12
5
  import {
13
6
  buildSessionContext,
14
7
  convertToLlm,
@@ -51,6 +44,7 @@ import {
51
44
  type MergeRequest,
52
45
  } from "./merge.ts";
53
46
  import { HELP_TEXT, parseBtwCommand } from "./router.ts";
47
+ import { configuredTaskRoutes } from "./task-models.ts";
54
48
 
55
49
  export const BTW_TASK = {
56
50
  id: "pi-herdr-btw/btw",
@@ -97,21 +91,6 @@ function sameStringArray(a: string[], b: string[]): boolean {
97
91
  return a.length === b.length && a.every((value, index) => value === b[index]);
98
92
  }
99
93
 
100
- function configuredBtwRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
101
- try {
102
- return resolveConfiguredTaskRoutes(ctx, BTW_TASK);
103
- } catch (error) {
104
- const { taskRouteCode, profileName } = error as TaskRouteError;
105
- throw new Error(
106
- taskRouteCode === "profile-missing"
107
- ? `BTW task profile ${profileName} is not configured. Run /task-models.`
108
- : taskRouteCode === "no-route"
109
- ? `BTW task profile ${profileName} has no available route. Run /task-models.`
110
- : "Couldn't read task model config. Run /task-models.",
111
- );
112
- }
113
- }
114
-
115
94
  /**
116
95
  * Decide whether the child can replay the parent's exact request prefix
117
96
  * (system prompt, tools, model, thinking) for provider prompt-cache reuse.
@@ -401,7 +380,6 @@ export async function registerBtwExtension(
401
380
  | Pick<ExtensionCommandContext, "sessionManager" | "isIdle" | "model" | "modelRegistry">
402
381
  | undefined;
403
382
  let sessionGeneration = 0;
404
- let missingTaskModelsConfigWarningSessionId: string | undefined;
405
383
  // Notifications need a UI context; route them through the last known ctx.
406
384
  let notifyFn: ((message: string, type: "info" | "warning" | "error") => void) | undefined;
407
385
  const coordinator = new MergeCoordinator(store, {
@@ -454,17 +432,6 @@ export async function registerBtwExtension(
454
432
  }
455
433
 
456
434
  pi.on("session_start", (_event, ctx) => {
457
- const sessionId = ctx.sessionManager.getSessionId();
458
- let taskModelsConfig: ReturnType<typeof loadTaskModelsConfig> | undefined;
459
- try {
460
- taskModelsConfig = loadTaskModelsConfig();
461
- } catch {
462
- ctx.ui.notify("Couldn't read task model config. Run /task-models.", "warning");
463
- }
464
- if (missingTaskModelsConfigWarningSessionId !== sessionId && taskModelsConfig?.source === "missing") {
465
- missingTaskModelsConfigWarningSessionId = sessionId;
466
- ctx.ui.notify("Task model config is missing; run /task-models to configure it.", "warning");
467
- }
468
435
  sessionGeneration += 1;
469
436
  sessionCtx = ctx;
470
437
  notifyFn = (message, type) => ctx.ui.notify(message, type);
@@ -599,7 +566,7 @@ export async function registerBtwExtension(
599
566
  const currentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null;
600
567
  const thinkingLevel = pi.getThinkingLevel();
601
568
  let launchRoute: ResolvedTaskRoute | undefined;
602
- for (const route of configuredBtwRoutes(ctx)) {
569
+ for (const route of configuredTaskRoutes(ctx, BTW_TASK, "BTW")) {
603
570
  try {
604
571
  const authenticated = (await ctx.modelRegistry.getApiKeyAndHeaders(route.model)).ok;
605
572
  if (launchGeneration !== sessionGeneration) return;
@@ -4,15 +4,8 @@ import {
4
4
  type ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { createHerdrClient, withWorktreeLock } from "@henryqw/pi-herdr";
7
- import {
8
- executeTaskRoutes,
9
- loadTaskModelsConfig,
10
- registerModelTask,
11
- resolveConfiguredTaskRoutes,
12
- type ModelTask,
13
- type ResolvedTaskRoute,
14
- type TaskRouteError,
15
- } from "@henryqw/pi-task-models";
7
+ import { executeTaskRoutes, registerModelTask, type ModelTask, type ResolvedTaskRoute } from "@henryqw/pi-task-models";
8
+ import { configuredTaskRoutes } from "./task-models.ts";
16
9
 
17
10
  const WIDGET_KEY = "pi-herdr-rename";
18
11
  const WIDGET_RESULT_MS = 2_000;
@@ -39,21 +32,6 @@ type GeneratedTitle = { display: string; branch: string };
39
32
 
40
33
  class RenameModelError extends Error {}
41
34
 
42
- function configuredRenameRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
43
- try {
44
- return resolveConfiguredTaskRoutes(ctx, RENAME_TASK);
45
- } catch (error) {
46
- const { taskRouteCode, profileName } = error as TaskRouteError;
47
- throw new RenameModelError(
48
- taskRouteCode === "profile-missing"
49
- ? `Rename task profile ${profileName} is not configured. Run /task-models.`
50
- : taskRouteCode === "no-route"
51
- ? `Rename task profile ${profileName} has no available route. Run /task-models.`
52
- : "Couldn't read task model config. Run /task-models.",
53
- );
54
- }
55
- }
56
-
57
35
  function validSubject(subject: string): boolean {
58
36
  return /^[a-z0-9]+(?: [a-z0-9]+)*$/.test(subject)
59
37
  && subject.length <= DISPLAY_MAX_CHARS
@@ -192,7 +170,7 @@ async function generateTitle(text: string, ctx: ExtensionContext, signal: AbortS
192
170
  };
193
171
 
194
172
  return executeTaskRoutes(
195
- configuredRenameRoutes(ctx),
173
+ configuredTaskRoutes(ctx, RENAME_TASK, "Rename"),
196
174
  async (route) => {
197
175
  const firstMessages = [{ role: "user" as const, content: prompt, timestamp: Date.now() }];
198
176
  const response = await complete(route, firstMessages);
@@ -358,19 +336,6 @@ export default function herdrRenameExtension(pi: ExtensionAPI): void {
358
336
  sequence++;
359
337
  automaticPending = false;
360
338
  latestUserText = latestSessionUserText(ctx);
361
- try {
362
- const taskModels = loadTaskModelsConfig();
363
- if (taskModels.source === "missing") {
364
- ctx.ui.notify("Task model config is missing; run /task-models to configure rename routing.", "warning");
365
- } else {
366
- const profileName = taskModels.value.tasks[RENAME_TASK.id] ?? RENAME_TASK.defaultProfile;
367
- if (!taskModels.value.profiles[profileName]) {
368
- ctx.ui.notify(`Configure rename task profile ${profileName} with /task-models.`, "warning");
369
- }
370
- }
371
- } catch {
372
- ctx.ui.notify("Couldn't read task model config. Run /task-models.", "warning");
373
- }
374
339
  const title = pi.getSessionName();
375
340
  automaticStarted = Boolean(title || latestUserText);
376
341
  const saved = savedTitle(ctx);
@@ -0,0 +1,48 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ loadTaskModelsConfig,
4
+ resolveConfiguredTaskRoutes,
5
+ type ModelTask,
6
+ type ResolvedTaskRoute,
7
+ type TaskRouteError,
8
+ } from "@henryqw/pi-task-models";
9
+
10
+ // Pi reloads extensions without a module cache, so process state keeps the warning once per session.
11
+ const WARNED_SESSION = Symbol.for("pi-herdr-tools/task-models-warned-session");
12
+
13
+ /** Resolve routes for `task`; profile errors name the feature, config errors keep pi-task-models' message. */
14
+ export function configuredTaskRoutes(ctx: ExtensionContext, task: ModelTask, label: string): ResolvedTaskRoute[] {
15
+ try {
16
+ return resolveConfiguredTaskRoutes(ctx, task);
17
+ } catch (error) {
18
+ const { taskRouteCode, profileName } = error as TaskRouteError;
19
+ if (taskRouteCode === "profile-missing") throw new Error(`${label} task profile ${profileName} is not configured. Run /task-models.`);
20
+ if (taskRouteCode === "no-route") throw new Error(`${label} task profile ${profileName} has no available route. Run /task-models.`);
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ /** Warn at most once per session when the shared config cannot route `tasks`. */
26
+ export function registerTaskModelsWarning(pi: ExtensionAPI, tasks: readonly ModelTask[]): void {
27
+ pi.on("session_start", (_event, ctx) => {
28
+ const state = globalThis as { [WARNED_SESSION]?: string };
29
+ const sessionId = ctx.sessionManager.getSessionId();
30
+ if (state[WARNED_SESSION] === sessionId) return;
31
+ let message: string | undefined;
32
+ try {
33
+ const config = loadTaskModelsConfig();
34
+ if (config.source === "missing") {
35
+ message = "Task model config is missing. Run /task-models to configure task routes.";
36
+ } else {
37
+ const missing = [...new Set(tasks.map((task) => config.value.tasks[task.id] ?? task.defaultProfile))]
38
+ .filter((profile) => !config.value.profiles[profile]);
39
+ if (missing.length) message = `Task model profile ${missing.join(", ")} is not configured. Run /task-models.`;
40
+ }
41
+ } catch {
42
+ message = "Couldn't read task model config. Run /task-models.";
43
+ }
44
+ if (!message) return;
45
+ state[WARNED_SESSION] = sessionId;
46
+ ctx.ui.notify(message, "warning");
47
+ });
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-tools",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Clone, hand off, name, and finish Pi conversations in Herdr, with mergeable side questions.",
5
5
  "keywords": ["pi-package", "pi", "herdr", "session", "clone", "side-thread", "title"],
6
6
  "type": "module",