mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
refactor(hooks): split session events into individual typed events
Major changes: - Replace monolithic SessionEvent with reason discriminator with individual event types: session_start, session_before_switch, session_switch, session_before_new, session_new, session_before_branch, session_branch, session_before_compact, session_compact, session_shutdown - Each event has dedicated result type (SessionBeforeSwitchResult, etc.) - HookHandler type now allows bare return statements (void in return type) - HookAPI.on() has proper overloads for each event with correct typing Additional fixes: - AgentSession now always subscribes to agent in constructor (was only subscribing when external subscribe() called, breaking internal handlers) - Standardize on undefined over null throughout codebase - HookUIContext methods return undefined instead of null - SessionManager methods return undefined instead of null - Simplify hook exports to 'export type * from types.js' - Add detailed JSDoc for skipConversationRestore vs cancel - Fix createBranchedSession to rebuild index in persist mode - newSession() now returns the session file path Updated all example hooks, tests, and emission sites to use new event types.
This commit is contained in:
parent
38d65dfe59
commit
d6283f99dc
43 changed files with 2129 additions and 640 deletions
|
|
@ -41,7 +41,7 @@ const factory: CustomToolFactory = (pi) => {
|
|||
|
||||
const answer = await pi.ui.select(params.question, params.options);
|
||||
|
||||
if (answer === null) {
|
||||
if (answer === undefined) {
|
||||
return {
|
||||
content: [{ type: "text", text: "User cancelled the selection" }],
|
||||
details: { question: params.question, options: params.options, answer: null },
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "shutdown") return;
|
||||
|
||||
pi.on("session_shutdown", async (_event, ctx) => {
|
||||
// Check for uncommitted changes
|
||||
const { stdout: status, code } = await pi.exec("git", ["status", "--porcelain"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,63 +2,57 @@
|
|||
* Confirm Destructive Actions Hook
|
||||
*
|
||||
* Prompts for confirmation before destructive session actions (clear, switch, branch).
|
||||
* Demonstrates how to cancel session events using the before_* variants.
|
||||
* Demonstrates how to cancel session events using the before_* events.
|
||||
*/
|
||||
|
||||
import type { SessionMessageEntry } from "@mariozechner/pi-coding-agent";
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
// Only handle before_* events (the ones that can be cancelled)
|
||||
if (event.reason === "before_new") {
|
||||
if (!ctx.hasUI) return;
|
||||
pi.on("session_before_new", async (_event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const confirmed = await ctx.ui.confirm("Clear session?", "This will delete all messages in the current session.");
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Clear cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_switch", async (_event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
// Check if there are unsaved changes (messages since last assistant response)
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const hasUnsavedWork = entries.some(
|
||||
(e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
|
||||
);
|
||||
|
||||
if (hasUnsavedWork) {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Clear session?",
|
||||
"This will delete all messages in the current session.",
|
||||
"Switch session?",
|
||||
"You have messages in the current session. Switch anyway?",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Clear cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (event.reason === "before_switch") {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
// Check if there are unsaved changes (messages since last assistant response)
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const hasUnsavedWork = entries.some(
|
||||
(e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
|
||||
);
|
||||
|
||||
if (hasUnsavedWork) {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Switch session?",
|
||||
"You have messages in the current session. Switch anyway?",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Switch cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.reason === "before_branch") {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const choice = await ctx.ui.select(`Branch from turn ${event.targetTurnIndex}?`, [
|
||||
"Yes, create branch",
|
||||
"No, stay in current session",
|
||||
]);
|
||||
|
||||
if (choice !== "Yes, create branch") {
|
||||
ctx.ui.notify("Branch cancelled", "info");
|
||||
ctx.ui.notify("Switch cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_branch", async (event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const choice = await ctx.ui.select(`Branch from turn ${event.entryIndex}?`, [
|
||||
"Yes, create branch",
|
||||
"No, stay in current session",
|
||||
]);
|
||||
|
||||
if (choice !== "Yes, create branch") {
|
||||
ctx.ui.notify("Branch cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ import { convertToLlm } from "@mariozechner/pi-coding-agent";
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "before_compact") return;
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
ctx.ui.notify("Custom compaction hook triggered", "info");
|
||||
|
||||
const { preparation, previousCompactions, signal } = event;
|
||||
|
|
|
|||
|
|
@ -5,47 +5,55 @@
|
|||
* Useful to ensure work is committed before switching context.
|
||||
*/
|
||||
|
||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
import type { HookAPI, HookEventContext } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
async function checkDirtyRepo(
|
||||
pi: HookAPI,
|
||||
ctx: HookEventContext,
|
||||
action: string,
|
||||
): Promise<{ cancel: boolean } | undefined> {
|
||||
// Check for uncommitted changes
|
||||
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
||||
|
||||
if (code !== 0) {
|
||||
// Not a git repo, allow the action
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = stdout.trim().length > 0;
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.hasUI) {
|
||||
// In non-interactive mode, block by default
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
// Count changed files
|
||||
const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
|
||||
|
||||
const choice = await ctx.ui.select(`You have ${changedFiles} uncommitted file(s). ${action} anyway?`, [
|
||||
"Yes, proceed anyway",
|
||||
"No, let me commit first",
|
||||
]);
|
||||
|
||||
if (choice !== "Yes, proceed anyway") {
|
||||
ctx.ui.notify("Commit your changes first", "warning");
|
||||
return { cancel: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
// Only guard destructive actions
|
||||
if (event.reason !== "before_new" && event.reason !== "before_switch" && event.reason !== "before_branch") {
|
||||
return;
|
||||
}
|
||||
pi.on("session_before_new", async (_event, ctx) => {
|
||||
return checkDirtyRepo(pi, ctx, "new session");
|
||||
});
|
||||
|
||||
// Check for uncommitted changes
|
||||
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
||||
pi.on("session_before_switch", async (_event, ctx) => {
|
||||
return checkDirtyRepo(pi, ctx, "switch session");
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
// Not a git repo, allow the action
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = stdout.trim().length > 0;
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.hasUI) {
|
||||
// In non-interactive mode, block by default
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
// Count changed files
|
||||
const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
|
||||
|
||||
const action =
|
||||
event.reason === "before_new" ? "new session" : event.reason === "before_switch" ? "switch session" : "branch";
|
||||
|
||||
const choice = await ctx.ui.select(`You have ${changedFiles} uncommitted file(s). ${action} anyway?`, [
|
||||
"Yes, proceed anyway",
|
||||
"No, let me commit first",
|
||||
]);
|
||||
|
||||
if (choice !== "Yes, proceed anyway") {
|
||||
ctx.ui.notify("Commit your changes first", "warning");
|
||||
return { cancel: true };
|
||||
}
|
||||
pi.on("session_before_branch", async (_event, ctx) => {
|
||||
return checkDirtyRepo(pi, ctx, "branch");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import * as fs from "node:fs";
|
|||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
||||
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "start") return;
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const triggerFile = "/tmp/agent-trigger.txt";
|
||||
|
||||
fs.watch(triggerFile, () => {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,8 @@ export default function (pi: HookAPI) {
|
|||
}
|
||||
});
|
||||
|
||||
pi.on("session", async (event, ctx) => {
|
||||
// Only handle before_branch events
|
||||
if (event.reason !== "before_branch") return;
|
||||
|
||||
const ref = checkpoints.get(event.targetTurnIndex);
|
||||
pi.on("session_before_branch", async (event, ctx) => {
|
||||
const ref = checkpoints.get(event.entryIndex);
|
||||
if (!ref) return;
|
||||
|
||||
if (!ctx.hasUI) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,496 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session Export - 2025-12-28T17-08-16-800Z_d1555962-63f5-402c-b6e5-24fa5d528fb2.jsonl</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgb(229, 229, 231);
|
||||
background: rgb(24, 24, 30);
|
||||
padding: 24px;
|
||||
}
|
||||
.container { max-width: 700px; margin: 0 auto; }
|
||||
.header {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: rgb(30, 30, 36);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
color: rgb(0, 215, 255);
|
||||
}
|
||||
.header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }
|
||||
.info-item { color: rgb(102, 102, 102); display: flex; align-items: baseline; }
|
||||
.info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }
|
||||
.info-value { color: rgb(229, 229, 231); flex: 1; }
|
||||
.info-value.cost { font-family: 'SF Mono', monospace; }
|
||||
.messages { display: flex; flex-direction: column; gap: 16px; }
|
||||
.message-timestamp { font-size: 10px; color: rgb(102, 102, 102); margin-bottom: 4px; opacity: 0.8; }
|
||||
.user-message {
|
||||
background: rgb(52, 53, 65);
|
||||
color: rgb(229, 229, 231);
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.assistant-message { padding: 0; }
|
||||
.assistant-text, .thinking-text {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.thinking-text { color: rgb(102, 102, 102); font-style: italic; white-space: pre-wrap; }
|
||||
.model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }
|
||||
.model-change-text { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.model-name { color: rgb(0, 215, 255); font-weight: bold; }
|
||||
.compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }
|
||||
.compaction-header { padding: 12px 16px; cursor: pointer; }
|
||||
.compaction-header:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.compaction-header-row { display: flex; align-items: center; gap: 8px; }
|
||||
.compaction-toggle { color: rgb(0, 215, 255); font-size: 10px; transition: transform 0.2s; }
|
||||
.compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }
|
||||
.compaction-title { color: rgb(229, 229, 231); font-weight: bold; }
|
||||
.compaction-hint { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.compaction-content { display: none; padding: 0 16px 16px 16px; }
|
||||
.compaction-container.expanded .compaction-content { display: block; }
|
||||
.compaction-summary { background: rgba(0, 0, 0, 0.1); border-radius: 4px; padding: 12px; }
|
||||
.compaction-summary-header { font-weight: bold; color: rgb(0, 215, 255); margin-bottom: 8px; font-size: 11px; }
|
||||
.compaction-summary-content { color: rgb(229, 229, 231); white-space: pre-wrap; word-wrap: break-word; }
|
||||
.tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }
|
||||
.tool-execution.user-bash { background: rgb(50, 48, 35); }
|
||||
.tool-execution.user-bash-error { background: rgb(60, 45, 35); }
|
||||
.tool-header, .tool-name { font-weight: bold; }
|
||||
.tool-path { color: rgb(0, 215, 255); word-break: break-all; }
|
||||
.line-numbers { color: rgb(255, 255, 0); }
|
||||
.line-count { color: rgb(102, 102, 102); }
|
||||
.tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.tool-output {
|
||||
margin-top: 12px;
|
||||
color: rgb(128, 128, 128);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tool-output > div { line-height: 1.4; }
|
||||
.tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.tool-output.expandable { cursor: pointer; }
|
||||
.tool-output.expandable:hover { opacity: 0.9; }
|
||||
.tool-output.expandable .output-full { display: none; }
|
||||
.tool-output.expandable.expanded .output-preview { display: none; }
|
||||
.tool-output.expandable.expanded .output-full { display: block; }
|
||||
.expand-hint { color: rgb(0, 215, 255); font-style: italic; margin-top: 4px; }
|
||||
.system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }
|
||||
.system-prompt-header, .tools-header { font-weight: bold; color: rgb(255, 255, 0); margin-bottom: 8px; }
|
||||
.system-prompt-content, .tools-content { color: rgb(102, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }
|
||||
.tool-item { margin: 4px 0; }
|
||||
.tool-item-name { font-weight: bold; color: rgb(229, 229, 231); }
|
||||
.tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }
|
||||
.diff-line-old { color: rgb(204, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-new { color: rgb(181, 189, 104); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-context { color: rgb(128, 128, 128); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.error-text { color: rgb(204, 102, 102); padding: 12px 16px; }
|
||||
.bash-status.warning { color: rgb(255, 255, 0); }
|
||||
.bash-status.error { color: rgb(204, 102, 102); }
|
||||
.bash-truncation.warning { color: rgb(255, 255, 0); }
|
||||
.footer { margin-top: 48px; padding: 20px; text-align: center; color: rgb(102, 102, 102); font-size: 10px; }
|
||||
.streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: rgb(102, 102, 102); font-size: 11px; }
|
||||
|
||||
/* Image styles */
|
||||
.message-images { margin-bottom: 12px; }
|
||||
.message-image { max-width: 100%; max-height: 400px; border-radius: 4px; margin: 4px 0; }
|
||||
|
||||
/* Markdown styles */
|
||||
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||
color: rgb(240, 198, 116);
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content h1 { font-size: 1.4em; text-decoration: underline; }
|
||||
.markdown-content h2 { font-size: 1.2em; }
|
||||
.markdown-content h3 { font-size: 1.1em; }
|
||||
.markdown-content p { margin: 0.5em 0; }
|
||||
.markdown-content a { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
.markdown-content a:hover { opacity: 0.8; }
|
||||
.markdown-content code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgb(138, 190, 183);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0.5em 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.markdown-content pre code {
|
||||
display: block;
|
||||
background: none;
|
||||
color: rgb(181, 189, 104);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.code-block-wrapper {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.code-block-header {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.code-block-footer {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.markdown-content blockquote {
|
||||
border-left: 3px solid rgb(128, 128, 128);
|
||||
padding-left: 12px;
|
||||
margin: 0.5em 0;
|
||||
color: rgb(128, 128, 128);
|
||||
font-style: italic;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.markdown-content li { margin: 0.25em 0; }
|
||||
.markdown-content li::marker { color: rgb(138, 190, 183); }
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(128, 128, 128);
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
margin: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown-content th, .markdown-content td {
|
||||
border: 1px solid rgb(128, 128, 128);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-content th {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Syntax highlighting (highlight.js) */
|
||||
.hljs { background: transparent; }
|
||||
.hljs-comment, .hljs-quote { color: rgb(106, 153, 85); }
|
||||
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: rgb(86, 156, 214); }
|
||||
.hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: rgb(181, 206, 168); }
|
||||
.hljs-string, .hljs-doctag, .hljs-regexp { color: rgb(206, 145, 120); }
|
||||
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: rgb(220, 220, 170); }
|
||||
.hljs-type, .hljs-class, .hljs-built_in { color: rgb(78, 201, 176); }
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-params { color: rgb(156, 220, 254); }
|
||||
.hljs-attribute { color: rgb(156, 220, 254); }
|
||||
.hljs-meta { color: rgb(86, 156, 214); }
|
||||
.hljs-formula { background: rgba(255, 255, 255, 0.05); }
|
||||
.hljs-deletion { color: rgb(204, 102, 102); }
|
||||
.hljs-emphasis { font-style: italic; }
|
||||
.hljs-strong { font-weight: bold; }
|
||||
.hljs-link { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
|
||||
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>pi v0.30.2</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Session:</span><span class="info-value">d1555962-63f5-402c-b6e5-24fa5d528fb2</span></div>
|
||||
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">12/28/2025, 6:08:16 PM</span></div>
|
||||
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">unknown</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Messages</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">User:</span><span class="info-value">1</span></div>
|
||||
<div class="info-item"><span class="info-label">Assistant:</span><span class="info-value">2</span></div>
|
||||
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Tokens & Cost</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Input:</span><span class="info-value">9 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Output:</span><span class="info-value">268 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read:</span><span class="info-value">2,462 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write:</span><span class="info-value">3,505 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Total:</span><span class="info-value">6,244 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Input Cost:</span><span class="info-value cost">$0.0000</span></div>
|
||||
<div class="info-item"><span class="info-label">Output Cost:</span><span class="info-value cost">$0.0040</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read Cost:</span><span class="info-value cost">$0.0007</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write Cost:</span><span class="info-value cost">$0.0131</span></div>
|
||||
<div class="info-item"><span class="info-label">Total Cost:</span><span class="info-value cost"><strong>$0.0179</strong></span></div>
|
||||
<div class="info-item"><span class="info-label">Context Usage:</span><span class="info-value">3,647 / 200,000 tokens (1.8%) - anthropic/claude-sonnet-4-5</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="system-prompt">
|
||||
<div class="system-prompt-header">System Prompt</div>
|
||||
<div class="system-prompt-content">You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
|
||||
|
||||
Available tools:
|
||||
- read: Read file contents
|
||||
- bash: Execute bash commands (ls, grep, find, etc.)
|
||||
- edit: Make surgical edits to files (find exact text and replace)
|
||||
- write: Create or overwrite files
|
||||
|
||||
Guidelines:
|
||||
- Use bash for file operations like ls, grep, find
|
||||
- Use read to examine files before editing. You must use this tool instead of cat or sed.
|
||||
- Use edit for precise changes (old text must match exactly)
|
||||
- Use write only for new files or complete rewrites
|
||||
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||
- Be concise in your responses
|
||||
- Show file paths clearly when working with files
|
||||
|
||||
Documentation:
|
||||
- Main documentation: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/README.md
|
||||
- Additional docs: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs
|
||||
- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)
|
||||
|
||||
# Project Context
|
||||
|
||||
The following project context files have been loaded:
|
||||
|
||||
## /Users/badlogic/workspaces/pi-mono/AGENTS.md
|
||||
|
||||
# Development Rules
|
||||
|
||||
## First Message
|
||||
If the user did not give you a concrete task in their first message,
|
||||
read README.md, then ask which module(s) to work on. Based on the answer, read the relevant README.md files in parallel.
|
||||
- packages/ai/README.md
|
||||
- packages/tui/README.md
|
||||
- packages/agent/README.md
|
||||
- packages/coding-agent/README.md
|
||||
- packages/mom/README.md
|
||||
- packages/pods/README.md
|
||||
- packages/web-ui/README.md
|
||||
|
||||
## Code Quality
|
||||
- No `any` types unless absolutely necessary
|
||||
- Check node_modules for external API type definitions instead of guessing
|
||||
- **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports.
|
||||
- NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead
|
||||
- Always ask before removing functionality or code that appears to be intentional
|
||||
|
||||
## Commands
|
||||
- After code changes: `npm run check` (get full output, no tail)
|
||||
- NEVER run: `npm run dev`, `npm run build`, `npm test`
|
||||
- Only run specific tests if user instructs: `npm test -- test/specific.test.ts`
|
||||
- NEVER commit unless user asks
|
||||
|
||||
## GitHub Issues
|
||||
When reading issues:
|
||||
- Always read all comments on the issue
|
||||
|
||||
When creating issues:
|
||||
- Add `pkg:*` labels to indicate which package(s) the issue affects
|
||||
- Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:mom`, `pkg:pods`, `pkg:tui`, `pkg:web-ui`
|
||||
- If an issue spans multiple packages, add all relevant labels
|
||||
|
||||
When closing issues via commit:
|
||||
- Include `fixes #<number>` or `closes #<number>` in the commit message
|
||||
- This automatically closes the issue when the commit is merged
|
||||
|
||||
## Tools
|
||||
- GitHub CLI for issues/PRs
|
||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
||||
- TUI interaction: use tmux
|
||||
|
||||
## Style
|
||||
- Keep answers short and concise
|
||||
- No emojis in commits, issues, PR comments, or code
|
||||
- No fluff or cheerful filler text
|
||||
- Technical prose only, be kind but direct (e.g., "Thanks @user" not "Thanks so much @user!")
|
||||
|
||||
## Changelog
|
||||
Location: `packages/*/CHANGELOG.md` (each package has its own)
|
||||
|
||||
### Format
|
||||
Use these sections under `## [Unreleased]`:
|
||||
- `### Breaking Changes` - API changes requiring migration
|
||||
- `### Added` - New features
|
||||
- `### Changed` - Changes to existing functionality
|
||||
- `### Fixed` - Bug fixes
|
||||
- `### Removed` - Removed features
|
||||
|
||||
### Rules
|
||||
- New entries ALWAYS go under `## [Unreleased]` section
|
||||
- NEVER modify already-released version sections (e.g., `## [0.12.2]`)
|
||||
- Each version section is immutable once released
|
||||
|
||||
### Attribution
|
||||
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/pi-mono/issues/123))`
|
||||
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@username](https://github.com/username))`
|
||||
|
||||
## Releasing
|
||||
|
||||
1. **Update CHANGELOGs**: Ensure all changes since last release are documented in the `[Unreleased]` section of each affected package's CHANGELOG.md
|
||||
|
||||
2. **Run release script**:
|
||||
```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```
|
||||
|
||||
The script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections.
|
||||
|
||||
### Tool Usage
|
||||
**CTRICIAL**: NEVER use sed/cat to read a file or a range of a file. Always use the read tool (use offset + limit for ranged reads).
|
||||
|
||||
|
||||
Current date and time: Sunday, December 28, 2025 at 06:08:16 PM GMT+1
|
||||
Current working directory: /Users/badlogic/workspaces/pi-mono/packages/coding-agent</div>
|
||||
</div>
|
||||
<div class="tools-list">
|
||||
<div class="tools-header">Available Tools</div>
|
||||
<div class="tools-content">
|
||||
<div class="tool-item"><span class="tool-item-name">read</span> - Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files.</div><div class="tool-item"><span class="tool-item-name">bash</span> - Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.</div><div class="tool-item"><span class="tool-item-name">edit</span> - Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.</div><div class="tool-item"><span class="tool-item-name">write</span> - Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="messages">
|
||||
<div class="user-message"><div class="message-timestamp">06:08:16 PM</div><div class="markdown-content"><p>Hello</p>
|
||||
</div></div><div class="assistant-message"><div class="message-timestamp">06:08:16 PM</div><div class="assistant-text markdown-content"><p>Hello! I'm Claude Code, here to help you with coding tasks in this pi-mono monorepo.</p>
|
||||
<p>Since you haven't given me a specific task yet, let me read the main README to understand the project better, then I can ask which module(s) you'd like to work on.</p>
|
||||
</div><div class="tool-execution" style="background-color: rgb(40, 50, 40)"><div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">~/workspaces/pi-mono/README.md</span></div><div class="tool-output expandable" onclick="this.classList.toggle('expanded')"><div class="output-preview"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |</code></pre><div class="expand-hint">... (78 more lines) - click to expand</div></div><div class="output-full"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-coding-agent</span>](<span class="hljs-link">packages/coding-agent</span>)**</span> | Interactive coding agent CLI |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-mom</span>](<span class="hljs-link">packages/mom</span>)**</span> | Slack bot that delegates messages to the pi coding agent |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-tui</span>](<span class="hljs-link">packages/tui</span>)**</span> | Terminal UI library with differential rendering |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-web-ui</span>](<span class="hljs-link">packages/web-ui</span>)**</span> | Web components for AI chat interfaces |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-pods</span>](<span class="hljs-link">packages/pods</span>)**</span> | CLI for managing vLLM deployments on GPU pods |
|
||||
|
||||
<span class="hljs-section">## Development</span>
|
||||
|
||||
<span class="hljs-section">### Setup</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm install # Install all dependencies
|
||||
npm run build # Build all packages
|
||||
npm run check # Lint, format, and type check
|
||||
```</span>
|
||||
|
||||
<span class="hljs-quote">> <span class="hljs-strong">**Note:**</span> `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.</span>
|
||||
|
||||
<span class="hljs-section">### CI</span>
|
||||
|
||||
GitHub Actions runs on push to <span class="hljs-code">`main`</span> and on pull requests. The workflow runs <span class="hljs-code">`npm run check`</span> and <span class="hljs-code">`npm run test`</span> for each package in parallel.
|
||||
|
||||
<span class="hljs-strong">**Do not add LLM API keys as secrets to this repository.**</span> Tests that require LLM access use <span class="hljs-code">`describe.skipIf()`</span> to skip when API keys are missing. This is intentional:
|
||||
|
||||
<span class="hljs-bullet">-</span> PRs from external contributors would have access to secrets in the CI environment
|
||||
<span class="hljs-bullet">-</span> Malicious PR code could exfiltrate API keys
|
||||
<span class="hljs-bullet">-</span> Tests that need LLM calls are skipped on CI and run locally by developers who have keys configured
|
||||
|
||||
If you need to run LLM-dependent tests, run them locally with your own API keys.
|
||||
|
||||
<span class="hljs-section">### Development</span>
|
||||
|
||||
Start watch builds for all packages:
|
||||
<span class="hljs-code">```bash
|
||||
npm run dev
|
||||
```</span>
|
||||
|
||||
Then run with tsx:
|
||||
<span class="hljs-code">```bash
|
||||
cd packages/coding-agent && npx tsx src/cli.ts
|
||||
cd packages/pods && npx tsx src/cli.ts
|
||||
```</span>
|
||||
|
||||
<span class="hljs-section">### Versioning (Lockstep)</span>
|
||||
|
||||
<span class="hljs-strong">**All packages MUST always have the same version number.**</span> Use these commands to bump versions:
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run version:patch # 0.7.5 -> 0.7.6
|
||||
npm run version:minor # 0.7.5 -> 0.8.0
|
||||
npm run version:major # 0.7.5 -> 1.0.0
|
||||
```</span>
|
||||
|
||||
These commands:
|
||||
<span class="hljs-bullet">1.</span> Update all package versions to the same number
|
||||
<span class="hljs-bullet">2.</span> Update inter-package dependency versions (e.g., <span class="hljs-code">`pi-agent`</span> depends on <span class="hljs-code">`pi-ai@^0.7.7`</span>)
|
||||
<span class="hljs-bullet">3.</span> Update <span class="hljs-code">`package-lock.json`</span>
|
||||
|
||||
<span class="hljs-strong">**Never manually edit version numbers.**</span> The lockstep system ensures consistency across the monorepo.
|
||||
|
||||
<span class="hljs-section">### Publishing</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```</span>
|
||||
|
||||
This handles version bump, CHANGELOG updates, commit, tag, publish, and push.
|
||||
|
||||
<span class="hljs-strong">**NPM Token Setup**</span>: Requires a granular access token with "Bypass 2FA on publish" enabled.
|
||||
<span class="hljs-bullet">-</span> Go to https://www.npmjs.com/settings/badlogic/tokens/
|
||||
<span class="hljs-bullet">-</span> Create a new "Granular Access Token" with "Bypass 2FA on publish"
|
||||
<span class="hljs-bullet">-</span> Set the token: <span class="hljs-code">`npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN`</span>
|
||||
|
||||
<span class="hljs-section">## License</span>
|
||||
|
||||
MIT</code></pre></div></div></div></div><div class="assistant-message"><div class="message-timestamp">06:08:20 PM</div><div class="assistant-text markdown-content"><p>Great! This is a monorepo with several packages for building AI agents and managing LLM deployments. </p>
|
||||
<p>Which module(s) would you like to work on?</p>
|
||||
<ul>
|
||||
<li><strong>ai</strong> - Unified multi-provider LLM API</li>
|
||||
<li><strong>agent</strong> - Agent runtime with tool calling and state management </li>
|
||||
<li><strong>coding-agent</strong> - Interactive coding agent CLI</li>
|
||||
<li><strong>mom</strong> - Slack bot that delegates to the coding agent</li>
|
||||
<li><strong>tui</strong> - Terminal UI library</li>
|
||||
<li><strong>web-ui</strong> - Web components for AI chat interfaces</li>
|
||||
<li><strong>pods</strong> - CLI for managing vLLM deployments</li>
|
||||
</ul>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Generated by pi coding-agent on 12/28/2025, 6:08:24 PM
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session Export - 2025-12-28T18-08-12-562Z_6646a9e5-779b-4f81-8d45-d8d32f99aa25.jsonl</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgb(229, 229, 231);
|
||||
background: rgb(24, 24, 30);
|
||||
padding: 24px;
|
||||
}
|
||||
.container { max-width: 700px; margin: 0 auto; }
|
||||
.header {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: rgb(30, 30, 36);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
color: rgb(0, 215, 255);
|
||||
}
|
||||
.header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }
|
||||
.info-item { color: rgb(102, 102, 102); display: flex; align-items: baseline; }
|
||||
.info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }
|
||||
.info-value { color: rgb(229, 229, 231); flex: 1; }
|
||||
.info-value.cost { font-family: 'SF Mono', monospace; }
|
||||
.messages { display: flex; flex-direction: column; gap: 16px; }
|
||||
.message-timestamp { font-size: 10px; color: rgb(102, 102, 102); margin-bottom: 4px; opacity: 0.8; }
|
||||
.user-message {
|
||||
background: rgb(52, 53, 65);
|
||||
color: rgb(229, 229, 231);
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.assistant-message { padding: 0; }
|
||||
.assistant-text, .thinking-text {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.thinking-text { color: rgb(102, 102, 102); font-style: italic; white-space: pre-wrap; }
|
||||
.model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }
|
||||
.model-change-text { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.model-name { color: rgb(0, 215, 255); font-weight: bold; }
|
||||
.compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }
|
||||
.compaction-header { padding: 12px 16px; cursor: pointer; }
|
||||
.compaction-header:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.compaction-header-row { display: flex; align-items: center; gap: 8px; }
|
||||
.compaction-toggle { color: rgb(0, 215, 255); font-size: 10px; transition: transform 0.2s; }
|
||||
.compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }
|
||||
.compaction-title { color: rgb(229, 229, 231); font-weight: bold; }
|
||||
.compaction-hint { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.compaction-content { display: none; padding: 0 16px 16px 16px; }
|
||||
.compaction-container.expanded .compaction-content { display: block; }
|
||||
.compaction-summary { background: rgba(0, 0, 0, 0.1); border-radius: 4px; padding: 12px; }
|
||||
.compaction-summary-header { font-weight: bold; color: rgb(0, 215, 255); margin-bottom: 8px; font-size: 11px; }
|
||||
.compaction-summary-content { color: rgb(229, 229, 231); white-space: pre-wrap; word-wrap: break-word; }
|
||||
.tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }
|
||||
.tool-execution.user-bash { background: rgb(50, 48, 35); }
|
||||
.tool-execution.user-bash-error { background: rgb(60, 45, 35); }
|
||||
.tool-header, .tool-name { font-weight: bold; }
|
||||
.tool-path { color: rgb(0, 215, 255); word-break: break-all; }
|
||||
.line-numbers { color: rgb(255, 255, 0); }
|
||||
.line-count { color: rgb(102, 102, 102); }
|
||||
.tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.tool-output {
|
||||
margin-top: 12px;
|
||||
color: rgb(128, 128, 128);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tool-output > div { line-height: 1.4; }
|
||||
.tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.tool-output.expandable { cursor: pointer; }
|
||||
.tool-output.expandable:hover { opacity: 0.9; }
|
||||
.tool-output.expandable .output-full { display: none; }
|
||||
.tool-output.expandable.expanded .output-preview { display: none; }
|
||||
.tool-output.expandable.expanded .output-full { display: block; }
|
||||
.expand-hint { color: rgb(0, 215, 255); font-style: italic; margin-top: 4px; }
|
||||
.system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }
|
||||
.system-prompt-header, .tools-header { font-weight: bold; color: rgb(255, 255, 0); margin-bottom: 8px; }
|
||||
.system-prompt-content, .tools-content { color: rgb(102, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }
|
||||
.tool-item { margin: 4px 0; }
|
||||
.tool-item-name { font-weight: bold; color: rgb(229, 229, 231); }
|
||||
.tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }
|
||||
.diff-line-old { color: rgb(204, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-new { color: rgb(181, 189, 104); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-context { color: rgb(128, 128, 128); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.error-text { color: rgb(204, 102, 102); padding: 12px 16px; }
|
||||
.bash-status.warning { color: rgb(255, 255, 0); }
|
||||
.bash-status.error { color: rgb(204, 102, 102); }
|
||||
.bash-truncation.warning { color: rgb(255, 255, 0); }
|
||||
.footer { margin-top: 48px; padding: 20px; text-align: center; color: rgb(102, 102, 102); font-size: 10px; }
|
||||
.streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: rgb(102, 102, 102); font-size: 11px; }
|
||||
|
||||
/* Image styles */
|
||||
.message-images { margin-bottom: 12px; }
|
||||
.message-image { max-width: 100%; max-height: 400px; border-radius: 4px; margin: 4px 0; }
|
||||
|
||||
/* Markdown styles */
|
||||
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||
color: rgb(240, 198, 116);
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content h1 { font-size: 1.4em; text-decoration: underline; }
|
||||
.markdown-content h2 { font-size: 1.2em; }
|
||||
.markdown-content h3 { font-size: 1.1em; }
|
||||
.markdown-content p { margin: 0.5em 0; }
|
||||
.markdown-content a { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
.markdown-content a:hover { opacity: 0.8; }
|
||||
.markdown-content code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgb(138, 190, 183);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0.5em 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.markdown-content pre code {
|
||||
display: block;
|
||||
background: none;
|
||||
color: rgb(181, 189, 104);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.code-block-wrapper {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.code-block-header {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.code-block-footer {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.markdown-content blockquote {
|
||||
border-left: 3px solid rgb(128, 128, 128);
|
||||
padding-left: 12px;
|
||||
margin: 0.5em 0;
|
||||
color: rgb(128, 128, 128);
|
||||
font-style: italic;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.markdown-content li { margin: 0.25em 0; }
|
||||
.markdown-content li::marker { color: rgb(138, 190, 183); }
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(128, 128, 128);
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
margin: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown-content th, .markdown-content td {
|
||||
border: 1px solid rgb(128, 128, 128);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-content th {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Syntax highlighting (highlight.js) */
|
||||
.hljs { background: transparent; }
|
||||
.hljs-comment, .hljs-quote { color: rgb(106, 153, 85); }
|
||||
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: rgb(86, 156, 214); }
|
||||
.hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: rgb(181, 206, 168); }
|
||||
.hljs-string, .hljs-doctag, .hljs-regexp { color: rgb(206, 145, 120); }
|
||||
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: rgb(220, 220, 170); }
|
||||
.hljs-type, .hljs-class, .hljs-built_in { color: rgb(78, 201, 176); }
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-params { color: rgb(156, 220, 254); }
|
||||
.hljs-attribute { color: rgb(156, 220, 254); }
|
||||
.hljs-meta { color: rgb(86, 156, 214); }
|
||||
.hljs-formula { background: rgba(255, 255, 255, 0.05); }
|
||||
.hljs-deletion { color: rgb(204, 102, 102); }
|
||||
.hljs-emphasis { font-style: italic; }
|
||||
.hljs-strong { font-weight: bold; }
|
||||
.hljs-link { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
|
||||
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>pi v0.30.2</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Session:</span><span class="info-value">6646a9e5-779b-4f81-8d45-d8d32f99aa25</span></div>
|
||||
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">12/28/2025, 7:08:12 PM</span></div>
|
||||
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">unknown</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Messages</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">User:</span><span class="info-value">1</span></div>
|
||||
<div class="info-item"><span class="info-label">Assistant:</span><span class="info-value">2</span></div>
|
||||
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Tokens & Cost</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Input:</span><span class="info-value">9 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Output:</span><span class="info-value">302 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read:</span><span class="info-value">2,462 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write:</span><span class="info-value">3,487 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Total:</span><span class="info-value">6,260 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Input Cost:</span><span class="info-value cost">$0.0000</span></div>
|
||||
<div class="info-item"><span class="info-label">Output Cost:</span><span class="info-value cost">$0.0045</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read Cost:</span><span class="info-value cost">$0.0007</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write Cost:</span><span class="info-value cost">$0.0131</span></div>
|
||||
<div class="info-item"><span class="info-label">Total Cost:</span><span class="info-value cost"><strong>$0.0184</strong></span></div>
|
||||
<div class="info-item"><span class="info-label">Context Usage:</span><span class="info-value">3,681 / 200,000 tokens (1.8%) - anthropic/claude-sonnet-4-5</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="system-prompt">
|
||||
<div class="system-prompt-header">System Prompt</div>
|
||||
<div class="system-prompt-content">You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
|
||||
|
||||
Available tools:
|
||||
- read: Read file contents
|
||||
- bash: Execute bash commands (ls, grep, find, etc.)
|
||||
- edit: Make surgical edits to files (find exact text and replace)
|
||||
- write: Create or overwrite files
|
||||
|
||||
Guidelines:
|
||||
- Use bash for file operations like ls, grep, find
|
||||
- Use read to examine files before editing. You must use this tool instead of cat or sed.
|
||||
- Use edit for precise changes (old text must match exactly)
|
||||
- Use write only for new files or complete rewrites
|
||||
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||
- Be concise in your responses
|
||||
- Show file paths clearly when working with files
|
||||
|
||||
Documentation:
|
||||
- Main documentation: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/README.md
|
||||
- Additional docs: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs
|
||||
- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)
|
||||
|
||||
# Project Context
|
||||
|
||||
The following project context files have been loaded:
|
||||
|
||||
## /Users/badlogic/workspaces/pi-mono/AGENTS.md
|
||||
|
||||
# Development Rules
|
||||
|
||||
## First Message
|
||||
If the user did not give you a concrete task in their first message,
|
||||
read README.md, then ask which module(s) to work on. Based on the answer, read the relevant README.md files in parallel.
|
||||
- packages/ai/README.md
|
||||
- packages/tui/README.md
|
||||
- packages/agent/README.md
|
||||
- packages/coding-agent/README.md
|
||||
- packages/mom/README.md
|
||||
- packages/pods/README.md
|
||||
- packages/web-ui/README.md
|
||||
|
||||
## Code Quality
|
||||
- No `any` types unless absolutely necessary
|
||||
- Check node_modules for external API type definitions instead of guessing
|
||||
- **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports.
|
||||
- NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead
|
||||
- Always ask before removing functionality or code that appears to be intentional
|
||||
|
||||
## Commands
|
||||
- After code changes: `npm run check` (get full output, no tail)
|
||||
- NEVER run: `npm run dev`, `npm run build`, `npm test`
|
||||
- Only run specific tests if user instructs: `npm test -- test/specific.test.ts`
|
||||
- NEVER commit unless user asks
|
||||
|
||||
## GitHub Issues
|
||||
When reading issues:
|
||||
- Always read all comments on the issue
|
||||
|
||||
When creating issues:
|
||||
- Add `pkg:*` labels to indicate which package(s) the issue affects
|
||||
- Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:mom`, `pkg:pods`, `pkg:tui`, `pkg:web-ui`
|
||||
- If an issue spans multiple packages, add all relevant labels
|
||||
|
||||
When closing issues via commit:
|
||||
- Include `fixes #<number>` or `closes #<number>` in the commit message
|
||||
- This automatically closes the issue when the commit is merged
|
||||
|
||||
## Tools
|
||||
- GitHub CLI for issues/PRs
|
||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
||||
- TUI interaction: use tmux
|
||||
|
||||
## Style
|
||||
- Keep answers short and concise
|
||||
- No emojis in commits, issues, PR comments, or code
|
||||
- No fluff or cheerful filler text
|
||||
- Technical prose only, be kind but direct (e.g., "Thanks @user" not "Thanks so much @user!")
|
||||
|
||||
## Changelog
|
||||
Location: `packages/*/CHANGELOG.md` (each package has its own)
|
||||
|
||||
### Format
|
||||
Use these sections under `## [Unreleased]`:
|
||||
- `### Breaking Changes` - API changes requiring migration
|
||||
- `### Added` - New features
|
||||
- `### Changed` - Changes to existing functionality
|
||||
- `### Fixed` - Bug fixes
|
||||
- `### Removed` - Removed features
|
||||
|
||||
### Rules
|
||||
- New entries ALWAYS go under `## [Unreleased]` section
|
||||
- NEVER modify already-released version sections (e.g., `## [0.12.2]`)
|
||||
- Each version section is immutable once released
|
||||
|
||||
### Attribution
|
||||
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/pi-mono/issues/123))`
|
||||
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@username](https://github.com/username))`
|
||||
|
||||
## Releasing
|
||||
|
||||
1. **Update CHANGELOGs**: Ensure all changes since last release are documented in the `[Unreleased]` section of each affected package's CHANGELOG.md
|
||||
|
||||
2. **Run release script**:
|
||||
```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```
|
||||
|
||||
The script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections.
|
||||
|
||||
### Tool Usage
|
||||
**CTRICIAL**: NEVER use sed/cat to read a file or a range of a file. Always use the read tool (use offset + limit for ranged reads).
|
||||
|
||||
|
||||
Current date and time: Sunday, December 28, 2025 at 07:08:12 PM GMT+1
|
||||
Current working directory: /Users/badlogic/workspaces/pi-mono/packages/coding-agent</div>
|
||||
</div>
|
||||
<div class="tools-list">
|
||||
<div class="tools-header">Available Tools</div>
|
||||
<div class="tools-content">
|
||||
<div class="tool-item"><span class="tool-item-name">read</span> - Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files.</div><div class="tool-item"><span class="tool-item-name">bash</span> - Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.</div><div class="tool-item"><span class="tool-item-name">edit</span> - Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.</div><div class="tool-item"><span class="tool-item-name">write</span> - Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="messages">
|
||||
<div class="user-message"><div class="message-timestamp">07:08:12 PM</div><div class="markdown-content"><p>Hello</p>
|
||||
</div></div><div class="assistant-message"><div class="message-timestamp">07:08:12 PM</div><div class="assistant-text markdown-content"><p>Hello! I'm Claude Code, ready to help with coding tasks in this pi-mono project.</p>
|
||||
<p>Since you haven't given me a specific task yet, let me first read the main README to understand the project structure:</p>
|
||||
</div><div class="tool-execution" style="background-color: rgb(40, 50, 40)"><div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">~/workspaces/pi-mono/README.md</span></div><div class="tool-output expandable" onclick="this.classList.toggle('expanded')"><div class="output-preview"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |</code></pre><div class="expand-hint">... (78 more lines) - click to expand</div></div><div class="output-full"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-coding-agent</span>](<span class="hljs-link">packages/coding-agent</span>)**</span> | Interactive coding agent CLI |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-mom</span>](<span class="hljs-link">packages/mom</span>)**</span> | Slack bot that delegates messages to the pi coding agent |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-tui</span>](<span class="hljs-link">packages/tui</span>)**</span> | Terminal UI library with differential rendering |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-web-ui</span>](<span class="hljs-link">packages/web-ui</span>)**</span> | Web components for AI chat interfaces |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-pods</span>](<span class="hljs-link">packages/pods</span>)**</span> | CLI for managing vLLM deployments on GPU pods |
|
||||
|
||||
<span class="hljs-section">## Development</span>
|
||||
|
||||
<span class="hljs-section">### Setup</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm install # Install all dependencies
|
||||
npm run build # Build all packages
|
||||
npm run check # Lint, format, and type check
|
||||
```</span>
|
||||
|
||||
<span class="hljs-quote">> <span class="hljs-strong">**Note:**</span> `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.</span>
|
||||
|
||||
<span class="hljs-section">### CI</span>
|
||||
|
||||
GitHub Actions runs on push to <span class="hljs-code">`main`</span> and on pull requests. The workflow runs <span class="hljs-code">`npm run check`</span> and <span class="hljs-code">`npm run test`</span> for each package in parallel.
|
||||
|
||||
<span class="hljs-strong">**Do not add LLM API keys as secrets to this repository.**</span> Tests that require LLM access use <span class="hljs-code">`describe.skipIf()`</span> to skip when API keys are missing. This is intentional:
|
||||
|
||||
<span class="hljs-bullet">-</span> PRs from external contributors would have access to secrets in the CI environment
|
||||
<span class="hljs-bullet">-</span> Malicious PR code could exfiltrate API keys
|
||||
<span class="hljs-bullet">-</span> Tests that need LLM calls are skipped on CI and run locally by developers who have keys configured
|
||||
|
||||
If you need to run LLM-dependent tests, run them locally with your own API keys.
|
||||
|
||||
<span class="hljs-section">### Development</span>
|
||||
|
||||
Start watch builds for all packages:
|
||||
<span class="hljs-code">```bash
|
||||
npm run dev
|
||||
```</span>
|
||||
|
||||
Then run with tsx:
|
||||
<span class="hljs-code">```bash
|
||||
cd packages/coding-agent && npx tsx src/cli.ts
|
||||
cd packages/pods && npx tsx src/cli.ts
|
||||
```</span>
|
||||
|
||||
<span class="hljs-section">### Versioning (Lockstep)</span>
|
||||
|
||||
<span class="hljs-strong">**All packages MUST always have the same version number.**</span> Use these commands to bump versions:
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run version:patch # 0.7.5 -> 0.7.6
|
||||
npm run version:minor # 0.7.5 -> 0.8.0
|
||||
npm run version:major # 0.7.5 -> 1.0.0
|
||||
```</span>
|
||||
|
||||
These commands:
|
||||
<span class="hljs-bullet">1.</span> Update all package versions to the same number
|
||||
<span class="hljs-bullet">2.</span> Update inter-package dependency versions (e.g., <span class="hljs-code">`pi-agent`</span> depends on <span class="hljs-code">`pi-ai@^0.7.7`</span>)
|
||||
<span class="hljs-bullet">3.</span> Update <span class="hljs-code">`package-lock.json`</span>
|
||||
|
||||
<span class="hljs-strong">**Never manually edit version numbers.**</span> The lockstep system ensures consistency across the monorepo.
|
||||
|
||||
<span class="hljs-section">### Publishing</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```</span>
|
||||
|
||||
This handles version bump, CHANGELOG updates, commit, tag, publish, and push.
|
||||
|
||||
<span class="hljs-strong">**NPM Token Setup**</span>: Requires a granular access token with "Bypass 2FA on publish" enabled.
|
||||
<span class="hljs-bullet">-</span> Go to https://www.npmjs.com/settings/badlogic/tokens/
|
||||
<span class="hljs-bullet">-</span> Create a new "Granular Access Token" with "Bypass 2FA on publish"
|
||||
<span class="hljs-bullet">-</span> Set the token: <span class="hljs-code">`npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN`</span>
|
||||
|
||||
<span class="hljs-section">## License</span>
|
||||
|
||||
MIT</code></pre></div></div></div></div><div class="assistant-message"><div class="message-timestamp">07:08:16 PM</div><div class="assistant-text markdown-content"><p>Great! I can see this is a monorepo for building AI agents and managing LLM deployments with several packages. </p>
|
||||
<p>Which module(s) would you like to work on?</p>
|
||||
<ul>
|
||||
<li><strong>@mariozechner/pi-ai</strong> - Unified multi-provider LLM API</li>
|
||||
<li><strong>@mariozechner/pi-agent-core</strong> - Agent runtime with tool calling</li>
|
||||
<li><strong>@mariozechner/pi-coding-agent</strong> - Interactive coding agent CLI (current directory)</li>
|
||||
<li><strong>@mariozechner/pi-mom</strong> - Slack bot integration</li>
|
||||
<li><strong>@mariozechner/pi-tui</strong> - Terminal UI library</li>
|
||||
<li><strong>@mariozechner/pi-web-ui</strong> - Web components for chat interfaces</li>
|
||||
<li><strong>@mariozechner/pi-pods</strong> - vLLM deployment CLI</li>
|
||||
</ul>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Generated by pi coding-agent on 12/28/2025, 7:08:19 PM
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Session Export - 2025-12-28T19-05-42-296Z_7225b2a7-7612-440d-84f1-fbcd8037d2f2.jsonl</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgb(229, 229, 231);
|
||||
background: rgb(24, 24, 30);
|
||||
padding: 24px;
|
||||
}
|
||||
.container { max-width: 700px; margin: 0 auto; }
|
||||
.header {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: rgb(30, 30, 36);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
color: rgb(0, 215, 255);
|
||||
}
|
||||
.header-info { display: flex; flex-direction: column; gap: 3px; font-size: 11px; }
|
||||
.info-item { color: rgb(102, 102, 102); display: flex; align-items: baseline; }
|
||||
.info-label { font-weight: 600; margin-right: 8px; min-width: 100px; }
|
||||
.info-value { color: rgb(229, 229, 231); flex: 1; }
|
||||
.info-value.cost { font-family: 'SF Mono', monospace; }
|
||||
.messages { display: flex; flex-direction: column; gap: 16px; }
|
||||
.message-timestamp { font-size: 10px; color: rgb(102, 102, 102); margin-bottom: 4px; opacity: 0.8; }
|
||||
.user-message {
|
||||
background: rgb(52, 53, 65);
|
||||
color: rgb(229, 229, 231);
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.assistant-message { padding: 0; }
|
||||
.assistant-text, .thinking-text {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.thinking-text { color: rgb(102, 102, 102); font-style: italic; white-space: pre-wrap; }
|
||||
.model-change { padding: 8px 16px; background: rgb(40, 40, 50); border-radius: 4px; }
|
||||
.model-change-text { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.model-name { color: rgb(0, 215, 255); font-weight: bold; }
|
||||
.compaction-container { background: rgb(60, 55, 35); border-radius: 4px; overflow: hidden; }
|
||||
.compaction-header { padding: 12px 16px; cursor: pointer; }
|
||||
.compaction-header:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.compaction-header-row { display: flex; align-items: center; gap: 8px; }
|
||||
.compaction-toggle { color: rgb(0, 215, 255); font-size: 10px; transition: transform 0.2s; }
|
||||
.compaction-container.expanded .compaction-toggle { transform: rotate(90deg); }
|
||||
.compaction-title { color: rgb(229, 229, 231); font-weight: bold; }
|
||||
.compaction-hint { color: rgb(102, 102, 102); font-size: 11px; }
|
||||
.compaction-content { display: none; padding: 0 16px 16px 16px; }
|
||||
.compaction-container.expanded .compaction-content { display: block; }
|
||||
.compaction-summary { background: rgba(0, 0, 0, 0.1); border-radius: 4px; padding: 12px; }
|
||||
.compaction-summary-header { font-weight: bold; color: rgb(0, 215, 255); margin-bottom: 8px; font-size: 11px; }
|
||||
.compaction-summary-content { color: rgb(229, 229, 231); white-space: pre-wrap; word-wrap: break-word; }
|
||||
.tool-execution { padding: 12px 16px; border-radius: 4px; margin-top: 8px; }
|
||||
.tool-execution.user-bash { background: rgb(50, 48, 35); }
|
||||
.tool-execution.user-bash-error { background: rgb(60, 45, 35); }
|
||||
.tool-header, .tool-name { font-weight: bold; }
|
||||
.tool-path { color: rgb(0, 215, 255); word-break: break-all; }
|
||||
.line-numbers { color: rgb(255, 255, 0); }
|
||||
.line-count { color: rgb(102, 102, 102); }
|
||||
.tool-command { font-weight: bold; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; }
|
||||
.tool-output {
|
||||
margin-top: 12px;
|
||||
color: rgb(128, 128, 128);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tool-output > div { line-height: 1.4; }
|
||||
.tool-output pre { margin: 0; font-family: inherit; color: inherit; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.tool-output.expandable { cursor: pointer; }
|
||||
.tool-output.expandable:hover { opacity: 0.9; }
|
||||
.tool-output.expandable .output-full { display: none; }
|
||||
.tool-output.expandable.expanded .output-preview { display: none; }
|
||||
.tool-output.expandable.expanded .output-full { display: block; }
|
||||
.expand-hint { color: rgb(0, 215, 255); font-style: italic; margin-top: 4px; }
|
||||
.system-prompt, .tools-list { background: rgb(60, 55, 40); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; }
|
||||
.system-prompt-header, .tools-header { font-weight: bold; color: rgb(255, 255, 0); margin-bottom: 8px; }
|
||||
.system-prompt-content, .tools-content { color: rgb(102, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-size: 11px; }
|
||||
.tool-item { margin: 4px 0; }
|
||||
.tool-item-name { font-weight: bold; color: rgb(229, 229, 231); }
|
||||
.tool-diff { margin-top: 12px; font-size: 11px; font-family: inherit; overflow-x: auto; max-width: 100%; }
|
||||
.diff-line-old { color: rgb(204, 102, 102); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-new { color: rgb(181, 189, 104); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.diff-line-context { color: rgb(128, 128, 128); white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
|
||||
.error-text { color: rgb(204, 102, 102); padding: 12px 16px; }
|
||||
.bash-status.warning { color: rgb(255, 255, 0); }
|
||||
.bash-status.error { color: rgb(204, 102, 102); }
|
||||
.bash-truncation.warning { color: rgb(255, 255, 0); }
|
||||
.footer { margin-top: 48px; padding: 20px; text-align: center; color: rgb(102, 102, 102); font-size: 10px; }
|
||||
.streaming-notice { background: rgb(50, 45, 35); padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; color: rgb(102, 102, 102); font-size: 11px; }
|
||||
|
||||
/* Image styles */
|
||||
.message-images { margin-bottom: 12px; }
|
||||
.message-image { max-width: 100%; max-height: 400px; border-radius: 4px; margin: 4px 0; }
|
||||
|
||||
/* Markdown styles */
|
||||
.markdown-content h1, .markdown-content h2, .markdown-content h3,
|
||||
.markdown-content h4, .markdown-content h5, .markdown-content h6 {
|
||||
color: rgb(240, 198, 116);
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content h1 { font-size: 1.4em; text-decoration: underline; }
|
||||
.markdown-content h2 { font-size: 1.2em; }
|
||||
.markdown-content h3 { font-size: 1.1em; }
|
||||
.markdown-content p { margin: 0.5em 0; }
|
||||
.markdown-content a { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
.markdown-content a:hover { opacity: 0.8; }
|
||||
.markdown-content code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgb(138, 190, 183);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.markdown-content pre {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0.5em 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.markdown-content pre code {
|
||||
display: block;
|
||||
background: none;
|
||||
color: rgb(181, 189, 104);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.code-block-wrapper {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.code-block-header {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.code-block-footer {
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 11px;
|
||||
}
|
||||
.markdown-content blockquote {
|
||||
border-left: 3px solid rgb(128, 128, 128);
|
||||
padding-left: 12px;
|
||||
margin: 0.5em 0;
|
||||
color: rgb(128, 128, 128);
|
||||
font-style: italic;
|
||||
}
|
||||
.markdown-content ul, .markdown-content ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.markdown-content li { margin: 0.25em 0; }
|
||||
.markdown-content li::marker { color: rgb(138, 190, 183); }
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(128, 128, 128);
|
||||
margin: 1em 0;
|
||||
}
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
margin: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown-content th, .markdown-content td {
|
||||
border: 1px solid rgb(128, 128, 128);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-content th {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Syntax highlighting (highlight.js) */
|
||||
.hljs { background: transparent; }
|
||||
.hljs-comment, .hljs-quote { color: rgb(106, 153, 85); }
|
||||
.hljs-keyword, .hljs-selector-tag, .hljs-addition { color: rgb(86, 156, 214); }
|
||||
.hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: rgb(181, 206, 168); }
|
||||
.hljs-string, .hljs-doctag, .hljs-regexp { color: rgb(206, 145, 120); }
|
||||
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: rgb(220, 220, 170); }
|
||||
.hljs-type, .hljs-class, .hljs-built_in { color: rgb(78, 201, 176); }
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-params { color: rgb(156, 220, 254); }
|
||||
.hljs-attribute { color: rgb(156, 220, 254); }
|
||||
.hljs-meta { color: rgb(86, 156, 214); }
|
||||
.hljs-formula { background: rgba(255, 255, 255, 0.05); }
|
||||
.hljs-deletion { color: rgb(204, 102, 102); }
|
||||
.hljs-emphasis { font-style: italic; }
|
||||
.hljs-strong { font-weight: bold; }
|
||||
.hljs-link { color: rgb(129, 162, 190); text-decoration: underline; }
|
||||
|
||||
@media print { body { background: white; color: black; } .tool-execution { border: 1px solid #ddd; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>pi v0.30.2</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Session:</span><span class="info-value">7225b2a7-7612-440d-84f1-fbcd8037d2f2</span></div>
|
||||
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">12/28/2025, 8:05:42 PM</span></div>
|
||||
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">unknown</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Messages</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">User:</span><span class="info-value">1</span></div>
|
||||
<div class="info-item"><span class="info-label">Assistant:</span><span class="info-value">2</span></div>
|
||||
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>Tokens & Cost</h1>
|
||||
<div class="header-info">
|
||||
<div class="info-item"><span class="info-label">Input:</span><span class="info-value">9 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Output:</span><span class="info-value">269 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read:</span><span class="info-value">2,462 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write:</span><span class="info-value">3,470 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Total:</span><span class="info-value">6,210 tokens</span></div>
|
||||
<div class="info-item"><span class="info-label">Input Cost:</span><span class="info-value cost">$0.0000</span></div>
|
||||
<div class="info-item"><span class="info-label">Output Cost:</span><span class="info-value cost">$0.0040</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Read Cost:</span><span class="info-value cost">$0.0007</span></div>
|
||||
<div class="info-item"><span class="info-label">Cache Write Cost:</span><span class="info-value cost">$0.0130</span></div>
|
||||
<div class="info-item"><span class="info-label">Total Cost:</span><span class="info-value cost"><strong>$0.0178</strong></span></div>
|
||||
<div class="info-item"><span class="info-label">Context Usage:</span><span class="info-value">3,648 / 200,000 tokens (1.8%) - anthropic/claude-sonnet-4-5</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="system-prompt">
|
||||
<div class="system-prompt-header">System Prompt</div>
|
||||
<div class="system-prompt-content">You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
|
||||
|
||||
Available tools:
|
||||
- read: Read file contents
|
||||
- bash: Execute bash commands (ls, grep, find, etc.)
|
||||
- edit: Make surgical edits to files (find exact text and replace)
|
||||
- write: Create or overwrite files
|
||||
|
||||
Guidelines:
|
||||
- Use bash for file operations like ls, grep, find
|
||||
- Use read to examine files before editing. You must use this tool instead of cat or sed.
|
||||
- Use edit for precise changes (old text must match exactly)
|
||||
- Use write only for new files or complete rewrites
|
||||
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||
- Be concise in your responses
|
||||
- Show file paths clearly when working with files
|
||||
|
||||
Documentation:
|
||||
- Main documentation: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/README.md
|
||||
- Additional docs: /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs
|
||||
- When asked about: custom models/providers (README sufficient), themes (docs/theme.md), skills (docs/skills.md), hooks (docs/hooks.md), custom tools (docs/custom-tools.md), RPC (docs/rpc.md)
|
||||
|
||||
# Project Context
|
||||
|
||||
The following project context files have been loaded:
|
||||
|
||||
## /Users/badlogic/workspaces/pi-mono/AGENTS.md
|
||||
|
||||
# Development Rules
|
||||
|
||||
## First Message
|
||||
If the user did not give you a concrete task in their first message,
|
||||
read README.md, then ask which module(s) to work on. Based on the answer, read the relevant README.md files in parallel.
|
||||
- packages/ai/README.md
|
||||
- packages/tui/README.md
|
||||
- packages/agent/README.md
|
||||
- packages/coding-agent/README.md
|
||||
- packages/mom/README.md
|
||||
- packages/pods/README.md
|
||||
- packages/web-ui/README.md
|
||||
|
||||
## Code Quality
|
||||
- No `any` types unless absolutely necessary
|
||||
- Check node_modules for external API type definitions instead of guessing
|
||||
- **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports.
|
||||
- NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead
|
||||
- Always ask before removing functionality or code that appears to be intentional
|
||||
|
||||
## Commands
|
||||
- After code changes: `npm run check` (get full output, no tail)
|
||||
- NEVER run: `npm run dev`, `npm run build`, `npm test`
|
||||
- Only run specific tests if user instructs: `npm test -- test/specific.test.ts`
|
||||
- NEVER commit unless user asks
|
||||
|
||||
## GitHub Issues
|
||||
When reading issues:
|
||||
- Always read all comments on the issue
|
||||
|
||||
When creating issues:
|
||||
- Add `pkg:*` labels to indicate which package(s) the issue affects
|
||||
- Available labels: `pkg:agent`, `pkg:ai`, `pkg:coding-agent`, `pkg:mom`, `pkg:pods`, `pkg:tui`, `pkg:web-ui`
|
||||
- If an issue spans multiple packages, add all relevant labels
|
||||
|
||||
When closing issues via commit:
|
||||
- Include `fixes #<number>` or `closes #<number>` in the commit message
|
||||
- This automatically closes the issue when the commit is merged
|
||||
|
||||
## Tools
|
||||
- GitHub CLI for issues/PRs
|
||||
- Add package labels to issues/PRs: pkg:agent, pkg:ai, pkg:coding-agent, pkg:mom, pkg:pods, pkg:tui, pkg:web-ui
|
||||
- TUI interaction: use tmux
|
||||
|
||||
## Style
|
||||
- Keep answers short and concise
|
||||
- No emojis in commits, issues, PR comments, or code
|
||||
- No fluff or cheerful filler text
|
||||
- Technical prose only, be kind but direct (e.g., "Thanks @user" not "Thanks so much @user!")
|
||||
|
||||
## Changelog
|
||||
Location: `packages/*/CHANGELOG.md` (each package has its own)
|
||||
|
||||
### Format
|
||||
Use these sections under `## [Unreleased]`:
|
||||
- `### Breaking Changes` - API changes requiring migration
|
||||
- `### Added` - New features
|
||||
- `### Changed` - Changes to existing functionality
|
||||
- `### Fixed` - Bug fixes
|
||||
- `### Removed` - Removed features
|
||||
|
||||
### Rules
|
||||
- New entries ALWAYS go under `## [Unreleased]` section
|
||||
- NEVER modify already-released version sections (e.g., `## [0.12.2]`)
|
||||
- Each version section is immutable once released
|
||||
|
||||
### Attribution
|
||||
- **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/badlogic/pi-mono/issues/123))`
|
||||
- **External contributions**: `Added feature X ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@username](https://github.com/username))`
|
||||
|
||||
## Releasing
|
||||
|
||||
1. **Update CHANGELOGs**: Ensure all changes since last release are documented in the `[Unreleased]` section of each affected package's CHANGELOG.md
|
||||
|
||||
2. **Run release script**:
|
||||
```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```
|
||||
|
||||
The script handles: version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections.
|
||||
|
||||
### Tool Usage
|
||||
**CTRICIAL**: NEVER use sed/cat to read a file or a range of a file. Always use the read tool (use offset + limit for ranged reads).
|
||||
|
||||
|
||||
Current date and time: Sunday, December 28, 2025 at 08:05:42 PM GMT+1
|
||||
Current working directory: /Users/badlogic/workspaces/pi-mono/packages/coding-agent</div>
|
||||
</div>
|
||||
<div class="tools-list">
|
||||
<div class="tools-header">Available Tools</div>
|
||||
<div class="tools-content">
|
||||
<div class="tool-item"><span class="tool-item-name">read</span> - Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files.</div><div class="tool-item"><span class="tool-item-name">bash</span> - Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.</div><div class="tool-item"><span class="tool-item-name">edit</span> - Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.</div><div class="tool-item"><span class="tool-item-name">write</span> - Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="messages">
|
||||
<div class="user-message"><div class="message-timestamp">08:05:42 PM</div><div class="markdown-content"><p>Hello</p>
|
||||
</div></div><div class="assistant-message"><div class="message-timestamp">08:05:42 PM</div><div class="assistant-text markdown-content"><p>Hello! I see you haven't given me a specific task yet. Let me start by reading the main README to understand the project structure.</p>
|
||||
</div><div class="tool-execution" style="background-color: rgb(40, 50, 40)"><div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">~/workspaces/pi-mono/README.md</span></div><div class="tool-output expandable" onclick="this.classList.toggle('expanded')"><div class="output-preview"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |</code></pre><div class="expand-hint">... (78 more lines) - click to expand</div></div><div class="output-full"><pre><code class="hljs"><span class="hljs-section"># Pi Monorepo</span>
|
||||
|
||||
Tools for building AI agents and managing LLM deployments.
|
||||
|
||||
<span class="hljs-section">## Packages</span>
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-ai</span>](<span class="hljs-link">packages/ai</span>)**</span> | Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.) |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-agent-core</span>](<span class="hljs-link">packages/agent</span>)**</span> | Agent runtime with tool calling and state management |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-coding-agent</span>](<span class="hljs-link">packages/coding-agent</span>)**</span> | Interactive coding agent CLI |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-mom</span>](<span class="hljs-link">packages/mom</span>)**</span> | Slack bot that delegates messages to the pi coding agent |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-tui</span>](<span class="hljs-link">packages/tui</span>)**</span> | Terminal UI library with differential rendering |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-web-ui</span>](<span class="hljs-link">packages/web-ui</span>)**</span> | Web components for AI chat interfaces |
|
||||
| <span class="hljs-strong">**[<span class="hljs-string">@mariozechner/pi-pods</span>](<span class="hljs-link">packages/pods</span>)**</span> | CLI for managing vLLM deployments on GPU pods |
|
||||
|
||||
<span class="hljs-section">## Development</span>
|
||||
|
||||
<span class="hljs-section">### Setup</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm install # Install all dependencies
|
||||
npm run build # Build all packages
|
||||
npm run check # Lint, format, and type check
|
||||
```</span>
|
||||
|
||||
<span class="hljs-quote">> <span class="hljs-strong">**Note:**</span> `npm run check` requires `npm run build` to be run first. The web-ui package uses `tsc` which needs compiled `.d.ts` files from dependencies.</span>
|
||||
|
||||
<span class="hljs-section">### CI</span>
|
||||
|
||||
GitHub Actions runs on push to <span class="hljs-code">`main`</span> and on pull requests. The workflow runs <span class="hljs-code">`npm run check`</span> and <span class="hljs-code">`npm run test`</span> for each package in parallel.
|
||||
|
||||
<span class="hljs-strong">**Do not add LLM API keys as secrets to this repository.**</span> Tests that require LLM access use <span class="hljs-code">`describe.skipIf()`</span> to skip when API keys are missing. This is intentional:
|
||||
|
||||
<span class="hljs-bullet">-</span> PRs from external contributors would have access to secrets in the CI environment
|
||||
<span class="hljs-bullet">-</span> Malicious PR code could exfiltrate API keys
|
||||
<span class="hljs-bullet">-</span> Tests that need LLM calls are skipped on CI and run locally by developers who have keys configured
|
||||
|
||||
If you need to run LLM-dependent tests, run them locally with your own API keys.
|
||||
|
||||
<span class="hljs-section">### Development</span>
|
||||
|
||||
Start watch builds for all packages:
|
||||
<span class="hljs-code">```bash
|
||||
npm run dev
|
||||
```</span>
|
||||
|
||||
Then run with tsx:
|
||||
<span class="hljs-code">```bash
|
||||
cd packages/coding-agent && npx tsx src/cli.ts
|
||||
cd packages/pods && npx tsx src/cli.ts
|
||||
```</span>
|
||||
|
||||
<span class="hljs-section">### Versioning (Lockstep)</span>
|
||||
|
||||
<span class="hljs-strong">**All packages MUST always have the same version number.**</span> Use these commands to bump versions:
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run version:patch # 0.7.5 -> 0.7.6
|
||||
npm run version:minor # 0.7.5 -> 0.8.0
|
||||
npm run version:major # 0.7.5 -> 1.0.0
|
||||
```</span>
|
||||
|
||||
These commands:
|
||||
<span class="hljs-bullet">1.</span> Update all package versions to the same number
|
||||
<span class="hljs-bullet">2.</span> Update inter-package dependency versions (e.g., <span class="hljs-code">`pi-agent`</span> depends on <span class="hljs-code">`pi-ai@^0.7.7`</span>)
|
||||
<span class="hljs-bullet">3.</span> Update <span class="hljs-code">`package-lock.json`</span>
|
||||
|
||||
<span class="hljs-strong">**Never manually edit version numbers.**</span> The lockstep system ensures consistency across the monorepo.
|
||||
|
||||
<span class="hljs-section">### Publishing</span>
|
||||
|
||||
<span class="hljs-code">```bash
|
||||
npm run release:patch # Bug fixes
|
||||
npm run release:minor # New features
|
||||
npm run release:major # Breaking changes
|
||||
```</span>
|
||||
|
||||
This handles version bump, CHANGELOG updates, commit, tag, publish, and push.
|
||||
|
||||
<span class="hljs-strong">**NPM Token Setup**</span>: Requires a granular access token with "Bypass 2FA on publish" enabled.
|
||||
<span class="hljs-bullet">-</span> Go to https://www.npmjs.com/settings/badlogic/tokens/
|
||||
<span class="hljs-bullet">-</span> Create a new "Granular Access Token" with "Bypass 2FA on publish"
|
||||
<span class="hljs-bullet">-</span> Set the token: <span class="hljs-code">`npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN`</span>
|
||||
|
||||
<span class="hljs-section">## License</span>
|
||||
|
||||
MIT</code></pre></div></div></div></div><div class="assistant-message"><div class="message-timestamp">08:05:45 PM</div><div class="assistant-text markdown-content"><p>Great! I can see this is a monorepo with several packages for building AI agents and managing LLM deployments. The main packages are:</p>
|
||||
<ul>
|
||||
<li><strong>pi-ai</strong> - Unified multi-provider LLM API</li>
|
||||
<li><strong>pi-agent-core</strong> - Agent runtime with tool calling and state management</li>
|
||||
<li><strong>pi-coding-agent</strong> - Interactive coding agent CLI (the package we're currently in)</li>
|
||||
<li><strong>pi-mom</strong> - Slack bot that delegates to the coding agent</li>
|
||||
<li><strong>pi-tui</strong> - Terminal UI library</li>
|
||||
<li><strong>pi-web-ui</strong> - Web components for AI chat interfaces</li>
|
||||
<li><strong>pi-pods</strong> - CLI for managing vLLM deployments on GPU pods</li>
|
||||
</ul>
|
||||
<p><strong>Which package(s) would you like to work on today?</strong></p>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Generated by pi coding-agent on 12/28/2025, 8:05:48 PM
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -30,7 +30,10 @@ import { exportSessionToHtml } from "./export-html.js";
|
|||
import type {
|
||||
HookCommandContext,
|
||||
HookRunner,
|
||||
SessionEventResult,
|
||||
SessionBeforeBranchResult,
|
||||
SessionBeforeCompactResult,
|
||||
SessionBeforeNewResult,
|
||||
SessionBeforeSwitchResult,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
} from "./hooks/index.js";
|
||||
|
|
@ -44,7 +47,7 @@ import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
|
|||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
||||
| { type: "auto_compaction_end"; result: CompactionResult | null; aborted: boolean; willRetry: boolean }
|
||||
| { type: "auto_compaction_end"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean }
|
||||
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
||||
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string };
|
||||
|
||||
|
|
@ -64,7 +67,7 @@ export interface AgentSessionConfig {
|
|||
/** File-based slash commands for expansion */
|
||||
fileCommands?: FileSlashCommand[];
|
||||
/** Hook runner (created in main.ts with wrapped tools) */
|
||||
hookRunner?: HookRunner | null;
|
||||
hookRunner?: HookRunner;
|
||||
/** Custom tools for session lifecycle events */
|
||||
customTools?: LoadedCustomTool[];
|
||||
skillsSettings?: Required<SkillsSettings>;
|
||||
|
|
@ -90,7 +93,7 @@ export interface ModelCycleResult {
|
|||
|
||||
/** Session statistics for /session command */
|
||||
export interface SessionStats {
|
||||
sessionFile: string | null;
|
||||
sessionFile: string | undefined;
|
||||
sessionId: string;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
|
|
@ -138,21 +141,21 @@ export class AgentSession {
|
|||
private _queuedMessages: string[] = [];
|
||||
|
||||
// Compaction state
|
||||
private _compactionAbortController: AbortController | null = null;
|
||||
private _autoCompactionAbortController: AbortController | null = null;
|
||||
private _compactionAbortController: AbortController | undefined = undefined;
|
||||
private _autoCompactionAbortController: AbortController | undefined = undefined;
|
||||
|
||||
// Retry state
|
||||
private _retryAbortController: AbortController | null = null;
|
||||
private _retryAbortController: AbortController | undefined = undefined;
|
||||
private _retryAttempt = 0;
|
||||
private _retryPromise: Promise<void> | null = null;
|
||||
private _retryResolve: (() => void) | null = null;
|
||||
private _retryPromise: Promise<void> | undefined = undefined;
|
||||
private _retryResolve: (() => void) | undefined = undefined;
|
||||
|
||||
// Bash execution state
|
||||
private _bashAbortController: AbortController | null = null;
|
||||
private _bashAbortController: AbortController | undefined = undefined;
|
||||
private _pendingBashMessages: BashExecutionMessage[] = [];
|
||||
|
||||
// Hook system
|
||||
private _hookRunner: HookRunner | null = null;
|
||||
private _hookRunner: HookRunner | undefined = undefined;
|
||||
private _turnIndex = 0;
|
||||
|
||||
// Custom tools for session lifecycle
|
||||
|
|
@ -169,10 +172,14 @@ export class AgentSession {
|
|||
this.settingsManager = config.settingsManager;
|
||||
this._scopedModels = config.scopedModels ?? [];
|
||||
this._fileCommands = config.fileCommands ?? [];
|
||||
this._hookRunner = config.hookRunner ?? null;
|
||||
this._hookRunner = config.hookRunner;
|
||||
this._customTools = config.customTools ?? [];
|
||||
this._skillsSettings = config.skillsSettings;
|
||||
this._modelRegistry = config.modelRegistry;
|
||||
|
||||
// Always subscribe to agent events for internal handling
|
||||
// (session persistence, hooks, auto-compaction, retry logic)
|
||||
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
||||
}
|
||||
|
||||
/** Model registry for API key resolution and model discovery */
|
||||
|
|
@ -192,7 +199,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
// Track last assistant message for auto-compaction check
|
||||
private _lastAssistantMessage: AssistantMessage | null = null;
|
||||
private _lastAssistantMessage: AssistantMessage | undefined = undefined;
|
||||
|
||||
/** Internal handler for agent events - shared by subscribe and reconnect */
|
||||
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
||||
|
|
@ -246,7 +253,7 @@ export class AgentSession {
|
|||
// Check auto-retry and auto-compaction after agent completes
|
||||
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
||||
const msg = this._lastAssistantMessage;
|
||||
this._lastAssistantMessage = null;
|
||||
this._lastAssistantMessage = undefined;
|
||||
|
||||
// Check for retryable errors first (overloaded, rate limit, server errors)
|
||||
if (this._isRetryableError(msg)) {
|
||||
|
|
@ -272,8 +279,8 @@ export class AgentSession {
|
|||
private _resolveRetry(): void {
|
||||
if (this._retryResolve) {
|
||||
this._retryResolve();
|
||||
this._retryResolve = null;
|
||||
this._retryPromise = null;
|
||||
this._retryResolve = undefined;
|
||||
this._retryPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +294,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
/** Find the last assistant message in agent state (including aborted ones) */
|
||||
private _findLastAssistantMessage(): AssistantMessage | null {
|
||||
private _findLastAssistantMessage(): AssistantMessage | undefined {
|
||||
const messages = this.agent.state.messages;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
|
|
@ -295,7 +302,7 @@ export class AgentSession {
|
|||
return msg as AssistantMessage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Emit hook events based on agent events */
|
||||
|
|
@ -334,11 +341,6 @@ export class AgentSession {
|
|||
subscribe(listener: AgentSessionEventListener): () => void {
|
||||
this._eventListeners.push(listener);
|
||||
|
||||
// Set up agent subscription if not already done
|
||||
if (!this._unsubscribeAgent) {
|
||||
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
||||
}
|
||||
|
||||
// Return unsubscribe function for this specific listener
|
||||
return () => {
|
||||
const index = this._eventListeners.indexOf(listener);
|
||||
|
|
@ -387,8 +389,8 @@ export class AgentSession {
|
|||
return this.agent.state;
|
||||
}
|
||||
|
||||
/** Current model (may be null if not yet selected) */
|
||||
get model(): Model<any> | null {
|
||||
/** Current model (may be undefined if not yet selected) */
|
||||
get model(): Model<any> | undefined {
|
||||
return this.agent.state.model;
|
||||
}
|
||||
|
||||
|
|
@ -404,7 +406,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether auto-compaction is currently running */
|
||||
get isCompacting(): boolean {
|
||||
return this._autoCompactionAbortController !== null || this._compactionAbortController !== null;
|
||||
return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** All messages including custom types like BashExecutionMessage */
|
||||
|
|
@ -417,9 +419,9 @@ export class AgentSession {
|
|||
return this.agent.getQueueMode();
|
||||
}
|
||||
|
||||
/** Current session file path, or null if sessions are disabled */
|
||||
get sessionFile(): string | null {
|
||||
return this.sessionManager.getSessionFile() ?? null;
|
||||
/** Current session file path, or undefined if sessions are disabled */
|
||||
get sessionFile(): string | undefined {
|
||||
return this.sessionManager.getSessionFile();
|
||||
}
|
||||
|
||||
/** Current session ID */
|
||||
|
|
@ -663,12 +665,11 @@ export class AgentSession {
|
|||
async reset(): Promise<boolean> {
|
||||
const previousSessionFile = this.sessionFile;
|
||||
|
||||
// Emit before_new event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_new event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_new")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_new",
|
||||
})) as SessionEventResult | undefined;
|
||||
type: "session_before_new",
|
||||
})) as SessionBeforeNewResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return false;
|
||||
|
|
@ -682,11 +683,10 @@ export class AgentSession {
|
|||
this._queuedMessages = [];
|
||||
this._reconnectToAgent();
|
||||
|
||||
// Emit session event with reason "new" to hooks
|
||||
// Emit session_new event to hooks
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "new",
|
||||
type: "session_new",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -722,17 +722,17 @@ export class AgentSession {
|
|||
* Cycle to next/previous model.
|
||||
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
||||
* @param direction - "forward" (default) or "backward"
|
||||
* @returns The new model info, or null if only one model available
|
||||
* @returns The new model info, or undefined if only one model available
|
||||
*/
|
||||
async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | null> {
|
||||
async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | undefined> {
|
||||
if (this._scopedModels.length > 0) {
|
||||
return this._cycleScopedModel(direction);
|
||||
}
|
||||
return this._cycleAvailableModel(direction);
|
||||
}
|
||||
|
||||
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
|
||||
if (this._scopedModels.length <= 1) return null;
|
||||
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
||||
if (this._scopedModels.length <= 1) return undefined;
|
||||
|
||||
const currentModel = this.model;
|
||||
let currentIndex = this._scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));
|
||||
|
|
@ -759,9 +759,9 @@ export class AgentSession {
|
|||
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
||||
}
|
||||
|
||||
private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
|
||||
private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
||||
const availableModels = await this._modelRegistry.getAvailable();
|
||||
if (availableModels.length <= 1) return null;
|
||||
if (availableModels.length <= 1) return undefined;
|
||||
|
||||
const currentModel = this.model;
|
||||
let currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));
|
||||
|
|
@ -816,10 +816,10 @@ export class AgentSession {
|
|||
|
||||
/**
|
||||
* Cycle to next thinking level.
|
||||
* @returns New level, or null if model doesn't support thinking
|
||||
* @returns New level, or undefined if model doesn't support thinking
|
||||
*/
|
||||
cycleThinkingLevel(): ThinkingLevel | null {
|
||||
if (!this.supportsThinking()) return null;
|
||||
cycleThinkingLevel(): ThinkingLevel | undefined {
|
||||
if (!this.supportsThinking()) return undefined;
|
||||
|
||||
const levels = this.getAvailableThinkingLevels();
|
||||
const currentIndex = levels.indexOf(this.thinkingLevel);
|
||||
|
|
@ -904,19 +904,18 @@ export class AgentSession {
|
|||
let hookCompaction: CompactionResult | undefined;
|
||||
let fromHook = false;
|
||||
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
||||
// Get previous compactions, newest first
|
||||
const previousCompactions = entries.filter((e): e is CompactionEntry => e.type === "compaction").reverse();
|
||||
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_compact",
|
||||
type: "session_before_compact",
|
||||
preparation,
|
||||
previousCompactions,
|
||||
customInstructions,
|
||||
model: this.model,
|
||||
signal: this._compactionAbortController.signal,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
throw new Error("Compaction cancelled");
|
||||
|
|
@ -971,8 +970,7 @@ export class AgentSession {
|
|||
|
||||
if (this._hookRunner && savedCompactionEntry) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "compact",
|
||||
type: "session_compact",
|
||||
compactionEntry: savedCompactionEntry,
|
||||
fromHook,
|
||||
});
|
||||
|
|
@ -985,7 +983,7 @@ export class AgentSession {
|
|||
details,
|
||||
};
|
||||
} finally {
|
||||
this._compactionAbortController = null;
|
||||
this._compactionAbortController = undefined;
|
||||
this._reconnectToAgent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1051,13 +1049,13 @@ export class AgentSession {
|
|||
|
||||
try {
|
||||
if (!this.model) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = await this._modelRegistry.getApiKey(this.model);
|
||||
if (!apiKey) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1065,29 +1063,28 @@ export class AgentSession {
|
|||
|
||||
const preparation = prepareCompaction(entries, settings);
|
||||
if (!preparation) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
let hookCompaction: CompactionResult | undefined;
|
||||
let fromHook = false;
|
||||
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
if (this._hookRunner?.hasHandlers("session_before_compact")) {
|
||||
// Get previous compactions, newest first
|
||||
const previousCompactions = entries.filter((e): e is CompactionEntry => e.type === "compaction").reverse();
|
||||
|
||||
const hookResult = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_compact",
|
||||
type: "session_before_compact",
|
||||
preparation,
|
||||
previousCompactions,
|
||||
customInstructions: undefined,
|
||||
model: this.model,
|
||||
signal: this._autoCompactionAbortController.signal,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (hookResult?.cancel) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1124,7 +1121,7 @@ export class AgentSession {
|
|||
}
|
||||
|
||||
if (this._autoCompactionAbortController.signal.aborted) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: true, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1140,8 +1137,7 @@ export class AgentSession {
|
|||
|
||||
if (this._hookRunner && savedCompactionEntry) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "compact",
|
||||
type: "session_compact",
|
||||
compactionEntry: savedCompactionEntry,
|
||||
fromHook,
|
||||
});
|
||||
|
|
@ -1167,7 +1163,7 @@ export class AgentSession {
|
|||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
this._emit({ type: "auto_compaction_end", result: null, aborted: false, willRetry: false });
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
|
||||
if (reason === "overflow") {
|
||||
throw new Error(
|
||||
|
|
@ -1175,7 +1171,7 @@ export class AgentSession {
|
|||
);
|
||||
}
|
||||
} finally {
|
||||
this._autoCompactionAbortController = null;
|
||||
this._autoCompactionAbortController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1267,7 +1263,7 @@ export class AgentSession {
|
|||
// Aborted during sleep - emit end event so UI can clean up
|
||||
const attempt = this._retryAttempt;
|
||||
this._retryAttempt = 0;
|
||||
this._retryAbortController = null;
|
||||
this._retryAbortController = undefined;
|
||||
this._emit({
|
||||
type: "auto_retry_end",
|
||||
success: false,
|
||||
|
|
@ -1277,7 +1273,7 @@ export class AgentSession {
|
|||
this._resolveRetry();
|
||||
return false;
|
||||
}
|
||||
this._retryAbortController = null;
|
||||
this._retryAbortController = undefined;
|
||||
|
||||
// Retry via continue() - use setTimeout to break out of event handler chain
|
||||
setTimeout(() => {
|
||||
|
|
@ -1329,7 +1325,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether auto-retry is currently in progress */
|
||||
get isRetrying(): boolean {
|
||||
return this._retryPromise !== null;
|
||||
return this._retryPromise !== undefined;
|
||||
}
|
||||
|
||||
/** Whether auto-retry is enabled */
|
||||
|
|
@ -1389,7 +1385,7 @@ export class AgentSession {
|
|||
|
||||
return result;
|
||||
} finally {
|
||||
this._bashAbortController = null;
|
||||
this._bashAbortController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1402,7 +1398,7 @@ export class AgentSession {
|
|||
|
||||
/** Whether a bash command is currently running */
|
||||
get isBashRunning(): boolean {
|
||||
return this._bashAbortController !== null;
|
||||
return this._bashAbortController !== undefined;
|
||||
}
|
||||
|
||||
/** Whether there are pending bash messages waiting to be flushed */
|
||||
|
|
@ -1439,15 +1435,14 @@ export class AgentSession {
|
|||
* @returns true if switch completed, false if cancelled by hook
|
||||
*/
|
||||
async switchSession(sessionPath: string): Promise<boolean> {
|
||||
const previousSessionFile = this.sessionFile;
|
||||
const previousSessionFile = this.sessionManager.getSessionFile();
|
||||
|
||||
// Emit before_switch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_switch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_switch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_switch",
|
||||
type: "session_before_switch",
|
||||
targetSessionFile: sessionPath,
|
||||
})) as SessionEventResult | undefined;
|
||||
})) as SessionBeforeSwitchResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return false;
|
||||
|
|
@ -1464,11 +1459,10 @@ export class AgentSession {
|
|||
// Reload messages
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
|
||||
// Emit session event to hooks
|
||||
// Emit session_switch event to hooks
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "switch",
|
||||
type: "session_switch",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
|
@ -1520,13 +1514,12 @@ export class AgentSession {
|
|||
|
||||
let skipConversationRestore = false;
|
||||
|
||||
// Emit before_branch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session")) {
|
||||
// Emit session_before_branch event (can be cancelled)
|
||||
if (this._hookRunner?.hasHandlers("session_before_branch")) {
|
||||
const result = (await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "before_branch",
|
||||
targetTurnIndex: entryIndex,
|
||||
})) as SessionEventResult | undefined;
|
||||
type: "session_before_branch",
|
||||
entryIndex: entryIndex,
|
||||
})) as SessionBeforeBranchResult | undefined;
|
||||
|
||||
if (result?.cancel) {
|
||||
return { selectedText, cancelled: true };
|
||||
|
|
@ -1534,27 +1527,20 @@ export class AgentSession {
|
|||
skipConversationRestore = result?.skipConversationRestore ?? false;
|
||||
}
|
||||
|
||||
// Create branched session ending before the selected message (returns null in --no-session mode)
|
||||
// User will re-enter/edit the selected message
|
||||
if (!selectedEntry.parentId) {
|
||||
throw new Error("Cannot branch from first message");
|
||||
}
|
||||
const newSessionFile = this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
||||
|
||||
// Update session file if we have one (file-based mode)
|
||||
if (newSessionFile !== null) {
|
||||
this.sessionManager.setSessionFile(newSessionFile);
|
||||
this.sessionManager.newSession();
|
||||
} else {
|
||||
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
||||
}
|
||||
|
||||
// Reload messages from entries (works for both file and in-memory mode)
|
||||
const sessionContext = this.sessionManager.buildSessionContext();
|
||||
|
||||
// Emit branch event to hooks (after branch completes)
|
||||
// Emit session_branch event to hooks (after branch completes)
|
||||
if (this._hookRunner) {
|
||||
await this._hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "branch",
|
||||
targetTurnIndex: entryIndex,
|
||||
type: "session_branch",
|
||||
previousSessionFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1664,9 +1650,9 @@ export class AgentSession {
|
|||
/**
|
||||
* Get text content of last assistant message.
|
||||
* Useful for /copy command.
|
||||
* @returns Text content, or null if no assistant message exists
|
||||
* @returns Text content, or undefined if no assistant message exists
|
||||
*/
|
||||
getLastAssistantText(): string | null {
|
||||
getLastAssistantText(): string | undefined {
|
||||
const lastAssistant = this.messages
|
||||
.slice()
|
||||
.reverse()
|
||||
|
|
@ -1678,7 +1664,7 @@ export class AgentSession {
|
|||
return true;
|
||||
});
|
||||
|
||||
if (!lastAssistant) return null;
|
||||
if (!lastAssistant) return undefined;
|
||||
|
||||
let text = "";
|
||||
for (const content of (lastAssistant as AssistantMessage).content) {
|
||||
|
|
@ -1687,7 +1673,7 @@ export class AgentSession {
|
|||
}
|
||||
}
|
||||
|
||||
return text.trim() || null;
|
||||
return text.trim() || undefined;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
@ -1704,7 +1690,7 @@ export class AgentSession {
|
|||
/**
|
||||
* Get the hook runner (for setting UI context and error handlers).
|
||||
*/
|
||||
get hookRunner(): HookRunner | null {
|
||||
get hookRunner(): HookRunner | undefined {
|
||||
return this._hookRunner;
|
||||
}
|
||||
|
||||
|
|
@ -1721,7 +1707,7 @@ export class AgentSession {
|
|||
*/
|
||||
private async _emitToolSessionEvent(
|
||||
reason: ToolSessionEvent["reason"],
|
||||
previousSessionFile: string | null,
|
||||
previousSessionFile: string | undefined,
|
||||
): Promise<void> {
|
||||
const event: ToolSessionEvent = {
|
||||
entries: this.sessionManager.getEntries(),
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ export class AuthStorage {
|
|||
/**
|
||||
* Get credential for a provider.
|
||||
*/
|
||||
get(provider: string): AuthCredential | null {
|
||||
return this.data[provider] ?? null;
|
||||
get(provider: string): AuthCredential | undefined {
|
||||
return this.data[provider] ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,7 +191,7 @@ export class AuthStorage {
|
|||
* 4. Environment variable
|
||||
* 5. Fallback resolver (models.json custom providers)
|
||||
*/
|
||||
async getApiKey(provider: string): Promise<string | null> {
|
||||
async getApiKey(provider: string): Promise<string | undefined> {
|
||||
// Runtime override takes highest priority
|
||||
const runtimeKey = this.runtimeOverrides.get(provider);
|
||||
if (runtimeKey) {
|
||||
|
|
@ -230,6 +230,6 @@ export class AuthStorage {
|
|||
if (envKey) return envKey;
|
||||
|
||||
// Fall back to custom resolver (e.g., models.json custom providers)
|
||||
return this.fallbackResolver?.(provider) ?? null;
|
||||
return this.fallbackResolver?.(provider) ?? undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export interface BashExecutorOptions {
|
|||
export interface BashResult {
|
||||
/** Combined stdout + stderr output (sanitized, possibly truncated) */
|
||||
output: string;
|
||||
/** Process exit code (null if killed/cancelled) */
|
||||
exitCode: number | null;
|
||||
/** Process exit code (undefined if killed/cancelled) */
|
||||
exitCode: number | undefined;
|
||||
/** Whether the command was cancelled via signal */
|
||||
cancelled: boolean;
|
||||
/** Whether the output was truncated */
|
||||
|
|
@ -88,7 +88,7 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|||
child.kill();
|
||||
resolve({
|
||||
output: "",
|
||||
exitCode: null,
|
||||
exitCode: undefined,
|
||||
cancelled: true,
|
||||
truncated: false,
|
||||
});
|
||||
|
|
@ -154,7 +154,7 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
|
|||
|
||||
resolve({
|
||||
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
||||
exitCode: code,
|
||||
exitCode: cancelled ? undefined : code,
|
||||
cancelled,
|
||||
truncated: truncationResult.truncated,
|
||||
fullOutputPath: tempFilePath,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import type { CompactionEntry, SessionEntry } from "./session-manager.js";
|
|||
|
||||
/**
|
||||
* Extract AgentMessage from an entry if it produces one.
|
||||
* Returns null for entries that don't contribute to LLM context.
|
||||
* Returns undefined for entries that don't contribute to LLM context.
|
||||
*/
|
||||
function getMessageFromEntry(entry: SessionEntry): AgentMessage | null {
|
||||
function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
||||
if (entry.type === "message") {
|
||||
return entry.message;
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | null {
|
|||
if (entry.type === "branch_summary") {
|
||||
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
|
||||
|
|
@ -69,20 +69,20 @@ export function calculateContextTokens(usage: Usage): number {
|
|||
* Get usage from an assistant message if available.
|
||||
* Skips aborted and error messages as they don't have valid usage data.
|
||||
*/
|
||||
function getAssistantUsage(msg: AgentMessage): Usage | null {
|
||||
function getAssistantUsage(msg: AgentMessage): Usage | undefined {
|
||||
if (msg.role === "assistant" && "usage" in msg) {
|
||||
const assistantMsg = msg as AssistantMessage;
|
||||
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
||||
return assistantMsg.usage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last non-aborted assistant message usage from session entries.
|
||||
*/
|
||||
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {
|
||||
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const entry = entries[i];
|
||||
if (entry.type === "message") {
|
||||
|
|
@ -90,7 +90,7 @@ export function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {
|
|||
if (usage) return usage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -398,9 +398,12 @@ export interface CompactionPreparation {
|
|||
boundaryStart: number;
|
||||
}
|
||||
|
||||
export function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {
|
||||
export function prepareCompaction(
|
||||
entries: SessionEntry[],
|
||||
settings: CompactionSettings,
|
||||
): CompactionPreparation | undefined {
|
||||
if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let prevCompactionIndex = -1;
|
||||
|
|
@ -421,7 +424,7 @@ export function prepareCompaction(entries: SessionEntry[], settings: CompactionS
|
|||
// Get UUID of first kept entry
|
||||
const firstKeptEntry = entries[cutPoint.firstKeptEntryIndex];
|
||||
if (!firstKeptEntry?.id) {
|
||||
return null; // Session needs migration
|
||||
return undefined; // Session needs migration
|
||||
}
|
||||
const firstKeptEntryId = firstKeptEntry.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,9 @@ function resolveToolPath(toolPath: string, cwd: string): string {
|
|||
*/
|
||||
function createNoOpUIContext(): HookUIContext {
|
||||
return {
|
||||
select: async () => null,
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export interface ToolAPI {
|
|||
export interface SessionEvent {
|
||||
/** All session entries (including pre-compaction history) */
|
||||
entries: SessionEntry[];
|
||||
/** Current session file path, or null in --no-session mode */
|
||||
sessionFile: string | null;
|
||||
/** Previous session file path, or null for "start" and "new" */
|
||||
previousSessionFile: string | null;
|
||||
/** Current session file path, or undefined in --no-session mode */
|
||||
sessionFile: string | undefined;
|
||||
/** Previous session file path, or undefined for "start" and "new" */
|
||||
previousSessionFile: string | undefined;
|
||||
/** Reason for the session event */
|
||||
reason: "start" | "switch" | "branch" | "new";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ function resolveColorValue(
|
|||
}
|
||||
|
||||
/** Load theme JSON from built-in or custom themes directory. */
|
||||
function loadThemeJson(name: string): ThemeJson | null {
|
||||
function loadThemeJson(name: string): ThemeJson | undefined {
|
||||
// Try built-in themes first
|
||||
const themesDir = getThemesDir();
|
||||
const builtinPath = path.join(themesDir, `${name}.json`);
|
||||
|
|
@ -129,7 +129,7 @@ function loadThemeJson(name: string): ThemeJson | null {
|
|||
try {
|
||||
return JSON.parse(readFileSync(builtinPath, "utf-8")) as ThemeJson;
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,11 +140,11 @@ function loadThemeJson(name: string): ThemeJson | null {
|
|||
try {
|
||||
return JSON.parse(readFileSync(customPath, "utf-8")) as ThemeJson;
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Build complete theme colors object, resolving theme JSON values against defaults. */
|
||||
|
|
@ -831,7 +831,9 @@ function formatMessage(
|
|||
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
const isError = message.cancelled || (message.exitCode !== 0 && message.exitCode !== null);
|
||||
const isError =
|
||||
message.cancelled ||
|
||||
(message.exitCode !== 0 && message.exitCode !== null && message.exitCode !== undefined);
|
||||
|
||||
html += `<div class="tool-execution user-bash${isError ? " user-bash-error" : ""}">`;
|
||||
html += timestampHtml;
|
||||
|
|
@ -844,7 +846,7 @@ function formatMessage(
|
|||
|
||||
if (message.cancelled) {
|
||||
html += `<div class="bash-status warning">(cancelled)</div>`;
|
||||
} else if (message.exitCode !== 0 && message.exitCode !== null) {
|
||||
} else if (message.exitCode !== 0 && message.exitCode !== null && message.exitCode !== undefined) {
|
||||
html += `<div class="bash-status error">(exit ${message.exitCode})</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1020,7 +1022,7 @@ function generateHtml(data: ParsedSessionData, filename: string, colors: ThemeCo
|
|||
const lastModelInfo = lastProvider ? `${lastProvider}/${lastModel}` : lastModel;
|
||||
|
||||
const contextWindow = data.contextWindow || 0;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : null;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : undefined;
|
||||
|
||||
let messagesHtml = "";
|
||||
for (const event of data.sessionEvents) {
|
||||
|
|
|
|||
|
|
@ -9,49 +9,4 @@ export {
|
|||
} from "./loader.js";
|
||||
export { execCommand, HookRunner, type HookErrorListener } from "./runner.js";
|
||||
export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
|
||||
export type {
|
||||
AgentEndEvent,
|
||||
AgentStartEvent,
|
||||
BeforeAgentStartEvent,
|
||||
BeforeAgentStartEventResult,
|
||||
BashToolResultEvent,
|
||||
ContextEvent,
|
||||
ContextEventResult,
|
||||
CustomToolResultEvent,
|
||||
EditToolResultEvent,
|
||||
ExecOptions,
|
||||
ExecResult,
|
||||
FindToolResultEvent,
|
||||
GrepToolResultEvent,
|
||||
HookAPI,
|
||||
HookCommandContext,
|
||||
HookError,
|
||||
HookEvent,
|
||||
HookEventContext,
|
||||
HookFactory,
|
||||
HookMessageRenderer,
|
||||
HookMessageRenderOptions,
|
||||
HookUIContext,
|
||||
LsToolResultEvent,
|
||||
ReadonlySessionManager,
|
||||
ReadToolResultEvent,
|
||||
RegisteredCommand,
|
||||
SessionEvent,
|
||||
SessionEventResult,
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
ToolResultEvent,
|
||||
ToolResultEventResult,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
WriteToolResultEvent,
|
||||
} from "./types.js";
|
||||
export {
|
||||
isBashToolResult,
|
||||
isEditToolResult,
|
||||
isFindToolResult,
|
||||
isGrepToolResult,
|
||||
isLsToolResult,
|
||||
isReadToolResult,
|
||||
isWriteToolResult,
|
||||
} from "./types.js";
|
||||
export type * from "./types.js";
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ import type {
|
|||
HookMessageRenderer,
|
||||
HookUIContext,
|
||||
RegisteredCommand,
|
||||
SessionEvent,
|
||||
SessionEventResult,
|
||||
SessionBeforeCompactResult,
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
ToolResultEventResult,
|
||||
|
|
@ -53,9 +52,9 @@ function createTimeout(ms: number): { promise: Promise<never>; clear: () => void
|
|||
|
||||
/** No-op UI context used when no UI is available */
|
||||
const noOpUIContext: HookUIContext = {
|
||||
select: async () => null,
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
|
|
@ -228,12 +227,26 @@ export class HookRunner {
|
|||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all hooks.
|
||||
* Returns the result from session/tool_result events (if any handler returns one).
|
||||
* Check if event type is a session "before_*" event that can be cancelled.
|
||||
*/
|
||||
async emit(event: HookEvent): Promise<SessionEventResult | ToolResultEventResult | undefined> {
|
||||
private isSessionBeforeEvent(
|
||||
type: string,
|
||||
): type is "session_before_switch" | "session_before_new" | "session_before_branch" | "session_before_compact" {
|
||||
return (
|
||||
type === "session_before_switch" ||
|
||||
type === "session_before_new" ||
|
||||
type === "session_before_branch" ||
|
||||
type === "session_before_compact"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all hooks.
|
||||
* Returns the result from session before_* / tool_result events (if any handler returns one).
|
||||
*/
|
||||
async emit(event: HookEvent): Promise<SessionBeforeCompactResult | ToolResultEventResult | undefined> {
|
||||
const ctx = this.createContext();
|
||||
let result: SessionEventResult | ToolResultEventResult | undefined;
|
||||
let result: SessionBeforeCompactResult | ToolResultEventResult | undefined;
|
||||
|
||||
for (const hook of this.hooks) {
|
||||
const handlers = hook.handlers.get(event.type);
|
||||
|
|
@ -241,11 +254,10 @@ export class HookRunner {
|
|||
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
// No timeout for before_compact events (like tool_call, they may take a while)
|
||||
const isBeforeCompact = event.type === "session" && (event as SessionEvent).reason === "before_compact";
|
||||
// No timeout for session_before_compact events (like tool_call, they may take a while)
|
||||
let handlerResult: unknown;
|
||||
|
||||
if (isBeforeCompact) {
|
||||
if (event.type === "session_before_compact") {
|
||||
handlerResult = await handler(event, ctx);
|
||||
} else {
|
||||
const timeout = createTimeout(this.timeout);
|
||||
|
|
@ -253,9 +265,9 @@ export class HookRunner {
|
|||
timeout.clear();
|
||||
}
|
||||
|
||||
// For session events, capture the result (for before_* cancellation)
|
||||
if (event.type === "session" && handlerResult) {
|
||||
result = handlerResult as SessionEventResult;
|
||||
// For session before_* events, capture the result (for cancellation)
|
||||
if (this.isSessionBeforeEvent(event.type) && handlerResult) {
|
||||
result = handlerResult as SessionBeforeCompactResult;
|
||||
// If cancelled, stop processing further hooks
|
||||
if (result.cancel) {
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -13,13 +13,7 @@ import type { CompactionPreparation, CompactionResult } from "../compaction.js";
|
|||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
import type { HookMessage } from "../messages.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
import type {
|
||||
CompactionEntry,
|
||||
SessionEntry,
|
||||
SessionHeader,
|
||||
SessionManager,
|
||||
SessionTreeNode,
|
||||
} from "../session-manager.js";
|
||||
import type { CompactionEntry, SessionManager } from "../session-manager.js";
|
||||
|
||||
/**
|
||||
* Read-only view of SessionManager for hooks.
|
||||
|
|
@ -64,7 +58,7 @@ export interface HookUIContext {
|
|||
* @param options - Array of string options
|
||||
* @returns Selected option string, or null if cancelled
|
||||
*/
|
||||
select(title: string, options: string[]): Promise<string | null>;
|
||||
select(title: string, options: string[]): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Show a confirmation dialog.
|
||||
|
|
@ -74,9 +68,9 @@ export interface HookUIContext {
|
|||
|
||||
/**
|
||||
* Show a text input dialog.
|
||||
* @returns User input, or null if cancelled
|
||||
* @returns User input, or undefined if cancelled
|
||||
*/
|
||||
input(title: string, placeholder?: string): Promise<string | null>;
|
||||
input(title: string, placeholder?: string): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Show a notification to the user.
|
||||
|
|
@ -110,69 +104,91 @@ export interface HookEventContext {
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
// Events
|
||||
// Session Events
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base fields shared by all session events.
|
||||
*/
|
||||
interface SessionEventBase {
|
||||
type: "session";
|
||||
/** Fired on initial session load */
|
||||
export interface SessionStartEvent {
|
||||
type: "session_start";
|
||||
}
|
||||
|
||||
/**
|
||||
* Event data for session events.
|
||||
* Discriminated union based on reason.
|
||||
*
|
||||
* Lifecycle:
|
||||
* - start: Initial session load
|
||||
* - before_switch / switch: Session switch (e.g., /resume command)
|
||||
* - before_new / new: New session (e.g., /new command)
|
||||
* - before_branch / branch: Session branch (e.g., /branch command)
|
||||
* - before_compact / compact: Before/after context compaction
|
||||
* - shutdown: Process exit (SIGINT/SIGTERM)
|
||||
*
|
||||
* "before_*" events fire before the action and can be cancelled via SessionEventResult.
|
||||
* Other events fire after the action completes.
|
||||
*/
|
||||
/** Fired before switching to another session (can be cancelled) */
|
||||
export interface SessionBeforeSwitchEvent {
|
||||
type: "session_before_switch";
|
||||
/** Session file we're switching to */
|
||||
targetSessionFile: string;
|
||||
}
|
||||
|
||||
/** Fired after switching to another session */
|
||||
export interface SessionSwitchEvent {
|
||||
type: "session_switch";
|
||||
/** Session file we came from */
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired before creating a new session (can be cancelled) */
|
||||
export interface SessionBeforeNewEvent {
|
||||
type: "session_before_new";
|
||||
}
|
||||
|
||||
/** Fired after creating a new session */
|
||||
export interface SessionNewEvent {
|
||||
type: "session_new";
|
||||
}
|
||||
|
||||
/** Fired before branching a session (can be cancelled) */
|
||||
export interface SessionBeforeBranchEvent {
|
||||
type: "session_before_branch";
|
||||
/** Index of the entry in the session (SessionManager.getEntries()) to branch from */
|
||||
entryIndex: number;
|
||||
}
|
||||
|
||||
/** Fired after branching a session */
|
||||
export interface SessionBranchEvent {
|
||||
type: "session_branch";
|
||||
previousSessionFile: string | undefined;
|
||||
}
|
||||
|
||||
/** Fired before context compaction (can be cancelled or customized) */
|
||||
export interface SessionBeforeCompactEvent {
|
||||
type: "session_before_compact";
|
||||
/** Compaction preparation with cut point, messages to summarize/keep, etc. */
|
||||
preparation: CompactionPreparation;
|
||||
/** Previous compaction entries, newest first. Use for iterative summarization. */
|
||||
previousCompactions: CompactionEntry[];
|
||||
/** Optional user-provided instructions for the summary */
|
||||
customInstructions?: string;
|
||||
/** Current model */
|
||||
model: Model<any>;
|
||||
/** Abort signal - hooks should pass this to LLM calls and check it periodically */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
/** Fired after context compaction */
|
||||
export interface SessionCompactEvent {
|
||||
type: "session_compact";
|
||||
compactionEntry: CompactionEntry;
|
||||
/** Whether the compaction entry was provided by a hook */
|
||||
fromHook: boolean;
|
||||
}
|
||||
|
||||
/** Fired on process exit (SIGINT/SIGTERM) */
|
||||
export interface SessionShutdownEvent {
|
||||
type: "session_shutdown";
|
||||
}
|
||||
|
||||
/** Union of all session event types */
|
||||
export type SessionEvent =
|
||||
| (SessionEventBase & {
|
||||
reason: "start" | "new" | "before_new" | "shutdown";
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "before_switch";
|
||||
/** Session file we're switching to */
|
||||
targetSessionFile: string;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "switch";
|
||||
/** Session file we came from */
|
||||
previousSessionFile: string | null;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "branch" | "before_branch";
|
||||
/** Index of the turn to branch from */
|
||||
targetTurnIndex: number;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "before_compact";
|
||||
/** Compaction preparation with cut point, messages to summarize/keep, etc. */
|
||||
preparation: CompactionPreparation;
|
||||
/** Previous compaction entries, newest first. Use for iterative summarization. */
|
||||
previousCompactions: CompactionEntry[];
|
||||
/** Optional user-provided instructions for the summary */
|
||||
customInstructions?: string;
|
||||
/** Current model */
|
||||
model: Model<any>;
|
||||
/** Abort signal - hooks should pass this to LLM calls and check it periodically */
|
||||
signal: AbortSignal;
|
||||
})
|
||||
| (SessionEventBase & {
|
||||
reason: "compact";
|
||||
compactionEntry: CompactionEntry;
|
||||
/** Whether the compaction entry was provided by a hook */
|
||||
fromHook: boolean;
|
||||
});
|
||||
| SessionStartEvent
|
||||
| SessionBeforeSwitchEvent
|
||||
| SessionSwitchEvent
|
||||
| SessionBeforeNewEvent
|
||||
| SessionNewEvent
|
||||
| SessionBeforeBranchEvent
|
||||
| SessionBranchEvent
|
||||
| SessionBeforeCompactEvent
|
||||
| SessionCompactEvent
|
||||
| SessionShutdownEvent;
|
||||
|
||||
/**
|
||||
* Event data for context event.
|
||||
|
|
@ -408,16 +424,45 @@ export interface BeforeAgentStartEventResult {
|
|||
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for session event handlers.
|
||||
* Allows hooks to cancel "before_*" actions.
|
||||
*/
|
||||
export interface SessionEventResult {
|
||||
/** If true, cancel the pending action (switch, clear, or branch) */
|
||||
/** Return type for session_before_switch handlers */
|
||||
export interface SessionBeforeSwitchResult {
|
||||
/** If true, cancel the switch */
|
||||
cancel?: boolean;
|
||||
/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */
|
||||
}
|
||||
|
||||
/** Return type for session_before_new handlers */
|
||||
export interface SessionBeforeNewResult {
|
||||
/** If true, cancel the new session */
|
||||
cancel?: boolean;
|
||||
}
|
||||
|
||||
/** Return type for session_before_branch handlers */
|
||||
export interface SessionBeforeBranchResult {
|
||||
/**
|
||||
* If true, abort the branch entirely. No new session file is created,
|
||||
* conversation stays unchanged.
|
||||
*/
|
||||
cancel?: boolean;
|
||||
/**
|
||||
* If true, the branch proceeds (new session file created, session state updated)
|
||||
* but the in-memory conversation is NOT rewound to the branch point.
|
||||
*
|
||||
* Use case: git-checkpoint hook that restores code state separately.
|
||||
* The hook handles state restoration itself, so it doesn't want the
|
||||
* agent's conversation to be rewound (which would lose recent context).
|
||||
*
|
||||
* - `cancel: true` → nothing happens, user stays in current session
|
||||
* - `skipConversationRestore: true` → branch happens, but messages stay as-is
|
||||
* - neither → branch happens AND messages rewind to branch point (default)
|
||||
*/
|
||||
skipConversationRestore?: boolean;
|
||||
/** Custom compaction result (for before_compact event) - SessionManager adds id/parentId */
|
||||
}
|
||||
|
||||
/** Return type for session_before_compact handlers */
|
||||
export interface SessionBeforeCompactResult {
|
||||
/** If true, cancel the compaction */
|
||||
cancel?: boolean;
|
||||
/** Custom compaction result - SessionManager adds id/parentId */
|
||||
compaction?: CompactionResult;
|
||||
}
|
||||
|
||||
|
|
@ -427,8 +472,10 @@ export interface SessionEventResult {
|
|||
|
||||
/**
|
||||
* Handler function type for each event.
|
||||
* Handlers can return R, undefined, or void (bare return statements).
|
||||
*/
|
||||
export type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements in handlers
|
||||
export type HookHandler<E, R = undefined> = (event: E, ctx: HookEventContext) => Promise<R | void> | R | void;
|
||||
|
||||
export interface HookMessageRenderOptions {
|
||||
/** Whether the view is expanded */
|
||||
|
|
@ -443,7 +490,7 @@ export type HookMessageRenderer<T = unknown> = (
|
|||
message: HookMessage<T>,
|
||||
options: HookMessageRenderOptions,
|
||||
theme: Theme,
|
||||
) => Component | null;
|
||||
) => Component | undefined;
|
||||
|
||||
/**
|
||||
* Context passed to hook command handlers.
|
||||
|
|
@ -478,21 +525,30 @@ export interface RegisteredCommand {
|
|||
* Hooks use pi.on() to subscribe to events and pi.sendMessage() to inject messages.
|
||||
*/
|
||||
export interface HookAPI {
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
on(event: "session", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult | void>): void;
|
||||
// Session events
|
||||
on(event: "session_start", handler: HookHandler<SessionStartEvent>): void;
|
||||
on(event: "session_before_switch", handler: HookHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;
|
||||
on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
|
||||
on(event: "session_before_new", handler: HookHandler<SessionBeforeNewEvent, SessionBeforeNewResult>): void;
|
||||
on(event: "session_new", handler: HookHandler<SessionNewEvent>): void;
|
||||
on(event: "session_before_branch", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;
|
||||
on(event: "session_branch", handler: HookHandler<SessionBranchEvent>): void;
|
||||
on(
|
||||
event: "before_agent_start",
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||
handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult | void>,
|
||||
event: "session_before_compact",
|
||||
handler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
||||
): void;
|
||||
on(event: "session_compact", handler: HookHandler<SessionCompactEvent>): void;
|
||||
on(event: "session_shutdown", handler: HookHandler<SessionShutdownEvent>): void;
|
||||
|
||||
// Context and agent events
|
||||
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult>): void;
|
||||
on(event: "before_agent_start", handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
|
||||
on(event: "agent_start", handler: HookHandler<AgentStartEvent>): void;
|
||||
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
||||
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
||||
on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
|
||||
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;
|
||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
||||
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;
|
||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult>): void;
|
||||
|
||||
/**
|
||||
* Send a custom message to the session. Creates a CustomMessageEntry that
|
||||
|
|
@ -545,7 +601,7 @@ export interface HookAPI {
|
|||
/**
|
||||
* Register a custom renderer for CustomMessageEntry with a specific customType.
|
||||
* The renderer is called when rendering the entry in the TUI.
|
||||
* Return null to use the default renderer.
|
||||
* Return nothing to use the default renderer.
|
||||
*/
|
||||
registerMessageRenderer<T = unknown>(customType: string, renderer: HookMessageRenderer<T>): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export interface BashExecutionMessage {
|
|||
role: "bashExecution";
|
||||
command: string;
|
||||
output: string;
|
||||
exitCode: number | null;
|
||||
exitCode: number | undefined;
|
||||
cancelled: boolean;
|
||||
truncated: boolean;
|
||||
fullOutputPath?: string;
|
||||
|
|
@ -86,7 +86,7 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
|
|||
}
|
||||
if (msg.cancelled) {
|
||||
text += "\n\n(command cancelled)";
|
||||
} else if (msg.exitCode !== null && msg.exitCode !== 0) {
|
||||
} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
|
||||
text += `\n\nCommand exited with code ${msg.exitCode}`;
|
||||
}
|
||||
if (msg.truncated && msg.fullOutputPath) {
|
||||
|
|
@ -145,7 +145,7 @@ export function createHookMessage(
|
|||
*/
|
||||
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
||||
return messages
|
||||
.map((m): Message | null => {
|
||||
.map((m): Message | undefined => {
|
||||
switch (m.role) {
|
||||
case "bashExecution":
|
||||
return {
|
||||
|
|
@ -182,8 +182,8 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|||
default:
|
||||
// biome-ignore lint/correctness/noSwitchDeclarations: fine
|
||||
const _exhaustiveCheck: never = m;
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((m) => m !== null);
|
||||
.filter((m) => m !== undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,11 +90,11 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|||
export class ModelRegistry {
|
||||
private models: Model<Api>[] = [];
|
||||
private customProviderApiKeys: Map<string, string> = new Map();
|
||||
private loadError: string | null = null;
|
||||
private loadError: string | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
readonly authStorage: AuthStorage,
|
||||
private modelsJsonPath: string | null = null,
|
||||
private modelsJsonPath: string | undefined = undefined,
|
||||
) {
|
||||
// Set up fallback resolver for custom provider API keys
|
||||
this.authStorage.setFallbackResolver((provider) => {
|
||||
|
|
@ -114,14 +114,14 @@ export class ModelRegistry {
|
|||
*/
|
||||
refresh(): void {
|
||||
this.customProviderApiKeys.clear();
|
||||
this.loadError = null;
|
||||
this.loadError = undefined;
|
||||
this.loadModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any error from loading models.json (null if no error).
|
||||
* Get any error from loading models.json (undefined if no error).
|
||||
*/
|
||||
getError(): string | null {
|
||||
getError(): string | undefined {
|
||||
return this.loadError;
|
||||
}
|
||||
|
||||
|
|
@ -160,9 +160,9 @@ export class ModelRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
private loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | null } {
|
||||
private loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | undefined } {
|
||||
if (!existsSync(modelsJsonPath)) {
|
||||
return { models: [], error: null };
|
||||
return { models: [], error: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -186,7 +186,7 @@ export class ModelRegistry {
|
|||
this.validateConfig(config);
|
||||
|
||||
// Parse models
|
||||
return { models: this.parseModels(config), error: null };
|
||||
return { models: this.parseModels(config), error: undefined };
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
|
|
@ -294,14 +294,14 @@ export class ModelRegistry {
|
|||
/**
|
||||
* Find a model by provider and ID.
|
||||
*/
|
||||
find(provider: string, modelId: string): Model<Api> | null {
|
||||
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;
|
||||
find(provider: string, modelId: string): Model<Api> | undefined {
|
||||
return this.models.find((m) => m.provider === provider && m.id === modelId) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key for a model.
|
||||
*/
|
||||
async getApiKey(model: Model<Api>): Promise<string | null> {
|
||||
async getApiKey(model: Model<Api>): Promise<string | undefined> {
|
||||
return this.authStorage.getApiKey(model.provider);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ function isAlias(id: string): boolean {
|
|||
|
||||
/**
|
||||
* Try to match a pattern to a model from the available models list.
|
||||
* Returns the matched model or null if no match found.
|
||||
* Returns the matched model or undefined if no match found.
|
||||
*/
|
||||
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | null {
|
||||
function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
||||
// Check for provider/modelId format (provider is everything before the first /)
|
||||
const slashIndex = modelPattern.indexOf("/");
|
||||
if (slashIndex !== -1) {
|
||||
|
|
@ -75,7 +75,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Separate into aliases and dated versions
|
||||
|
|
@ -94,9 +94,9 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
|
|||
}
|
||||
|
||||
export interface ParsedModelResult {
|
||||
model: Model<Api> | null;
|
||||
model: Model<Api> | undefined;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
warning: string | null;
|
||||
warning: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,14 +116,14 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
|
|||
// Try exact match first
|
||||
const exactMatch = tryMatchModel(pattern, availableModels);
|
||||
if (exactMatch) {
|
||||
return { model: exactMatch, thinkingLevel: "off", warning: null };
|
||||
return { model: exactMatch, thinkingLevel: "off", warning: undefined };
|
||||
}
|
||||
|
||||
// No match - try splitting on last colon if present
|
||||
const lastColonIndex = pattern.lastIndexOf(":");
|
||||
if (lastColonIndex === -1) {
|
||||
// No colons, pattern simply doesn't match any model
|
||||
return { model: null, thinkingLevel: "off", warning: null };
|
||||
return { model: undefined, thinkingLevel: "off", warning: undefined };
|
||||
}
|
||||
|
||||
const prefix = pattern.substring(0, lastColonIndex);
|
||||
|
|
@ -193,9 +193,9 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
|||
}
|
||||
|
||||
export interface InitialModelResult {
|
||||
model: Model<Api> | null;
|
||||
model: Model<Api> | undefined;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
fallbackMessage: string | null;
|
||||
fallbackMessage: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -227,7 +227,7 @@ export async function findInitialModel(options: {
|
|||
modelRegistry,
|
||||
} = options;
|
||||
|
||||
let model: Model<Api> | null = null;
|
||||
let model: Model<Api> | undefined;
|
||||
let thinkingLevel: ThinkingLevel = "off";
|
||||
|
||||
// 1. CLI args take priority
|
||||
|
|
@ -237,7 +237,7 @@ export async function findInitialModel(options: {
|
|||
console.error(chalk.red(`Model ${cliProvider}/${cliModel} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
return { model: found, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: found, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 2. Use first model from scoped models (skip if continuing/resuming)
|
||||
|
|
@ -245,7 +245,7 @@ export async function findInitialModel(options: {
|
|||
return {
|
||||
model: scopedModels[0].model,
|
||||
thinkingLevel: scopedModels[0].thinkingLevel,
|
||||
fallbackMessage: null,
|
||||
fallbackMessage: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ export async function findInitialModel(options: {
|
|||
if (defaultThinkingLevel) {
|
||||
thinkingLevel = defaultThinkingLevel;
|
||||
}
|
||||
return { model, thinkingLevel, fallbackMessage: null };
|
||||
return { model, thinkingLevel, fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,16 +270,16 @@ export async function findInitialModel(options: {
|
|||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
|
||||
if (match) {
|
||||
return { model: match, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// If no default found, use first available
|
||||
return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: availableModels[0], thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// 5. No model found
|
||||
return { model: null, thinkingLevel: "off", fallbackMessage: null };
|
||||
return { model: undefined, thinkingLevel: "off", fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,10 +288,10 @@ export async function findInitialModel(options: {
|
|||
export async function restoreModelFromSession(
|
||||
savedProvider: string,
|
||||
savedModelId: string,
|
||||
currentModel: Model<Api> | null,
|
||||
currentModel: Model<Api> | undefined,
|
||||
shouldPrintMessages: boolean,
|
||||
modelRegistry: ModelRegistry,
|
||||
): Promise<{ model: Model<Api> | null; fallbackMessage: string | null }> {
|
||||
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
||||
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
||||
|
||||
// Check if restored model exists and has a valid API key
|
||||
|
|
@ -301,7 +301,7 @@ export async function restoreModelFromSession(
|
|||
if (shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));
|
||||
}
|
||||
return { model: restoredModel, fallbackMessage: null };
|
||||
return { model: restoredModel, fallbackMessage: undefined };
|
||||
}
|
||||
|
||||
// Model not found or no API key - fall back
|
||||
|
|
@ -327,7 +327,7 @@ export async function restoreModelFromSession(
|
|||
|
||||
if (availableModels.length > 0) {
|
||||
// Try to find a default model from known providers
|
||||
let fallbackModel: Model<Api> | null = null;
|
||||
let fallbackModel: Model<Api> | undefined;
|
||||
for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
|
||||
const defaultId = defaultModelPerProvider[provider];
|
||||
const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
|
||||
|
|
@ -353,5 +353,5 @@ export async function restoreModelFromSession(
|
|||
}
|
||||
|
||||
// No models available
|
||||
return { model: null, fallbackMessage: null };
|
||||
return { model: undefined, fallbackMessage: undefined };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||
customToolsResult = result;
|
||||
}
|
||||
|
||||
let hookRunner: HookRunner | null = null;
|
||||
let hookRunner: HookRunner | undefined;
|
||||
if (options.hooks !== undefined) {
|
||||
if (options.hooks.length > 0) {
|
||||
const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ export class SessionManager {
|
|||
private labelsById: Map<string, string> = new Map();
|
||||
private leafId: string = "";
|
||||
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | null, persist: boolean) {
|
||||
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
|
||||
this.cwd = cwd;
|
||||
this.sessionDir = sessionDir;
|
||||
this.persist = persist;
|
||||
|
|
@ -484,7 +484,7 @@ export class SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
newSession(): void {
|
||||
newSession(): string | undefined {
|
||||
this.sessionId = randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const header: SessionHeader = {
|
||||
|
|
@ -503,6 +503,7 @@ export class SessionManager {
|
|||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
||||
}
|
||||
return this.sessionFile;
|
||||
}
|
||||
|
||||
private _buildIndex(): void {
|
||||
|
|
@ -841,9 +842,9 @@ export class SessionManager {
|
|||
/**
|
||||
* Create a new session file containing only the path from root to the specified leaf.
|
||||
* Useful for extracting a single conversation path from a branched session.
|
||||
* Returns the new session file path, or null if not persisting.
|
||||
* Returns the new session file path, or undefined if not persisting.
|
||||
*/
|
||||
createBranchedSession(leafId: string): string | null {
|
||||
createBranchedSession(leafId: string): string | undefined {
|
||||
const path = this.getPath(leafId);
|
||||
if (path.length === 0) {
|
||||
throw new Error(`Entry ${leafId} not found`);
|
||||
|
|
@ -883,6 +884,7 @@ export class SessionManager {
|
|||
// Write fresh label entries at the end
|
||||
const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
||||
let parentId = lastEntryId;
|
||||
const labelEntries: LabelEntry[] = [];
|
||||
for (const { targetId, label } of labelsToWrite) {
|
||||
const labelEntry: LabelEntry = {
|
||||
type: "label",
|
||||
|
|
@ -894,8 +896,12 @@ export class SessionManager {
|
|||
};
|
||||
appendFileSync(newSessionFile, `${JSON.stringify(labelEntry)}\n`);
|
||||
pathEntryIds.add(labelEntry.id);
|
||||
labelEntries.push(labelEntry);
|
||||
parentId = labelEntry.id;
|
||||
}
|
||||
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
||||
this.sessionId = newSessionId;
|
||||
this._buildIndex();
|
||||
return newSessionFile;
|
||||
}
|
||||
|
||||
|
|
@ -917,7 +923,7 @@ export class SessionManager {
|
|||
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
||||
this.sessionId = newSessionId;
|
||||
this._buildIndex();
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -927,7 +933,7 @@ export class SessionManager {
|
|||
*/
|
||||
static create(cwd: string, sessionDir?: string): SessionManager {
|
||||
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
||||
return new SessionManager(cwd, dir, null, true);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -956,12 +962,12 @@ export class SessionManager {
|
|||
if (mostRecent) {
|
||||
return new SessionManager(cwd, dir, mostRecent, true);
|
||||
}
|
||||
return new SessionManager(cwd, dir, null, true);
|
||||
return new SessionManager(cwd, dir, undefined, true);
|
||||
}
|
||||
|
||||
/** Create an in-memory session (no file persistence) */
|
||||
static inMemory(cwd: string = process.cwd()): SessionManager {
|
||||
return new SessionManager(cwd, "", null, false);
|
||||
return new SessionManager(cwd, "", undefined, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -38,33 +38,7 @@ export type {
|
|||
ToolUIContext,
|
||||
} from "./core/custom-tools/index.js";
|
||||
export { discoverAndLoadCustomTools, loadCustomTools } from "./core/custom-tools/index.js";
|
||||
export type {
|
||||
AgentEndEvent,
|
||||
AgentStartEvent,
|
||||
BashToolResultEvent,
|
||||
BeforeAgentStartEvent,
|
||||
BeforeAgentStartEventResult,
|
||||
CustomToolResultEvent,
|
||||
EditToolResultEvent,
|
||||
FindToolResultEvent,
|
||||
GrepToolResultEvent,
|
||||
HookAPI,
|
||||
HookEvent,
|
||||
HookEventContext,
|
||||
HookFactory,
|
||||
HookUIContext,
|
||||
LsToolResultEvent,
|
||||
ReadToolResultEvent,
|
||||
SessionEvent,
|
||||
SessionEventResult,
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
ToolResultEvent,
|
||||
ToolResultEventResult,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
WriteToolResultEvent,
|
||||
} from "./core/hooks/index.js";
|
||||
export type * from "./core/hooks/index.js";
|
||||
// Hook system types and type guards
|
||||
export {
|
||||
isBashToolResult,
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"
|
|||
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
|
||||
import { ensureTool } from "./utils/tools-manager.js";
|
||||
|
||||
async function checkForNewVersion(currentVersion: string): Promise<string | null> {
|
||||
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch("https://registry.npmjs.org/@mariozechner/pi -coding-agent/latest");
|
||||
if (!response.ok) return null;
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = (await response.json()) as { version?: string };
|
||||
const latestVersion = data.version;
|
||||
|
|
@ -45,26 +45,26 @@ async function checkForNewVersion(currentVersion: string): Promise<string | null
|
|||
return latestVersion;
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function runInteractiveMode(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
changelogMarkdown: string | null,
|
||||
changelogMarkdown: string | undefined,
|
||||
modelFallbackMessage: string | undefined,
|
||||
modelsJsonError: string | null,
|
||||
modelsJsonError: string | undefined,
|
||||
migratedProviders: string[],
|
||||
versionCheckPromise: Promise<string | null>,
|
||||
versionCheckPromise: Promise<string | undefined>,
|
||||
initialMessages: string[],
|
||||
customTools: LoadedCustomTool[],
|
||||
setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void,
|
||||
initialMessage?: string,
|
||||
initialImages?: ImageContent[],
|
||||
fdPath: string | null = null,
|
||||
fdPath: string | undefined = undefined,
|
||||
): Promise<void> {
|
||||
const mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath);
|
||||
|
||||
|
|
@ -143,9 +143,9 @@ async function prepareInitialMessage(parsed: Args): Promise<{
|
|||
};
|
||||
}
|
||||
|
||||
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {
|
||||
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | undefined {
|
||||
if (parsed.continue || parsed.resume) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lastVersion = settingsManager.getLastChangelogVersion();
|
||||
|
|
@ -165,10 +165,10 @@ function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager):
|
|||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createSessionManager(parsed: Args, cwd: string): SessionManager | null {
|
||||
function createSessionManager(parsed: Args, cwd: string): SessionManager | undefined {
|
||||
if (parsed.noSession) {
|
||||
return SessionManager.inMemory();
|
||||
}
|
||||
|
|
@ -183,8 +183,8 @@ function createSessionManager(parsed: Args, cwd: string): SessionManager | null
|
|||
if (parsed.sessionDir) {
|
||||
return SessionManager.create(cwd, parsed.sessionDir);
|
||||
}
|
||||
// Default case (new session) returns null, SDK will create one
|
||||
return null;
|
||||
// Default case (new session) returns undefined, SDK will create one
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
||||
|
|
@ -207,7 +207,7 @@ function discoverSystemPromptFile(): string | undefined {
|
|||
function buildSessionOptions(
|
||||
parsed: Args,
|
||||
scopedModels: ScopedModel[],
|
||||
sessionManager: SessionManager | null,
|
||||
sessionManager: SessionManager | undefined,
|
||||
modelRegistry: ModelRegistry,
|
||||
): CreateAgentSessionOptions {
|
||||
const options: CreateAgentSessionOptions = {};
|
||||
|
|
@ -408,7 +408,7 @@ export async function main(args: string[]) {
|
|||
if (mode === "rpc") {
|
||||
await runRpcMode(session);
|
||||
} else if (isInteractive) {
|
||||
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);
|
||||
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
||||
const changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);
|
||||
|
||||
if (scopedModels.length > 0) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class BashExecutionComponent extends Container {
|
|||
private command: string;
|
||||
private outputLines: string[] = [];
|
||||
private status: "running" | "complete" | "cancelled" | "error" = "running";
|
||||
private exitCode: number | null = null;
|
||||
private exitCode: number | undefined = undefined;
|
||||
private loader: Loader;
|
||||
private truncationResult?: TruncationResult;
|
||||
private fullOutputPath?: string;
|
||||
|
|
@ -90,13 +90,17 @@ export class BashExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
setComplete(
|
||||
exitCode: number | null,
|
||||
exitCode: number | undefined,
|
||||
cancelled: boolean,
|
||||
truncationResult?: TruncationResult,
|
||||
fullOutputPath?: string,
|
||||
): void {
|
||||
this.exitCode = exitCode;
|
||||
this.status = cancelled ? "cancelled" : exitCode !== 0 && exitCode !== null ? "error" : "complete";
|
||||
this.status = cancelled
|
||||
? "cancelled"
|
||||
: exitCode !== 0 && exitCode !== undefined && exitCode !== null
|
||||
? "error"
|
||||
: "complete";
|
||||
this.truncationResult = truncationResult;
|
||||
this.fullOutputPath = fullOutputPath;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,18 +36,18 @@ export class ModelSelectorComponent extends Container {
|
|||
private allModels: ModelItem[] = [];
|
||||
private filteredModels: ModelItem[] = [];
|
||||
private selectedIndex: number = 0;
|
||||
private currentModel: Model<any> | null;
|
||||
private currentModel?: Model<any>;
|
||||
private settingsManager: SettingsManager;
|
||||
private modelRegistry: ModelRegistry;
|
||||
private onSelectCallback: (model: Model<any>) => void;
|
||||
private onCancelCallback: () => void;
|
||||
private errorMessage: string | null = null;
|
||||
private errorMessage?: string;
|
||||
private tui: TUI;
|
||||
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
||||
|
||||
constructor(
|
||||
tui: TUI,
|
||||
currentModel: Model<any> | null,
|
||||
currentModel: Model<any> | undefined,
|
||||
settingsManager: SettingsManager,
|
||||
modelRegistry: ModelRegistry,
|
||||
scopedModels: ReadonlyArray<ScopedModelItem>,
|
||||
|
|
|
|||
|
|
@ -67,14 +67,14 @@ export class InteractiveMode {
|
|||
private version: string;
|
||||
private isInitialized = false;
|
||||
private onInputCallback?: (text: string) => void;
|
||||
private loadingAnimation: Loader | null = null;
|
||||
private loadingAnimation: Loader | undefined = undefined;
|
||||
|
||||
private lastSigintTime = 0;
|
||||
private lastEscapeTime = 0;
|
||||
private changelogMarkdown: string | null = null;
|
||||
private changelogMarkdown: string | undefined = undefined;
|
||||
|
||||
// Streaming message tracking
|
||||
private streamingComponent: AssistantMessageComponent | null = null;
|
||||
private streamingComponent: AssistantMessageComponent | undefined = undefined;
|
||||
|
||||
// Tool execution tracking: toolCallId -> component
|
||||
private pendingTools = new Map<string, ToolExecutionComponent>();
|
||||
|
|
@ -92,22 +92,22 @@ export class InteractiveMode {
|
|||
private isBashMode = false;
|
||||
|
||||
// Track current bash execution component
|
||||
private bashComponent: BashExecutionComponent | null = null;
|
||||
private bashComponent: BashExecutionComponent | undefined = undefined;
|
||||
|
||||
// Track pending bash components (shown in pending area, moved to chat on submit)
|
||||
private pendingBashComponents: BashExecutionComponent[] = [];
|
||||
|
||||
// Auto-compaction state
|
||||
private autoCompactionLoader: Loader | null = null;
|
||||
private autoCompactionLoader: Loader | undefined = undefined;
|
||||
private autoCompactionEscapeHandler?: () => void;
|
||||
|
||||
// Auto-retry state
|
||||
private retryLoader: Loader | null = null;
|
||||
private retryLoader: Loader | undefined = undefined;
|
||||
private retryEscapeHandler?: () => void;
|
||||
|
||||
// Hook UI state
|
||||
private hookSelector: HookSelectorComponent | null = null;
|
||||
private hookInput: HookInputComponent | null = null;
|
||||
private hookSelector: HookSelectorComponent | undefined = undefined;
|
||||
private hookInput: HookInputComponent | undefined = undefined;
|
||||
|
||||
// Custom tools for custom rendering
|
||||
private customTools: Map<string, LoadedCustomTool>;
|
||||
|
|
@ -126,10 +126,10 @@ export class InteractiveMode {
|
|||
constructor(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
changelogMarkdown: string | null = null,
|
||||
changelogMarkdown: string | undefined = undefined,
|
||||
customTools: LoadedCustomTool[] = [],
|
||||
private setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void = () => {},
|
||||
fdPath: string | null = null,
|
||||
fdPath: string | undefined = undefined,
|
||||
) {
|
||||
this.session = session;
|
||||
this.version = version;
|
||||
|
|
@ -350,7 +350,7 @@ export class InteractiveMode {
|
|||
await this.emitToolSessionEvent({
|
||||
entries,
|
||||
sessionFile: this.session.sessionFile,
|
||||
previousSessionFile: null,
|
||||
previousSessionFile: undefined,
|
||||
reason: "start",
|
||||
});
|
||||
|
||||
|
|
@ -395,10 +395,9 @@ export class InteractiveMode {
|
|||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
|
||||
// Emit session event
|
||||
// Emit session_start event
|
||||
await hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "start",
|
||||
type: "session_start",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +441,7 @@ export class InteractiveMode {
|
|||
/**
|
||||
* Show a selector for hooks.
|
||||
*/
|
||||
private showHookSelector(title: string, options: string[]): Promise<string | null> {
|
||||
private showHookSelector(title: string, options: string[]): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
this.hookSelector = new HookSelectorComponent(
|
||||
title,
|
||||
|
|
@ -453,7 +452,7 @@ export class InteractiveMode {
|
|||
},
|
||||
() => {
|
||||
this.hideHookSelector();
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -470,7 +469,7 @@ export class InteractiveMode {
|
|||
private hideHookSelector(): void {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
this.hookSelector = null;
|
||||
this.hookSelector = undefined;
|
||||
this.ui.setFocus(this.editor);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -486,7 +485,7 @@ export class InteractiveMode {
|
|||
/**
|
||||
* Show a text input for hooks.
|
||||
*/
|
||||
private showHookInput(title: string, placeholder?: string): Promise<string | null> {
|
||||
private showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
this.hookInput = new HookInputComponent(
|
||||
title,
|
||||
|
|
@ -497,7 +496,7 @@ export class InteractiveMode {
|
|||
},
|
||||
() => {
|
||||
this.hideHookInput();
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -514,7 +513,7 @@ export class InteractiveMode {
|
|||
private hideHookInput(): void {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
this.hookInput = null;
|
||||
this.hookInput = undefined;
|
||||
this.ui.setFocus(this.editor);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -874,7 +873,7 @@ export class InteractiveMode {
|
|||
}
|
||||
this.pendingTools.clear();
|
||||
}
|
||||
this.streamingComponent = null;
|
||||
this.streamingComponent = undefined;
|
||||
this.footer.invalidate();
|
||||
}
|
||||
this.ui.requestRender();
|
||||
|
|
@ -920,12 +919,12 @@ export class InteractiveMode {
|
|||
case "agent_end":
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
this.loadingAnimation = undefined;
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
if (this.streamingComponent) {
|
||||
this.chatContainer.removeChild(this.streamingComponent);
|
||||
this.streamingComponent = null;
|
||||
this.streamingComponent = undefined;
|
||||
}
|
||||
this.pendingTools.clear();
|
||||
this.ui.requestRender();
|
||||
|
|
@ -964,7 +963,7 @@ export class InteractiveMode {
|
|||
// Stop loader
|
||||
if (this.autoCompactionLoader) {
|
||||
this.autoCompactionLoader.stop();
|
||||
this.autoCompactionLoader = null;
|
||||
this.autoCompactionLoader = undefined;
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
// Handle result
|
||||
|
|
@ -1018,7 +1017,7 @@ export class InteractiveMode {
|
|||
// Stop loader
|
||||
if (this.retryLoader) {
|
||||
this.retryLoader.stop();
|
||||
this.retryLoader = null;
|
||||
this.retryLoader = undefined;
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
// Show error only on final failure (success shows normal response)
|
||||
|
|
@ -1228,10 +1227,9 @@ export class InteractiveMode {
|
|||
private async shutdown(): Promise<void> {
|
||||
// Emit shutdown event to hooks
|
||||
const hookRunner = this.session.hookRunner;
|
||||
if (hookRunner?.hasHandlers("session")) {
|
||||
if (hookRunner?.hasHandlers("session_shutdown")) {
|
||||
await hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "shutdown",
|
||||
type: "session_shutdown",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1265,7 +1263,7 @@ export class InteractiveMode {
|
|||
|
||||
private cycleThinkingLevel(): void {
|
||||
const newLevel = this.session.cycleThinkingLevel();
|
||||
if (newLevel === null) {
|
||||
if (newLevel === undefined) {
|
||||
this.showStatus("Current model does not support thinking");
|
||||
} else {
|
||||
this.footer.updateState(this.session.state);
|
||||
|
|
@ -1277,7 +1275,7 @@ export class InteractiveMode {
|
|||
private async cycleModel(direction: "forward" | "backward"): Promise<void> {
|
||||
try {
|
||||
const result = await this.session.cycleModel(direction);
|
||||
if (result === null) {
|
||||
if (result === undefined) {
|
||||
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
||||
this.showStatus(msg);
|
||||
} else {
|
||||
|
|
@ -1612,13 +1610,13 @@ export class InteractiveMode {
|
|||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Clear UI state
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.streamingComponent = null;
|
||||
this.streamingComponent = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
// Switch session via AgentSession (emits hook and tool session events)
|
||||
|
|
@ -1874,7 +1872,7 @@ export class InteractiveMode {
|
|||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
|
|
@ -1884,7 +1882,7 @@ export class InteractiveMode {
|
|||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.streamingComponent = null;
|
||||
this.streamingComponent = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
|
|
@ -1962,12 +1960,12 @@ export class InteractiveMode {
|
|||
}
|
||||
} catch (error) {
|
||||
if (this.bashComponent) {
|
||||
this.bashComponent.setComplete(null, false);
|
||||
this.bashComponent.setComplete(undefined, false);
|
||||
}
|
||||
this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
}
|
||||
|
||||
this.bashComponent = null;
|
||||
this.bashComponent = undefined;
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -1987,7 +1985,7 @@ export class InteractiveMode {
|
|||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
|
|
@ -2039,7 +2037,7 @@ export class InteractiveMode {
|
|||
stop(): void {
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.footer.dispose();
|
||||
if (this.unsubscribe) {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ export async function runPrintMode(
|
|||
hookRunner.setAppendEntryHandler((customType, data) => {
|
||||
session.sessionManager.appendCustomEntry(customType, data);
|
||||
});
|
||||
// Emit session event
|
||||
// Emit session_start event
|
||||
await hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "start",
|
||||
type: "session_start",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +58,7 @@ export async function runPrintMode(
|
|||
await tool.onSession({
|
||||
entries,
|
||||
sessionFile: session.sessionFile,
|
||||
previousSessionFile: null,
|
||||
previousSessionFile: undefined,
|
||||
reason: "start",
|
||||
});
|
||||
} catch (_err) {
|
||||
|
|
|
|||
|
|
@ -51,17 +51,17 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
* Create a hook UI context that uses the RPC protocol.
|
||||
*/
|
||||
const createHookUIContext = (): HookUIContext => ({
|
||||
async select(title: string, options: string[]): Promise<string | null> {
|
||||
async select(title: string, options: string[]): Promise<string | undefined> {
|
||||
const id = crypto.randomUUID();
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingHookRequests.set(id, {
|
||||
resolve: (response: RpcHookUIResponse) => {
|
||||
if ("cancelled" in response && response.cancelled) {
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
} else if ("value" in response) {
|
||||
resolve(response.value);
|
||||
} else {
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
}
|
||||
},
|
||||
reject,
|
||||
|
|
@ -89,17 +89,17 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
});
|
||||
},
|
||||
|
||||
async input(title: string, placeholder?: string): Promise<string | null> {
|
||||
async input(title: string, placeholder?: string): Promise<string | undefined> {
|
||||
const id = crypto.randomUUID();
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingHookRequests.set(id, {
|
||||
resolve: (response: RpcHookUIResponse) => {
|
||||
if ("cancelled" in response && response.cancelled) {
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
} else if ("value" in response) {
|
||||
resolve(response.value);
|
||||
} else {
|
||||
resolve(null);
|
||||
resolve(undefined);
|
||||
}
|
||||
},
|
||||
reject,
|
||||
|
|
@ -144,10 +144,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
hookRunner.setAppendEntryHandler((customType, data) => {
|
||||
session.sessionManager.appendCustomEntry(customType, data);
|
||||
});
|
||||
// Emit session event
|
||||
// Emit session_start event
|
||||
await hookRunner.emit({
|
||||
type: "session",
|
||||
reason: "start",
|
||||
type: "session_start",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +158,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
await tool.onSession({
|
||||
entries,
|
||||
sessionFile: session.sessionFile,
|
||||
previousSessionFile: null,
|
||||
previousSessionFile: undefined,
|
||||
reason: "start",
|
||||
});
|
||||
} catch (_err) {
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ export type RpcCommand =
|
|||
// ============================================================================
|
||||
|
||||
export interface RpcSessionState {
|
||||
model: Model<any> | null;
|
||||
model?: Model<any>;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
isStreaming: boolean;
|
||||
isCompacting: boolean;
|
||||
queueMode: "all" | "one-at-a-time";
|
||||
sessionFile: string | null;
|
||||
sessionFile?: string;
|
||||
sessionId: string;
|
||||
autoCompactionEnabled: boolean;
|
||||
messageCount: number;
|
||||
|
|
|
|||
|
|
@ -184,14 +184,14 @@ async function downloadTool(tool: "fd" | "rg"): Promise<string> {
|
|||
|
||||
// Ensure a tool is available, downloading if necessary
|
||||
// Returns the path to the tool, or null if unavailable
|
||||
export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Promise<string | null> {
|
||||
export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Promise<string | undefined> {
|
||||
const existingPath = getToolPath(tool);
|
||||
if (existingPath) {
|
||||
return existingPath;
|
||||
}
|
||||
|
||||
const config = TOOLS[tool];
|
||||
if (!config) return null;
|
||||
if (!config) return undefined;
|
||||
|
||||
// Tool not found - download it
|
||||
if (!silent) {
|
||||
|
|
@ -208,6 +208,6 @@ export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Pr
|
|||
if (!silent) {
|
||||
console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
|||
createSession(true);
|
||||
|
||||
// Verify sessions are disabled
|
||||
expect(session.sessionFile).toBeNull();
|
||||
expect(session.sessionFile).toBeUndefined();
|
||||
|
||||
// Send one message
|
||||
await session.prompt("Say hi");
|
||||
|
|
@ -121,8 +121,8 @@ describe.skipIf(!API_KEY)("AgentSession branching", () => {
|
|||
// After branching, conversation should be empty
|
||||
expect(session.messages.length).toBe(0);
|
||||
|
||||
// Session file should still be null (no file created)
|
||||
expect(session.sessionFile).toBeNull();
|
||||
// Session file should still be undefined (no file created)
|
||||
expect(session.sessionFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should branch from middle of conversation", async () => {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
|
|||
|
||||
// First message should be the summary (a user message with summary content)
|
||||
const firstMsg = messages[0];
|
||||
expect(firstMsg.role).toBe("user");
|
||||
expect(firstMsg.role).toBe("compactionSummary");
|
||||
}, 120000);
|
||||
|
||||
it("should maintain valid session state after compaction", async () => {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,16 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { HookAPI } from "../src/core/hooks/index.js";
|
||||
import type { HookAPI, SessionBeforeCompactEvent, SessionCompactEvent } from "../src/core/hooks/index.js";
|
||||
|
||||
describe("Documentation example", () => {
|
||||
it("custom compaction example should type-check correctly", () => {
|
||||
// This is the example from hooks.md - verify it compiles
|
||||
const exampleHook = (pi: HookAPI) => {
|
||||
pi.on("session", async (event, ctx) => {
|
||||
if (event.reason !== "before_compact") return;
|
||||
|
||||
// After narrowing, these should all be accessible
|
||||
// sessionManager and modelRegistry come from ctx, not event
|
||||
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx) => {
|
||||
// All these should be accessible on the event
|
||||
const { preparation, previousCompactions, model } = event;
|
||||
// sessionManager and modelRegistry come from ctx, not event
|
||||
const { sessionManager, modelRegistry } = ctx;
|
||||
const { messagesToSummarize, messagesToKeep, tokensBefore, firstKeptEntryId, cutPoint } = preparation;
|
||||
|
||||
|
|
@ -51,12 +49,10 @@ describe("Documentation example", () => {
|
|||
expect(typeof exampleHook).toBe("function");
|
||||
});
|
||||
|
||||
it("compact event should have correct fields after narrowing", () => {
|
||||
it("compact event should have correct fields", () => {
|
||||
const checkCompactEvent = (pi: HookAPI) => {
|
||||
pi.on("session", async (event, _ctx) => {
|
||||
if (event.reason !== "compact") return;
|
||||
|
||||
// After narrowing, these should all be accessible
|
||||
pi.on("session_compact", async (event: SessionCompactEvent) => {
|
||||
// These should all be accessible
|
||||
const entry = event.compactionEntry;
|
||||
const fromHook = event.fromHook;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ import { getModel } from "@mariozechner/pi-ai";
|
|||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AgentSession } from "../src/core/agent-session.js";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { HookRunner, type LoadedHook, type SessionEvent } from "../src/core/hooks/index.js";
|
||||
import {
|
||||
HookRunner,
|
||||
type LoadedHook,
|
||||
type SessionBeforeCompactEvent,
|
||||
type SessionCompactEvent,
|
||||
type SessionEvent,
|
||||
} from "../src/core/hooks/index.js";
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import { SettingsManager } from "../src/core/settings-manager.js";
|
||||
|
|
@ -40,19 +46,25 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
});
|
||||
|
||||
function createHook(
|
||||
onBeforeCompact?: (event: SessionEvent) => { cancel?: boolean; compaction?: any } | undefined,
|
||||
onCompact?: (event: SessionEvent) => void,
|
||||
onBeforeCompact?: (event: SessionBeforeCompactEvent) => { cancel?: boolean; compaction?: any } | undefined,
|
||||
onCompact?: (event: SessionCompactEvent) => void,
|
||||
): LoadedHook {
|
||||
const handlers = new Map<string, ((event: any, ctx: any) => Promise<any>)[]>();
|
||||
|
||||
handlers.set("session", [
|
||||
async (event: SessionEvent) => {
|
||||
handlers.set("session_before_compact", [
|
||||
async (event: SessionBeforeCompactEvent) => {
|
||||
capturedEvents.push(event);
|
||||
|
||||
if (event.reason === "before_compact" && onBeforeCompact) {
|
||||
if (onBeforeCompact) {
|
||||
return onBeforeCompact(event);
|
||||
}
|
||||
if (event.reason === "compact" && onCompact) {
|
||||
return undefined;
|
||||
},
|
||||
]);
|
||||
|
||||
handlers.set("session_compact", [
|
||||
async (event: SessionCompactEvent) => {
|
||||
capturedEvents.push(event);
|
||||
if (onCompact) {
|
||||
onCompact(event);
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -89,9 +101,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
hookRunner = new HookRunner(hooks, tempDir, sessionManager, modelRegistry);
|
||||
hookRunner.setUIContext(
|
||||
{
|
||||
select: async () => null,
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
},
|
||||
|
|
@ -121,30 +133,28 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
|
||||
await session.compact();
|
||||
|
||||
const beforeCompactEvents = capturedEvents.filter((e) => e.reason === "before_compact");
|
||||
const compactEvents = capturedEvents.filter((e) => e.reason === "compact");
|
||||
const beforeCompactEvents = capturedEvents.filter(
|
||||
(e): e is SessionBeforeCompactEvent => e.type === "session_before_compact",
|
||||
);
|
||||
const compactEvents = capturedEvents.filter((e): e is SessionCompactEvent => e.type === "session_compact");
|
||||
|
||||
expect(beforeCompactEvents.length).toBe(1);
|
||||
expect(compactEvents.length).toBe(1);
|
||||
|
||||
const beforeEvent = beforeCompactEvents[0];
|
||||
if (beforeEvent.reason === "before_compact") {
|
||||
expect(beforeEvent.preparation).toBeDefined();
|
||||
expect(beforeEvent.preparation.cutPoint.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(beforeEvent.preparation.messagesToSummarize).toBeDefined();
|
||||
expect(beforeEvent.preparation.messagesToKeep).toBeDefined();
|
||||
expect(beforeEvent.preparation.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(beforeEvent.model).toBeDefined();
|
||||
// sessionManager and modelRegistry are now on ctx, not event
|
||||
}
|
||||
expect(beforeEvent.preparation).toBeDefined();
|
||||
expect(beforeEvent.preparation.cutPoint.firstKeptEntryIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(beforeEvent.preparation.messagesToSummarize).toBeDefined();
|
||||
expect(beforeEvent.preparation.messagesToKeep).toBeDefined();
|
||||
expect(beforeEvent.preparation.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(beforeEvent.model).toBeDefined();
|
||||
// sessionManager and modelRegistry are now on ctx, not event
|
||||
|
||||
const afterEvent = compactEvents[0];
|
||||
if (afterEvent.reason === "compact") {
|
||||
expect(afterEvent.compactionEntry).toBeDefined();
|
||||
expect(afterEvent.compactionEntry.summary.length).toBeGreaterThan(0);
|
||||
expect(afterEvent.compactionEntry.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(afterEvent.fromHook).toBe(false);
|
||||
}
|
||||
expect(afterEvent.compactionEntry).toBeDefined();
|
||||
expect(afterEvent.compactionEntry.summary.length).toBeGreaterThan(0);
|
||||
expect(afterEvent.compactionEntry.tokensBefore).toBeGreaterThanOrEqual(0);
|
||||
expect(afterEvent.fromHook).toBe(false);
|
||||
}, 120000);
|
||||
|
||||
it("should allow hooks to cancel compaction", async () => {
|
||||
|
|
@ -156,7 +166,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
|
||||
await expect(session.compact()).rejects.toThrow("Compaction cancelled");
|
||||
|
||||
const compactEvents = capturedEvents.filter((e) => e.reason === "compact");
|
||||
const compactEvents = capturedEvents.filter((e) => e.type === "session_compact");
|
||||
expect(compactEvents.length).toBe(0);
|
||||
}, 120000);
|
||||
|
||||
|
|
@ -164,7 +174,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
const customSummary = "Custom summary from hook";
|
||||
|
||||
const hook = createHook((event) => {
|
||||
if (event.reason === "before_compact") {
|
||||
if (event.type === "session_before_compact") {
|
||||
return {
|
||||
compaction: {
|
||||
summary: customSummary,
|
||||
|
|
@ -187,11 +197,11 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
|
||||
expect(result.summary).toBe(customSummary);
|
||||
|
||||
const compactEvents = capturedEvents.filter((e) => e.reason === "compact");
|
||||
const compactEvents = capturedEvents.filter((e) => e.type === "session_compact");
|
||||
expect(compactEvents.length).toBe(1);
|
||||
|
||||
const afterEvent = compactEvents[0];
|
||||
if (afterEvent.reason === "compact") {
|
||||
if (afterEvent.type === "session_compact") {
|
||||
expect(afterEvent.compactionEntry.summary).toBe(customSummary);
|
||||
expect(afterEvent.fromHook).toBe(true);
|
||||
}
|
||||
|
|
@ -206,11 +216,11 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
|
||||
await session.compact();
|
||||
|
||||
const compactEvents = capturedEvents.filter((e) => e.reason === "compact");
|
||||
const compactEvents = capturedEvents.filter((e) => e.type === "session_compact");
|
||||
expect(compactEvents.length).toBe(1);
|
||||
|
||||
const afterEvent = compactEvents[0];
|
||||
if (afterEvent.reason === "compact") {
|
||||
if (afterEvent.type === "session_compact") {
|
||||
// sessionManager is now on ctx, use session.sessionManager directly
|
||||
const entries = session.sessionManager.getEntries();
|
||||
const hasCompactionEntry = entries.some((e: { type: string }) => e.type === "compaction");
|
||||
|
|
@ -224,13 +234,19 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
resolvedPath: "/test/throwing-hook.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session",
|
||||
"session_before_compact",
|
||||
[
|
||||
async (event: SessionEvent) => {
|
||||
async (event: SessionBeforeCompactEvent) => {
|
||||
capturedEvents.push(event);
|
||||
throw new Error("Hook intentionally throws");
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"session_compact",
|
||||
[
|
||||
async (event: SessionCompactEvent) => {
|
||||
capturedEvents.push(event);
|
||||
if (event.reason === "before_compact") {
|
||||
throw new Error("Hook intentionally failed");
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
|
|
@ -252,12 +268,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
expect(result.summary).toBeDefined();
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
|
||||
const compactEvents = capturedEvents.filter((e) => e.reason === "compact");
|
||||
const compactEvents = capturedEvents.filter((e): e is SessionCompactEvent => e.type === "session_compact");
|
||||
expect(compactEvents.length).toBe(1);
|
||||
|
||||
if (compactEvents[0].reason === "compact") {
|
||||
expect(compactEvents[0].fromHook).toBe(false);
|
||||
}
|
||||
expect(compactEvents[0].fromHook).toBe(false);
|
||||
}, 120000);
|
||||
|
||||
it("should call multiple hooks in order", async () => {
|
||||
|
|
@ -268,15 +281,19 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
resolvedPath: "/test/hook1.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session",
|
||||
"session_before_compact",
|
||||
[
|
||||
async (event: SessionEvent) => {
|
||||
if (event.reason === "before_compact") {
|
||||
callOrder.push("hook1-before");
|
||||
}
|
||||
if (event.reason === "compact") {
|
||||
callOrder.push("hook1-after");
|
||||
}
|
||||
async () => {
|
||||
callOrder.push("hook1-before");
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"session_compact",
|
||||
[
|
||||
async () => {
|
||||
callOrder.push("hook1-after");
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
|
|
@ -293,15 +310,19 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
resolvedPath: "/test/hook2.ts",
|
||||
handlers: new Map<string, ((event: any, ctx: any) => Promise<any>)[]>([
|
||||
[
|
||||
"session",
|
||||
"session_before_compact",
|
||||
[
|
||||
async (event: SessionEvent) => {
|
||||
if (event.reason === "before_compact") {
|
||||
callOrder.push("hook2-before");
|
||||
}
|
||||
if (event.reason === "compact") {
|
||||
callOrder.push("hook2-after");
|
||||
}
|
||||
async () => {
|
||||
callOrder.push("hook2-before");
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"session_compact",
|
||||
[
|
||||
async () => {
|
||||
callOrder.push("hook2-after");
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
|
|
@ -324,12 +345,10 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
}, 120000);
|
||||
|
||||
it("should pass correct data in before_compact event", async () => {
|
||||
let capturedBeforeEvent: (SessionEvent & { reason: "before_compact" }) | null = null;
|
||||
let capturedBeforeEvent: SessionBeforeCompactEvent | null = null;
|
||||
|
||||
const hook = createHook((event) => {
|
||||
if (event.reason === "before_compact") {
|
||||
capturedBeforeEvent = event;
|
||||
}
|
||||
capturedBeforeEvent = event;
|
||||
return undefined;
|
||||
});
|
||||
createSession([hook]);
|
||||
|
|
@ -370,7 +389,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
|
|||
const customSummary = "Custom summary with modified values";
|
||||
|
||||
const hook = createHook((event) => {
|
||||
if (event.reason === "before_compact") {
|
||||
if (event.type === "session_before_compact") {
|
||||
return {
|
||||
compaction: {
|
||||
summary: customSummary,
|
||||
|
|
|
|||
|
|
@ -180,9 +180,9 @@ describe("getLastAssistantUsage", () => {
|
|||
expect(usage!.input).toBe(100);
|
||||
});
|
||||
|
||||
it("should return null if no assistant messages", () => {
|
||||
it("should return undefined if no assistant messages", () => {
|
||||
const entries: SessionEntry[] = [createMessageEntry(createUserMessage("Hello"))];
|
||||
expect(getLastAssistantUsage(entries)).toBeNull();
|
||||
expect(getLastAssistantUsage(entries)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -301,8 +301,8 @@ describe("buildSessionContext", () => {
|
|||
const loaded = buildSessionContext(entries);
|
||||
// summary + kept (u2, a2) + after (u3, a3) = 5
|
||||
expect(loaded.messages.length).toBe(5);
|
||||
expect(loaded.messages[0].role).toBe("user");
|
||||
expect((loaded.messages[0] as any).content).toContain("Summary of 1,a,2,b");
|
||||
expect(loaded.messages[0].role).toBe("compactionSummary");
|
||||
expect((loaded.messages[0] as any).summary).toContain("Summary of 1,a,2,b");
|
||||
});
|
||||
|
||||
it("should handle multiple compactions (only latest matters)", () => {
|
||||
|
|
@ -325,7 +325,7 @@ describe("buildSessionContext", () => {
|
|||
const loaded = buildSessionContext(entries);
|
||||
// summary + kept from u3 (u3, c) + after (u4, d) = 5
|
||||
expect(loaded.messages.length).toBe(5);
|
||||
expect((loaded.messages[0] as any).content).toContain("Second summary");
|
||||
expect((loaded.messages[0] as any).summary).toContain("Second summary");
|
||||
});
|
||||
|
||||
it("should keep all messages when firstKeptEntryId is first entry", () => {
|
||||
|
|
@ -443,8 +443,8 @@ describe.skipIf(!process.env.ANTHROPIC_OAUTH_TOKEN)("LLM summarization", () => {
|
|||
|
||||
// Should have summary + kept messages
|
||||
expect(reloaded.messages.length).toBeLessThan(loaded.messages.length);
|
||||
expect(reloaded.messages[0].role).toBe("user");
|
||||
expect((reloaded.messages[0] as any).content).toContain(compactionResult.summary);
|
||||
expect(reloaded.messages[0].role).toBe("compactionSummary");
|
||||
expect((reloaded.messages[0] as any).summary).toContain(compactionResult.summary);
|
||||
|
||||
console.log("Original messages:", loaded.messages.length);
|
||||
console.log("After compaction:", reloaded.messages.length);
|
||||
|
|
|
|||
|
|
@ -66,21 +66,21 @@ describe("parseModelPattern", () => {
|
|||
const result = parseModelPattern("claude-sonnet-4-5", allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("partial match returns best model", () => {
|
||||
const result = parseModelPattern("sonnet", allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("no match returns null model", () => {
|
||||
const result = parseModelPattern("nonexistent", allModels);
|
||||
expect(result.model).toBeNull();
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -89,14 +89,14 @@ describe("parseModelPattern", () => {
|
|||
const result = parseModelPattern("sonnet:high", allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("gpt-4o:medium returns gpt-4o with medium thinking level", () => {
|
||||
const result = parseModelPattern("gpt-4o:medium", allModels);
|
||||
expect(result.model?.id).toBe("gpt-4o");
|
||||
expect(result.thinkingLevel).toBe("medium");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("all valid thinking levels work", () => {
|
||||
|
|
@ -104,7 +104,7 @@ describe("parseModelPattern", () => {
|
|||
const result = parseModelPattern(`sonnet:${level}`, allModels);
|
||||
expect(result.model?.id).toBe("claude-sonnet-4-5");
|
||||
expect(result.thinkingLevel).toBe(level);
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -131,7 +131,7 @@ describe("parseModelPattern", () => {
|
|||
const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels);
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("openrouter/qwen/qwen3-coder:exacto matches with provider prefix", () => {
|
||||
|
|
@ -139,14 +139,14 @@ describe("parseModelPattern", () => {
|
|||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("qwen3-coder:exacto:high matches model with high thinking level", () => {
|
||||
const result = parseModelPattern("qwen/qwen3-coder:exacto:high", allModels);
|
||||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("openrouter/qwen/qwen3-coder:exacto:high matches with provider and thinking level", () => {
|
||||
|
|
@ -154,14 +154,14 @@ describe("parseModelPattern", () => {
|
|||
expect(result.model?.id).toBe("qwen/qwen3-coder:exacto");
|
||||
expect(result.model?.provider).toBe("openrouter");
|
||||
expect(result.thinkingLevel).toBe("high");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
test("gpt-4o:extended matches the extended model", () => {
|
||||
const result = parseModelPattern("openai/gpt-4o:extended", allModels);
|
||||
expect(result.model?.id).toBe("openai/gpt-4o:extended");
|
||||
expect(result.thinkingLevel).toBe("off");
|
||||
expect(result.warning).toBeNull();
|
||||
expect(result.warning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T
|
|||
|
||||
// Initially null
|
||||
let text = await client.getLastAssistantText();
|
||||
expect(text).toBeNull();
|
||||
expect(text).toBeUndefined();
|
||||
|
||||
// Send prompt
|
||||
await client.promptAndWait("Reply with just: test123");
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function compaction(id: string, parentId: string | null, summary: string, firstK
|
|||
};
|
||||
}
|
||||
|
||||
function branchSummary(id: string, parentId: string | null, fromId: string, summary: string): BranchSummaryEntry {
|
||||
function branchSummary(id: string, parentId: string | null, summary: string, fromId: string): BranchSummaryEntry {
|
||||
return { type: "branch_summary", id, parentId, timestamp: "2025-01-01T00:00:00Z", summary, fromId };
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ describe("buildSessionContext", () => {
|
|||
|
||||
// Should have: summary + kept (3,4) + after (6,7) = 5 messages
|
||||
expect(ctx.messages).toHaveLength(5);
|
||||
expect((ctx.messages[0] as any).content).toContain("Summary of first two turns");
|
||||
expect((ctx.messages[0] as any).summary).toContain("Summary of first two turns");
|
||||
expect((ctx.messages[1] as any).content).toBe("second");
|
||||
expect((ctx.messages[2] as any).content[0].text).toBe("response2");
|
||||
expect((ctx.messages[3] as any).content).toBe("third");
|
||||
|
|
@ -150,7 +150,7 @@ describe("buildSessionContext", () => {
|
|||
|
||||
// Summary + all messages (1,2,4)
|
||||
expect(ctx.messages).toHaveLength(4);
|
||||
expect((ctx.messages[0] as any).content).toContain("Empty summary");
|
||||
expect((ctx.messages[0] as any).summary).toContain("Empty summary");
|
||||
});
|
||||
|
||||
it("multiple compactions uses latest", () => {
|
||||
|
|
@ -167,7 +167,7 @@ describe("buildSessionContext", () => {
|
|||
|
||||
// Should use second summary, keep from 4
|
||||
expect(ctx.messages).toHaveLength(4);
|
||||
expect((ctx.messages[0] as any).content).toContain("Second summary");
|
||||
expect((ctx.messages[0] as any).summary).toContain("Second summary");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ describe("buildSessionContext", () => {
|
|||
const ctx = buildSessionContext(entries, "5");
|
||||
|
||||
expect(ctx.messages).toHaveLength(4);
|
||||
expect((ctx.messages[2] as any).content).toContain("Summary of abandoned work");
|
||||
expect((ctx.messages[2] as any).summary).toContain("Summary of abandoned work");
|
||||
expect((ctx.messages[3] as any).content).toBe("new direction");
|
||||
});
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ describe("buildSessionContext", () => {
|
|||
// Main path to 7: summary + kept(3,4) + after(6,7)
|
||||
const ctxMain = buildSessionContext(entries, "7");
|
||||
expect(ctxMain.messages).toHaveLength(5);
|
||||
expect((ctxMain.messages[0] as any).content).toContain("Compacted history");
|
||||
expect((ctxMain.messages[0] as any).summary).toContain("Compacted history");
|
||||
expect((ctxMain.messages[1] as any).content).toBe("q2");
|
||||
expect((ctxMain.messages[2] as any).content[0].text).toBe("r2");
|
||||
expect((ctxMain.messages[3] as any).content).toBe("q3");
|
||||
|
|
@ -243,7 +243,7 @@ describe("buildSessionContext", () => {
|
|||
expect((ctxBranch.messages[0] as any).content).toBe("start");
|
||||
expect((ctxBranch.messages[1] as any).content[0].text).toBe("r1");
|
||||
expect((ctxBranch.messages[2] as any).content).toBe("q2");
|
||||
expect((ctxBranch.messages[3] as any).content).toContain("Tried wrong approach");
|
||||
expect((ctxBranch.messages[3] as any).summary).toContain("Tried wrong approach");
|
||||
expect((ctxBranch.messages[4] as any).content).toBe("better approach");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ describe("createBranchedSession", () => {
|
|||
|
||||
// Create branched session from id2 (should only have 1 -> 2)
|
||||
const result = session.createBranchedSession(id2);
|
||||
expect(result).toBeNull(); // in-memory returns null
|
||||
expect(result).toBeUndefined(); // in-memory returns null
|
||||
|
||||
// Session should now only have entries 1 and 2
|
||||
const entries = session.getEntries();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue