114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { Action, ActionPanel, Clipboard, Icon, List, useNavigation } from "@vicinae/api";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { ChatView } from "./lib/components/ChatView";
|
|
import { sessionToPlainText } from "./lib/markdown";
|
|
import { clearSessions, deleteSession, listSessions, loadSession } from "./lib/sessions";
|
|
import type { SessionSummary } from "./lib/types";
|
|
import { formatRelativeTime, truncate } from "./lib/utils";
|
|
|
|
/**
|
|
* Browse and manage past conversations.
|
|
*
|
|
* Loading a transcript is deferred until the user actually opens one: the
|
|
* index holds only summaries, so this view stays fast no matter how much
|
|
* history has piled up.
|
|
*/
|
|
export default function SessionsCommand() {
|
|
const { push } = useNavigation();
|
|
const [sessions, setSessions] = useState<SessionSummary[] | undefined>(undefined);
|
|
|
|
const refresh = useCallback(() => {
|
|
void listSessions()
|
|
.then(setSessions)
|
|
.catch(() => setSessions([]));
|
|
}, []);
|
|
|
|
useEffect(refresh, [refresh]);
|
|
|
|
const openSession = useCallback(
|
|
(id: string) => {
|
|
void loadSession(id).then((session) => {
|
|
if (session) push(<ChatView initialSession={session} />);
|
|
});
|
|
},
|
|
[push],
|
|
);
|
|
|
|
const remove = useCallback(
|
|
(id: string) => {
|
|
void deleteSession(id).then(refresh);
|
|
},
|
|
[refresh],
|
|
);
|
|
|
|
const copyConversation = useCallback((id: string) => {
|
|
// Loads the full transcript rather than copying the index preview.
|
|
void loadSession(id).then((session) => {
|
|
if (session) void Clipboard.copy(sessionToPlainText(session.turns));
|
|
});
|
|
}, []);
|
|
|
|
const removeAll = useCallback(() => {
|
|
void clearSessions().then(refresh);
|
|
}, [refresh]);
|
|
|
|
const loaded = sessions !== undefined;
|
|
const items = sessions ?? [];
|
|
|
|
return (
|
|
<List
|
|
isLoading={!loaded}
|
|
navigationTitle="Chat Sessions"
|
|
searchBarPlaceholder="Search conversations…"
|
|
>
|
|
{loaded && items.length === 0 ? (
|
|
<List.EmptyView
|
|
icon={Icon.SpeechBubble}
|
|
title="No conversations yet"
|
|
description="Open the chat command and ask something. Conversations are saved automatically."
|
|
/>
|
|
) : null}
|
|
|
|
{items.map((summary) => (
|
|
<List.Item
|
|
key={summary.id}
|
|
id={summary.id}
|
|
icon={Icon.SpeechBubble}
|
|
title={truncate(summary.title, 70)}
|
|
subtitle={truncate(summary.preview, 90)}
|
|
accessories={[{ text: `${summary.turnCount} turns` }, { text: formatRelativeTime(summary.updatedAt) }]}
|
|
actions={
|
|
<ActionPanel>
|
|
<Action
|
|
title="Open Conversation"
|
|
icon={Icon.SpeechBubble}
|
|
onAction={() => openSession(summary.id)}
|
|
/>
|
|
<Action
|
|
title="Copy Conversation"
|
|
shortcut={{ key: "c", modifiers: ["cmd", "shift"] }}
|
|
onAction={() => copyConversation(summary.id)}
|
|
/>
|
|
<ActionPanel.Section title="Danger Zone">
|
|
<Action
|
|
title="Delete Conversation"
|
|
icon={Icon.Trash}
|
|
style="destructive"
|
|
shortcut={{ key: "backspace", modifiers: ["cmd"] }}
|
|
onAction={() => remove(summary.id)}
|
|
/>
|
|
<Action
|
|
title="Delete All Conversations"
|
|
icon={Icon.Trash}
|
|
style="destructive"
|
|
onAction={removeAll}
|
|
/>
|
|
</ActionPanel.Section>
|
|
</ActionPanel>
|
|
}
|
|
/>
|
|
))}
|
|
</List>
|
|
);
|
|
}
|