diff --git a/README.md b/README.md index 624000a..4ee5427 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,28 @@ The plugin creates a default global config on first run. // "model": "anthropic/claude-haiku-4-5", // Update title every N idle events (1 = every time you pause) - "updateThreshold": 1 + "updateThreshold": 1, + + // Title format with placeholders: + // - {title} - AI-generated title based on conversation + // - {cwd} - Full current working directory path + // - {cwdTip} - Last folder name of cwd (e.g., "my-project") + // - {cwdTip:N} - Last N folder segments (e.g., {cwdTip:2} -> "github/my-project") + // - {cwdTip:N:sep} - Last N segments with custom separator + "titleFormat": "{title}" } ``` +### Example Formats + +| Format | Example Output | +|--------|----------------| +| `{title}` | `Debugging API errors` | +| `[{cwdTip}] {title}` | `[my-project] Debugging API errors` | +| `[{cwdTip:2}] {title}` | `[github/my-project] Debugging API errors` | +| `[{cwdTip:2: - }] {title}` | `[github - my-project] Debugging API errors` | +| `{title} ({cwdTip})` | `Debugging API errors (my-project)` | + ## License MIT diff --git a/index.ts b/index.ts index 643fcbb..6d731e7 100644 --- a/index.ts +++ b/index.ts @@ -261,6 +261,49 @@ function cleanTitle(raw: string): string { return cleaned } +/** + * Placeholder values available for title formatting + */ +interface PlaceholderValues { + title: string + cwd: string +} + +/** + * Resolve a cwdTip placeholder with optional parameters + * Formats: + * {cwdTip} - last folder name (default) + * {cwdTip:N} - last N folder segments, joined by "/" + * {cwdTip:N:sep} - last N folder segments, joined by custom separator + */ +function resolveCwdTip(cwd: string, depth: number, separator: string): string { + const segments = cwd.split('/').filter(s => s.length > 0) + const selected = segments.slice(-depth) + return selected.join(separator) +} + +/** + * Apply placeholder replacements to title format + * Supports: {title}, {cwd}, {cwdTip}, {cwdTip:N}, {cwdTip:N:separator} + */ +function applyTitleFormat(format: string, values: PlaceholderValues): string { + let result = format + .replace(/\{title\}/g, values.title) + .replace(/\{cwd\}/g, values.cwd) + .replace(/\{cwdTip(?::(\d+)(?::([^}]+))?)?\}/g, (_match, depthStr, separator) => { + const depth = depthStr ? parseInt(depthStr, 10) : 1 + const sep = separator ?? '/' + return resolveCwdTip(values.cwd, depth, sep) + }) + + // Truncate final result if too long + if (result.length > 100) { + result = result.substring(0, 97) + "..." + } + + return result +} + /** * Generate title from conversation context using AI */ @@ -351,10 +394,11 @@ async function updateSessionTitle( client: OpenCodeClient, sessionId: string, logger: Logger, - config: ReturnType + config: ReturnType, + cwd: string ): Promise { try { - logger.info('update-title', 'Title update triggered', { sessionId }) + logger.info('update-title', 'Title update triggered', { sessionId, cwd }) // Extract smart context const turns = await extractSmartContext(client, sessionId, logger) @@ -381,22 +425,32 @@ async function updateSessionTitle( // Format context const context = formatContextForTitle(turns) - // Generate title - const newTitle = await generateTitleFromContext( + // Generate title from AI + const generatedTitle = await generateTitleFromContext( context, config.model, logger, client ) - if (!newTitle) { + if (!generatedTitle) { logger.warn('update-title', 'Title generation returned null', { sessionId }) return } + // Apply title format with placeholders + const placeholderValues: PlaceholderValues = { + title: generatedTitle, + cwd: cwd + } + + const newTitle = applyTitleFormat(config.titleFormat, placeholderValues) + logger.info('update-title', 'Updating session with new title', { sessionId, - title: newTitle + generatedTitle, + titleFormat: config.titleFormat, + finalTitle: newTitle }) // Update session @@ -434,11 +488,15 @@ const SmartTitlePlugin: Plugin = async (ctx) => { const logger = new Logger(config.debug) const { client } = ctx + const cwd = ctx.directory || process.cwd() + logger.info('plugin', 'Smart Title plugin initialized', { enabled: config.enabled, debug: config.debug, model: config.model, updateThreshold: config.updateThreshold, + titleFormat: config.titleFormat, + cwd, globalConfigFile: join(homedir(), ".config", "opencode", "smart-title.jsonc"), projectConfigFile: ctx.directory ? join(ctx.directory, ".opencode", "smart-title.jsonc") : "N/A", logDirectory: join(homedir(), ".config", "opencode", "logs", "smart-title") @@ -485,7 +543,7 @@ const SmartTitlePlugin: Plugin = async (ctx) => { }) // Fire and forget - don't block the event handler - updateSessionTitle(client, sessionId, logger, config).catch((error) => { + updateSessionTitle(client, sessionId, logger, config, cwd).catch((error) => { logger.error('event', 'Title update failed', { sessionId, error: error.message, diff --git a/lib/config.ts b/lib/config.ts index 13397f4..c83b1b0 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -10,12 +10,22 @@ export interface PluginConfig { debug: boolean model?: string updateThreshold: number + /** + * Title format with placeholders: + * - {title} - AI-generated title + * - {cwd} - Full current working directory path + * - {cwdTip} - Last folder name of cwd (e.g., "my-project") + * - {cwdTip:N} - Last N folder segments (e.g., {cwdTip:2} -> "github/my-project") + * - {cwdTip:N:sep} - Last N segments with custom separator (e.g., {cwdTip:2: - } -> "github - my-project") + */ + titleFormat: string } const defaultConfig: PluginConfig = { enabled: true, debug: false, - updateThreshold: 1 + updateThreshold: 1, + titleFormat: '{title}' } const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode') @@ -94,7 +104,16 @@ function createDefaultConfig(): void { // "model": "anthropic/claude-haiku-4-5", // Update title every N idle events (default: 1) - "updateThreshold": 1 + "updateThreshold": 1, + + // Title format with placeholders: + // - {title} - AI-generated title based on conversation + // - {cwd} - Full current working directory path + // - {cwdTip} - Last folder name of cwd (e.g., "my-project") + // - {cwdTip:N} - Last N folder segments (e.g., {cwdTip:2} -> "github/my-project") + // - {cwdTip:N:sep} - Last N segments with custom separator (e.g., {cwdTip:2: - } -> "github - my-project") + // Example: "[{cwdTip}] {title}" produces "[my-project] Debugging API errors" + "titleFormat": "{title}" } ` @@ -137,7 +156,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig { enabled: globalConfig.enabled ?? config.enabled, debug: globalConfig.debug ?? config.debug, model: globalConfig.model ?? config.model, - updateThreshold: globalConfig.updateThreshold ?? config.updateThreshold + updateThreshold: globalConfig.updateThreshold ?? config.updateThreshold, + titleFormat: globalConfig.titleFormat ?? config.titleFormat } } } else { @@ -151,7 +171,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig { enabled: projectConfig.enabled ?? config.enabled, debug: projectConfig.debug ?? config.debug, model: projectConfig.model ?? config.model, - updateThreshold: projectConfig.updateThreshold ?? config.updateThreshold + updateThreshold: projectConfig.updateThreshold ?? config.updateThreshold, + titleFormat: projectConfig.titleFormat ?? config.titleFormat } } } diff --git a/package.json b/package.json index c325dff..3ec6dbe 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", "@tarquinen/opencode-auth-provider": "^0.1.7", - "ai": "^5.0.98", + "ai": "^6.0.43", "jsonc-parser": "^3.3.1" }, "devDependencies": {