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.
|
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
||||||
- Agents: clarify node_modules read-only guidance in agent instructions.
|
- Agents: clarify node_modules read-only guidance in agent instructions.
|
||||||
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
|
- 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
|
### Fixes
|
||||||
- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) — thanks @longmaba.
|
- 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;
|
arch?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
defaultModel?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
capabilities?: string[];
|
capabilities?: string[];
|
||||||
};
|
};
|
||||||
@@ -564,6 +565,7 @@ export function buildRuntimeLine(
|
|||||||
arch?: string;
|
arch?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
defaultModel?: string;
|
||||||
},
|
},
|
||||||
runtimeChannel?: string,
|
runtimeChannel?: string,
|
||||||
runtimeCapabilities: string[] = [],
|
runtimeCapabilities: string[] = [],
|
||||||
@@ -579,6 +581,7 @@ export function buildRuntimeLine(
|
|||||||
: "",
|
: "",
|
||||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||||
|
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
||||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||||
runtimeChannel
|
runtimeChannel
|
||||||
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
||||||
|
|||||||
@@ -57,6 +57,23 @@ describe("readFirstUserMessageFromTranscript", () => {
|
|||||||
expect(result).toBe("Array message content");
|
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", () => {
|
test("skips non-user messages to find first user message", () => {
|
||||||
const sessionId = "test-session-3";
|
const sessionId = "test-session-3";
|
||||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||||
@@ -246,6 +263,22 @@ describe("readLastMessagePreviewFromTranscript", () => {
|
|||||||
expect(result).toBe("Array content response");
|
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", () => {
|
test("uses sessionFile parameter when provided", () => {
|
||||||
const sessionId = "test-last-custom";
|
const sessionId = "test-last-custom";
|
||||||
const customPath = path.join(tmpDir, "custom-last.jsonl");
|
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 (typeof content === "string") return content.trim() || null;
|
||||||
if (!Array.isArray(content)) return null;
|
if (!Array.isArray(content)) return null;
|
||||||
for (const part of content) {
|
for (const part of content) {
|
||||||
if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
if (!part || typeof part.text !== "string") continue;
|
||||||
return part.text.trim();
|
if (part.type === "text" || part.type === "output_text" || part.type === "input_text") {
|
||||||
|
const trimmed = part.text.trim();
|
||||||
|
if (trimmed) return trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ export function deriveSessionTitle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (firstUserMessage?.trim()) {
|
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) {
|
if (entry.sessionId) {
|
||||||
|
|||||||
@@ -79,13 +79,23 @@ export class FilterableSelectList implements Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleInput(keyData: string): void {
|
handleInput(keyData: string): void {
|
||||||
|
const allowVimNav = !this.filterText.trim();
|
||||||
|
|
||||||
// Navigation: arrows, vim j/k, or ctrl+p/ctrl+n
|
// 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");
|
this.selectList.handleInput("\x1b[A");
|
||||||
return;
|
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");
|
this.selectList.handleInput("\x1b[B");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,14 +238,24 @@ export class SearchableSelectList implements Component {
|
|||||||
handleInput(keyData: string): void {
|
handleInput(keyData: string): void {
|
||||||
if (isKeyRelease(keyData)) return;
|
if (isKeyRelease(keyData)) return;
|
||||||
|
|
||||||
|
const allowVimNav = !this.searchInput.getValue().trim();
|
||||||
|
|
||||||
// Navigation keys
|
// 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.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||||
this.notifySelectionChange();
|
this.notifySelectionChange();
|
||||||
return;
|
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.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);
|
||||||
this.notifySelectionChange();
|
this.notifySelectionChange();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
|||||||
// Build description: time + message preview
|
// Build description: time + message preview
|
||||||
const timePart = session.updatedAt ? formatRelativeTime(session.updatedAt) : "";
|
const timePart = session.updatedAt ? formatRelativeTime(session.updatedAt) : "";
|
||||||
const preview = session.lastMessagePreview?.replace(/\s+/g, " ").trim();
|
const preview = session.lastMessagePreview?.replace(/\s+/g, " ").trim();
|
||||||
const description = preview ? `${timePart} · ${preview}` : timePart;
|
const description =
|
||||||
|
timePart && preview ? `${timePart} · ${preview}` : (preview ?? timePart);
|
||||||
return {
|
return {
|
||||||
value: session.key,
|
value: session.key,
|
||||||
label,
|
label,
|
||||||
|
|||||||
Reference in New Issue
Block a user