mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 03:04:28 +00:00
Add settings manager, show changelog on startup if not shown yet.
This commit is contained in:
parent
16740ea077
commit
c82f9f4f8c
8 changed files with 264 additions and 16 deletions
15
packages/coding-agent/CHANGELOG.md
Normal file
15
packages/coding-agent/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.7.7] - Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Automatic changelog viewer on startup in interactive mode. When starting a new session (not continuing/resuming), the agent will display all changelog entries since the last version you used in a scrollable markdown viewer. The last shown version is tracked in `~/.pi/agent/settings.json`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING**: Renamed project context file from `AGENT.md` to `AGENTS.md`. The system now looks for `AGENTS.md` or `CLAUDE.md` (with `AGENTS.md` preferred). Existing `AGENT.md` files will need to be renamed to `AGENTS.md` to continue working. (fixes [#9](https://github.com/badlogic/pi-mono/pull/9))
|
||||||
|
|
||||||
|
## [0.7.6] - 2025-11-13
|
||||||
|
|
||||||
|
Previous releases did not maintain a changelog.
|
||||||
|
|
@ -134,26 +134,26 @@ Paste multiple lines of text (e.g., code snippets, logs) and they'll be automati
|
||||||
|
|
||||||
## Project Context Files
|
## Project Context Files
|
||||||
|
|
||||||
The agent automatically loads context from `AGENT.md` or `CLAUDE.md` files at the start of new sessions (not when continuing/resuming). These files are loaded in hierarchical order to support both global preferences and monorepo structures.
|
The agent automatically loads context from `AGENTS.md` or `CLAUDE.md` files at the start of new sessions (not when continuing/resuming). These files are loaded in hierarchical order to support both global preferences and monorepo structures.
|
||||||
|
|
||||||
### File Locations
|
### File Locations
|
||||||
|
|
||||||
Context files are loaded in this order:
|
Context files are loaded in this order:
|
||||||
|
|
||||||
1. **Global context**: `~/.pi/agent/AGENT.md` or `CLAUDE.md`
|
1. **Global context**: `~/.pi/agent/AGENTS.md` or `CLAUDE.md`
|
||||||
- Applies to all your coding sessions
|
- Applies to all your coding sessions
|
||||||
- Great for personal coding preferences and workflows
|
- Great for personal coding preferences and workflows
|
||||||
|
|
||||||
2. **Parent directories** (top-most first down to current directory)
|
2. **Parent directories** (top-most first down to current directory)
|
||||||
- Walks up from current directory to filesystem root
|
- Walks up from current directory to filesystem root
|
||||||
- Each directory can have its own `AGENT.md` or `CLAUDE.md`
|
- Each directory can have its own `AGENTS.md` or `CLAUDE.md`
|
||||||
- Perfect for monorepos with shared context at higher levels
|
- Perfect for monorepos with shared context at higher levels
|
||||||
|
|
||||||
3. **Current directory**: Your project's `AGENT.md` or `CLAUDE.md`
|
3. **Current directory**: Your project's `AGENTS.md` or `CLAUDE.md`
|
||||||
- Most specific context, loaded last
|
- Most specific context, loaded last
|
||||||
- Overwrites or extends parent/global context
|
- Overwrites or extends parent/global context
|
||||||
|
|
||||||
**File preference**: In each directory, `AGENT.md` is preferred over `CLAUDE.md` if both exist.
|
**File preference**: In each directory, `AGENTS.md` is preferred over `CLAUDE.md` if both exist.
|
||||||
|
|
||||||
### What to Include
|
### What to Include
|
||||||
|
|
||||||
|
|
@ -364,9 +364,9 @@ You: Read ~/agent-tools/screenshot/README.md and use that tool to take a screens
|
||||||
|
|
||||||
The agent will read the README, understand the tool, and invoke it via bash as needed. If you need a new tool, ask the agent to write it for you.
|
The agent will read the README, understand the tool, and invoke it via bash as needed. If you need a new tool, ask the agent to write it for you.
|
||||||
|
|
||||||
You can also reference tool READMEs in your `AGENT.md` files to make them automatically available:
|
You can also reference tool READMEs in your `AGENTS.md` files to make them automatically available:
|
||||||
- Global: `~/.pi/agent/AGENT.md` - available in all sessions
|
- Global: `~/.pi/agent/AGENTS.md` - available in all sessions
|
||||||
- Project-specific: `./AGENT.md` - available in this project
|
- Project-specific: `./AGENTS.md` - available in this project
|
||||||
|
|
||||||
**Real-world example:**
|
**Real-world example:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"CHANGELOG.md"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
|
|
|
||||||
107
packages/coding-agent/src/changelog.ts
Normal file
107
packages/coding-agent/src/changelog.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { existsSync, readFileSync } from "fs";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
export interface ChangelogEntry {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse changelog entries from CHANGELOG.md
|
||||||
|
* Scans for ## lines and collects content until next ## or EOF
|
||||||
|
*/
|
||||||
|
export function parseChangelog(changelogPath: string): ChangelogEntry[] {
|
||||||
|
if (!existsSync(changelogPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(changelogPath, "utf-8");
|
||||||
|
const lines = content.split("\n");
|
||||||
|
const entries: ChangelogEntry[] = [];
|
||||||
|
|
||||||
|
let currentLines: string[] = [];
|
||||||
|
let currentVersion: { major: number; minor: number; patch: number } | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Check if this is a version header (## [x.y.z] ...)
|
||||||
|
if (line.startsWith("## ")) {
|
||||||
|
// Save previous entry if exists
|
||||||
|
if (currentVersion && currentLines.length > 0) {
|
||||||
|
entries.push({
|
||||||
|
...currentVersion,
|
||||||
|
content: currentLines.join("\n").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse version from this line
|
||||||
|
const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/);
|
||||||
|
if (versionMatch) {
|
||||||
|
currentVersion = {
|
||||||
|
major: Number.parseInt(versionMatch[1], 10),
|
||||||
|
minor: Number.parseInt(versionMatch[2], 10),
|
||||||
|
patch: Number.parseInt(versionMatch[3], 10),
|
||||||
|
};
|
||||||
|
currentLines = [line];
|
||||||
|
} else {
|
||||||
|
// Reset if we can't parse version
|
||||||
|
currentVersion = null;
|
||||||
|
currentLines = [];
|
||||||
|
}
|
||||||
|
} else if (currentVersion) {
|
||||||
|
// Collect lines for current version
|
||||||
|
currentLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save last entry
|
||||||
|
if (currentVersion && currentLines.length > 0) {
|
||||||
|
entries.push({
|
||||||
|
...currentVersion,
|
||||||
|
content: currentLines.join("\n").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Warning: Could not parse changelog: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
||||||
|
*/
|
||||||
|
export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number {
|
||||||
|
if (v1.major !== v2.major) return v1.major - v2.major;
|
||||||
|
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
||||||
|
return v1.patch - v2.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entries newer than lastVersion
|
||||||
|
*/
|
||||||
|
export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] {
|
||||||
|
// Parse lastVersion
|
||||||
|
const parts = lastVersion.split(".").map(Number);
|
||||||
|
const last: ChangelogEntry = {
|
||||||
|
major: parts[0] || 0,
|
||||||
|
minor: parts[1] || 0,
|
||||||
|
patch: parts[2] || 0,
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return entries.filter((entry) => compareVersions(entry, last) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the CHANGELOG.md file
|
||||||
|
*/
|
||||||
|
export function getChangelogPath(): string {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
return join(__dirname, "../CHANGELOG.md");
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,9 @@ import { existsSync, readFileSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { dirname, join, resolve } from "path";
|
import { dirname, join, resolve } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./changelog.js";
|
||||||
import { SessionManager } from "./session-manager.js";
|
import { SessionManager } from "./session-manager.js";
|
||||||
|
import { SettingsManager } from "./settings-manager.js";
|
||||||
import { codingTools } from "./tools/index.js";
|
import { codingTools } from "./tools/index.js";
|
||||||
import { SessionSelectorComponent } from "./tui/session-selector.js";
|
import { SessionSelectorComponent } from "./tui/session-selector.js";
|
||||||
import { TuiRenderer } from "./tui/tui-renderer.js";
|
import { TuiRenderer } from "./tui/tui-renderer.js";
|
||||||
|
|
@ -221,10 +223,10 @@ Guidelines:
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for AGENT.md or CLAUDE.md in a directory (prefers AGENT.md)
|
* Look for AGENTS.md or CLAUDE.md in a directory (prefers AGENTS.md)
|
||||||
*/
|
*/
|
||||||
function loadContextFileFromDir(dir: string): { path: string; content: string } | null {
|
function loadContextFileFromDir(dir: string): { path: string; content: string } | null {
|
||||||
const candidates = ["AGENT.md", "CLAUDE.md"];
|
const candidates = ["AGENTS.md", "CLAUDE.md"];
|
||||||
for (const filename of candidates) {
|
for (const filename of candidates) {
|
||||||
const filePath = join(dir, filename);
|
const filePath = join(dir, filename);
|
||||||
if (existsSync(filePath)) {
|
if (existsSync(filePath)) {
|
||||||
|
|
@ -243,7 +245,7 @@ function loadContextFileFromDir(dir: string): { path: string; content: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all project context files in order:
|
* Load all project context files in order:
|
||||||
* 1. Global: ~/.pi/agent/AGENT.md or CLAUDE.md
|
* 1. Global: ~/.pi/agent/AGENTS.md or CLAUDE.md
|
||||||
* 2. Parent directories (top-most first) down to cwd
|
* 2. Parent directories (top-most first) down to cwd
|
||||||
* Each returns {path, content} for separate messages
|
* Each returns {path, content} for separate messages
|
||||||
*/
|
*/
|
||||||
|
|
@ -316,8 +318,13 @@ async function selectSession(sessionManager: SessionManager): Promise<string | n
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runInteractiveMode(agent: Agent, sessionManager: SessionManager, version: string): Promise<void> {
|
async function runInteractiveMode(
|
||||||
const renderer = new TuiRenderer(agent, sessionManager, version);
|
agent: Agent,
|
||||||
|
sessionManager: SessionManager,
|
||||||
|
version: string,
|
||||||
|
changelogMarkdown: string | null = null,
|
||||||
|
): Promise<void> {
|
||||||
|
const renderer = new TuiRenderer(agent, sessionManager, version, changelogMarkdown);
|
||||||
|
|
||||||
// Initialize TUI
|
// Initialize TUI
|
||||||
await renderer.init();
|
await renderer.init();
|
||||||
|
|
@ -581,8 +588,38 @@ export async function main(args: string[]) {
|
||||||
// RPC mode - headless operation
|
// RPC mode - headless operation
|
||||||
await runRpcMode(agent, sessionManager);
|
await runRpcMode(agent, sessionManager);
|
||||||
} else if (isInteractive) {
|
} else if (isInteractive) {
|
||||||
|
// Check if we should show changelog (only in interactive mode, only for new sessions)
|
||||||
|
let changelogMarkdown: string | null = null;
|
||||||
|
if (!parsed.continue && !parsed.resume) {
|
||||||
|
const settingsManager = new SettingsManager();
|
||||||
|
const lastVersion = settingsManager.getLastChangelogVersion();
|
||||||
|
|
||||||
|
// Check if we need to show changelog
|
||||||
|
if (!lastVersion) {
|
||||||
|
// First run - show all entries
|
||||||
|
const changelogPath = getChangelogPath();
|
||||||
|
const entries = parseChangelog(changelogPath);
|
||||||
|
if (entries.length > 0) {
|
||||||
|
changelogMarkdown = entries.map((e) => e.content).join("\n\n");
|
||||||
|
settingsManager.setLastChangelogVersion(VERSION);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse current and last versions
|
||||||
|
const currentParts = VERSION.split(".").map(Number);
|
||||||
|
const current = { major: currentParts[0] || 0, minor: currentParts[1] || 0, patch: currentParts[2] || 0 };
|
||||||
|
const changelogPath = getChangelogPath();
|
||||||
|
const entries = parseChangelog(changelogPath);
|
||||||
|
const newEntries = getNewEntries(entries, lastVersion);
|
||||||
|
|
||||||
|
if (newEntries.length > 0) {
|
||||||
|
changelogMarkdown = newEntries.map((e) => e.content).join("\n\n");
|
||||||
|
settingsManager.setLastChangelogVersion(VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No messages and not RPC - use TUI
|
// No messages and not RPC - use TUI
|
||||||
await runInteractiveMode(agent, sessionManager, VERSION);
|
await runInteractiveMode(agent, sessionManager, VERSION, changelogMarkdown);
|
||||||
} else {
|
} else {
|
||||||
// CLI mode with messages
|
// CLI mode with messages
|
||||||
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
||||||
|
|
|
||||||
55
packages/coding-agent/src/settings-manager.ts
Normal file
55
packages/coding-agent/src/settings-manager.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
lastChangelogVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SettingsManager {
|
||||||
|
private settingsPath: string;
|
||||||
|
private settings: Settings;
|
||||||
|
|
||||||
|
constructor(baseDir?: string) {
|
||||||
|
const dir = baseDir || join(homedir(), ".pi", "agent");
|
||||||
|
this.settingsPath = join(dir, "settings.json");
|
||||||
|
this.settings = this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private load(): Settings {
|
||||||
|
if (!existsSync(this.settingsPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(this.settingsPath, "utf-8");
|
||||||
|
return JSON.parse(content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Warning: Could not read settings file: ${error}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private save(): void {
|
||||||
|
try {
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = dirname(this.settingsPath);
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Warning: Could not save settings file: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastChangelogVersion(): string | undefined {
|
||||||
|
return this.settings.lastChangelogVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastChangelogVersion(version: string): void {
|
||||||
|
this.settings.lastChangelogVersion = version;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/coding-agent/src/tui/dynamic-border.ts
Normal file
17
packages/coding-agent/src/tui/dynamic-border.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Component } from "@mariozechner/pi-tui";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic border component that adjusts to viewport width
|
||||||
|
*/
|
||||||
|
export class DynamicBorder implements Component {
|
||||||
|
private color: (str: string) => string;
|
||||||
|
|
||||||
|
constructor(color: (str: string) => string = chalk.blue) {
|
||||||
|
this.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(width: number): string[] {
|
||||||
|
return [this.color("─".repeat(Math.max(1, width)))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
CombinedAutocompleteProvider,
|
CombinedAutocompleteProvider,
|
||||||
Container,
|
Container,
|
||||||
Loader,
|
Loader,
|
||||||
|
Markdown,
|
||||||
ProcessTerminal,
|
ProcessTerminal,
|
||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -15,6 +16,7 @@ import { exportSessionToHtml } from "../export-html.js";
|
||||||
import type { SessionManager } from "../session-manager.js";
|
import type { SessionManager } from "../session-manager.js";
|
||||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||||
import { CustomEditor } from "./custom-editor.js";
|
import { CustomEditor } from "./custom-editor.js";
|
||||||
|
import { DynamicBorder } from "./dynamic-border.js";
|
||||||
import { FooterComponent } from "./footer.js";
|
import { FooterComponent } from "./footer.js";
|
||||||
import { ModelSelectorComponent } from "./model-selector.js";
|
import { ModelSelectorComponent } from "./model-selector.js";
|
||||||
import { ThinkingSelectorComponent } from "./thinking-selector.js";
|
import { ThinkingSelectorComponent } from "./thinking-selector.js";
|
||||||
|
|
@ -39,6 +41,7 @@ export class TuiRenderer {
|
||||||
private loadingAnimation: Loader | null = null;
|
private loadingAnimation: Loader | null = null;
|
||||||
private onInterruptCallback?: () => void;
|
private onInterruptCallback?: () => void;
|
||||||
private lastSigintTime = 0;
|
private lastSigintTime = 0;
|
||||||
|
private changelogMarkdown: string | null = null;
|
||||||
|
|
||||||
// Streaming message tracking
|
// Streaming message tracking
|
||||||
private streamingComponent: AssistantMessageComponent | null = null;
|
private streamingComponent: AssistantMessageComponent | null = null;
|
||||||
|
|
@ -55,10 +58,11 @@ export class TuiRenderer {
|
||||||
// Track if this is the first user message (to skip spacer)
|
// Track if this is the first user message (to skip spacer)
|
||||||
private isFirstUserMessage = true;
|
private isFirstUserMessage = true;
|
||||||
|
|
||||||
constructor(agent: Agent, sessionManager: SessionManager, version: string) {
|
constructor(agent: Agent, sessionManager: SessionManager, version: string, changelogMarkdown: string | null = null) {
|
||||||
this.agent = agent;
|
this.agent = agent;
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
|
this.changelogMarkdown = changelogMarkdown;
|
||||||
this.ui = new TUI(new ProcessTerminal());
|
this.ui = new TUI(new ProcessTerminal());
|
||||||
this.chatContainer = new Container();
|
this.chatContainer = new Container();
|
||||||
this.statusContainer = new Container();
|
this.statusContainer = new Container();
|
||||||
|
|
@ -125,6 +129,18 @@ export class TuiRenderer {
|
||||||
this.ui.addChild(new Spacer(1));
|
this.ui.addChild(new Spacer(1));
|
||||||
this.ui.addChild(header);
|
this.ui.addChild(header);
|
||||||
this.ui.addChild(new Spacer(1));
|
this.ui.addChild(new Spacer(1));
|
||||||
|
|
||||||
|
// Add changelog if provided
|
||||||
|
if (this.changelogMarkdown) {
|
||||||
|
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||||
|
this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0));
|
||||||
|
this.ui.addChild(new Spacer(1));
|
||||||
|
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), undefined, undefined, undefined, 1, 0));
|
||||||
|
this.ui.addChild(new Spacer(1));
|
||||||
|
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||||
|
this.ui.addChild(new Spacer(1));
|
||||||
|
}
|
||||||
|
|
||||||
this.ui.addChild(this.chatContainer);
|
this.ui.addChild(this.chatContainer);
|
||||||
this.ui.addChild(this.statusContainer);
|
this.ui.addChild(this.statusContainer);
|
||||||
this.ui.addChild(new Spacer(1));
|
this.ui.addChild(new Spacer(1));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue