diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index 1bfadd121d..9491a56076 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -124,6 +124,7 @@ const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) => text={content} parseIncompleteMarkdown={isStreaming} className="text-gray-100" + onClickSaveCommand={(cmd) => model.addSavedCommand(cmd)} codeBlockMaxWidthAtom={model.codeBlockMaxWidth} /> ); diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 112a4cc79e..24bf0386a8 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -23,6 +23,7 @@ import { AIPanelHeader } from "./aipanelheader"; import { AIPanelInput } from "./aipanelinput"; import { AIPanelMessages } from "./aipanelmessages"; import { AIRateLimitStrip } from "./airatelimitstrip"; +import { SavedCommandsPanel } from "./savedcommandspanel"; import { WaveUIMessage } from "./aitypes"; import { BYOKAnnouncement } from "./byokannouncement"; import { TelemetryRequiredMessage } from "./telemetryrequired"; @@ -596,6 +597,7 @@ const AIPanelComponentInner = memo(() => { /> )} + diff --git a/frontend/app/aipanel/aitypes.ts b/frontend/app/aipanel/aitypes.ts index fbce463a73..48955a532b 100644 --- a/frontend/app/aipanel/aitypes.ts +++ b/frontend/app/aipanel/aitypes.ts @@ -39,6 +39,13 @@ export type UseChatSetMessagesType = ( messages: WaveUIMessage[] | ((messages: WaveUIMessage[]) => WaveUIMessage[]) ) => void; +export interface SavedCommand { + id: string; + text: string; + createdts?: number; + updatedts?: number; +} + export type UseChatSendMessageType = ( message?: | (Omit & { diff --git a/frontend/app/aipanel/savedcommandspanel.tsx b/frontend/app/aipanel/savedcommandspanel.tsx new file mode 100644 index 0000000000..bef5fdd39c --- /dev/null +++ b/frontend/app/aipanel/savedcommandspanel.tsx @@ -0,0 +1,125 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { IconButton } from "@/app/element/iconbutton"; +import { useAtomValue } from "jotai"; +import { memo, useEffect, useState } from "react"; +import { SavedCommand } from "./aitypes"; +import { WaveAIModel } from "./waveai-model"; + +const formatCommandPreview = (text: string): string => { + const firstLine = text.trim().split("\n")[0] ?? ""; + return firstLine.length > 72 ? `${firstLine.slice(0, 69)}...` : firstLine; +}; + +const SavedCommandCard = memo(({ command }: { command: SavedCommand }) => { + const model = WaveAIModel.getInstance(); + + return ( +
+
+
+ {formatCommandPreview(command.text || "Untitled command")} +
+
+ void model.runSavedCommand(command.text), + disabled: command.text.trim().length === 0, + }} + /> + model.appendText(command.text, true, { scrollToBottom: true }), + disabled: command.text.trim().length === 0, + }} + /> + model.removeSavedCommand(command.id), + }} + /> +
+
+