feat: initial Lam Agent extension (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user