v0.2.0: add message operation, role filter, showTools, full session IDs
- Add message operation to retrieve a single message by ID (cleaner output, higher default maxLength of 8000) - Add role filter to messages operation (user/assistant/system) - Hide tool-call parts by default in messages/message output; showTools:true to include them - Show full session IDs instead of truncating to 12 chars - Make per-message maxLength configurable (default 2000 for messages, 8000 for message) - Increase search snippet length from 300 to 500 chars - Add local development & testing docs to AGENTS.md with symlink instructions - Bump version from 0.1.0 to 0.2.0
This commit is contained in:
45
AGENTS.md
45
AGENTS.md
@@ -74,12 +74,17 @@ The `memory` tool dispatches to internal handlers by `tool` name, keeping the ag
|
|||||||
| help | Show available operations, or details for a specific one | tool (optional) |
|
| help | Show available operations, or details for a specific one | tool (optional) |
|
||||||
| summary | Quick counts: projects, sessions, messages, todos | — |
|
| summary | Quick counts: projects, sessions, messages, todos | — |
|
||||||
| sessions | List recent sessions, optionally filtered by project | limit, projectPath |
|
| sessions | List recent sessions, optionally filtered by project | limit, projectPath |
|
||||||
| messages | Read messages from a specific session | sessionId, limit |
|
| messages | Read messages from a session, with filtering options | sessionId, limit, role, showTools, maxLength |
|
||||||
|
| message | Read a single message by ID (cleaner output, no tool-call noise) | messageId, maxLength |
|
||||||
| search | Text search across all conversations (LIKE-based) | query, limit |
|
| search | Text search across all conversations (LIKE-based) | query, limit |
|
||||||
| compactions | List/read compaction checkpoints for a session | sessionId, read (1-based index) |
|
| compactions | List/read compaction checkpoints for a session | sessionId, read (1-based index) |
|
||||||
| context | Current context window usage (% , tokens, model, status) | — |
|
| context | Current context window usage (% , tokens, model, status) | — |
|
||||||
| plans | List or read saved plan files | read (filename) |
|
| plans | List or read saved plan files | read (filename) |
|
||||||
|
|
||||||
|
**`messages` operation** hides tool-call parts (Read, Write, Bash, etc.) by default for cleaner output. Set `showTools: true` to include them. Use `role` to filter to `"user"`, `"assistant"`, or `"system"`. Use `maxLength` to override the default 2000-char per-message limit.
|
||||||
|
|
||||||
|
**`message` operation** retrieves a single message by its ID (`msg_...`). Default maxLength is 8000 (higher since it's a single message). Tool calls are hidden by default; set `showTools: true` to include them.
|
||||||
|
|
||||||
### Database Access
|
### Database Access
|
||||||
|
|
||||||
- Uses `bun:sqlite` native driver — no subprocess, no `sqlite3` CLI dependency
|
- Uses `bun:sqlite` native driver — no subprocess, no `sqlite3` CLI dependency
|
||||||
@@ -123,6 +128,41 @@ All write operations (compaction triggering) go through the OpenCode client SDK
|
|||||||
|
|
||||||
**`memory_compact` must NOT await `ctx.client.session.summarize()`** — it returns immediately and schedules via `setTimeout(() => { ... }, 0)` because compaction cannot start until the tool returns control to the event loop.
|
**`memory_compact` must NOT await `ctx.client.session.summarize()`** — it returns immediately and schedules via `setTimeout(() => { ... }, 0)` because compaction cannot start until the tool returns control to the event loop.
|
||||||
|
|
||||||
|
## Local Development & Testing
|
||||||
|
|
||||||
|
OpenCode installs plugins from npm into `~/.cache/opencode/node_modules/`. When doing local development, you must symlink your local repo to that location, otherwise OpenCode will load the stale npm-published version even after you rebuild.
|
||||||
|
|
||||||
|
### Setup (one-time)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove the npm-installed copy and replace with a symlink
|
||||||
|
rm -rf ~/.cache/opencode/node_modules/@alkdev/open-memory
|
||||||
|
ln -s /workspace/@alkdev/open-memory ~/.cache/opencode/node_modules/@alkdev/open-memory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Iteration loop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build # rebuild dist/index.js
|
||||||
|
bun run typecheck # verify types
|
||||||
|
bun run lint # verify style
|
||||||
|
bun run test # run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
After rebuilding, restart OpenCode to pick up the new build. OpenCode loads plugins at startup and caches the ESM module in memory for the session.
|
||||||
|
|
||||||
|
### Also clear Bun's global cache
|
||||||
|
|
||||||
|
If you previously installed via `bun add`, Bun caches the package in `~/.bun/install/cache/`. After publishing a new version, clear it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf ~/.bun/install/cache/@alkdev/open-memory*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without the symlink
|
||||||
|
|
||||||
|
Without the symlink, `bun run build` will update `dist/index.js` in your repo, but OpenCode will still load from `~/.cache/opencode/node_modules/@alkdev/open-memory/dist/index.js` which is a separate copy from the npm registry. Rebuilding does NOT update that copy. This is the most common source of "my changes aren't showing up" during plugin development.
|
||||||
|
|
||||||
## Key Conventions
|
## Key Conventions
|
||||||
|
|
||||||
- No comments unless requested
|
- No comments unless requested
|
||||||
@@ -157,6 +197,9 @@ Read-only tool for introspecting your session history and context state. Availab
|
|||||||
- `memory({tool: "summary"})` — quick counts of projects, sessions, messages, todos
|
- `memory({tool: "summary"})` — quick counts of projects, sessions, messages, todos
|
||||||
- `memory({tool: "sessions"})` — list recent sessions (useful for finding past work)
|
- `memory({tool: "sessions"})` — list recent sessions (useful for finding past work)
|
||||||
- `memory({tool: "messages", args: {sessionId: "..."}})` — read a session's conversation
|
- `memory({tool: "messages", args: {sessionId: "..."}})` — read a session's conversation
|
||||||
|
- `memory({tool: "messages", args: {sessionId: "...", role: "assistant"}})` — read only assistant messages
|
||||||
|
- `memory({tool: "messages", args: {sessionId: "...", showTools: true}})` — include tool-call output
|
||||||
|
- `memory({tool: "message", args: {messageId: "msg_..."}})` — read a single message by ID
|
||||||
- `memory({tool: "search", args: {query: "..."}})` — search across all conversations
|
- `memory({tool: "search", args: {query: "..."}})` — search across all conversations
|
||||||
- `memory({tool: "compactions", args: {sessionId: "..."}})` — view compaction checkpoints
|
- `memory({tool: "compactions", args: {sessionId: "..."}})` — view compaction checkpoints
|
||||||
- `memory({tool: "context"})` — check your current context window usage
|
- `memory({tool: "context"})` — check your current context window usage
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@alkdev/open-memory",
|
"name": "@alkdev/open-memory",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "OpenCode plugin for session memory browsing, context awareness, and compaction management.",
|
"description": "OpenCode plugin for session memory browsing, context awareness, and compaction management.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const formatSessionList = (rows: Record<string, unknown>[]): string => {
|
|||||||
lines.push("|----|-------|---------|----------|");
|
lines.push("|----|-------|---------|----------|");
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const id = `${String(row.id ?? "").slice(0, 12)}...`;
|
const id = String(row.id ?? "");
|
||||||
const title = String(row.title ?? "untitled").slice(0, 40);
|
const title = String(row.title ?? "untitled").slice(0, 40);
|
||||||
const updated = String(row.updated ?? "");
|
const updated = String(row.updated ?? "");
|
||||||
const msgs = String(row.msgs ?? "0");
|
const msgs = String(row.msgs ?? "0");
|
||||||
@@ -22,9 +22,14 @@ export const formatSessionList = (rows: Record<string, unknown>[]): string => {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatMessageList = (rows: Record<string, unknown>[]): string => {
|
export const formatMessageList = (
|
||||||
|
rows: Record<string, unknown>[],
|
||||||
|
options?: { maxLength?: number },
|
||||||
|
): string => {
|
||||||
if (rows.length === 0) return "No messages found.";
|
if (rows.length === 0) return "No messages found.";
|
||||||
|
|
||||||
|
const maxLen = options?.maxLength ?? 2000;
|
||||||
|
|
||||||
const lines: string[] = ["# Conversation\n"];
|
const lines: string[] = ["# Conversation\n"];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -35,9 +40,34 @@ export const formatMessageList = (rows: Record<string, unknown>[]): string => {
|
|||||||
const header = `${icon} **${role}** _${time}_`;
|
const header = `${icon} **${role}** _${time}_`;
|
||||||
|
|
||||||
lines.push(header);
|
lines.push(header);
|
||||||
lines.push(text.slice(0, 2000));
|
const truncated = text.length > maxLen;
|
||||||
|
lines.push(
|
||||||
|
truncated
|
||||||
|
? text.slice(0, maxLen) +
|
||||||
|
`\n... (${text.length} chars total, use maxLength or message tool for full content)`
|
||||||
|
: text,
|
||||||
|
);
|
||||||
lines.push("---");
|
lines.push("---");
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatSingleMessage = (
|
||||||
|
row: Record<string, unknown>,
|
||||||
|
options?: { maxLength?: number },
|
||||||
|
): string => {
|
||||||
|
const maxLen = options?.maxLength ?? 8000;
|
||||||
|
const role = String(row.role ?? "unknown");
|
||||||
|
const time = String(row.time ?? "");
|
||||||
|
const text = String(row.text ?? "");
|
||||||
|
const icon = role === "user" ? "👤" : role === "assistant" ? "🤖" : "📝";
|
||||||
|
|
||||||
|
const truncated = text.length > maxLen;
|
||||||
|
const displayText = truncated
|
||||||
|
? text.slice(0, maxLen) +
|
||||||
|
`\n... (${text.length} chars total, increase maxLength for full content)`
|
||||||
|
: text;
|
||||||
|
|
||||||
|
return [`${icon} **${role}** _${time}_\n`, displayText].join("\n");
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const searchConversations = (searchTerm: string, limit: number): string =
|
|||||||
COALESCE(s.title, 'untitled') AS title,
|
COALESCE(s.title, 'untitled') AS title,
|
||||||
json_extract(m.data, '$.role') AS role,
|
json_extract(m.data, '$.role') AS role,
|
||||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||||
substr(json_extract(p.data, '$.text'), 1, 300) AS snippet
|
substr(json_extract(p.data, '$.text'), 1, 500) AS snippet
|
||||||
FROM part p
|
FROM part p
|
||||||
JOIN message m ON m.id = p.message_id
|
JOIN message m ON m.id = p.message_id
|
||||||
JOIN session s ON s.id = m.session_id
|
JOIN session s ON s.id = m.session_id
|
||||||
@@ -38,14 +38,14 @@ export const searchConversations = (searchTerm: string, limit: number): string =
|
|||||||
const lines: string[] = [`# Search: "${searchTerm}"\n`];
|
const lines: string[] = [`# Search: "${searchTerm}"\n`];
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const sessionId = String(row.session_id ?? "").slice(0, 16);
|
const sessionId = String(row.session_id ?? "");
|
||||||
const title = String(row.title ?? "untitled");
|
const title = String(row.title ?? "untitled");
|
||||||
const time = String(row.time ?? "");
|
const time = String(row.time ?? "");
|
||||||
const role = String(row.role ?? "unknown");
|
const role = String(row.role ?? "unknown");
|
||||||
const snippet = String(row.snippet ?? "");
|
const snippet = String(row.snippet ?? "");
|
||||||
|
|
||||||
lines.push(`### ${title} (${time})`);
|
lines.push(`### ${title} (${time})`);
|
||||||
lines.push(`- Session: \`${sessionId}...\``);
|
lines.push(`- Session: \`${sessionId}\``);
|
||||||
lines.push(`- Role: ${role}`);
|
lines.push(`- Role: ${role}`);
|
||||||
lines.push(`- Snippet: ${snippet}...`);
|
lines.push(`- Snippet: ${snippet}...`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|||||||
73
src/tools.ts
73
src/tools.ts
@@ -1,7 +1,7 @@
|
|||||||
import type { PluginInput, ToolDefinition } from "@opencode-ai/plugin";
|
import type { PluginInput, ToolDefinition } from "@opencode-ai/plugin";
|
||||||
import { tool } from "@opencode-ai/plugin";
|
import { tool } from "@opencode-ai/plugin";
|
||||||
import type { ContextTracker } from "./context/tracker.js";
|
import type { ContextTracker } from "./context/tracker.js";
|
||||||
import { formatMessageList, formatSessionList } from "./history/format.js";
|
import { formatMessageList, formatSessionList, formatSingleMessage } from "./history/format.js";
|
||||||
import { runQuery } from "./history/queries.js";
|
import { runQuery } from "./history/queries.js";
|
||||||
import { searchConversations } from "./history/search.js";
|
import { searchConversations } from "./history/search.js";
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ Call \`memory({tool: "<name>", args: {...}})\` to use one.
|
|||||||
|------|-------------|----------|
|
|------|-------------|----------|
|
||||||
| summary | Count of projects, sessions, messages, todos | — |
|
| summary | Count of projects, sessions, messages, todos | — |
|
||||||
| sessions | List recent sessions, optionally filtered by project | limit, projectPath |
|
| sessions | List recent sessions, optionally filtered by project | limit, projectPath |
|
||||||
| messages | Read messages from a session as formatted conversation | sessionId, limit |
|
| messages | Read messages from a session as formatted conversation | sessionId, limit, role, showTools, maxLength |
|
||||||
|
| message | Read a single message by ID (cleaner output, no tool-call noise) | messageId, maxLength |
|
||||||
| search | Text search across all conversations | query, limit |
|
| search | Text search across all conversations | query, limit |
|
||||||
| compactions | List/read compaction checkpoints for a session | sessionId, read (1-based index) |
|
| compactions | List/read compaction checkpoints for a session | sessionId, read (1-based index) |
|
||||||
| context | Current context window usage (% , tokens, status) | — |
|
| context | Current context window usage (% , tokens, status) | — |
|
||||||
@@ -29,6 +30,8 @@ Call \`memory({tool: "<name>", args: {...}})\` to use one.
|
|||||||
Examples:
|
Examples:
|
||||||
- \`memory({tool: "search", args: {query: "safetensors"}})\`
|
- \`memory({tool: "search", args: {query: "safetensors"}})\`
|
||||||
- \`memory({tool: "compactions", args: {sessionId: "ses_abc", read: 1}})\`
|
- \`memory({tool: "compactions", args: {sessionId: "ses_abc", read: 1}})\`
|
||||||
|
- \`memory({tool: "messages", args: {sessionId: "ses_abc", role: "assistant"}})\`
|
||||||
|
- \`memory({tool: "message", args: {messageId: "msg_abc"}})\`
|
||||||
- \`memory({tool: "help", args: {tool: "search"}})\``;
|
- \`memory({tool: "help", args: {tool: "search"}})\``;
|
||||||
|
|
||||||
const TOOL_HELP: Record<string, string> = {
|
const TOOL_HELP: Record<string, string> = {
|
||||||
@@ -36,7 +39,10 @@ const TOOL_HELP: Record<string, string> = {
|
|||||||
sessions: `**sessions** — List recent sessions with titles, update times, message counts.
|
sessions: `**sessions** — List recent sessions with titles, update times, message counts.
|
||||||
Args: limit (number, default 10), projectPath (string, optional filter by worktree path).`,
|
Args: limit (number, default 10), projectPath (string, optional filter by worktree path).`,
|
||||||
messages: `**messages** — Read messages from a specific session as formatted conversation.
|
messages: `**messages** — Read messages from a specific session as formatted conversation.
|
||||||
Args: sessionId (string, required), limit (number, default 50).`,
|
By default, tool-call parts (Read, Write, Bash, etc.) are hidden to reduce noise. Set showTools: true to include them.
|
||||||
|
Args: sessionId (string, required), limit (number, default 50), role (string, optional filter: "user", "assistant", "system"), showTools (boolean, default false), maxLength (number, default 2000 per message).`,
|
||||||
|
message: `**message** — Read a single message by ID. Shows the message with tool calls hidden by default, formatted cleanly.
|
||||||
|
Args: messageId (string, required), showTools (boolean, default false), maxLength (number, default 8000).`,
|
||||||
search: `**search** — Text search across all conversations. Returns matching snippets with session references.
|
search: `**search** — Text search across all conversations. Returns matching snippets with session references.
|
||||||
Args: query (string, required), limit (number, default 10).`,
|
Args: query (string, required), limit (number, default 10).`,
|
||||||
compactions: `**compactions** — List compaction checkpoints for a session. Compactions are summaries created when context was freed. Use 'read' to get the full summary text — these act as checkpoints showing what was important at that point.
|
compactions: `**compactions** — List compaction checkpoints for a session. Compactions are summaries created when context was freed. Use 'read' to get the full summary text — these act as checkpoints showing what was important at that point.
|
||||||
@@ -133,22 +139,71 @@ const handlers: Record<string, MemoryHandler> = {
|
|||||||
messages(args) {
|
messages(args) {
|
||||||
const sessionId = args.sessionId as string;
|
const sessionId = args.sessionId as string;
|
||||||
const limit = (args.limit as number) ?? 50;
|
const limit = (args.limit as number) ?? 50;
|
||||||
|
const roleFilter = args.role as string | undefined;
|
||||||
|
const showTools = (args.showTools as boolean) ?? false;
|
||||||
|
const maxLength = args.maxLength as number | undefined;
|
||||||
if (!sessionId) return "sessionId is required.";
|
if (!sessionId) return "sessionId is required.";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
type MessageRow = { role: string; time: string; text: string };
|
type MessageRow = { role: string; time: string; text: string };
|
||||||
|
let rows: MessageRow[];
|
||||||
|
|
||||||
|
if (roleFilter) {
|
||||||
|
rows = runQuery<MessageRow>(
|
||||||
|
`SELECT json_extract(m.data, '$.role') AS role,
|
||||||
|
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||||
|
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||||
|
FROM message m LEFT JOIN part p ON p.message_id = m.id ${showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'"}
|
||||||
|
WHERE m.session_id = $sessionId AND json_extract(m.data, '$.role') = $role
|
||||||
|
GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
||||||
|
{ $sessionId: sessionId, $role: roleFilter, $limit: limit },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
rows = runQuery<MessageRow>(
|
||||||
|
`SELECT json_extract(m.data, '$.role') AS role,
|
||||||
|
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||||
|
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||||
|
FROM message m LEFT JOIN part p ON p.message_id = m.id ${showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'"}
|
||||||
|
WHERE m.session_id = $sessionId
|
||||||
|
GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
||||||
|
{ $sessionId: sessionId, $limit: limit },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
const filterDesc = roleFilter ? ` with role="${roleFilter}"` : "";
|
||||||
|
return `No messages found for session ${sessionId}${filterDesc}.`;
|
||||||
|
}
|
||||||
|
return formatMessageList(rows, { maxLength });
|
||||||
|
} catch (err) {
|
||||||
|
return `Failed to query messages: ${err instanceof Error ? err.message : String(err)}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
message(args) {
|
||||||
|
const messageId = args.messageId as string;
|
||||||
|
const showTools = (args.showTools as boolean) ?? false;
|
||||||
|
const maxLength = args.maxLength as number | undefined;
|
||||||
|
if (!messageId) return "messageId is required.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
type MessageRow = { role: string; time: string; text: string };
|
||||||
|
const partFilter = showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'";
|
||||||
|
|
||||||
const rows = runQuery<MessageRow>(
|
const rows = runQuery<MessageRow>(
|
||||||
`SELECT json_extract(m.data, '$.role') AS role,
|
`SELECT json_extract(m.data, '$.role') AS role,
|
||||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||||
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||||
FROM message m LEFT JOIN part p ON p.message_id = m.id AND json_extract(p.data, '$.type') = 'text'
|
FROM message m LEFT JOIN part p ON p.message_id = m.id ${partFilter}
|
||||||
WHERE m.session_id = $sessionId GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
WHERE m.id = $messageId
|
||||||
{ $sessionId: sessionId, $limit: limit },
|
GROUP BY m.id`,
|
||||||
|
{ $messageId: messageId },
|
||||||
);
|
);
|
||||||
if (!rows || rows.length === 0) return `No messages found for session ${sessionId}.`;
|
|
||||||
return formatMessageList(rows);
|
if (!rows || rows.length === 0) return `No message found with ID ${messageId}.`;
|
||||||
|
return formatSingleMessage(rows[0], { maxLength });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return `Failed to query messages: ${err instanceof Error ? err.message : String(err)}`;
|
return `Failed to query message: ${err instanceof Error ? err.message : String(err)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { formatMessageList, formatSessionList } from "../src/history/format";
|
import { formatMessageList, formatSessionList, formatSingleMessage } from "../src/history/format";
|
||||||
|
|
||||||
describe("formatSessionList", () => {
|
describe("formatSessionList", () => {
|
||||||
test("returns message for empty list", () => {
|
test("returns message for empty list", () => {
|
||||||
expect(formatSessionList([])).toBe("No sessions found.");
|
expect(formatSessionList([])).toBe("No sessions found.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("formats sessions as markdown table", () => {
|
test("formats sessions as markdown table with full IDs", () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
{ id: "ses_abc123def", title: "Test Session", updated: "2024-01-15 10:30:00", msgs: 12 },
|
{ id: "ses_abc123def", title: "Test Session", updated: "2024-01-15 10:30:00", msgs: 12 },
|
||||||
];
|
];
|
||||||
const result = formatSessionList(rows);
|
const result = formatSessionList(rows);
|
||||||
expect(result).toContain("# Recent Sessions");
|
expect(result).toContain("# Recent Sessions");
|
||||||
expect(result).toContain("| ID | Title | Updated | Messages |");
|
expect(result).toContain("| ID | Title | Updated | Messages |");
|
||||||
expect(result).toContain("ses_abc123de...");
|
expect(result).toContain("ses_abc123def");
|
||||||
expect(result).toContain("Test Session");
|
expect(result).toContain("Test Session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("truncates long IDs and titles", () => {
|
test("shows full IDs even for long session IDs", () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
{
|
{
|
||||||
id: "ses_verylongidthatshouldbetruncated1234567890",
|
id: "ses_verylongidthatshouldnotbetruncated1234567890",
|
||||||
title: "A very long title that should be truncated for display",
|
title: "A very long title that should be truncated for display",
|
||||||
updated: "2024-01-15",
|
updated: "2024-01-15",
|
||||||
msgs: 5,
|
msgs: 5,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const result = formatSessionList(rows);
|
const result = formatSessionList(rows);
|
||||||
expect(result).toContain("ses_verylong...");
|
expect(result).toContain("ses_verylongidthatshouldnotbetruncated1234567890");
|
||||||
|
expect(result).toContain("A very long title that should be truncat");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles untitled sessions", () => {
|
test("handles untitled sessions", () => {
|
||||||
@@ -55,10 +56,41 @@ describe("formatMessageList", () => {
|
|||||||
expect(result).toContain("Hi there");
|
expect(result).toContain("Hi there");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("truncates long text", () => {
|
test("truncates long text at default maxLength 2000", () => {
|
||||||
const longText = "x".repeat(3000);
|
const longText = "x".repeat(3000);
|
||||||
const rows = [{ role: "assistant", time: "2024-01-15", text: longText }];
|
const rows = [{ role: "assistant", time: "2024-01-15", text: longText }];
|
||||||
const result = formatMessageList(rows);
|
const result = formatMessageList(rows);
|
||||||
expect(result.length).toBeLessThan(longText.length + 200);
|
expect(result.length).toBeLessThan(longText.length + 200);
|
||||||
|
expect(result).toContain("3000 chars total");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects custom maxLength", () => {
|
||||||
|
const longText = "x".repeat(500);
|
||||||
|
const rows = [{ role: "assistant", time: "2024-01-15", text: longText }];
|
||||||
|
const result = formatMessageList(rows, { maxLength: 100 });
|
||||||
|
expect(result).toContain("500 chars total");
|
||||||
|
expect(result.length).toBeLessThan(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not truncate short text", () => {
|
||||||
|
const rows = [{ role: "assistant", time: "2024-01-15", text: "Short message" }];
|
||||||
|
const result = formatMessageList(rows);
|
||||||
|
expect(result).toContain("Short message");
|
||||||
|
expect(result).not.toContain("chars total");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatSingleMessage", () => {
|
||||||
|
test("formats a single message", () => {
|
||||||
|
const row = { role: "assistant", time: "2024-01-15 10:00:00", text: "Hello world" };
|
||||||
|
const result = formatSingleMessage(row);
|
||||||
|
expect(result).toContain("assistant");
|
||||||
|
expect(result).toContain("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects custom maxLength", () => {
|
||||||
|
const row = { role: "assistant", time: "2024-01-15", text: "x".repeat(10000) };
|
||||||
|
const result = formatSingleMessage(row, { maxLength: 500 });
|
||||||
|
expect(result).toContain("10000 chars total");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user