fix: plugin event shape mismatches, nav highlight bug, and README update
- Fix session.created/deleted reading props.info.id instead of props.id - Fix session.diff reading FileDiff[] array instead of string - Fix file.edited reading props.file instead of props.filePath - Add auto-session creation fallback from tool/chat hooks - Add flushSession() for intermediate trace sends on session.idle - Fix dashboard nav: /dashboard exact match prevents false active state - Update README with TypeScript SDK and OpenCode plugin sections
This commit is contained in:
@@ -5,6 +5,19 @@ import { loadConfig } from "./config.js";
|
||||
import { SessionState } from "./state.js";
|
||||
import { truncate, safeJsonValue } from "./utils.js";
|
||||
|
||||
/**
|
||||
* OpenCode Event shapes (from @opencode-ai/sdk):
|
||||
*
|
||||
* session.created → { type, properties: { info: Session } }
|
||||
* session.idle → { type, properties: { sessionID: string } }
|
||||
* session.deleted → { type, properties: { info: Session } }
|
||||
* session.error → { type, properties: { sessionID?: string, error?: ... } }
|
||||
* session.diff → { type, properties: { sessionID: string, diff: FileDiff[] } }
|
||||
* file.edited → { type, properties: { file: string } }
|
||||
*
|
||||
* Session = { id, projectID, directory, title, ... }
|
||||
*/
|
||||
|
||||
const plugin: Plugin = async ({ project, directory, worktree }) => {
|
||||
const config = loadConfig();
|
||||
|
||||
@@ -13,6 +26,8 @@ const plugin: Plugin = async ({ project, directory, worktree }) => {
|
||||
return {};
|
||||
}
|
||||
|
||||
console.log(`[agentlens] Plugin enabled — endpoint: ${config.endpoint}`);
|
||||
|
||||
init({
|
||||
apiKey: config.apiKey,
|
||||
endpoint: config.endpoint,
|
||||
@@ -22,6 +37,11 @@ const plugin: Plugin = async ({ project, directory, worktree }) => {
|
||||
|
||||
const state = new SessionState();
|
||||
|
||||
/** Helper: get a session ID from the active traces (fallback for events that lack one) */
|
||||
function getAnySessionId(): string | undefined {
|
||||
return state.getActiveSessionIds()[0];
|
||||
}
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
const type = event.type;
|
||||
@@ -29,66 +49,118 @@ const plugin: Plugin = async ({ project, directory, worktree }) => {
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
if (type === "session.created" && props?.["id"]) {
|
||||
state.startSession(String(props["id"]), {
|
||||
project: project.id,
|
||||
directory,
|
||||
worktree,
|
||||
});
|
||||
if (type === "session.created") {
|
||||
// props.info is a Session object with { id, projectID, ... }
|
||||
const info = props?.["info"] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const sessionId = info?.["id"] as string | undefined;
|
||||
if (sessionId) {
|
||||
state.startSession(sessionId, {
|
||||
project: project.id,
|
||||
directory,
|
||||
worktree,
|
||||
title: info?.["title"] as string | undefined,
|
||||
});
|
||||
console.log(`[agentlens] Session started: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "session.idle") {
|
||||
const sessionId = props?.["sessionID"] ?? props?.["id"];
|
||||
if (sessionId) await flush();
|
||||
// props.sessionID is the session ID string
|
||||
const sessionId =
|
||||
(props?.["sessionID"] as string) || getAnySessionId();
|
||||
if (sessionId) {
|
||||
// Flush intermediate trace so data isn't lost if session ends abruptly
|
||||
state.flushSession(sessionId);
|
||||
await flush();
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "session.error") {
|
||||
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
|
||||
const sessionId =
|
||||
(props?.["sessionID"] as string) || getAnySessionId() || "";
|
||||
if (sessionId) {
|
||||
const trace = state.getTrace(sessionId);
|
||||
if (trace) {
|
||||
const error = props?.["error"] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
trace.addEvent({
|
||||
type: EventTypeValues.ERROR,
|
||||
name: String(props?.["error"] ?? "session error"),
|
||||
metadata: safeJsonValue(props) as JsonValue,
|
||||
name: String(
|
||||
error?.["name"] ?? error?.["message"] ?? "session error",
|
||||
),
|
||||
metadata: safeJsonValue(error ?? props) as JsonValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "session.deleted") {
|
||||
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
|
||||
if (sessionId) state.endSession(sessionId);
|
||||
// props.info is a Session object with { id, ... }
|
||||
const info = props?.["info"] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const sessionId =
|
||||
(info?.["id"] as string) || getAnySessionId() || "";
|
||||
if (sessionId) {
|
||||
state.endSession(sessionId);
|
||||
await flush();
|
||||
console.log(`[agentlens] Session ended and flushed: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "session.diff") {
|
||||
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
|
||||
// props.sessionID + props.diff (FileDiff[])
|
||||
const sessionId =
|
||||
(props?.["sessionID"] as string) || getAnySessionId() || "";
|
||||
if (sessionId) {
|
||||
const trace = state.getTrace(sessionId);
|
||||
if (trace) {
|
||||
trace.setMetadata({
|
||||
diff: truncate(String(props?.["diff"] ?? ""), 5000),
|
||||
});
|
||||
const diffs = props?.["diff"];
|
||||
trace.setMetadata(
|
||||
safeJsonValue({
|
||||
diff: Array.isArray(diffs)
|
||||
? diffs.map((d: Record<string, unknown>) => ({
|
||||
path: d?.["path"],
|
||||
additions: d?.["additions"],
|
||||
deletions: d?.["deletions"],
|
||||
}))
|
||||
: diffs,
|
||||
}) as JsonValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "file.edited") {
|
||||
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
|
||||
// props.file is a string (file path), no sessionID on this event
|
||||
const file = props?.["file"] as string | undefined;
|
||||
const sessionId = getAnySessionId();
|
||||
const trace = sessionId ? state.getTrace(sessionId) : undefined;
|
||||
if (trace) {
|
||||
if (trace && file) {
|
||||
trace.addEvent({
|
||||
type: EventTypeValues.CUSTOM,
|
||||
name: "file.edited",
|
||||
metadata: safeJsonValue({
|
||||
filePath: props?.["filePath"],
|
||||
}) as JsonValue,
|
||||
metadata: safeJsonValue({ filePath: file }) as JsonValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
// Auto-create session if we missed session.created event
|
||||
if (!state.getTrace(input.sessionID)) {
|
||||
state.startSession(input.sessionID, {
|
||||
project: project.id,
|
||||
directory,
|
||||
worktree,
|
||||
});
|
||||
console.log(
|
||||
`[agentlens] Auto-created session from tool call: ${input.sessionID}`,
|
||||
);
|
||||
}
|
||||
state.startToolCall(
|
||||
input.callID,
|
||||
input.tool,
|
||||
@@ -107,6 +179,17 @@ const plugin: Plugin = async ({ project, directory, worktree }) => {
|
||||
},
|
||||
|
||||
"chat.message": async (input) => {
|
||||
// Auto-create session if we missed session.created event
|
||||
if (!state.getTrace(input.sessionID)) {
|
||||
state.startSession(input.sessionID, {
|
||||
project: project.id,
|
||||
directory,
|
||||
worktree,
|
||||
});
|
||||
console.log(
|
||||
`[agentlens] Auto-created session from chat.message: ${input.sessionID}`,
|
||||
);
|
||||
}
|
||||
if (input.model) {
|
||||
state.recordLLMCall(input.sessionID, {
|
||||
model: input.model,
|
||||
|
||||
Reference in New Issue
Block a user