From ccc0a8b6c8a9390b286b10fe1f3f9e9a855e5478 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 3 Jan 2026 02:58:30 +0100 Subject: [PATCH] feat(coding-agent): added LSP format-on-write with server warmup - Added LSP format on write to automatically format code files using language server formatters after writing. - Implemented LSP server warmup at startup to avoid cold-start delays when first writing files. - Added LSP server status display in welcome banner to show which language servers are active and ready. - Fixed recent sessions display in welcome banner which was never wired up. - Improved grep tool description to document pagination options and clarify recursive search behavior. --- packages/coding-agent/CHANGELOG.md | 11 +++ packages/coding-agent/src/core/sdk.ts | 1 + .../coding-agent/src/core/session-manager.ts | 61 ++++++++++++++ .../coding-agent/src/core/settings-manager.ts | 13 +++ packages/coding-agent/src/core/tools/bash.ts | 3 +- packages/coding-agent/src/core/tools/grep.ts | 5 +- packages/coding-agent/src/core/tools/index.ts | 29 ++++++- .../coding-agent/src/core/tools/lsp/client.ts | 25 ++++++ .../coding-agent/src/core/tools/lsp/index.ts | 82 ++++++++++++++++++- packages/coding-agent/src/core/tools/write.ts | 27 +++++- packages/coding-agent/src/main.ts | 14 +++- .../interactive/components/settings-defs.ts | 9 ++ .../modes/interactive/components/welcome.ts | 41 +++++++++- .../src/modes/interactive/interactive-mode.ts | 21 ++++- 14 files changed, 325 insertions(+), 17 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4a993188c..42e00763c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,14 +2,25 @@ ## [Unreleased] +### Changed + +- Improved grep tool description to document pagination options (`headLimit`, `offset`) and clarify recursive search behavior + ### Added +- LSP format on write: Write tool now automatically formats code files using LSP after writing. Uses the language server's built-in formatter (e.g., rustfmt for Rust, gofmt for Go). Controlled via `lsp.formatOnWrite` setting (enabled by default). - LSP diagnostics on write: Write tool now returns LSP diagnostics (errors/warnings) after writing code files. This gives immediate feedback on syntax errors and type issues. Controlled via `lsp.diagnosticsOnWrite` setting (enabled by default). +- LSP server warmup at startup: LSP servers are now started at launch to avoid cold-start delays when first writing files. +- LSP server status in welcome banner: Shows which language servers are active and ready. - Edit fuzzy match setting: Added `edit.fuzzyMatch` setting (enabled by default) to control whether the edit tool accepts high-confidence fuzzy matches for whitespace/indentation differences. Toggle via `/settings`. - Multi-server LSP diagnostics: Diagnostics now query all applicable language servers for a file type. For TypeScript/JavaScript projects with Biome, this means both type errors (from tsserver) and lint errors (from Biome) are reported together. - Comprehensive LSP server configurations for 40+ languages including Rust, Go, Python, Java, Kotlin, Scala, Haskell, OCaml, Elixir, Ruby, PHP, C#, Lua, Nix, and many more. Each server includes sensible defaults for args, settings, and init options. - Extended LSP config file search paths: Now searches for `lsp.json`, `.lsp.json` in project root and `.pi/` subdirectory, plus user-level configs in `~/.pi/` and home directory. +### Fixed + +- Recent sessions now show in welcome banner (was never wired up). + ## [1.340.0] - 2026-01-03 ### Changed diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index fafa184bd..f6615cc82 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -572,6 +572,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const builtInTools = options.tools ?? createCodingTools(cwd, options.hasUI ?? false, sessionContext, { + lspFormatOnWrite: settingsManager.getLspFormatOnWrite(), lspDiagnosticsOnWrite: settingsManager.getLspDiagnosticsOnWrite(), editFuzzyMatch: settingsManager.getEditFuzzyMatch(), }); diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 9e45c7219..97a4c96c8 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -28,6 +28,7 @@ export interface SessionHeader { type: "session"; version?: number; // v1 sessions don't have this id: string; + title?: string; // Auto-generated title from first message timestamp: string; cwd: string; parentSession?: string; @@ -454,6 +455,66 @@ export function findMostRecentSession(sessionDir: string): string | null { } } +/** Recent session info for display */ +export interface RecentSessionInfo { + name: string; + path: string; + timeAgo: string; +} + +/** Format a time difference as a human-readable string */ +function formatTimeAgo(date: Date): string { + const now = Date.now(); + const diffMs = now - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return "just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +/** Get recent sessions for display in welcome screen */ +export function getRecentSessions(sessionDir: string, limit = 3): RecentSessionInfo[] { + try { + const files = readdirSync(sessionDir) + .filter((f) => f.endsWith(".jsonl")) + .map((f) => join(sessionDir, f)) + .filter(isValidSessionFile) + .map((path) => { + const stat = statSync(path); + // Try to get session name from first line + let name = path.split("/").pop()?.replace(".jsonl", "") ?? "Unknown"; + try { + const content = readFileSync(path, "utf-8"); + const firstLine = content.split("\n")[0]; + if (firstLine) { + const header = JSON.parse(firstLine) as SessionHeader; + if (header.type === "session" && header.id) { + name = header.id; + } + } + } catch { + // Use filename as fallback + } + return { path, name, mtime: stat.mtime }; + }) + .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()) + .slice(0, limit); + + return files.map((f) => ({ + name: f.name.length > 30 ? `${f.name.slice(0, 27)}...` : f.name, + path: f.path, + timeAgo: formatTimeAgo(f.mtime), + })); + } catch { + return []; + } +} + /** * Manages conversation sessions as append-only trees stored in JSONL files. * diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 146a23817..7d543b4fd 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -52,6 +52,7 @@ export interface MCPSettings { } export interface LspSettings { + formatOnWrite?: boolean; // default: true (format files using LSP after write tool writes code files) diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files) } @@ -485,6 +486,18 @@ export class SettingsManager { this.save(); } + getLspFormatOnWrite(): boolean { + return this.settings.lsp?.formatOnWrite ?? true; + } + + setLspFormatOnWrite(enabled: boolean): void { + if (!this.globalSettings.lsp) { + this.globalSettings.lsp = {}; + } + this.globalSettings.lsp.formatOnWrite = enabled; + this.save(); + } + getLspDiagnosticsOnWrite(): boolean { return this.settings.lsp?.diagnosticsOnWrite ?? true; } diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index bfdcd3b73..cb3d1d255 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -66,8 +66,7 @@ Usage notes: - If the commands are independent and can run in parallel, make multiple bash tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two bash tool calls in parallel. - If the commands depend on each other and must run sequentially, use a single bash call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead. - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail - - DO NOT use newlines to separate commands (newlines are ok in quoted strings) - - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.`, + - DO NOT use newlines to separate commands (newlines are ok in quoted strings)`, parameters: bashSchema, execute: async ( _toolCallId: string, diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 7d8745c5e..ec34a9743 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -68,10 +68,11 @@ export function createGrepTool(cwd: string): AgentTool { Usage: - ALWAYS use grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a bash command. The grep tool has been optimized for correct permissions and access. +- Searches recursively by default - no need for -r flag - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+") -- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust") +- Filter files with glob parameter (e.g., "*.ts", "**/*.spec.ts") or type parameter (e.g., "ts", "py", "rust") - equivalent to grep's --include - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts -- Use task tool for open-ended searches requiring multiple rounds +- Pagination: Use headLimit to limit results (like \`| head -N\`), offset to skip first N results - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code) - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\``, parameters: grepSchema, diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 97350c699..0c4b209ce 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -1,13 +1,25 @@ export { type AskToolDetails, askTool, createAskTool } from "./ask.js"; export { type BashToolDetails, bashTool, createBashTool } from "./bash.js"; -export { createEditTool, editTool } from "./edit.js"; +export { createEditTool, type EditToolOptions, editTool } from "./edit.js"; // Exa MCP tools (22 tools) export { exaTools } from "./exa/index.js"; export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types.js"; export { createFindTool, type FindToolDetails, findTool } from "./find.js"; export { createGrepTool, type GrepToolDetails, grepTool } from "./grep.js"; export { createLsTool, type LsToolDetails, lsTool } from "./ls.js"; -export { createLspTool, type LspToolDetails, lspTool } from "./lsp/index.js"; +export { + createLspTool, + type FileDiagnosticsResult, + type FileFormatResult, + formatFile, + getDiagnosticsForFile, + getLspStatus, + type LspServerStatus, + type LspToolDetails, + type LspWarmupResult, + lspTool, + warmupLspServers, +} from "./lsp/index.js"; export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook.js"; export { createReadTool, type ReadToolDetails, readTool } from "./read.js"; export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index.js"; @@ -41,7 +53,7 @@ import { createEditTool, editTool } from "./edit.js"; import { createFindTool, findTool } from "./find.js"; import { createGrepTool, grepTool } from "./grep.js"; import { createLsTool, lsTool } from "./ls.js"; -import { createLspTool, getDiagnosticsForFile, lspTool } from "./lsp/index.js"; +import { createLspTool, formatFile, getDiagnosticsForFile, lspTool } from "./lsp/index.js"; import { createNotebookTool, notebookTool } from "./notebook.js"; import { createReadTool, readTool } from "./read.js"; import { createTaskTool, taskTool } from "./task/index.js"; @@ -61,6 +73,10 @@ export interface SessionContext { export interface CodingToolsOptions { /** Whether to fetch LSP diagnostics after write tool writes files (default: true) */ lspDiagnosticsOnWrite?: boolean; + /** Whether to format files using LSP after write tool writes (default: true) */ + lspFormatOnWrite?: boolean; + /** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */ + editFuzzyMatch?: boolean; } // Factory function type @@ -71,12 +87,17 @@ const toolDefs: Record = { ask: { tool: askTool, create: createAskTool }, read: { tool: readTool, create: createReadTool }, bash: { tool: bashTool, create: createBashTool }, - edit: { tool: editTool, create: createEditTool }, + edit: { + tool: editTool, + create: (cwd, _ctx, options) => createEditTool(cwd, { fuzzyMatch: options?.editFuzzyMatch ?? true }), + }, write: { tool: writeTool, create: (cwd, _ctx, options) => { + const enableFormat = options?.lspFormatOnWrite ?? true; const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true; return createWriteTool(cwd, { + formatOnWrite: enableFormat ? (absolutePath) => formatFile(absolutePath, cwd) : undefined, getDiagnostics: enableDiagnostics ? (absolutePath) => getDiagnosticsForFile(absolutePath, cwd) : undefined, }); }, diff --git a/packages/coding-agent/src/core/tools/lsp/client.ts b/packages/coding-agent/src/core/tools/lsp/client.ts index 2837c2fda..3b83cec6c 100644 --- a/packages/coding-agent/src/core/tools/lsp/client.ts +++ b/packages/coding-agent/src/core/tools/lsp/client.ts @@ -98,6 +98,12 @@ const CLIENT_CAPABILITIES = { properties: ["edit"], }, }, + formatting: { + dynamicRegistration: false, + }, + rangeFormatting: { + dynamicRegistration: false, + }, publishDiagnostics: { relatedInformation: true, versionSupport: false, @@ -587,6 +593,25 @@ export function shutdownAll(): void { clients.clear(); } +/** Status of an LSP server */ +export interface LspServerStatus { + name: string; + status: "connecting" | "ready" | "error"; + fileTypes: string[]; + error?: string; +} + +/** + * Get status of all active LSP clients. + */ +export function getActiveClients(): LspServerStatus[] { + return Array.from(clients.values()).map((client) => ({ + name: client.config.command, + status: "ready" as const, + fileTypes: client.config.fileTypes, + })); +} + // ============================================================================= // Process Cleanup // ============================================================================= diff --git a/packages/coding-agent/src/core/tools/lsp/index.ts b/packages/coding-agent/src/core/tools/lsp/index.ts index 0be214106..e5ff1473e 100644 --- a/packages/coding-agent/src/core/tools/lsp/index.ts +++ b/packages/coding-agent/src/core/tools/lsp/index.ts @@ -12,7 +12,7 @@ import { sendRequest, } from "./client.js"; import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config.js"; -import { applyWorkspaceEdit } from "./edits.js"; +import { applyTextEdits, applyWorkspaceEdit } from "./edits.js"; import { renderCall, renderResult } from "./render.js"; import * as rustAnalyzer from "./rust-analyzer.js"; import { @@ -32,6 +32,7 @@ import { lspSchema, type ServerConfig, type SymbolInformation, + type TextEdit, type WorkspaceEdit, } from "./types.js"; import { @@ -318,6 +319,85 @@ export async function getDiagnosticsForFile( }; } +/** Result from formatFile */ +export interface FileFormatResult { + /** Whether an LSP server with formatting support was available */ + available: boolean; + /** Name of the LSP server used (if available) */ + serverName?: string; + /** Whether formatting was applied */ + formatted: boolean; + /** Error message if formatting failed */ + error?: string; +} + +/** Default formatting options for LSP */ +const DEFAULT_FORMAT_OPTIONS = { + tabSize: 3, + insertSpaces: true, + trimTrailingWhitespace: true, + insertFinalNewline: true, + trimFinalNewlines: true, +}; + +/** + * Format a file using LSP. + * Uses the first available server that supports formatting. + * + * @param absolutePath - Absolute path to the file + * @param cwd - Working directory for LSP config resolution + * @returns Format result indicating success/failure + */ +export async function formatFile(absolutePath: string, cwd: string): Promise { + const config = getConfig(cwd); + const servers = getServersForFile(config, absolutePath); + + if (servers.length === 0) { + return { available: false, formatted: false }; + } + + const uri = fileToUri(absolutePath); + + // Try each server until one successfully formats + for (const [serverName, serverConfig] of servers) { + try { + const client = await getOrCreateClient(serverConfig, cwd); + + // Check if server supports formatting + const caps = client.serverCapabilities; + if (!caps?.documentFormattingProvider) { + continue; + } + + // Ensure file is open and synced + await ensureFileOpen(client, absolutePath); + await refreshFile(client, absolutePath); + + // Request formatting + const edits = (await sendRequest(client, "textDocument/formatting", { + textDocument: { uri }, + options: DEFAULT_FORMAT_OPTIONS, + })) as TextEdit[] | null; + + if (!edits || edits.length === 0) { + // No changes needed - file already formatted + return { available: true, serverName, formatted: false }; + } + + // Apply the formatting edits + await applyTextEdits(absolutePath, edits); + + // Notify LSP of the change so diagnostics update + await refreshFile(client, absolutePath); + + return { available: true, serverName, formatted: true }; + } catch {} + } + + // No server could format + return { available: false, formatted: false }; +} + export function createLspTool(cwd: string): AgentTool { return { name: "lsp", diff --git a/packages/coding-agent/src/core/tools/write.ts b/packages/coding-agent/src/core/tools/write.ts index daae23dfe..1f312789f 100644 --- a/packages/coding-agent/src/core/tools/write.ts +++ b/packages/coding-agent/src/core/tools/write.ts @@ -2,7 +2,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core"; import { Type } from "@sinclair/typebox"; import { mkdir, writeFile } from "fs/promises"; import { dirname } from "path"; -import type { FileDiagnosticsResult } from "./lsp/index.js"; +import type { FileDiagnosticsResult, FileFormatResult } from "./lsp/index.js"; import { resolveToCwd } from "./path-utils.js"; const writeSchema = Type.Object({ @@ -12,12 +12,18 @@ const writeSchema = Type.Object({ /** Options for creating the write tool */ export interface WriteToolOptions { + /** Callback to format file using LSP after writing */ + formatOnWrite?: (absolutePath: string) => Promise; /** Callback to get LSP diagnostics after writing a file */ getDiagnostics?: (absolutePath: string) => Promise; } /** Details returned by the write tool for TUI rendering */ export interface WriteToolDetails { + /** Whether the file was formatted */ + wasFormatted: boolean; + /** Format result (if available) */ + formatResult?: FileFormatResult; /** Whether LSP diagnostics were retrieved */ hasDiagnostics: boolean; /** Diagnostic result (if available) */ @@ -92,7 +98,17 @@ Usage: signal.removeEventListener("abort", onAbort); } - // Get LSP diagnostics if callback provided + // Format file if callback provided (before diagnostics) + let formatResult: FileFormatResult | undefined; + if (options.formatOnWrite) { + try { + formatResult = await options.formatOnWrite(absolutePath); + } catch { + // Ignore formatting errors - don't fail the write + } + } + + // Get LSP diagnostics if callback provided (after formatting) let diagnosticsResult: FileDiagnosticsResult | undefined; if (options.getDiagnostics) { try { @@ -105,6 +121,11 @@ Usage: // Build result text let resultText = `Successfully wrote ${content.length} bytes to ${path}`; + // Note if file was formatted + if (formatResult?.formatted) { + resultText += ` (formatted by ${formatResult.serverName})`; + } + // Append diagnostics if available and there are issues if (diagnosticsResult?.available && diagnosticsResult.diagnostics.length > 0) { resultText += `\n\nLSP Diagnostics (${diagnosticsResult.summary}):\n`; @@ -114,6 +135,8 @@ Usage: resolve({ content: [{ type: "text", text: resultText }], details: { + wasFormatted: formatResult?.formatted ?? false, + formatResult, hasDiagnostics: diagnosticsResult?.available ?? false, diagnostics: diagnosticsResult, }, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 2abd6e993..2848f3a75 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -62,11 +62,20 @@ async function runInteractiveMode( initialMessages: string[], customTools: LoadedCustomTool[], setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void, + lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined, initialMessage?: string, initialImages?: ImageContent[], fdPath: string | undefined = undefined, ): Promise { - const mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath); + const mode = new InteractiveMode( + session, + version, + changelogMarkdown, + customTools, + setToolUIContext, + lspServers, + fdPath, + ); await mode.init(); @@ -393,7 +402,7 @@ export async function main(args: string[]) { } time("buildSessionOptions"); - const { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions); + const { session, customToolsResult, modelFallbackMessage, lspServers } = await createAgentSession(sessionOptions); time("createAgentSession"); if (!isInteractive && !session.model) { @@ -449,6 +458,7 @@ export async function main(args: string[]) { parsed.messages, customToolsResult.tools, customToolsResult.setUIContext, + lspServers, initialMessage, initialImages, fdPath, diff --git a/packages/coding-agent/src/modes/interactive/components/settings-defs.ts b/packages/coding-agent/src/modes/interactive/components/settings-defs.ts index 5b7745433..54e04532a 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-defs.ts @@ -135,6 +135,15 @@ export const SETTINGS_DEFS: SettingDef[] = [ get: (sm) => sm.getMCPProjectConfigEnabled(), set: (sm, v) => sm.setMCPProjectConfigEnabled(v), }, + { + id: "lspFormatOnWrite", + tab: "config", + type: "boolean", + label: "LSP format on write", + description: "Automatically format code files using LSP after writing", + get: (sm) => sm.getLspFormatOnWrite(), + set: (sm, v) => sm.setLspFormatOnWrite(v), + }, { id: "lspDiagnosticsOnWrite", tab: "config", diff --git a/packages/coding-agent/src/modes/interactive/components/welcome.ts b/packages/coding-agent/src/modes/interactive/components/welcome.ts index 361f9c2b8..69d93cf9c 100644 --- a/packages/coding-agent/src/modes/interactive/components/welcome.ts +++ b/packages/coding-agent/src/modes/interactive/components/welcome.ts @@ -7,6 +7,12 @@ export interface RecentSession { timeAgo: string; } +export interface LspServerInfo { + name: string; + status: "ready" | "error" | "connecting"; + fileTypes: string[]; +} + /** * Premium welcome screen with block-based Pi logo and two-column layout. */ @@ -15,12 +21,20 @@ export class WelcomeComponent implements Component { private modelName: string; private providerName: string; private recentSessions: RecentSession[]; + private lspServers: LspServerInfo[]; - constructor(version: string, modelName: string, providerName: string, recentSessions: RecentSession[] = []) { + constructor( + version: string, + modelName: string, + providerName: string, + recentSessions: RecentSession[] = [], + lspServers: LspServerInfo[] = [], + ) { this.version = version; this.modelName = modelName; this.providerName = providerName; this.recentSessions = recentSessions; + this.lspServers = lspServers; } invalidate(): void {} @@ -34,6 +48,10 @@ export class WelcomeComponent implements Component { this.recentSessions = sessions; } + setLspServers(servers: LspServerInfo[]): void { + this.lspServers = servers; + } + render(termWidth: number): string[] { // Box dimensions - responsive with min/max const minWidth = 80; @@ -76,13 +94,32 @@ export class WelcomeComponent implements Component { } } + // LSP servers content + const lspLines: string[] = []; + if (this.lspServers.length === 0) { + lspLines.push(` ${theme.fg("dim", "No LSP servers")}`); + } else { + for (const server of this.lspServers) { + const icon = + server.status === "ready" + ? theme.fg("success", "●") + : server.status === "connecting" + ? theme.fg("warning", "○") + : theme.fg("error", "●"); + const exts = server.fileTypes.slice(0, 3).join(" "); + lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`); + } + } + // Right column const rightLines = [ ` ${theme.bold(theme.fg("accent", "Tips"))}`, ` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`, ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`, ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`, - ` ${theme.fg("dim", "/status")}${theme.fg("muted", " for loaded extensions")}`, + separator, + ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`, + ...lspLines, separator, ` ${theme.bold(theme.fg("accent", "Recent sessions"))}`, ...sessionLines, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index b4a63a371..de72a315e 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -28,7 +28,7 @@ import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.j import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index.js"; import type { HookUIContext } from "../../core/hooks/index.js"; import { createCompactionSummaryMessage } from "../../core/messages.js"; -import { type SessionContext, SessionManager } from "../../core/session-manager.js"; +import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager.js"; import { loadSkills } from "../../core/skills.js"; import { loadProjectContextFiles } from "../../core/system-prompt.js"; import type { TruncationResult } from "../../core/tools/truncate.js"; @@ -158,6 +158,9 @@ export class InteractiveMode { changelogMarkdown: string | undefined = undefined, customTools: LoadedCustomTool[] = [], private setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void = () => {}, + private lspServers: + | Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> + | undefined = undefined, fdPath: string | undefined = undefined, ) { this.session = session; @@ -225,8 +228,22 @@ export class InteractiveMode { const modelName = this.session.model?.name ?? "Unknown"; const providerName = this.session.model?.provider ?? "Unknown"; + // Get recent sessions + const recentSessions = getRecentSessions(this.sessionManager.getSessionDir()).map((s) => ({ + name: s.name, + timeAgo: s.timeAgo, + })); + + // Convert LSP servers to welcome format + const lspServerInfo = + this.lspServers?.map((s) => ({ + name: s.name, + status: s.status as "ready" | "error" | "connecting", + fileTypes: s.fileTypes, + })) ?? []; + // Add welcome header - const welcome = new WelcomeComponent(this.version, modelName, providerName); + const welcome = new WelcomeComponent(this.version, modelName, providerName, recentSessions, lspServerInfo); // Setup UI layout this.ui.addChild(new Spacer(1));