fix: polish session picker filtering (#1271) (thanks @Whoaa512)
This commit is contained in:
@@ -63,7 +63,7 @@ Docs: https://docs.clawd.bot
|
||||
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
||||
- Agents: clarify node_modules read-only guidance in agent instructions.
|
||||
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
|
||||
- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview.
|
||||
- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) — thanks @Whoaa512.
|
||||
|
||||
### Fixes
|
||||
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) — thanks @longmaba.
|
||||
|
||||
@@ -155,6 +155,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
arch?: string;
|
||||
node?: string;
|
||||
model?: string;
|
||||
defaultModel?: string;
|
||||
channel?: string;
|
||||
capabilities?: string[];
|
||||
};
|
||||
@@ -564,6 +565,7 @@ export function buildRuntimeLine(
|
||||
arch?: string;
|
||||
node?: string;
|
||||
model?: string;
|
||||
defaultModel?: string;
|
||||
},
|
||||
runtimeChannel?: string,
|
||||
runtimeCapabilities: string[] = [],
|
||||
@@ -579,6 +581,7 @@ export function buildRuntimeLine(
|
||||
: "",
|
||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||
runtimeChannel
|
||||
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
||||
|
||||
@@ -57,6 +57,23 @@ describe("readFirstUserMessageFromTranscript", () => {
|
||||
expect(result).toBe("Array message content");
|
||||
});
|
||||
|
||||
test("returns first user message from transcript with input_text content", () => {
|
||||
const sessionId = "test-session-2b";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const lines = [
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Input text content" }],
|
||||
},
|
||||
}),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
|
||||
expect(result).toBe("Input text content");
|
||||
});
|
||||
test("skips non-user messages to find first user message", () => {
|
||||
const sessionId = "test-session-3";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
@@ -246,6 +263,22 @@ describe("readLastMessagePreviewFromTranscript", () => {
|
||||
expect(result).toBe("Array content response");
|
||||
});
|
||||
|
||||
test("handles output_text content format", () => {
|
||||
const sessionId = "test-last-output-text";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const lines = [
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Output text response" }],
|
||||
},
|
||||
}),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
|
||||
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
|
||||
expect(result).toBe("Output text response");
|
||||
});
|
||||
test("uses sessionFile parameter when provided", () => {
|
||||
const sessionId = "test-last-custom";
|
||||
const customPath = path.join(tmpDir, "custom-last.jsonl");
|
||||
|
||||
@@ -91,8 +91,10 @@ function extractTextFromContent(content: TranscriptMessage["content"]): string |
|
||||
if (typeof content === "string") return content.trim() || null;
|
||||
if (!Array.isArray(content)) return null;
|
||||
for (const part of content) {
|
||||
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
||||
return part.text.trim();
|
||||
if (!part || typeof part.text !== "string") continue;
|
||||
if (part.type === "text" || part.type === "output_text" || part.type === "input_text") {
|
||||
const trimmed = part.text.trim();
|
||||
if (trimmed) return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -84,7 +84,8 @@ export function deriveSessionTitle(
|
||||
}
|
||||
|
||||
if (firstUserMessage?.trim()) {
|
||||
return truncateTitle(firstUserMessage.trim(), DERIVED_TITLE_MAX_LEN);
|
||||
const normalized = firstUserMessage.replace(/\s+/g, " ").trim();
|
||||
return truncateTitle(normalized, DERIVED_TITLE_MAX_LEN);
|
||||
}
|
||||
|
||||
if (entry.sessionId) {
|
||||
|
||||
@@ -79,13 +79,23 @@ export class FilterableSelectList implements Component {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const allowVimNav = !this.filterText.trim();
|
||||
|
||||
// Navigation: arrows, vim j/k, or ctrl+p/ctrl+n
|
||||
if (matchesKey(keyData, "up") || matchesKey(keyData, "ctrl+p") || keyData === "k") {
|
||||
if (
|
||||
matchesKey(keyData, "up") ||
|
||||
matchesKey(keyData, "ctrl+p") ||
|
||||
(allowVimNav && keyData === "k")
|
||||
) {
|
||||
this.selectList.handleInput("\x1b[A");
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(keyData, "down") || matchesKey(keyData, "ctrl+n") || keyData === "j") {
|
||||
if (
|
||||
matchesKey(keyData, "down") ||
|
||||
matchesKey(keyData, "ctrl+n") ||
|
||||
(allowVimNav && keyData === "j")
|
||||
) {
|
||||
this.selectList.handleInput("\x1b[B");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,14 +238,24 @@ export class SearchableSelectList implements Component {
|
||||
handleInput(keyData: string): void {
|
||||
if (isKeyRelease(keyData)) return;
|
||||
|
||||
const allowVimNav = !this.searchInput.getValue().trim();
|
||||
|
||||
// Navigation keys
|
||||
if (matchesKey(keyData, "up") || matchesKey(keyData, "ctrl+p") || keyData === "k") {
|
||||
if (
|
||||
matchesKey(keyData, "up") ||
|
||||
matchesKey(keyData, "ctrl+p") ||
|
||||
(allowVimNav && keyData === "k")
|
||||
) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.notifySelectionChange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(keyData, "down") || matchesKey(keyData, "ctrl+n") || keyData === "j") {
|
||||
if (
|
||||
matchesKey(keyData, "down") ||
|
||||
matchesKey(keyData, "ctrl+n") ||
|
||||
(allowVimNav && keyData === "j")
|
||||
) {
|
||||
this.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);
|
||||
this.notifySelectionChange();
|
||||
return;
|
||||
|
||||
@@ -151,7 +151,8 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
// Build description: time + message preview
|
||||
const timePart = session.updatedAt ? formatRelativeTime(session.updatedAt) : "";
|
||||
const preview = session.lastMessagePreview?.replace(/\s+/g, " ").trim();
|
||||
const description = preview ? `${timePart} · ${preview}` : timePart;
|
||||
const description =
|
||||
timePart && preview ? `${timePart} · ${preview}` : (preview ?? timePart);
|
||||
return {
|
||||
value: session.key,
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user