feat: preserve Herdr task counts
This commit is contained in:
@@ -93,13 +93,13 @@ Tool calls remain compact in the conversation. Expand a tool result with Pi's no
|
|||||||
|
|
||||||
## Herdr Integration
|
## Herdr Integration
|
||||||
|
|
||||||
When Pi runs inside Herdr, the extension reports the most specific `in-progress` task as a `task` pane metadata token. If nothing is in progress, it falls back to a blocked task. Parallel current tasks are compressed as `#7 Implement auth +2`.
|
When Pi runs inside Herdr, the extension reports the most specific `in-progress` task as a `task` pane metadata token. If nothing is in progress, it falls back to a blocked task. Parallel current tasks use a separate `task_count` token so Herdr can truncate long task names responsively while keeping the remaining count visible, for example `#7 Implement auth · +2`.
|
||||||
|
|
||||||
Add `$task` to the Herdr agent sidebar rows in `~/.config/herdr/config.toml`:
|
Add `$task` and `$task_count` to the same Herdr agent sidebar row in `~/.config/herdr/config.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[ui.sidebar.agents]
|
[ui.sidebar.agents]
|
||||||
rows = [["state_icon", "workspace", "tab"], ["agent", "$ask"], ["$task"]]
|
rows = [["state_icon", "workspace", "tab"], ["agent", "$ask", "$ask_count"], ["$task", "$task_count"]]
|
||||||
```
|
```
|
||||||
|
|
||||||
Apply the change to a running Herdr server:
|
Apply the change to a running Herdr server:
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-todo-herdr",
|
"name": "pi-todo-herdr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-todo-herdr",
|
"name": "pi-todo-herdr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.81.1",
|
"@earendil-works/pi-ai": "^0.81.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-todo-herdr",
|
"name": "pi-todo-herdr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "Hierarchical task tools for Pi with a live widget and Herdr sidebar integration.",
|
"description": "Hierarchical task tools for Pi with a live widget and Herdr sidebar integration.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
|
|||||||
+18
-9
@@ -3,7 +3,8 @@ import type { Task } from "./types.ts";
|
|||||||
import { orderedTreeRows } from "./ui.ts";
|
import { orderedTreeRows } from "./ui.ts";
|
||||||
|
|
||||||
const SOURCE = "pi-todo-herdr";
|
const SOURCE = "pi-todo-herdr";
|
||||||
const TOKEN = "task";
|
const TASK_TOKEN = "task";
|
||||||
|
const COUNT_TOKEN = "task_count";
|
||||||
let reportSeq = Date.now() * 1000;
|
let reportSeq = Date.now() * 1000;
|
||||||
|
|
||||||
function nextSeq(): number {
|
function nextSeq(): number {
|
||||||
@@ -24,7 +25,7 @@ function hasDescendantWithStatus(task: Task, status: Task["status"], byId: Map<n
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function currentTaskLabel(tasks: Task[]): string | null {
|
export function currentTaskTokens(tasks: Task[]): Record<string, string | null> {
|
||||||
const byId = new Map(tasks.map((task) => [task.id, task]));
|
const byId = new Map(tasks.map((task) => [task.id, task]));
|
||||||
const ordered = orderedTreeRows(tasks).map(({ task }) => task);
|
const ordered = orderedTreeRows(tasks).map(({ task }) => task);
|
||||||
const inProgress = ordered.filter(
|
const inProgress = ordered.filter(
|
||||||
@@ -34,9 +35,17 @@ export function currentTaskLabel(tasks: Task[]): string | null {
|
|||||||
inProgress.length > 0
|
inProgress.length > 0
|
||||||
? inProgress
|
? inProgress
|
||||||
: ordered.filter((task) => task.status === "blocked" && !hasDescendantWithStatus(task, "blocked", byId));
|
: ordered.filter((task) => task.status === "blocked" && !hasDescendantWithStatus(task, "blocked", byId));
|
||||||
if (candidates.length === 0) return null;
|
return {
|
||||||
const suffix = candidates.length > 1 ? ` +${candidates.length - 1}` : "";
|
[TASK_TOKEN]: candidates.length > 0 ? `#${candidates[0]!.id} ${candidates[0]!.name}` : null,
|
||||||
return `#${candidates[0]!.id} ${candidates[0]!.name}${suffix}`;
|
[COUNT_TOKEN]: candidates.length > 1 ? `+${candidates.length - 1}` : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentTaskLabel(tasks: Task[]): string | null {
|
||||||
|
const tokens = currentTaskTokens(tasks);
|
||||||
|
const task = tokens[TASK_TOKEN];
|
||||||
|
const count = tokens[COUNT_TOKEN];
|
||||||
|
return task ? `${task}${count ? ` ${count}` : ""}` : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
|
function sendAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
|
||||||
@@ -69,7 +78,7 @@ async function send(request: unknown): Promise<void> {
|
|||||||
await sendAttempt(request, 1500);
|
await sendAttempt(request, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reportToken(value: string | null): Promise<void> {
|
async function reportTokens(tokens: Record<string, string | null>): Promise<void> {
|
||||||
if (!isHerdrEnvironment()) return;
|
if (!isHerdrEnvironment()) return;
|
||||||
await send({
|
await send({
|
||||||
id: `${SOURCE}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
id: `${SOURCE}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||||
@@ -77,16 +86,16 @@ async function reportToken(value: string | null): Promise<void> {
|
|||||||
params: {
|
params: {
|
||||||
pane_id: process.env.HERDR_PANE_ID,
|
pane_id: process.env.HERDR_PANE_ID,
|
||||||
source: SOURCE,
|
source: SOURCE,
|
||||||
tokens: { [TOKEN]: value },
|
tokens,
|
||||||
seq: nextSeq(),
|
seq: nextSeq(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reportCurrentTask(tasks: Task[]): Promise<void> {
|
export function reportCurrentTask(tasks: Task[]): Promise<void> {
|
||||||
return reportToken(currentTaskLabel(tasks));
|
return reportTokens(currentTaskTokens(tasks));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearCurrentTask(): Promise<void> {
|
export function clearCurrentTask(): Promise<void> {
|
||||||
return reportToken(null);
|
return reportTokens({ [TASK_TOKEN]: null, [COUNT_TOKEN]: null });
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { visibleWidth } from "@earendil-works/pi-tui";
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
||||||
import { currentTaskLabel } from "../src/herdr.ts";
|
import { currentTaskLabel, currentTaskTokens } from "../src/herdr.ts";
|
||||||
import { TaskState } from "../src/state.ts";
|
import { TaskState } from "../src/state.ts";
|
||||||
import {
|
import {
|
||||||
renderProgressHeader,
|
renderProgressHeader,
|
||||||
@@ -106,6 +106,7 @@ test("selects the most specific in-progress task and compresses parallel leaves"
|
|||||||
{ name: "Parallel", description: "Parallel", status: "in-progress" },
|
{ name: "Parallel", description: "Parallel", status: "in-progress" },
|
||||||
]);
|
]);
|
||||||
assert.equal(currentTaskLabel(state.list()), "#2 Child +1");
|
assert.equal(currentTaskLabel(state.list()), "#2 Child +1");
|
||||||
|
assert.deepEqual(currentTaskTokens(state.list()), { task: "#2 Child", task_count: "+1" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses blocked tasks only when nothing is in progress", () => {
|
test("uses blocked tasks only when nothing is in progress", () => {
|
||||||
@@ -115,8 +116,10 @@ test("uses blocked tasks only when nothing is in progress", () => {
|
|||||||
{ name: "Blocked two", description: "Two", status: "blocked" },
|
{ name: "Blocked two", description: "Two", status: "blocked" },
|
||||||
]);
|
]);
|
||||||
assert.equal(currentTaskLabel(state.list()), "#1 Blocked one +1");
|
assert.equal(currentTaskLabel(state.list()), "#1 Blocked one +1");
|
||||||
|
assert.deepEqual(currentTaskTokens(state.list()), { task: "#1 Blocked one", task_count: "+1" });
|
||||||
state.update([{ id: 1, status: "done" }, { id: 2, status: "done" }]);
|
state.update([{ id: 1, status: "done" }, { id: 2, status: "done" }]);
|
||||||
assert.equal(currentTaskLabel(state.list()), null);
|
assert.equal(currentTaskLabel(state.list()), null);
|
||||||
|
assert.deepEqual(currentTaskTokens(state.list()), { task: null, task_count: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hides an empty widget and renders a populated one", () => {
|
test("hides an empty widget and renders a populated one", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user