mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 21:03:42 +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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- Great for personal coding preferences and workflows
|
||||
|
||||
2. **Parent directories** (top-most first down to current directory)
|
||||
- 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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
You can also reference tool READMEs in your `AGENT.md` files to make them automatically available:
|
||||
- Global: `~/.pi/agent/AGENT.md` - available in all sessions
|
||||
- Project-specific: `./AGENT.md` - available in this project
|
||||
You can also reference tool READMEs in your `AGENTS.md` files to make them automatically available:
|
||||
- Global: `~/.pi/agent/AGENTS.md` - available in all sessions
|
||||
- Project-specific: `./AGENTS.md` - available in this project
|
||||
|
||||
**Real-world example:**
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"scripts": {
|
||||
"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 { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { getChangelogPath, getNewEntries, parseChangelog } from "./changelog.js";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import { codingTools } from "./tools/index.js";
|
||||
import { SessionSelectorComponent } from "./tui/session-selector.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 {
|
||||
const candidates = ["AGENT.md", "CLAUDE.md"];
|
||||
const candidates = ["AGENTS.md", "CLAUDE.md"];
|
||||
for (const filename of candidates) {
|
||||
const filePath = join(dir, filename);
|
||||
if (existsSync(filePath)) {
|
||||
|
|
@ -243,7 +245,7 @@ function loadContextFileFromDir(dir: string): { path: string; content: string }
|
|||
|
||||
/**
|
||||
* 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
|
||||
* 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> {
|
||||
const renderer = new TuiRenderer(agent, sessionManager, version);
|
||||
async function runInteractiveMode(
|
||||
agent: Agent,
|
||||
sessionManager: SessionManager,
|
||||
version: string,
|
||||
changelogMarkdown: string | null = null,
|
||||
): Promise<void> {
|
||||
const renderer = new TuiRenderer(agent, sessionManager, version, changelogMarkdown);
|
||||
|
||||
// Initialize TUI
|
||||
await renderer.init();
|
||||
|
|
@ -581,8 +588,38 @@ export async function main(args: string[]) {
|
|||
// RPC mode - headless operation
|
||||
await runRpcMode(agent, sessionManager);
|
||||
} 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
|
||||
await runInteractiveMode(agent, sessionManager, VERSION);
|
||||
await runInteractiveMode(agent, sessionManager, VERSION, changelogMarkdown);
|
||||
} else {
|
||||
// CLI mode with messages
|
||||
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,
|
||||
Container,
|
||||
Loader,
|
||||
Markdown,
|
||||
ProcessTerminal,
|
||||
Spacer,
|
||||
Text,
|
||||
|
|
@ -15,6 +16,7 @@ import { exportSessionToHtml } from "../export-html.js";
|
|||
import type { SessionManager } from "../session-manager.js";
|
||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||
import { CustomEditor } from "./custom-editor.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { FooterComponent } from "./footer.js";
|
||||
import { ModelSelectorComponent } from "./model-selector.js";
|
||||
import { ThinkingSelectorComponent } from "./thinking-selector.js";
|
||||
|
|
@ -39,6 +41,7 @@ export class TuiRenderer {
|
|||
private loadingAnimation: Loader | null = null;
|
||||
private onInterruptCallback?: () => void;
|
||||
private lastSigintTime = 0;
|
||||
private changelogMarkdown: string | null = null;
|
||||
|
||||
// Streaming message tracking
|
||||
private streamingComponent: AssistantMessageComponent | null = null;
|
||||
|
|
@ -55,10 +58,11 @@ export class TuiRenderer {
|
|||
// Track if this is the first user message (to skip spacer)
|
||||
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.sessionManager = sessionManager;
|
||||
this.version = version;
|
||||
this.changelogMarkdown = changelogMarkdown;
|
||||
this.ui = new TUI(new ProcessTerminal());
|
||||
this.chatContainer = new Container();
|
||||
this.statusContainer = new Container();
|
||||
|
|
@ -125,6 +129,18 @@ export class TuiRenderer {
|
|||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(header);
|
||||
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.statusContainer);
|
||||
this.ui.addChild(new Spacer(1));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue