Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/features/claude-code-session-state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,31 @@ export function getMainSessionID(): string | undefined {
export function _resetForTesting(): void {
_mainSessionID = undefined
subagentSessions.clear()
sessionAgentMap.clear()
sessionModelMap.clear()
}

const sessionAgentMap = new Map<string, string>()
const sessionModelMap = new Map<string, { providerID: string; modelID: string }>()

export function setSessionAgent(sessionID: string, agent: string): void {
if (!sessionAgentMap.has(sessionID)) {
sessionAgentMap.set(sessionID, agent)
}
}

export function setSessionModel(sessionID: string, model: { providerID: string; modelID: string }): void {
sessionModelMap.set(sessionID, model)
}

export function getSessionModel(sessionID: string): { providerID: string; modelID: string } | undefined {
return sessionModelMap.get(sessionID)
}

export function clearSessionModel(sessionID: string): void {
sessionModelMap.delete(sessionID)
}

export function updateSessionAgent(sessionID: string, agent: string): void {
sessionAgentMap.set(sessionID, agent)
}
Expand Down
10 changes: 9 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import {
setMainSession,
getMainSessionID,
setSessionAgent,
setSessionModel,
getSessionModel,
updateSessionAgent,
clearSessionAgent,
} from "./features/claude-code-session-state";
Expand Down Expand Up @@ -380,7 +382,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
setSessionAgent(input.sessionID, input.agent);
}

const message = (output as { message: { variant?: string } }).message
const message = (output as { message: { variant?: string; model?: { providerID?: string; modelID?: string } } }).message
if (message.model?.providerID && message.model?.modelID) {
setSessionModel(input.sessionID, {
providerID: message.model.providerID,
modelID: message.model.modelID,
});
}
if (firstMessageVariantGate.shouldOverride(input.sessionID)) {
const variant = resolveAgentVariant(pluginConfig, input.agent)
if (variant !== undefined) {
Expand Down
19 changes: 19 additions & 0 deletions src/tools/session-manager/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export async function getMainSessions(options: GetMainSessionsOptions): Promise<

if (options.directory && meta.directory !== options.directory) continue

if (!meta.preview) {
meta.preview = await getFirstUserMessagePreview(meta.id)
}

sessions.push(meta)
} catch {
continue
Expand All @@ -45,6 +49,21 @@ export async function getMainSessions(options: GetMainSessionsOptions): Promise<
return sessions.sort((a, b) => b.time.updated - a.time.updated)
}

async function getFirstUserMessagePreview(sessionID: string, maxLength = 50): Promise<string | undefined> {
const messages = await readSessionMessages(sessionID)
const firstUserMessage = messages.find((m) => m.role === "user")
if (!firstUserMessage) return undefined

for (const part of firstUserMessage.parts) {
if (part.type === "text" && part.text) {
const text = part.text.trim().replace(/\s+/g, " ")
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + "..."
}
}
return undefined
}

export async function getAllSessions(): Promise<string[]> {
if (!existsSync(MESSAGE_STORAGE)) return []

Expand Down
9 changes: 5 additions & 4 deletions src/tools/session-manager/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ export const session_list: ToolDefinition = tool({
try {
const directory = args.project_path ?? process.cwd()
let sessions = await getMainSessions({ directory })
let sessionIDs = sessions.map((s) => s.id)

if (args.from_date || args.to_date) {
sessionIDs = await filterSessionsByDate(sessionIDs, args.from_date, args.to_date)
const filteredIDs = await filterSessionsByDate(sessions.map((s) => s.id), args.from_date, args.to_date)
const filteredSet = new Set(filteredIDs)
sessions = sessions.filter((s) => filteredSet.has(s.id))
}

if (args.limit && args.limit > 0) {
sessionIDs = sessionIDs.slice(0, args.limit)
sessions = sessions.slice(0, args.limit)
}

return await formatSessionList(sessionIDs)
return await formatSessionList(sessions)
} catch (e) {
return `Error: ${e instanceof Error ? e.message : String(e)}`
}
Expand Down
3 changes: 3 additions & 0 deletions src/tools/session-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface MessagePart {

export interface SessionInfo {
id: string
title?: string
message_count: number
first_message?: Date
last_message?: Date
Expand Down Expand Up @@ -65,6 +66,8 @@ export interface SessionMetadata {
deletions: number
files: number
}
/** First user message preview (auto-generated for display) */
preview?: string
}

export interface SessionListArgs {
Expand Down
4 changes: 2 additions & 2 deletions src/tools/session-manager/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
filterSessionsByDate,
searchInSession,
} from "./utils"
import type { SessionInfo, SessionMessage, SearchResult } from "./types"
import type { SessionInfo, SessionMessage, SearchResult, SessionMetadata } from "./types"

describe("session-manager utils", () => {
test("formatSessionList handles empty array", async () => {
// #given
const sessions: string[] = []
const sessions: SessionMetadata[] = []

// #when
const result = await formatSessionList(sessions)
Expand Down
67 changes: 57 additions & 10 deletions src/tools/session-manager/utils.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,73 @@
import type { SessionInfo, SessionMessage, SearchResult } from "./types"
import type { SessionInfo, SessionMessage, SearchResult, SessionMetadata } from "./types"
import { getSessionInfo, readSessionMessages } from "./storage"

export async function formatSessionList(sessionIDs: string[]): Promise<string> {
if (sessionIDs.length === 0) {
export async function formatSessionList(sessions: SessionMetadata[]): Promise<string> {
if (sessions.length === 0) {
return "No sessions found."
}

const infos = (await Promise.all(sessionIDs.map((id) => getSessionInfo(id)))).filter(
(info): info is SessionInfo => info !== null
)
interface EnrichedInfo extends SessionInfo {
preview?: string
files?: number
additions?: number
deletions?: number
}

const infos: EnrichedInfo[] = []
for (const meta of sessions) {
const info = await getSessionInfo(meta.id)
if (info) {
const enriched: EnrichedInfo = {
...info,
title: meta.title,
preview: meta.preview,
files: meta.summary?.files,
additions: meta.summary?.additions,
deletions: meta.summary?.deletions,
}
infos.push(enriched)
}
}

if (infos.length === 0) {
return "No valid sessions found."
}

const headers = ["Session ID", "Messages", "First", "Last", "Agents"]
const formatDateTime = (date: Date | undefined): string => {
if (!date) return "N/A"
const d = date.toISOString().split("T")
const time = d[1].substring(0, 5)
return `${d[0]} ${time}`
}

const formatChanges = (info: EnrichedInfo): string => {
if (info.files === undefined) return "-"
const parts: string[] = []
if (info.files > 0) parts.push(`${info.files}F`)
if (info.additions && info.additions > 0) parts.push(`+${info.additions}`)
if (info.deletions && info.deletions > 0) parts.push(`-${info.deletions}`)
return parts.length > 0 ? parts.join("/") : "-"
}

const getDisplayTitle = (info: EnrichedInfo): string => {
if (info.title && info.title.trim()) return truncate(info.title, 30)
if (info.preview) return truncate(info.preview, 30)
return "(untitled)"
}

const truncate = (str: string, maxLen: number): string => {
if (str.length <= maxLen) return str
return str.substring(0, maxLen - 3) + "..."
}

const headers = ["Session ID", "Title/Preview", "Msgs", "Updated", "Changes", "Agents"]
const rows = infos.map((info) => [
info.id,
getDisplayTitle(info),
info.message_count.toString(),
info.first_message?.toISOString().split("T")[0] ?? "N/A",
info.last_message?.toISOString().split("T")[0] ?? "N/A",
info.agents_used.join(", ") || "none",
formatDateTime(info.last_message),
formatChanges(info),
truncate(info.agents_used.join(", ") || "none", 20),
])

const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)))
Expand Down
5 changes: 4 additions & 1 deletion src/tools/skill/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types"
import type { LoadedSkill } from "../../features/opencode-skill-loader"
import { getAllSkills, extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content"
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
import { getSessionModel } from "../../features/claude-code-session-state"
import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager"
import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js"

Expand Down Expand Up @@ -163,7 +164,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
args: {
name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'code-review')"),
},
async execute(args: SkillArgs, ctx?: { agent?: string }) {
async execute(args: SkillArgs, ctx?: { agent?: string; sessionID?: string }) {
const skills = await getSkills()
const skill = skills.find(s => s.name === args.name)

Expand All @@ -183,11 +184,13 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
}

const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd()
const currentModel = ctx?.sessionID ? getSessionModel(ctx.sessionID) : undefined

const output = [
`## Skill: ${skill.name}`,
"",
`**Base directory**: ${dir}`,
...(currentModel ? [`**Current Model**: ${currentModel.providerID}/${currentModel.modelID}`] : []),
"",
body,
]
Expand Down
18 changes: 15 additions & 3 deletions src/tools/slashcommand/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared"
import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
import { loadBuiltinCommands } from "../../features/builtin-commands"
import { getSessionModel } from "../../features/claude-code-session-state"
import type { CommandScope, CommandMetadata, CommandInfo, SlashcommandToolOptions } from "./types"

function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
Expand Down Expand Up @@ -100,7 +101,12 @@ function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
}
}

async function formatLoadedCommand(cmd: CommandInfo): Promise<string> {
interface CurrentModelInfo {
providerID: string
modelID: string
}

async function formatLoadedCommand(cmd: CommandInfo, currentModel?: CurrentModelInfo): Promise<string> {
const sections: string[] = []

sections.push(`# /${cmd.name} Command\n`)
Expand All @@ -126,6 +132,11 @@ async function formatLoadedCommand(cmd: CommandInfo): Promise<string> {
}

sections.push(`**Scope**: ${cmd.scope}\n`)

if (currentModel) {
sections.push(`**Current Model**: ${currentModel.providerID}/${currentModel.modelID}\n`)
}

sections.push("---\n")
sections.push("## Command Instructions\n")

Expand Down Expand Up @@ -230,7 +241,7 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T
),
},

async execute(args) {
async execute(args, ctx) {
const allItems = await getAllItems()

if (!args.command) {
Expand All @@ -244,7 +255,8 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T
)

if (exactMatch) {
return await formatLoadedCommand(exactMatch)
const currentModel = ctx?.sessionID ? getSessionModel(ctx.sessionID) : undefined
return await formatLoadedCommand(exactMatch, currentModel)
}

const partialMatches = allItems.filter((cmd) =>
Expand Down
Loading