From 16ec566152b6681bd3086c2455d7654edf566071 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Mar 2026 22:59:35 -0700 Subject: [PATCH 1/2] Add saved commands panel for AI shell snippets --- frontend/app/aipanel/aimessage.tsx | 1 + frontend/app/aipanel/aipanel.tsx | 2 + frontend/app/aipanel/aitypes.ts | 7 ++ frontend/app/aipanel/savedcommandspanel.tsx | 116 ++++++++++++++++++++ frontend/app/aipanel/waveai-model.tsx | 66 +++++++++++ frontend/app/element/streamdown.test.ts | 21 ++++ frontend/app/element/streamdown.tsx | 57 +++++++++- frontend/types/gotypes.d.ts | 9 ++ pkg/waveobj/objrtinfo.go | 14 ++- 9 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 frontend/app/aipanel/savedcommandspanel.tsx create mode 100644 frontend/app/element/streamdown.test.ts 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..80419b0723 --- /dev/null +++ b/frontend/app/aipanel/savedcommandspanel.tsx @@ -0,0 +1,116 @@ +// 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")} +
+
+ model.appendText(command.text, true, { scrollToBottom: true }), + disabled: command.text.trim().length === 0, + }} + /> + model.removeSavedCommand(command.id), + }} + /> +
+
+