@dungle-scrubs/tallow 0.8.7 → 0.8.8
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 +1 -1
- package/dist/cli.js +244 -54
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/sdk.d.ts +39 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +322 -13
- package/dist/sdk.js.map +1 -1
- package/dist/session-utils.d.ts +8 -0
- package/dist/session-utils.d.ts.map +1 -1
- package/dist/session-utils.js +38 -1
- package/dist/session-utils.js.map +1 -1
- package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
- package/extensions/__integration__/worktree.test.ts +88 -0
- package/extensions/_icons/index.ts +2 -3
- package/extensions/_shared/inline-preview.ts +2 -4
- package/extensions/_shared/interop-events.ts +48 -0
- package/extensions/_shared/shell-policy.ts +2 -1
- package/extensions/_shared/tallow-paths.ts +48 -0
- package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
- package/extensions/agent-commands-tool/index.ts +38 -6
- package/extensions/background-task-tool/index.ts +2 -2
- package/extensions/bash-tool-enhanced/index.ts +3 -4
- package/extensions/cd-tool/index.ts +3 -3
- package/extensions/claude-bridge/index.ts +2 -1
- package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
- package/extensions/command-prompt/index.ts +36 -0
- package/extensions/context-files/index.ts +2 -1
- package/extensions/context-fork/index.ts +2 -1
- package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
- package/extensions/context-usage/index.ts +169 -8
- package/extensions/debug/__tests__/analysis.test.ts +35 -2
- package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
- package/extensions/debug/analysis.ts +53 -16
- package/extensions/debug/index.ts +84 -10
- package/extensions/debug/logger.ts +2 -2
- package/extensions/health/index.ts +2 -2
- package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
- package/extensions/hooks/extension.json +3 -1
- package/extensions/hooks/hooks.schema.json +17 -1
- package/extensions/hooks/index.ts +52 -7
- package/extensions/lsp/index.ts +2 -7
- package/extensions/output-styles-tool/index.ts +2 -4
- package/extensions/plan-mode-tool/extension.json +1 -0
- package/extensions/plan-mode-tool/index.ts +119 -4
- package/extensions/prompt-suggestions/index.ts +2 -3
- package/extensions/random-spinner/index.ts +2 -3
- package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
- package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
- package/extensions/read-tool-enhanced/extension.json +4 -4
- package/extensions/read-tool-enhanced/index.ts +112 -10
- package/extensions/read-tool-enhanced/notebook.ts +526 -0
- package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
- package/extensions/rewind/snapshots.ts +18 -6
- package/extensions/session-memory/index.ts +3 -2
- package/extensions/stats/stats-log.ts +2 -3
- package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
- package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
- package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
- package/extensions/subagent-tool/agents.ts +77 -16
- package/extensions/subagent-tool/index.ts +213 -48
- package/extensions/subagent-tool/model-router.ts +3 -3
- package/extensions/subagent-tool/process.ts +233 -22
- package/extensions/subagent-tool/schema.ts +11 -0
- package/extensions/subagent-tool/widget.ts +15 -2
- package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
- package/extensions/tasks/state/index.ts +2 -2
- package/extensions/theme-selector/index.ts +3 -7
- package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
- package/extensions/worktree/extension.json +31 -0
- package/extensions/worktree/index.ts +149 -0
- package/extensions/worktree/lifecycle.ts +372 -0
- package/package.json +1 -1
- package/skills/tallow-expert/SKILL.md +1 -1
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { execSync } from "node:child_process";
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
21
|
import {
|
|
24
22
|
type AgentToolResult,
|
|
25
23
|
type BashToolDetails,
|
|
@@ -30,6 +28,7 @@ import { Text } from "@mariozechner/pi-tui";
|
|
|
30
28
|
import { getIcon } from "../_icons/index.js";
|
|
31
29
|
import { INTEROP_API_CHANNELS } from "../_shared/interop-events.js";
|
|
32
30
|
import { enforceExplicitPolicy, recordAudit } from "../_shared/shell-policy.js";
|
|
31
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
33
32
|
import type { PromotedTaskHandle } from "../background-task-tool/index.js";
|
|
34
33
|
import {
|
|
35
34
|
appendSection,
|
|
@@ -54,7 +53,7 @@ const DEFAULT_AUTO_BG_TIMEOUT_MS = 30_000;
|
|
|
54
53
|
*/
|
|
55
54
|
function readAutoBackgroundTimeout(): number {
|
|
56
55
|
try {
|
|
57
|
-
const settingsPath =
|
|
56
|
+
const settingsPath = getTallowSettingsPath();
|
|
58
57
|
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
59
58
|
const settings = JSON.parse(raw) as { bashAutoBackgroundTimeout?: number };
|
|
60
59
|
if (typeof settings.bashAutoBackgroundTimeout === "number") {
|
|
@@ -73,7 +72,7 @@ function readAutoBackgroundTimeout(): number {
|
|
|
73
72
|
*/
|
|
74
73
|
function readMaintainProjectDir(): boolean {
|
|
75
74
|
try {
|
|
76
|
-
const settingsPath =
|
|
75
|
+
const settingsPath = getTallowSettingsPath();
|
|
77
76
|
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
78
77
|
const settings = JSON.parse(raw) as { BASH_MAINTAIN_PROJECT_WORKING_DIR?: boolean };
|
|
79
78
|
return settings.BASH_MAINTAIN_PROJECT_WORKING_DIR === true;
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
import { join, resolve } from "node:path";
|
|
10
|
+
import { resolve } from "node:path";
|
|
12
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
12
|
import { Type } from "@sinclair/typebox";
|
|
13
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Check if BASH_MAINTAIN_PROJECT_WORKING_DIR is enabled in settings.
|
|
@@ -19,7 +19,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
19
19
|
*/
|
|
20
20
|
function isMaintainProjectDirEnabled(): boolean {
|
|
21
21
|
try {
|
|
22
|
-
const raw = readFileSync(
|
|
22
|
+
const raw = readFileSync(getTallowSettingsPath(), "utf-8");
|
|
23
23
|
return (
|
|
24
24
|
(JSON.parse(raw) as { BASH_MAINTAIN_PROJECT_WORKING_DIR?: boolean })
|
|
25
25
|
.BASH_MAINTAIN_PROJECT_WORKING_DIR === true
|
|
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
|
|
|
14
14
|
import * as os from "node:os";
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import { getTallowHomeDir } from "../_shared/tallow-paths.js";
|
|
17
18
|
|
|
18
19
|
const CLAUDE_DIR = ".claude";
|
|
19
20
|
/** Project-level config directories (current + legacy) */
|
|
@@ -165,7 +166,7 @@ export function getNonCollidingSkillPaths(
|
|
|
165
166
|
*/
|
|
166
167
|
export default function (pi: ExtensionAPI): void {
|
|
167
168
|
pi.on("resources_discover", async (event) => {
|
|
168
|
-
const agentDir =
|
|
169
|
+
const agentDir = getTallowHomeDir();
|
|
169
170
|
const cwd = (event as { cwd: string }).cwd;
|
|
170
171
|
const knownNames = collectKnownSkillNames(agentDir, cwd);
|
|
171
172
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, join, resolve } from "node:path";
|
|
5
|
+
import { discoverPluginPromptSourcesFromEnv } from "../index.js";
|
|
6
|
+
|
|
7
|
+
let tmpRoot: string | null = null;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (tmpRoot) {
|
|
11
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
12
|
+
tmpRoot = null;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("discoverPluginPromptSourcesFromEnv", () => {
|
|
17
|
+
it("returns empty list for empty env input", () => {
|
|
18
|
+
expect(discoverPluginPromptSourcesFromEnv("")).toEqual([]);
|
|
19
|
+
expect(discoverPluginPromptSourcesFromEnv(undefined)).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses plugin commands dirs and infers namespaces", () => {
|
|
23
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "command-prompt-plugin-sources-"));
|
|
24
|
+
const a = join(tmpRoot, "plugin-a", "commands");
|
|
25
|
+
const b = join(tmpRoot, "plugin-b", "commands");
|
|
26
|
+
mkdirSync(a, { recursive: true });
|
|
27
|
+
mkdirSync(b, { recursive: true });
|
|
28
|
+
|
|
29
|
+
const value = `${a}${delimiter}${b}`;
|
|
30
|
+
const sources = discoverPluginPromptSourcesFromEnv(value);
|
|
31
|
+
|
|
32
|
+
expect(sources).toHaveLength(2);
|
|
33
|
+
expect(sources[0]).toEqual({ namespace: "plugin-a", promptsDirs: [resolve(a)] });
|
|
34
|
+
expect(sources[1]).toEqual({ namespace: "plugin-b", promptsDirs: [resolve(b)] });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("deduplicates and skips missing directories", () => {
|
|
38
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "command-prompt-plugin-sources-"));
|
|
39
|
+
const a = join(tmpRoot, "plugin-a", "commands");
|
|
40
|
+
const missing = join(tmpRoot, "missing", "commands");
|
|
41
|
+
mkdirSync(a, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const value = `${a}${delimiter}${missing}${delimiter}${a}`;
|
|
44
|
+
const sources = discoverPluginPromptSourcesFromEnv(value);
|
|
45
|
+
|
|
46
|
+
expect(sources).toHaveLength(1);
|
|
47
|
+
expect(sources[0]).toEqual({ namespace: "plugin-a", promptsDirs: [resolve(a)] });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import * as fs from "node:fs";
|
|
30
30
|
import * as os from "node:os";
|
|
31
31
|
import * as path from "node:path";
|
|
32
|
+
import { delimiter } from "node:path";
|
|
32
33
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
33
34
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
34
35
|
import { Key } from "@mariozechner/pi-tui";
|
|
@@ -327,6 +328,40 @@ function discoverPackagePromptSources(settingsPath: string): PackagePromptSource
|
|
|
327
328
|
return results;
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Discover plugin command sources from a path-list environment variable.
|
|
333
|
+
*
|
|
334
|
+
* SDK populates `TALLOW_PLUGIN_COMMANDS_DIRS` with absolute `commands/`
|
|
335
|
+
* directories from resolved Claude-style plugins.
|
|
336
|
+
*
|
|
337
|
+
* @param envValue - Optional env value override for tests
|
|
338
|
+
* @returns Namespaced prompt sources inferred from plugin command dirs
|
|
339
|
+
*/
|
|
340
|
+
export function discoverPluginPromptSourcesFromEnv(envValue?: string): PackagePromptSource[] {
|
|
341
|
+
const raw = (envValue ?? process.env.TALLOW_PLUGIN_COMMANDS_DIRS ?? "").trim();
|
|
342
|
+
if (!raw) return [];
|
|
343
|
+
|
|
344
|
+
const seen = new Set<string>();
|
|
345
|
+
const sources: PackagePromptSource[] = [];
|
|
346
|
+
|
|
347
|
+
for (const dir of raw
|
|
348
|
+
.split(delimiter)
|
|
349
|
+
.map((v) => v.trim())
|
|
350
|
+
.filter(Boolean)) {
|
|
351
|
+
if (!fs.existsSync(dir)) continue;
|
|
352
|
+
const normalized = path.resolve(dir);
|
|
353
|
+
if (seen.has(normalized)) continue;
|
|
354
|
+
seen.add(normalized);
|
|
355
|
+
|
|
356
|
+
// commands/ lives at pluginRoot/commands → namespace = pluginRoot basename
|
|
357
|
+
const pluginRoot = path.dirname(normalized);
|
|
358
|
+
const namespace = path.basename(pluginRoot);
|
|
359
|
+
sources.push({ namespace, promptsDirs: [normalized] });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return sources;
|
|
363
|
+
}
|
|
364
|
+
|
|
330
365
|
/**
|
|
331
366
|
* Discovers ALL prompts across multiple package directories (top-level +
|
|
332
367
|
* one-deep subdirs). Merges results from all dirs, deduplicating by name
|
|
@@ -575,6 +610,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
575
610
|
const packageSources = [
|
|
576
611
|
...discoverPackagePromptSources(projectSettings),
|
|
577
612
|
...discoverPackagePromptSources(globalSettings),
|
|
613
|
+
...discoverPluginPromptSourcesFromEnv(),
|
|
578
614
|
];
|
|
579
615
|
|
|
580
616
|
for (const { namespace, promptsDirs } of packageSources) {
|
|
@@ -18,6 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
20
20
|
import { createLazyInitializer } from "../_shared/lazy-init.js";
|
|
21
|
+
import { getTallowHomeDir } from "../_shared/tallow-paths.js";
|
|
21
22
|
|
|
22
23
|
const CONTEXT_FILENAMES = ["CLAUDE.md", "AGENTS.md"] as const;
|
|
23
24
|
|
|
@@ -166,7 +167,7 @@ function findSubdirRuleFiles(baseDir: string, maxDepth: number = 5): string[] {
|
|
|
166
167
|
*/
|
|
167
168
|
function collectMissingFiles(cwd: string): ContextFile[] {
|
|
168
169
|
const files: ContextFile[] = [];
|
|
169
|
-
const globalDir =
|
|
170
|
+
const globalDir = getTallowHomeDir();
|
|
170
171
|
|
|
171
172
|
// --- Global level ---
|
|
172
173
|
// If pi loaded ~/.tallow/AGENTS.md, we pick up CLAUDE.md (and vice versa)
|
|
@@ -25,6 +25,7 @@ import { stripFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
|
25
25
|
import { Text } from "@mariozechner/pi-tui";
|
|
26
26
|
import { createLazyInitializer } from "../_shared/lazy-init.js";
|
|
27
27
|
import { isShellInterpolationEnabled } from "../_shared/shell-policy.js";
|
|
28
|
+
import { getTallowHomeDir } from "../_shared/tallow-paths.js";
|
|
28
29
|
import { expandShellCommands } from "../shell-interpolation/index.js";
|
|
29
30
|
import type { FrontmatterIndex } from "./frontmatter-index.js";
|
|
30
31
|
import { buildFrontmatterIndex } from "./frontmatter-index.js";
|
|
@@ -197,7 +198,7 @@ function getPackageAgentDirs(settingsPath: string): string[] {
|
|
|
197
198
|
* @returns Map of agent name → config
|
|
198
199
|
*/
|
|
199
200
|
function loadAllAgents(): Map<string, AgentConfig> {
|
|
200
|
-
const agentDir =
|
|
201
|
+
const agentDir = getTallowHomeDir();
|
|
201
202
|
const userDir = path.join(agentDir, "agents");
|
|
202
203
|
const userClaudeDir = path.join(os.homedir(), ".claude", "agents");
|
|
203
204
|
const projectDir = path.join(process.cwd(), ".tallow", "agents");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { computeToolResultMemoryStats } from "../index.js";
|
|
3
|
+
|
|
4
|
+
const RETENTION_MARKER = "__tallow_summarized_tool_result__";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a branch entry with a toolResult message payload.
|
|
8
|
+
*
|
|
9
|
+
* @param id - Tool call id
|
|
10
|
+
* @param text - Text payload
|
|
11
|
+
* @param details - Optional details payload
|
|
12
|
+
* @returns Session branch entry-like object
|
|
13
|
+
*/
|
|
14
|
+
function toolResultEntry(
|
|
15
|
+
id: string,
|
|
16
|
+
text: string,
|
|
17
|
+
details?: Record<string, unknown>
|
|
18
|
+
): { message: Record<string, unknown>; type: string } {
|
|
19
|
+
return {
|
|
20
|
+
type: "message",
|
|
21
|
+
message: {
|
|
22
|
+
role: "toolResult",
|
|
23
|
+
toolCallId: id,
|
|
24
|
+
toolName: "bash",
|
|
25
|
+
content: [{ type: "text", text }],
|
|
26
|
+
details,
|
|
27
|
+
isError: false,
|
|
28
|
+
timestamp: Date.now(),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("computeToolResultMemoryStats", () => {
|
|
34
|
+
test("returns zero stats for empty branches", () => {
|
|
35
|
+
const stats = computeToolResultMemoryStats([]);
|
|
36
|
+
expect(stats.totalResults).toBe(0);
|
|
37
|
+
expect(stats.summarizedResults).toBe(0);
|
|
38
|
+
expect(stats.retainedBytes).toBe(0);
|
|
39
|
+
expect(stats.reclaimedBytes).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("counts retained and reclaimed bytes for summarized historical results", () => {
|
|
43
|
+
const summarized = toolResultEntry("tc_1", "short summary", {
|
|
44
|
+
[RETENTION_MARKER]: true,
|
|
45
|
+
originalBytes: 20_000,
|
|
46
|
+
});
|
|
47
|
+
const full = toolResultEntry("tc_2", "x".repeat(2_000), { foo: "bar" });
|
|
48
|
+
const nonToolMessage = {
|
|
49
|
+
type: "message",
|
|
50
|
+
message: {
|
|
51
|
+
role: "assistant",
|
|
52
|
+
content: [{ type: "text", text: "ok" }],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const stats = computeToolResultMemoryStats([summarized, full, nonToolMessage]);
|
|
57
|
+
expect(stats.totalResults).toBe(2);
|
|
58
|
+
expect(stats.summarizedResults).toBe(1);
|
|
59
|
+
expect(stats.retainedBytes).toBeGreaterThan(0);
|
|
60
|
+
expect(stats.reclaimedBytes).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
|
-
import * as os from "node:os";
|
|
19
|
-
import * as path from "node:path";
|
|
20
18
|
import type { ContextUsage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
21
19
|
import { DEFAULT_COMPACTION_SETTINGS } from "@mariozechner/pi-coding-agent";
|
|
22
20
|
import { getIcon } from "../_icons/index.js";
|
|
21
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
23
22
|
|
|
24
23
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
25
24
|
|
|
@@ -29,6 +28,9 @@ const GRID_COLS = 10;
|
|
|
29
28
|
/** Chars/4 heuristic for token estimation */
|
|
30
29
|
const CHARS_PER_TOKEN = 4;
|
|
31
30
|
|
|
31
|
+
/** Marker key attached by sdk tool-result retention summarization. */
|
|
32
|
+
const TOOL_RESULT_RETENTION_MARKER = "__tallow_summarized_tool_result__";
|
|
33
|
+
|
|
32
34
|
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
33
35
|
|
|
34
36
|
interface CompactionConfig {
|
|
@@ -36,12 +38,28 @@ interface CompactionConfig {
|
|
|
36
38
|
readonly reserveTokens: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/** Tool-result payload memory summary for the active branch. */
|
|
42
|
+
export interface ToolResultMemoryStats {
|
|
43
|
+
readonly reclaimedBytes: number;
|
|
44
|
+
readonly retainedBytes: number;
|
|
45
|
+
readonly summarizedResults: number;
|
|
46
|
+
readonly totalResults: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Zero-value fallback for tool-result memory stats. */
|
|
50
|
+
const EMPTY_TOOL_RESULT_MEMORY_STATS: ToolResultMemoryStats = {
|
|
51
|
+
reclaimedBytes: 0,
|
|
52
|
+
retainedBytes: 0,
|
|
53
|
+
summarizedResults: 0,
|
|
54
|
+
totalResults: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Reads compaction settings from ~/.tallow/settings.json.
|
|
41
59
|
* Falls back to DEFAULT_COMPACTION_SETTINGS if unreadable.
|
|
42
60
|
*/
|
|
43
61
|
function readCompactionConfig(): CompactionConfig {
|
|
44
|
-
const settingsPath =
|
|
62
|
+
const settingsPath = getTallowSettingsPath();
|
|
45
63
|
try {
|
|
46
64
|
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
47
65
|
const settings = JSON.parse(raw) as {
|
|
@@ -172,6 +190,102 @@ function estimateToolTokens(
|
|
|
172
190
|
return total;
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Estimate UTF-8 bytes for a JSON-serializable value.
|
|
195
|
+
*
|
|
196
|
+
* @param value - Any serializable value
|
|
197
|
+
* @returns Byte length of JSON representation, or 0 when unavailable
|
|
198
|
+
*/
|
|
199
|
+
function safeJsonBytes(value: unknown): number {
|
|
200
|
+
if (value == null) return 0;
|
|
201
|
+
try {
|
|
202
|
+
return Buffer.byteLength(JSON.stringify(value), "utf-8");
|
|
203
|
+
} catch {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Type guard for plain object records.
|
|
210
|
+
*
|
|
211
|
+
* @param value - Unknown value
|
|
212
|
+
* @returns True when value is a non-null object and not an array
|
|
213
|
+
*/
|
|
214
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
215
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Estimate current in-memory payload bytes for a tool-result message.
|
|
220
|
+
*
|
|
221
|
+
* @param message - Candidate message object
|
|
222
|
+
* @returns Estimated payload bytes for content + details
|
|
223
|
+
*/
|
|
224
|
+
function estimateToolResultMessageBytes(message: Record<string, unknown>): number {
|
|
225
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
226
|
+
let contentBytes = 0;
|
|
227
|
+
|
|
228
|
+
for (const block of content) {
|
|
229
|
+
if (!isRecord(block)) continue;
|
|
230
|
+
if (block.type === "text") {
|
|
231
|
+
contentBytes += Buffer.byteLength(typeof block.text === "string" ? block.text : "", "utf-8");
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (block.type === "image") {
|
|
235
|
+
contentBytes += Buffer.byteLength(typeof block.data === "string" ? block.data : "", "utf-8");
|
|
236
|
+
contentBytes += Buffer.byteLength(
|
|
237
|
+
typeof block.mimeType === "string" ? block.mimeType : "",
|
|
238
|
+
"utf-8"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const detailsBytes = safeJsonBytes(message.details);
|
|
244
|
+
return contentBytes + detailsBytes;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Compute tool-result payload memory stats from branch entries.
|
|
249
|
+
*
|
|
250
|
+
* @param branchEntries - Session branch entries from `sessionManager.getBranch()`
|
|
251
|
+
* @returns Aggregated tool-result memory stats
|
|
252
|
+
*/
|
|
253
|
+
export function computeToolResultMemoryStats(
|
|
254
|
+
branchEntries: Array<{ type: string; message?: unknown }>
|
|
255
|
+
): ToolResultMemoryStats {
|
|
256
|
+
if (branchEntries.length === 0) return EMPTY_TOOL_RESULT_MEMORY_STATS;
|
|
257
|
+
|
|
258
|
+
let reclaimedBytes = 0;
|
|
259
|
+
let retainedBytes = 0;
|
|
260
|
+
let summarizedResults = 0;
|
|
261
|
+
let totalResults = 0;
|
|
262
|
+
|
|
263
|
+
for (const entry of branchEntries) {
|
|
264
|
+
if (entry.type !== "message" || !isRecord(entry.message)) continue;
|
|
265
|
+
if (entry.message.role !== "toolResult") continue;
|
|
266
|
+
|
|
267
|
+
totalResults += 1;
|
|
268
|
+
const currentBytes = estimateToolResultMessageBytes(entry.message);
|
|
269
|
+
retainedBytes += currentBytes;
|
|
270
|
+
|
|
271
|
+
const details = entry.message.details;
|
|
272
|
+
if (!isRecord(details) || details[TOOL_RESULT_RETENTION_MARKER] !== true) continue;
|
|
273
|
+
summarizedResults += 1;
|
|
274
|
+
const originalBytes =
|
|
275
|
+
typeof details.originalBytes === "number" && Number.isFinite(details.originalBytes)
|
|
276
|
+
? Math.max(0, details.originalBytes)
|
|
277
|
+
: 0;
|
|
278
|
+
reclaimedBytes += Math.max(0, originalBytes - currentBytes);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
reclaimedBytes,
|
|
283
|
+
retainedBytes,
|
|
284
|
+
summarizedResults,
|
|
285
|
+
totalResults,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
175
289
|
// ── System prompt parsing ────────────────────────────────────────────────────
|
|
176
290
|
|
|
177
291
|
/** @internal */
|
|
@@ -247,6 +361,18 @@ export function formatTokens(count: number): string {
|
|
|
247
361
|
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
248
362
|
}
|
|
249
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Formats byte counts with compact units.
|
|
366
|
+
*
|
|
367
|
+
* @param count - Byte count
|
|
368
|
+
* @returns Human-readable byte string
|
|
369
|
+
*/
|
|
370
|
+
function formatBytes(count: number): string {
|
|
371
|
+
if (count < 1024) return `${count}B`;
|
|
372
|
+
if (count < 1024 * 1024) return `${(count / 1024).toFixed(1)}KB`;
|
|
373
|
+
return `${(count / (1024 * 1024)).toFixed(1)}MB`;
|
|
374
|
+
}
|
|
375
|
+
|
|
250
376
|
/**
|
|
251
377
|
* Renders the waffle chart grid.
|
|
252
378
|
* Each cell represents a proportional slice of the context window.
|
|
@@ -302,7 +428,8 @@ function buildLegend(
|
|
|
302
428
|
contextWindow: number,
|
|
303
429
|
modelId: string,
|
|
304
430
|
usedTokens: number,
|
|
305
|
-
usedPercent: number
|
|
431
|
+
usedPercent: number,
|
|
432
|
+
toolResultMemory: ToolResultMemoryStats
|
|
306
433
|
): string[] {
|
|
307
434
|
const lines: string[] = [];
|
|
308
435
|
|
|
@@ -323,6 +450,23 @@ function buildLegend(
|
|
|
323
450
|
);
|
|
324
451
|
}
|
|
325
452
|
|
|
453
|
+
if (toolResultMemory.totalResults > 0) {
|
|
454
|
+
lines.push("");
|
|
455
|
+
lines.push(`${DIM}Historical tool-result payloads${RESET}`);
|
|
456
|
+
lines.push(
|
|
457
|
+
`${getIcon("in_progress")} results: ${toolResultMemory.totalResults} total, ` +
|
|
458
|
+
`${toolResultMemory.summarizedResults} summarized`
|
|
459
|
+
);
|
|
460
|
+
lines.push(
|
|
461
|
+
`${getIcon("in_progress")} retained bytes: ${formatBytes(toolResultMemory.retainedBytes)}`
|
|
462
|
+
);
|
|
463
|
+
if (toolResultMemory.reclaimedBytes > 0) {
|
|
464
|
+
lines.push(
|
|
465
|
+
`${getIcon("success")} reclaimed bytes: ${formatBytes(toolResultMemory.reclaimedBytes)}`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
326
470
|
return lines;
|
|
327
471
|
}
|
|
328
472
|
|
|
@@ -342,6 +486,7 @@ interface ContextUsageDetails {
|
|
|
342
486
|
readonly emptyChar: string;
|
|
343
487
|
readonly tokens: number;
|
|
344
488
|
}>;
|
|
489
|
+
readonly toolResultMemory: ToolResultMemoryStats;
|
|
345
490
|
}
|
|
346
491
|
|
|
347
492
|
/**
|
|
@@ -386,8 +531,12 @@ export default function contextUsageExtension(pi: ExtensionAPI): void {
|
|
|
386
531
|
const systemPrompt = ctx.getSystemPrompt();
|
|
387
532
|
const tools = pi.getAllTools();
|
|
388
533
|
const modelId = ctx.model?.id ?? "unknown-model";
|
|
534
|
+
const branchEntries = ctx.sessionManager.getBranch() as Array<{
|
|
535
|
+
type: string;
|
|
536
|
+
message?: unknown;
|
|
537
|
+
}>;
|
|
389
538
|
|
|
390
|
-
const details = buildDetails(usage, systemPrompt, tools, modelId);
|
|
539
|
+
const details = buildDetails(usage, systemPrompt, tools, modelId, branchEntries);
|
|
391
540
|
|
|
392
541
|
pi.sendMessage({
|
|
393
542
|
customType: "context-usage",
|
|
@@ -405,13 +554,15 @@ export default function contextUsageExtension(pi: ExtensionAPI): void {
|
|
|
405
554
|
* @param systemPrompt - Current system prompt
|
|
406
555
|
* @param tools - Registered tools
|
|
407
556
|
* @param modelId - Current model identifier
|
|
557
|
+
* @param branchEntries - Current session branch entries
|
|
408
558
|
* @returns Details object for the message renderer
|
|
409
559
|
*/
|
|
410
560
|
function buildDetails(
|
|
411
561
|
usage: ContextUsage,
|
|
412
562
|
systemPrompt: string,
|
|
413
563
|
tools: Array<{ name: string; description: string; parameters: unknown }>,
|
|
414
|
-
modelId: string
|
|
564
|
+
modelId: string,
|
|
565
|
+
branchEntries: Array<{ type: string; message?: unknown }>
|
|
415
566
|
): ContextUsageDetails {
|
|
416
567
|
const compaction = readCompactionConfig();
|
|
417
568
|
const includeAutocompact = compaction.enabled;
|
|
@@ -440,6 +591,8 @@ function buildDetails(
|
|
|
440
591
|
const freeTokens = Math.max(0, contextWindow - usedTokens - reserveTokens);
|
|
441
592
|
categories[5].tokens = freeTokens;
|
|
442
593
|
|
|
594
|
+
const toolResultMemory = computeToolResultMemoryStats(branchEntries);
|
|
595
|
+
|
|
443
596
|
return {
|
|
444
597
|
modelId,
|
|
445
598
|
contextWindow,
|
|
@@ -452,6 +605,7 @@ function buildDetails(
|
|
|
452
605
|
emptyChar: c.emptyChar,
|
|
453
606
|
tokens: c.tokens,
|
|
454
607
|
})),
|
|
608
|
+
toolResultMemory,
|
|
455
609
|
};
|
|
456
610
|
}
|
|
457
611
|
|
|
@@ -461,7 +615,7 @@ function buildDetails(
|
|
|
461
615
|
* @returns Array of rendered lines
|
|
462
616
|
*/
|
|
463
617
|
function renderFromDetails(details: ContextUsageDetails): string[] {
|
|
464
|
-
const { categories, contextWindow, modelId, usedTokens } = details;
|
|
618
|
+
const { categories, contextWindow, modelId, toolResultMemory, usedTokens } = details;
|
|
465
619
|
const usedPercent = contextWindow > 0 ? (usedTokens / contextWindow) * 100 : 0;
|
|
466
620
|
|
|
467
621
|
const lines: string[] = [];
|
|
@@ -469,7 +623,14 @@ function renderFromDetails(details: ContextUsageDetails): string[] {
|
|
|
469
623
|
lines.push(` ${BOLD}Context Usage${RESET}`);
|
|
470
624
|
|
|
471
625
|
const chartLines = renderWaffleChart(categories, contextWindow);
|
|
472
|
-
const legendLines = buildLegend(
|
|
626
|
+
const legendLines = buildLegend(
|
|
627
|
+
categories,
|
|
628
|
+
contextWindow,
|
|
629
|
+
modelId,
|
|
630
|
+
usedTokens,
|
|
631
|
+
usedPercent,
|
|
632
|
+
toolResultMemory
|
|
633
|
+
);
|
|
473
634
|
|
|
474
635
|
const maxLines = Math.max(chartLines.length, legendLines.length);
|
|
475
636
|
for (let i = 0; i < maxLines; i++) {
|
|
@@ -19,12 +19,23 @@ import type { LogEntry } from "../logger.js";
|
|
|
19
19
|
* @param ok - Whether the call succeeded
|
|
20
20
|
* @returns A LogEntry for a tool result
|
|
21
21
|
*/
|
|
22
|
-
function toolResult(
|
|
22
|
+
function toolResult(
|
|
23
|
+
name: string,
|
|
24
|
+
durationMs: number,
|
|
25
|
+
ok = true,
|
|
26
|
+
extra: Record<string, unknown> = {}
|
|
27
|
+
): LogEntry {
|
|
23
28
|
return {
|
|
24
29
|
ts: new Date().toISOString(),
|
|
25
30
|
cat: "tool",
|
|
26
31
|
evt: "result",
|
|
27
|
-
data: {
|
|
32
|
+
data: {
|
|
33
|
+
name,
|
|
34
|
+
durationMs,
|
|
35
|
+
ok,
|
|
36
|
+
toolCallId: `tc_${Math.random().toString(36).slice(2, 6)}`,
|
|
37
|
+
...extra,
|
|
38
|
+
},
|
|
28
39
|
};
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -166,6 +177,17 @@ describe("formatToolTimings()", () => {
|
|
|
166
177
|
expect(output).toContain("| Tool |");
|
|
167
178
|
expect(output).toContain("| bash |");
|
|
168
179
|
});
|
|
180
|
+
|
|
181
|
+
it("includes payload and summarized columns", () => {
|
|
182
|
+
const stats = summarizeToolTimings([
|
|
183
|
+
toolResult("bash", 100, true, { payloadBytes: 2_048, summarizedByRetention: true }),
|
|
184
|
+
]);
|
|
185
|
+
const output = formatToolTimings(stats);
|
|
186
|
+
|
|
187
|
+
expect(output).toContain("Avg payload");
|
|
188
|
+
expect(output).toContain("2.0KB");
|
|
189
|
+
expect(output).toContain("| 1 |");
|
|
190
|
+
});
|
|
169
191
|
});
|
|
170
192
|
|
|
171
193
|
// ── groupErrors() ────────────────────────────────────────────
|
|
@@ -350,4 +372,15 @@ describe("formatEntries()", () => {
|
|
|
350
372
|
expect(output).toContain("agent=sub_1");
|
|
351
373
|
expect(output).toContain("exit=1");
|
|
352
374
|
});
|
|
375
|
+
|
|
376
|
+
it("highlights payload bytes and retention summaries", () => {
|
|
377
|
+
const entry = toolResult("bash", 12, true, {
|
|
378
|
+
payloadBytes: 2_048,
|
|
379
|
+
summarizedByRetention: true,
|
|
380
|
+
});
|
|
381
|
+
const output = formatEntries([entry]);
|
|
382
|
+
|
|
383
|
+
expect(output).toContain("payload=2.0KB");
|
|
384
|
+
expect(output).toContain("summarized");
|
|
385
|
+
});
|
|
353
386
|
});
|
|
@@ -20,11 +20,14 @@ interface CommandContextOptions {
|
|
|
20
20
|
let harness: ExtensionHarness;
|
|
21
21
|
let tmpHome: string;
|
|
22
22
|
let savedHome: string | undefined;
|
|
23
|
+
let savedTallowHome: string | undefined;
|
|
23
24
|
|
|
24
25
|
beforeEach(async () => {
|
|
25
26
|
tmpHome = mkdtempSync(join(tmpdir(), "tallow-debug-diag-cmd-test-"));
|
|
26
27
|
savedHome = process.env.HOME;
|
|
28
|
+
savedTallowHome = process.env.TALLOW_CODING_AGENT_DIR;
|
|
27
29
|
process.env.HOME = tmpHome;
|
|
30
|
+
process.env.TALLOW_CODING_AGENT_DIR = join(tmpHome, ".tallow");
|
|
28
31
|
(globalThis as Record<string, unknown>).__piDebugLogger = undefined;
|
|
29
32
|
|
|
30
33
|
harness = ExtensionHarness.create();
|
|
@@ -37,6 +40,11 @@ afterEach(() => {
|
|
|
37
40
|
} else {
|
|
38
41
|
delete process.env.HOME;
|
|
39
42
|
}
|
|
43
|
+
if (savedTallowHome !== undefined) {
|
|
44
|
+
process.env.TALLOW_CODING_AGENT_DIR = savedTallowHome;
|
|
45
|
+
} else {
|
|
46
|
+
delete process.env.TALLOW_CODING_AGENT_DIR;
|
|
47
|
+
}
|
|
40
48
|
(globalThis as Record<string, unknown>).__piDebugLogger = undefined;
|
|
41
49
|
rmSync(tmpHome, { recursive: true, force: true });
|
|
42
50
|
});
|
|
@@ -59,8 +67,9 @@ function getCommand(name: string): Omit<RegisteredCommand, "name"> {
|
|
|
59
67
|
* @returns Absolute path to the seeded debug log
|
|
60
68
|
*/
|
|
61
69
|
function seedDebugLog(): string {
|
|
62
|
-
const
|
|
63
|
-
|
|
70
|
+
const tallowHome = process.env.TALLOW_CODING_AGENT_DIR ?? join(tmpHome, ".tallow");
|
|
71
|
+
const logPath = join(tallowHome, "debug.log");
|
|
72
|
+
mkdirSync(tallowHome, { recursive: true });
|
|
64
73
|
writeFileSync(
|
|
65
74
|
logPath,
|
|
66
75
|
[
|