-
Notifications
You must be signed in to change notification settings - Fork 92
feat(memory): add read-only Memory TUI #1878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { Box, Text, useInput } from "ink"; | ||
| import { useNavigate, useParams } from "react-router"; | ||
| import { JsonDetail } from "../../../components/JsonDetail"; | ||
| import { KeyValueTable } from "../../../components/KeyValueTable.js"; | ||
| import { Layout } from "../../../components/Layout"; | ||
| import { darkTheme } from "../../../components/ui/_core.js"; | ||
| import { Divider } from "../../../components/ui/divider/Divider.js"; | ||
| import { Spinner } from "../../../components/ui/spinner"; | ||
| import type { ScreenProps } from "../../types"; | ||
| import { coreOptsFromCtx } from "../../utils"; | ||
|
|
||
| function useMemoryDetail({ ctx, core }: ScreenProps, memoryId: string | undefined) { | ||
| const opts = coreOptsFromCtx(ctx); | ||
| return useQuery({ | ||
| queryKey: ["memory", opts.region, memoryId, "full"], | ||
| queryFn: () => core.memory.getMemory(memoryId!, "full", opts), | ||
| enabled: memoryId !== undefined, | ||
| }); | ||
| } | ||
|
|
||
| export function MemoryGetScreen(props: ScreenProps) { | ||
| const navigate = useNavigate(); | ||
| const { memoryId } = useParams(); | ||
| const detail = useMemoryDetail(props, memoryId); | ||
| const memory = detail.data?.memory; | ||
|
|
||
| useInput((input, key) => { | ||
| if (key.escape) { | ||
| navigate(-1); | ||
| return; | ||
| } | ||
| if (input === "r" && detail.isError) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: do we want to allow the user to refresh even if there isn't an error? (ex. maybe something changed and they want to check). This looks consistent with https://github.com/aws/agentcore-cli/blob/refactor/src/handlers/runtime/get/screen.tsx#L48 so just curious what behavior we want.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice catch I think we would want that right? maybe I created a new resource while the TUI screen was open and I just want to refresh it to see if it shows up. Let me add that quickly in a follow up in both places |
||
| void detail.refetch(); | ||
| return; | ||
| } | ||
| if (detail.isError || !memory) return; | ||
| if (key.return && memoryId) { | ||
| navigate(`/agentcore/memory/get/${encodeURIComponent(memoryId)}/json`); | ||
| } | ||
| }); | ||
|
|
||
| return ( | ||
| <Layout | ||
| breadcrumb={["agentcore", "memory", "get", memoryId ?? ""]} | ||
| keyHints={[ | ||
| ...(!detail.isPending && !detail.isError ? [{ key: "enter", label: "open detail" }] : []), | ||
| ...(detail.isError ? [{ key: "r", label: "retry" }] : []), | ||
| { key: "esc", label: "back" }, | ||
| { key: "ctl+c", label: "quit" }, | ||
| ]} | ||
| > | ||
| {detail.isPending ? ( | ||
| <Spinner label="Loading Memory…" /> | ||
| ) : detail.isError ? ( | ||
| <Text color="red">Error: {(detail.error as Error).message}</Text> | ||
| ) : ( | ||
| <Box flexDirection="column"> | ||
| <Box flexDirection="column" paddingLeft={1}> | ||
| <KeyValueTable | ||
| items={{ | ||
| name: memory?.name ?? "", | ||
| id: memory?.id ?? "", | ||
| status: memory?.status ?? "", | ||
| eventExpiryDays: memory?.eventExpiryDuration?.toString() ?? "-", | ||
| strategies: memory?.strategies?.length.toString() ?? "0", | ||
| updatedAt: memory?.updatedAt?.toISOString() ?? "-", | ||
| ...(memory?.failureReason ? { failureReason: memory.failureReason } : {}), | ||
| arn: memory?.arn ?? "", | ||
| }} | ||
| /> | ||
| </Box> | ||
|
|
||
| <Divider /> | ||
|
|
||
| <Box paddingLeft={1}> | ||
| <Text color={darkTheme.colors.focus}>❯ </Text> | ||
| <Text bold color={darkTheme.colors.focus}> | ||
| {"detail".padEnd(9)} | ||
| </Text> | ||
| <Text color={darkTheme.colors.muted}>show the full JSON definition</Text> | ||
| </Box> | ||
| </Box> | ||
| )} | ||
| </Layout> | ||
| ); | ||
| } | ||
|
|
||
| export function MemoryGetJsonScreen(props: ScreenProps) { | ||
| const { memoryId } = useParams(); | ||
| const detail = useMemoryDetail(props, memoryId); | ||
|
|
||
| return ( | ||
| <JsonDetail | ||
| breadcrumb={["agentcore", "memory", "get", memoryId ?? "", "json"]} | ||
| isPending={detail.isPending} | ||
| error={detail.isError ? (detail.error as Error) : null} | ||
| data={detail.data?.memory} | ||
| loadingLabel="Loading Memory…" | ||
| onRetry={() => void detail.refetch()} | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,15 @@ | ||
| import { withTuiOnEmptyFlagsAndArgs } from "../../middleware"; | ||
| import { Router } from "../../router"; | ||
| import { renderTui } from "../../tui"; | ||
| import type { AppIO } from "../../io"; | ||
| import type { Core } from "../types"; | ||
| import { createHelpDefault } from "../help"; | ||
| import { createGetMemoryHandler } from "./get"; | ||
| import { createListMemoriesHandler } from "./list"; | ||
|
|
||
| export function createMemoryHandler(core: Core, io: AppIO): Router { | ||
| return new Router("memory", "manage AgentCore Memories") | ||
| .default(createHelpDefault(io)) | ||
| .use(withTuiOnEmptyFlagsAndArgs(core, io)) | ||
| .default(renderTui(core, io)) | ||
| .handler(createGetMemoryHandler(core)) | ||
| .handler(createListMemoriesHandler(core)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; | ||
| import { useNavigate } from "react-router"; | ||
| import { formatTimestamp } from "../../../components/formatTimestamp"; | ||
| import { PaginatedTablePicker } from "../../../components/PaginatedTablePicker"; | ||
| import type { DataTableColumn } from "../../../components/ui/data-table"; | ||
| import type { ScreenProps } from "../../types"; | ||
| import { coreOptsFromCtx } from "../../utils"; | ||
|
|
||
| interface MemoryRow extends Record<string, unknown> { | ||
| memoryId: string; | ||
| status: string; | ||
| updatedAt: string; | ||
| } | ||
|
|
||
| export const memoryColumns = [ | ||
| { key: "memoryId", header: "id", flex: true }, | ||
| { key: "status", header: "status", width: 13 }, | ||
| { | ||
| key: "updatedAt", | ||
| header: "updated UTC", | ||
| width: 16, | ||
| render: formatTimestamp, | ||
| }, | ||
| ] satisfies DataTableColumn<MemoryRow>[]; | ||
|
|
||
| function toRow(memory: MemorySummary): MemoryRow { | ||
| return { | ||
| memoryId: memory.id ?? "", | ||
| status: memory.status ?? "-", | ||
| updatedAt: memory.updatedAt?.toISOString() ?? "-", | ||
| }; | ||
| } | ||
|
|
||
| export function MemoryListScreen({ ctx, core }: ScreenProps) { | ||
| const opts = coreOptsFromCtx(ctx); | ||
| const navigate = useNavigate(); | ||
|
|
||
| return ( | ||
| <PaginatedTablePicker | ||
| breadcrumb={["agentcore", "memory", "list"]} | ||
| queryKey={["memories", opts.region]} | ||
| loadPage={async (token, pageSize) => { | ||
| const response = await core.memory.listMemories(token, pageSize, opts); | ||
| return { | ||
| items: response.memories ?? [], | ||
| nextToken: response.nextToken, | ||
| }; | ||
| }} | ||
| toRow={toRow} | ||
| columns={memoryColumns} | ||
| getValue={(row) => row.memoryId} | ||
| onSelect={(memoryId) => navigate(`/agentcore/memory/get/${encodeURIComponent(memoryId)}`)} | ||
| onBack={() => navigate("/agentcore/memory")} | ||
| loadingMessage="Loading Memories…" | ||
| errorMessage={(error) => `Error: ${error.message}`} | ||
| emptyMessage="No Memories found in this Region." | ||
| emptyPageMessage="No Memories on this page." | ||
| /> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love to see us using so many common components!