@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
|
@@ -101,6 +101,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
101
101
|
let planModeEnabled = false;
|
|
102
102
|
let executionMode = false;
|
|
103
103
|
let todoItems: TodoItem[] = [];
|
|
104
|
+
let currentStepIndex: number | null = null;
|
|
104
105
|
let normalModeTools: string[] = [];
|
|
105
106
|
|
|
106
107
|
/**
|
|
@@ -149,6 +150,45 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
149
150
|
return normalModeTools;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Computes the current step index from todo completion state.
|
|
155
|
+
*
|
|
156
|
+
* @param items - Ordered todo items
|
|
157
|
+
* @returns Zero-based index of first incomplete step, or null when complete
|
|
158
|
+
*/
|
|
159
|
+
function computeCurrentStepIndex(items: readonly TodoItem[]): number | null {
|
|
160
|
+
const nextIndex = items.findIndex((item) => !item.completed);
|
|
161
|
+
return nextIndex >= 0 ? nextIndex : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Synchronizes currentStepIndex with todo completion state.
|
|
166
|
+
*
|
|
167
|
+
* @returns Updated current step index
|
|
168
|
+
*/
|
|
169
|
+
function syncCurrentStepIndex(): number | null {
|
|
170
|
+
currentStepIndex = computeCurrentStepIndex(todoItems);
|
|
171
|
+
return currentStepIndex;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets the current step item for execution guidance.
|
|
176
|
+
*
|
|
177
|
+
* @returns Current step todo item, or null if all steps are complete
|
|
178
|
+
*/
|
|
179
|
+
function getCurrentStep(): TodoItem | null {
|
|
180
|
+
if (todoItems.length === 0) return null;
|
|
181
|
+
if (
|
|
182
|
+
currentStepIndex === null ||
|
|
183
|
+
currentStepIndex < 0 ||
|
|
184
|
+
currentStepIndex >= todoItems.length ||
|
|
185
|
+
todoItems[currentStepIndex]?.completed
|
|
186
|
+
) {
|
|
187
|
+
syncCurrentStepIndex();
|
|
188
|
+
}
|
|
189
|
+
return currentStepIndex === null ? null : (todoItems[currentStepIndex] ?? null);
|
|
190
|
+
}
|
|
191
|
+
|
|
152
192
|
pi.registerFlag("plan", {
|
|
153
193
|
description: "Start in plan mode (read-only exploration)",
|
|
154
194
|
type: "boolean",
|
|
@@ -227,6 +267,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
227
267
|
planModeEnabled = !planModeEnabled;
|
|
228
268
|
executionMode = false;
|
|
229
269
|
todoItems = [];
|
|
270
|
+
currentStepIndex = null;
|
|
230
271
|
|
|
231
272
|
if (planModeEnabled) {
|
|
232
273
|
captureNormalModeTools();
|
|
@@ -249,6 +290,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
249
290
|
executing: executionMode,
|
|
250
291
|
normalTools: normalModeTools,
|
|
251
292
|
todos: todoItems,
|
|
293
|
+
currentStepIndex,
|
|
252
294
|
});
|
|
253
295
|
}
|
|
254
296
|
|
|
@@ -347,6 +389,7 @@ Use action "enable" to enter plan mode, "disable" to exit, or "status" to check
|
|
|
347
389
|
planModeEnabled = shouldEnable;
|
|
348
390
|
executionMode = false;
|
|
349
391
|
todoItems = [];
|
|
392
|
+
currentStepIndex = null;
|
|
350
393
|
|
|
351
394
|
let activeTools: string[];
|
|
352
395
|
if (planModeEnabled) {
|
|
@@ -455,18 +498,27 @@ Do NOT attempt to make changes - just describe what you would do.`,
|
|
|
455
498
|
}
|
|
456
499
|
|
|
457
500
|
if (executionMode && todoItems.length > 0) {
|
|
501
|
+
syncCurrentStepIndex();
|
|
502
|
+
const currentStep = getCurrentStep();
|
|
458
503
|
const remaining = todoItems.filter((t) => !t.completed);
|
|
459
504
|
const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
|
|
505
|
+
const currentStepText = currentStep
|
|
506
|
+
? `${currentStep.step}. ${currentStep.text}`
|
|
507
|
+
: "(all steps completed)";
|
|
460
508
|
return {
|
|
461
509
|
message: {
|
|
462
510
|
customType: "plan-execution-context",
|
|
463
511
|
content: `[EXECUTING PLAN - Full tool access enabled]
|
|
464
512
|
|
|
513
|
+
Current step:
|
|
514
|
+
${currentStepText}
|
|
515
|
+
|
|
465
516
|
Remaining steps:
|
|
466
517
|
${todoList}
|
|
467
518
|
|
|
468
519
|
Execute each step in order.
|
|
469
|
-
After completing a step, include a [DONE:n] tag in your response
|
|
520
|
+
After completing a step, include a [DONE:n] tag in your response.
|
|
521
|
+
If you receive [PLAN GUIDANCE — Step n: ...], treat it as user steering for that step and apply it before continuing.`,
|
|
470
522
|
display: false,
|
|
471
523
|
},
|
|
472
524
|
};
|
|
@@ -479,12 +531,59 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
479
531
|
if (!isAssistantMessage(event.message)) return;
|
|
480
532
|
|
|
481
533
|
const text = getTextContent(event.message);
|
|
482
|
-
|
|
534
|
+
const completedCount = markCompletedSteps(text, todoItems);
|
|
535
|
+
if (completedCount > 0) {
|
|
536
|
+
syncCurrentStepIndex();
|
|
483
537
|
updateStatus(ctx);
|
|
484
538
|
}
|
|
485
539
|
persistState();
|
|
486
540
|
});
|
|
487
541
|
|
|
542
|
+
// Offer user steering when a tool is blocked during tracked execution.
|
|
543
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
544
|
+
if (!executionMode || !event.isError || !ctx.hasUI || todoItems.length === 0) return;
|
|
545
|
+
|
|
546
|
+
const currentStep = getCurrentStep();
|
|
547
|
+
if (!currentStep) return;
|
|
548
|
+
|
|
549
|
+
const errorText = Array.isArray(event.content)
|
|
550
|
+
? event.content
|
|
551
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
552
|
+
.map((block) => block.text)
|
|
553
|
+
.join("\n")
|
|
554
|
+
.trim()
|
|
555
|
+
: "";
|
|
556
|
+
|
|
557
|
+
const shouldProvideGuidance = await ctx.ui.confirm(
|
|
558
|
+
"Plan step blocked",
|
|
559
|
+
[
|
|
560
|
+
`Tool "${event.toolName}" was blocked while executing step ${currentStep.step}.`,
|
|
561
|
+
"",
|
|
562
|
+
currentStep.text,
|
|
563
|
+
"",
|
|
564
|
+
`Error: ${errorText || "No error details provided."}`,
|
|
565
|
+
"",
|
|
566
|
+
"Provide guidance before execution continues?",
|
|
567
|
+
].join("\n")
|
|
568
|
+
);
|
|
569
|
+
if (!shouldProvideGuidance) return;
|
|
570
|
+
|
|
571
|
+
const guidance = await ctx.ui.editor(`Guidance for step ${currentStep.step}:`, "");
|
|
572
|
+
const trimmedGuidance = guidance?.trim();
|
|
573
|
+
if (!trimmedGuidance) return;
|
|
574
|
+
|
|
575
|
+
pi.sendUserMessage(
|
|
576
|
+
[
|
|
577
|
+
`[PLAN GUIDANCE — Step ${currentStep.step}: ${currentStep.text}]`,
|
|
578
|
+
`Tool "${event.toolName}" was blocked.`,
|
|
579
|
+
"",
|
|
580
|
+
"User guidance:",
|
|
581
|
+
trimmedGuidance,
|
|
582
|
+
].join("\n"),
|
|
583
|
+
{ deliverAs: "steer" }
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
488
587
|
// Handle plan completion and plan mode UI
|
|
489
588
|
pi.on("agent_end", async (event, ctx) => {
|
|
490
589
|
// Check if execution is complete
|
|
@@ -501,6 +600,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
501
600
|
);
|
|
502
601
|
executionMode = false;
|
|
503
602
|
todoItems = [];
|
|
603
|
+
currentStepIndex = null;
|
|
504
604
|
restoreNormalModeTools();
|
|
505
605
|
updateStatus(ctx);
|
|
506
606
|
persistState(); // Save cleared state so resume doesn't restore old execution mode
|
|
@@ -543,12 +643,19 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
543
643
|
if (choice?.startsWith("Execute")) {
|
|
544
644
|
planModeEnabled = false;
|
|
545
645
|
executionMode = todoItems.length > 0;
|
|
646
|
+
if (executionMode) {
|
|
647
|
+
syncCurrentStepIndex();
|
|
648
|
+
} else {
|
|
649
|
+
currentStepIndex = null;
|
|
650
|
+
}
|
|
546
651
|
restoreNormalModeTools();
|
|
547
652
|
updateStatus(ctx);
|
|
653
|
+
persistState();
|
|
548
654
|
|
|
655
|
+
const currentStep = getCurrentStep();
|
|
549
656
|
const execMessage =
|
|
550
|
-
|
|
551
|
-
? `Execute the plan. Start with: ${
|
|
657
|
+
currentStep !== null
|
|
658
|
+
? `Execute the plan. Start with step ${currentStep.step}: ${currentStep.text}`
|
|
552
659
|
: "Execute the plan you just created.";
|
|
553
660
|
pi.sendMessage(
|
|
554
661
|
{ customType: "plan-mode-execute", content: execMessage, display: true },
|
|
@@ -587,6 +694,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
587
694
|
executing?: boolean;
|
|
588
695
|
normalTools?: string[];
|
|
589
696
|
todos?: TodoItem[];
|
|
697
|
+
currentStepIndex?: number | null;
|
|
590
698
|
};
|
|
591
699
|
}
|
|
592
700
|
| undefined;
|
|
@@ -596,6 +704,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
596
704
|
executionMode = planModeEntry.data.executing ?? executionMode;
|
|
597
705
|
normalModeTools = planModeEntry.data.normalTools ?? normalModeTools;
|
|
598
706
|
todoItems = planModeEntry.data.todos ?? todoItems;
|
|
707
|
+
currentStepIndex = planModeEntry.data.currentStepIndex ?? currentStepIndex;
|
|
599
708
|
}
|
|
600
709
|
|
|
601
710
|
// On resume: re-scan messages to rebuild completion state
|
|
@@ -628,6 +737,12 @@ After completing a step, include a [DONE:n] tag in your response.`,
|
|
|
628
737
|
markCompletedSteps(allText, todoItems);
|
|
629
738
|
}
|
|
630
739
|
|
|
740
|
+
if (executionMode && todoItems.length > 0) {
|
|
741
|
+
syncCurrentStepIndex();
|
|
742
|
+
} else {
|
|
743
|
+
currentStepIndex = null;
|
|
744
|
+
}
|
|
745
|
+
|
|
631
746
|
if (normalModeTools.length === 0) {
|
|
632
747
|
captureNormalModeTools();
|
|
633
748
|
}
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
15
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
18
16
|
import { completeSimple } from "@mariozechner/pi-ai";
|
|
19
17
|
import {
|
|
@@ -22,6 +20,7 @@ import {
|
|
|
22
20
|
type ExtensionContext,
|
|
23
21
|
} from "@mariozechner/pi-coding-agent";
|
|
24
22
|
import type { EditorTheme } from "@mariozechner/pi-tui";
|
|
23
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
25
24
|
import { AutocompleteEngine, type ConversationContext } from "./autocomplete.js";
|
|
26
25
|
import { GENERAL_TEMPLATES, type PromptTemplate } from "./templates.js";
|
|
27
26
|
|
|
@@ -30,7 +29,7 @@ import { GENERAL_TEMPLATES, type PromptTemplate } from "./templates.js";
|
|
|
30
29
|
/** Read a setting from ~/.tallow/settings.json with a typed fallback. */
|
|
31
30
|
function readSetting<T>(key: string, fallback: T): T {
|
|
32
31
|
try {
|
|
33
|
-
const raw = readFileSync(
|
|
32
|
+
const raw = readFileSync(getTallowSettingsPath(), "utf-8");
|
|
34
33
|
const settings = JSON.parse(raw);
|
|
35
34
|
return key in settings ? (settings[key] as T) : fallback;
|
|
36
35
|
} catch {
|
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
|
-
import * as os from "node:os";
|
|
16
|
-
import * as path from "node:path";
|
|
17
15
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
18
16
|
import { Loader } from "@mariozechner/pi-tui";
|
|
17
|
+
import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
|
|
19
18
|
|
|
20
19
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
21
20
|
|
|
@@ -599,7 +598,7 @@ function resolve(name: string): SpinnerPreset | undefined {
|
|
|
599
598
|
* @returns Parsed spinner settings with defaults
|
|
600
599
|
*/
|
|
601
600
|
function readSettings(): SpinnerSettings {
|
|
602
|
-
const settingsPath =
|
|
601
|
+
const settingsPath = getTallowSettingsPath();
|
|
603
602
|
try {
|
|
604
603
|
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
605
604
|
return JSON.parse(raw) as SpinnerSettings;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
7
|
+
import readSummary from "../index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a small notebook fixture for end-to-end read-tool execution.
|
|
11
|
+
*
|
|
12
|
+
* @returns Serialized notebook JSON string
|
|
13
|
+
*/
|
|
14
|
+
function createNotebookFixture(): string {
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
metadata: {
|
|
17
|
+
kernelspec: {
|
|
18
|
+
language: "python",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
cells: [
|
|
22
|
+
{
|
|
23
|
+
cell_type: "markdown",
|
|
24
|
+
source: ["# Header\n", "Notebook body\n"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
cell_type: "code",
|
|
28
|
+
execution_count: 1,
|
|
29
|
+
source: ["print('ok')\n"],
|
|
30
|
+
outputs: [
|
|
31
|
+
{
|
|
32
|
+
output_type: "stream",
|
|
33
|
+
name: "stdout",
|
|
34
|
+
text: ["ok\n"],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a minimal extension context stub for tool execution.
|
|
44
|
+
*
|
|
45
|
+
* @param cwd - Working directory for tool execution
|
|
46
|
+
* @returns Partial context cast to ExtensionContext
|
|
47
|
+
*/
|
|
48
|
+
function stubContext(cwd: string): ExtensionContext {
|
|
49
|
+
return {
|
|
50
|
+
hasUI: false,
|
|
51
|
+
ui: { setWorkingMessage() {} },
|
|
52
|
+
cwd,
|
|
53
|
+
} as unknown as ExtensionContext;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let notebookPath: string;
|
|
57
|
+
let tmpDir: string;
|
|
58
|
+
|
|
59
|
+
beforeAll(() => {
|
|
60
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "notebook-read-test-"));
|
|
61
|
+
notebookPath = path.join(tmpDir, "sample.ipynb");
|
|
62
|
+
fs.writeFileSync(notebookPath, createNotebookFixture(), "utf-8");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(() => {
|
|
66
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("read tool notebook integration", () => {
|
|
70
|
+
test("reads notebook with summarized result and full text details", async () => {
|
|
71
|
+
const harness = ExtensionHarness.create();
|
|
72
|
+
await harness.loadExtension(readSummary);
|
|
73
|
+
|
|
74
|
+
const tool = harness.tools.get("read");
|
|
75
|
+
expect(tool).toBeDefined();
|
|
76
|
+
if (!tool) return;
|
|
77
|
+
|
|
78
|
+
const updates: Array<{ details?: Record<string, unknown> }> = [];
|
|
79
|
+
const result = await tool.execute(
|
|
80
|
+
"test-id",
|
|
81
|
+
{ path: notebookPath },
|
|
82
|
+
new AbortController().signal,
|
|
83
|
+
(partial) => {
|
|
84
|
+
updates.push(partial as { details?: Record<string, unknown> });
|
|
85
|
+
},
|
|
86
|
+
stubContext(tmpDir)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const summary = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
90
|
+
const details = result.details as Record<string, unknown>;
|
|
91
|
+
|
|
92
|
+
expect(summary).toContain("sample.ipynb (2 cells");
|
|
93
|
+
expect(details.__notebook_read__).toBe(true);
|
|
94
|
+
expect(details._cellCount).toBe(2);
|
|
95
|
+
expect(typeof details._fullText).toBe("string");
|
|
96
|
+
expect(String(details._fullText)).toContain("Cell 1 [markdown]");
|
|
97
|
+
expect(String(details._fullText)).toContain("```python");
|
|
98
|
+
expect(updates.some((update) => Boolean(update.details?._preview))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatNotebookOutput,
|
|
4
|
+
isNotebook,
|
|
5
|
+
NotebookParseError,
|
|
6
|
+
parseNotebook,
|
|
7
|
+
summarizeNotebookCounts,
|
|
8
|
+
} from "../notebook.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a representative notebook fixture with markdown, code, and rich outputs.
|
|
12
|
+
*
|
|
13
|
+
* @returns Serialized notebook JSON string
|
|
14
|
+
*/
|
|
15
|
+
function createSampleNotebook(): string {
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
metadata: {
|
|
18
|
+
kernelspec: {
|
|
19
|
+
language: "python",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
cells: [
|
|
23
|
+
{
|
|
24
|
+
cell_type: "markdown",
|
|
25
|
+
source: ["# Notebook title\n", "Some markdown context\n"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
cell_type: "code",
|
|
29
|
+
execution_count: 5,
|
|
30
|
+
source: ["print('hello')\n"],
|
|
31
|
+
outputs: [
|
|
32
|
+
{
|
|
33
|
+
output_type: "stream",
|
|
34
|
+
name: "stdout",
|
|
35
|
+
text: ["hello\n"],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
cell_type: "code",
|
|
41
|
+
execution_count: 6,
|
|
42
|
+
source: ["1 / 0\n"],
|
|
43
|
+
outputs: [
|
|
44
|
+
{
|
|
45
|
+
output_type: "error",
|
|
46
|
+
ename: "ZeroDivisionError",
|
|
47
|
+
evalue: "division by zero",
|
|
48
|
+
traceback: [
|
|
49
|
+
"Traceback (most recent call last):",
|
|
50
|
+
"ZeroDivisionError: division by zero",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
cell_type: "code",
|
|
57
|
+
execution_count: 7,
|
|
58
|
+
source: ["plot()\n"],
|
|
59
|
+
outputs: [
|
|
60
|
+
{
|
|
61
|
+
output_type: "display_data",
|
|
62
|
+
data: {
|
|
63
|
+
"image/png": "iVBORw0KGgoAAAANSUhEUgAA",
|
|
64
|
+
"text/html": "<img src='plot.png' />",
|
|
65
|
+
},
|
|
66
|
+
metadata: {
|
|
67
|
+
"image/png": {
|
|
68
|
+
height: 600,
|
|
69
|
+
width: 800,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("isNotebook", () => {
|
|
80
|
+
test("detects .ipynb path", () => {
|
|
81
|
+
expect(isNotebook("/tmp/analysis.ipynb")).toBe(true);
|
|
82
|
+
expect(isNotebook("/tmp/analysis.IPYNB")).toBe(true);
|
|
83
|
+
expect(isNotebook("/tmp/script.py")).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("parseNotebook", () => {
|
|
88
|
+
test("parses notebook cells and language", () => {
|
|
89
|
+
const parsed = parseNotebook(createSampleNotebook());
|
|
90
|
+
expect(parsed.language).toBe("python");
|
|
91
|
+
expect(parsed.cells).toHaveLength(4);
|
|
92
|
+
expect(parsed.cells[0]?.cell_type).toBe("markdown");
|
|
93
|
+
expect(parsed.cells[1]?.cell_type).toBe("code");
|
|
94
|
+
expect(parsed.cells[1]?.execution_count).toBe(5);
|
|
95
|
+
expect(parsed.cells[1]?.outputs?.[0]?.output_type).toBe("stream");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("throws NotebookParseError for invalid JSON", () => {
|
|
99
|
+
expect(() => parseNotebook("not-json")).toThrow(NotebookParseError);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("throws NotebookParseError when cells are missing", () => {
|
|
103
|
+
expect(() => parseNotebook(JSON.stringify({ metadata: {} }))).toThrow("missing cells array");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("formatNotebookOutput", () => {
|
|
108
|
+
test("formats code cells, outputs, errors, and image placeholders", () => {
|
|
109
|
+
const parsed = parseNotebook(createSampleNotebook());
|
|
110
|
+
const output = formatNotebookOutput(parsed);
|
|
111
|
+
|
|
112
|
+
expect(output).toContain("[Notebook: python | 4 cells");
|
|
113
|
+
expect(output).toContain("Cell 2 [code]");
|
|
114
|
+
expect(output).toContain("# [5]");
|
|
115
|
+
expect(output).toContain("```python");
|
|
116
|
+
expect(output).toContain("[Output 1: stream]");
|
|
117
|
+
expect(output).toContain("hello");
|
|
118
|
+
expect(output).toContain("[Output 1: error]");
|
|
119
|
+
expect(output).toContain("ZeroDivisionError: division by zero");
|
|
120
|
+
expect(output).toContain("[Image output: PNG 800x600]");
|
|
121
|
+
expect(output).toContain("[HTML output]");
|
|
122
|
+
expect(output).not.toContain("iVBORw0KGgoAAAANSUhEUgAA");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("summarizeNotebookCounts", () => {
|
|
127
|
+
test("returns code/markdown/raw/output totals", () => {
|
|
128
|
+
const parsed = parseNotebook(createSampleNotebook());
|
|
129
|
+
const counts = summarizeNotebookCounts(parsed);
|
|
130
|
+
|
|
131
|
+
expect(counts.codeCells).toBe(3);
|
|
132
|
+
expect(counts.markdownCells).toBe(1);
|
|
133
|
+
expect(counts.rawCells).toBe(0);
|
|
134
|
+
expect(counts.outputCount).toBe(3);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "read-tool-enhanced",
|
|
3
3
|
"version": "0.3.0",
|
|
4
|
-
"description": "Enhanced read tool with live truncated preview
|
|
5
|
-
"whenToUse": "Use when you need improved `read` previews, truncation, and PDF extraction.",
|
|
4
|
+
"description": "Enhanced read tool with live truncated preview plus PDF and notebook extraction",
|
|
5
|
+
"whenToUse": "Use when you need improved `read` previews, truncation, and PDF/notebook extraction.",
|
|
6
6
|
"capabilities": {
|
|
7
7
|
"tools": ["read"],
|
|
8
8
|
"events": ["context", "session_start"]
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"subprocess": false
|
|
15
15
|
},
|
|
16
16
|
"category": "utility",
|
|
17
|
-
"tags": ["context", "optimization", "display", "pdf"],
|
|
18
|
-
"files": ["index.ts", "pdf.ts"],
|
|
17
|
+
"tags": ["context", "optimization", "display", "pdf", "notebook"],
|
|
18
|
+
"files": ["index.ts", "notebook.ts", "pdf.ts"],
|
|
19
19
|
"relationships": [
|
|
20
20
|
{
|
|
21
21
|
"name": "tool-display",
|