Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
29ae6e5
code added for the modify button functionality
qjhuangAWS Jul 16, 2025
cd6e9fc
Fix ESLint errors for modify button functionality
qjhuangAWS Jul 16, 2025
3fe72b5
Fix additional ESLint errors for modify button functionality
qjhuangAWS Jul 16, 2025
3fd78fc
Fix all remaining ESLint errors in tests
qjhuangAWS Jul 16, 2025
61dc562
Fix ESLint indentation errors with --fix
qjhuangAWS Jul 16, 2025
e697418
move the test files to _test_ folder
qjhuangAWS Jul 16, 2025
033d717
Resolve merge conflicts between feature/modify-button-functionality a…
qjhuangAWS Jul 16, 2025
0a8e977
fix: restore modify button functionality accidentally removed during …
qjhuangAWS Jul 17, 2025
e4f4554
fixing unit test failure
qjhuangAWS Jul 17, 2025
8728dee
fix: update e2e test snapshots for modify button functionality
qjhuangAWS Jul 17, 2025
b62f960
fix: resolve ESLint errors in test file
qjhuangAWS Jul 17, 2025
8645c68
Fix E2E test snapshots by downgrading Playwright to v1.50.0
qjhuangAWS Jul 17, 2025
4c7d308
Merge branch 'main' into feature/modify-button-functionality
qjhuangAWS Jul 17, 2025
e635558
Revert "Fix E2E test snapshots by downgrading Playwright to v1.50.0"
qjhuangAWS Jul 17, 2025
a673556
Revert "fix: update e2e test snapshots for modify button functionality"
qjhuangAWS Jul 17, 2025
6d2b373
commented code removed and datamodel.md updated
qjhuangAWS Jul 18, 2025
a2bc864
revert changes made to formatting
qjhuangAWS Jul 18, 2025
8145acd
removed live-server
qjhuangAWS Jul 18, 2025
b4e99fe
update snapshot on passing case "keep the content inside window bound…
qjhuangAWS Jul 18, 2025
62d77aa
removed the package-lock.json changes
qjhuangAWS Jul 19, 2025
a577973
removed the package-lock.json changes version fix
qjhuangAWS Jul 19, 2025
06aa15d
resolved comments in pr for reused code block, naming, and sample-dat…
qjhuangAWS Jul 21, 2025
1cba30d
Revert "update snapshot on passing case "keep the content inside wind…
qjhuangAWS Jul 22, 2025
131b318
Merge branch 'aws:main' into feature/modify-button-functionality
qjhuangAWS Jul 23, 2025
602bc0d
Merge branch 'feature/modify-button-functionality' of https://github.…
qjhuangAWS Jul 23, 2025
5b93259
removed uncessary added changes
qjhuangAWS Jul 24, 2025
4814b72
Merge branch 'aws:main' into feature/modify-button-functionality
qjhuangAWS Aug 5, 2025
47e07eb
Merge branch 'feature/modify-button-functionality' of https://github.…
qjhuangAWS Aug 5, 2025
2ba9398
Clean structure with all comments addressed
qjhuangAWS Aug 5, 2025
3a03c12
Fix main ESLint errors in implementation files
qjhuangAWS Aug 5, 2025
6fb5354
ESLlinting errors fixed for the test cases
qjhuangAWS Aug 5, 2025
d89ec05
removed uncessary change
qjhuangAWS Aug 5, 2025
68abfc7
removed duplicate notes on DATAMODEL.md
qjhuangAWS Aug 5, 2025
e490672
updated datamodel.md
qjhuangAWS Aug 5, 2025
5f3a1d0
Added dynamic resizing and background theme change to textarea
qjhuangAWS Aug 8, 2025
3e1d3aa
fixed ESlint issues for push
qjhuangAWS Aug 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions example/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export enum Commands {
INSERT_CODE = '/insert-dummy-code',
COMMAND_WITH_PROMPT = '/with-prompt',
SHOW_STICKY_CARD = '/show-sticky-card',

BORDERED_CARDS = '/bordered-cards',
SHELL_WITH_MODIFY = '/shell-with-modify',
REPLACE_FOLLOWUPS = '/replace-followups',
STATUS_CARDS = '/cards-with-status-colors',
HEADER_TYPES = '/cards-header-types',
Expand All @@ -21,12 +22,10 @@ export enum Commands {
INFORMATION_CARDS = '/information-cards',
CONFIRMATION_BUTTONS = '/confirmation-buttons',
BUTTONS = '/buttons',
BORDERED_CARDS = '/bordered-cards',

NOTIFY = '/show-notification',
CLEAR = '/clear',
CLEAR_CONTEXT_ITEMS = '/clear-context-items',
CLEAR_LOGS = '/clear-logs',
SHOW_CUSTOM_FORM = '/show-custom-form',
VOTE = '/vote'
}
}
11 changes: 11 additions & 0 deletions example/src/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,15 @@ export class Connector {
}, INITIAL_STREAM_DELAY);
}, 150);
});
public runShellCommand(cmd: string | null | undefined): void {
// example: POST it to your server
fetch('/api/run-shell', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: cmd }),
})
.then(res => res.json())
.then(result => Log(`shell result: ${JSON.stringify(result)}`))
.catch(err => Log(`shell error: ${err}`));
}
}
211 changes: 177 additions & 34 deletions example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ import {
mcpToolRunSampleCard,
mcpToolRunSampleCardInit,
sampleRulesList,
shellCommandWithModifyEditable,
} from './samples/sample-data';
import escapeHTML from 'escape-html';
import './styles/styles.scss';
import { ThemeBuilder } from './theme-builder/theme-builder';
import { Commands } from './commands';

const shellOriginals = new Map<string, ChatItem>();
export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => {
const connector = new Connector();
let streamingMessageId: string | null;
Expand Down Expand Up @@ -150,37 +152,47 @@ Model - ${optionsValues['model-select'] !== '' ? optionsValues['model-select'] :
Log(`MynahUI focus state changed: <b>${focusState.toString()}</b>`);
},
onPromptTopBarItemAdded: (tabId: string, item: QuickActionCommand) => {
Log(`Prompt top bar item <b>${item.command}</b> added on tab <b>${tabId}</b>`);
Log(`Prompt top bar item <b>${item.command}</b> added on tab <b>${tabId}</b>`);

mynahUI.updateStore(tabId, {
promptTopBarContextItems: [
...((mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command) ?? []),
item,
],
});
promptTopBarContextItems: [
...((mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command) ?? []),
item,
],
});
},
onPromptTopBarItemRemoved: (tabId: string, item: QuickActionCommand) => {
Log(`Prompt top bar item <b>${item.command}</b> removed on tab <b>${tabId}</b>`);
Log(`Prompt top bar item <b>${item.command}</b> removed on tab <b>${tabId}</b>`);

mynahUI.updateStore(tabId, {
promptTopBarContextItems:(mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command)
});
promptTopBarContextItems: (mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command)
});
},
onPromptTopBarButtonClick: (tabId: string, button: ChatItemButton) => {
Log(`Top bar button <b>${button.id}</b> clicked on tab <b>${tabId}</b>`);

const topBarOverlay = mynahUI.openTopBarButtonOverlay({tabId, topBarButtonOverlay: sampleRulesList,
events: {
onClose: () => {Log(`Top bar overlay closed on tab <b>${tabId}</b>`)},
onGroupClick: (group) => {Log(`Top bar overlay group clicked <b>${group}</b> on tab <b>${tabId}</b>`)},
onItemClick: (item) => { Log(`Top bar overlay item clicked <b>${item.id}</b> on tab <b>${tabId}</b>`); topBarOverlay.update(sampleRulesList)},
onKeyPress: (e) => {Log(`Key pressed on top bar overlay`); if (e.key === KeyMap.ESCAPE) {
topBarOverlay.close();
}}

}})

Log(`Top bar button <b>${button.id}</b> clicked on tab <b>${tabId}</b>`);

const topBarOverlay = mynahUI.openTopBarButtonOverlay({
tabId,
topBarButtonOverlay: sampleRulesList,
events: {
onClose: () => {
Log(`Top bar overlay closed on tab <b>${tabId}</b>`)
},
onGroupClick: (group) => {
Log(`Top bar overlay group clicked <b>${group}</b> on tab <b>${tabId}</b>`)
},
onItemClick: (item) => {
Log(`Top bar overlay item clicked <b>${item.id}</b> on tab <b>${tabId}</b>`);
topBarOverlay.update(sampleRulesList)
},
onKeyPress: (e) => {
Log(`Key pressed on top bar overlay`);
if (e.key === KeyMap.ESCAPE) {
topBarOverlay.close();
}
}
}
});
},
onTabBarButtonClick: (tabId: string, buttonId: string) => {
if (buttonId.match('mcp-')) {
Expand Down Expand Up @@ -311,18 +323,19 @@ Model - ${optionsValues['model-select'] !== '' ? optionsValues['model-select'] :
);
} else if (buttonId === 'show-pinned-context') {
showPinnedContext = !showPinnedContext;
if (showPinnedContext){
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: promptTopBarTitle,
promptTopBarButton: rulesButton,
}),
); } else {
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: ``,
}),
)
if (showPinnedContext) {
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: promptTopBarTitle,
promptTopBarButton: rulesButton,
}),
);
} else {
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: ``,
}),
);
}
} else if (buttonId === 'splash-loader') {
mynahUI.toggleSplashLoader(true, 'Showing splash loader...');
Expand Down Expand Up @@ -1327,6 +1340,9 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
mynahUI.addCustomContextToPrompt(tabId, commands, insertPosition);
},
onInBodyButtonClicked: (tabId: string, messageId: string, action) => {
const items = mynahUI.getTabData(tabId).getValue('chatItems') as ChatItem[];
const current = items.find(ci => ci.messageId === messageId)!;

if (action.id === 'allow-readonly-tools') {
mynahUI.updateChatAnswerWithMessageId(tabId, messageId, {
muted: true,
Expand Down Expand Up @@ -1376,6 +1392,121 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
},
},
});
} else if (action.id === 'modify-bash-command') {
const chatItems = mynahUI.getTabData(tabId).getValue('chatItems') as ChatItem[];
const current = chatItems.find(ci => ci.messageId === messageId)!;

if (current.editable === true || !current.body || current.body.toString().trim() === '') {
return false;
}

shellOriginals.set(messageId, structuredClone(current));

const raw = current.body.toString();
const m = raw.match(/```(?:bash)?\s*([\s\S]*?)```/);
const code = (m ? m[1] : raw).trim();

if (!code) {
return false;
}

mynahUI.updateChatAnswerWithMessageId(tabId, messageId, {
body: code,
editable: true,
header: {
...current.header!,
buttons: [
{ id: 'save-bash-command', text: 'Save', icon: MynahIcons.OK, status: 'clear' },
{ id: 'cancel-bash-edit', text: 'Cancel', icon: MynahIcons.CANCEL, status: 'dimmed-clear' },
],
},
});

return false;
} else if (action.id === 'save-bash-command') {
const original = shellOriginals.get(messageId)!;
shellOriginals.delete(messageId);

const currentItems = mynahUI.getTabData(tabId).getValue('chatItems') as ChatItem[];
const currentItem = currentItems.find(ci => ci.messageId === messageId)!;

const unwrapped = (currentItem.body ?? '').toString().trim();
const fenced = unwrapped ? ['```bash', unwrapped, '```'].join('\n') : original.body;

const allItems = mynahUI.getTabData(tabId).getValue('chatItems') as ChatItem[];
const itemIndex = allItems.findIndex(ci => ci.messageId === messageId);

if (itemIndex !== -1) {
const updatedItem = {
...original,
body: fenced,
editable: false,
};

const newItems = [...allItems];
newItems[itemIndex] = updatedItem;

mynahUI.updateStore(tabId, {
chatItems: newItems,
});
}

return false;
} else if (action.id === 'cancel-bash-edit') {
const original = shellOriginals.get(messageId)!;
shellOriginals.delete(messageId);

const allItems = mynahUI.getTabData(tabId).getValue('chatItems') as ChatItem[];
const itemIndex = allItems.findIndex(ci => ci.messageId === messageId);

if (itemIndex !== -1) {
const restoredItem = {
...original,
editable: false,
};

const newItems = [...allItems];
newItems[itemIndex] = restoredItem;

mynahUI.updateStore(tabId, {
chatItems: newItems,
});
}

return false;
} else if (action.id === 'reject-bash-command' || action.id === 'run-bash-command') {
// 1) re-grab the chat item
const chatItems = mynahUI.getTabData(tabId)!.getValue('chatItems') as ChatItem[];
const currentChatItem = chatItems.find(ci => ci.messageId === messageId);
if (!currentChatItem) return;

// 2) reference your original sample for body/buttons
const original = shellCommandWithModifyEditable;
const originalBody = original.body;
const originalButtons = original.header!.buttons!;

if (action.id === 'reject-bash-command') {
console.log('✋ Reject');
Log(`Reject clicked for ${messageId}`);
mynahUI.updateChatAnswerWithMessageId(tabId, messageId, {
body: originalBody,
editable: false,
type: ChatItemType.ANSWER,
codeBlockActions: { copy: null, 'insert-to-cursor': null },
header: {
...currentChatItem.header!,
body: original.header!.body, // reset the header text
buttons: originalButtons, // and buttons
},
});
return false;
} else { // run-bash-command
console.log('▶️ Run');
Log(`Run clicked for ${messageId}`);
connector.runShellCommand(currentChatItem.body);
// optionally leave the card as-is or give feedback…
return false;
}
} else if (action.id === 'quick-start') {
mynahUI.updateStore(tabId, {
tabHeaderDetails: null,
Expand Down Expand Up @@ -1612,6 +1743,11 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
break;
case Commands.HEADER_TYPES:
sampleHeaderTypes.forEach((ci) => mynahUI.addChatItem(tabId, ci));
// Add the shell command with modify button example
mynahUI.addChatItem(tabId, {
...shellCommandWithModifyEditable,
messageId: generateUID(),
});
break;
case Commands.SUMMARY_CARD:
const cardId = generateUID();
Expand Down Expand Up @@ -1754,6 +1890,13 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
mynahUI.addChatItem(tabId, exampleCustomRendererWithDomBuilderJson);
mynahUI.addChatItem(tabId, defaultFollowUps);
break;
case Commands.SHELL_WITH_MODIFY:
mynahUI.addChatItem(tabId, {
...shellCommandWithModifyEditable,
messageId: generateUID(),
});
mynahUI.addChatItem(tabId, defaultFollowUps);
break;
case Commands.COMMAND_WITH_PROMPT:
const realPromptText = prompt.escapedPrompt?.trim() ?? '';
mynahUI.addChatItem(tabId, {
Expand Down
40 changes: 39 additions & 1 deletion example/src/samples/sample-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,10 @@ export const defaultFollowUps: ChatItem = {
pillText: 'Sticky card',
command: Commands.SHOW_STICKY_CARD,
},
{
pillText: 'Shell with modify',
command: Commands.SHELL_WITH_MODIFY,
},
{
pillText: 'Some auto reply',
prompt: 'Some random auto reply here.',
Expand Down Expand Up @@ -1307,6 +1311,7 @@ export const exampleBorderedCard = (): ChatItem => {
messageId: generateUID(),
type: ChatItemType.ANSWER,
border: true,
padding: true,
header: {
padding: true,
iconForegroundStatus: 'warning',
Expand Down Expand Up @@ -2337,4 +2342,37 @@ export const sampleMCPDetails = (title: string): DetailedList => {
export const sampleRulesList: DetailedList = {selectable: 'clickable', list: [{children: [{id: 'README', icon: MynahIcons.CHECK_LIST,
description: 'README',actions: [{ id: 'README.md', icon: MynahIcons.OK, status: 'clear' }]}]},
{groupName: '.amazonq/rules', childrenIndented: true, icon: MynahIcons.FOLDER , actions: [{ id: 'java-expert.md', icon: MynahIcons.OK, status: 'clear' }], children: [{id: 'java-expert.md', icon: MynahIcons.CHECK_LIST,
description: 'java-expert',actions: [{ id: 'java-expert.md', icon: MynahIcons.OK, status: 'clear' }]}]}]}
description: 'java-expert',actions: [{ id: 'java-expert.md', icon: MynahIcons.OK, status: 'clear' }]}]}]}

export const shellCommandWithModifyEditable: ChatItem = {
fullWidth: true,
padding: false,
type: ChatItemType.ANSWER,
messageId: 'shell-cmd-1',
body: ['```bash', 'npm run build', '```'].join('\n'),
editable: false, // start view-only
header: {
// pick an existing icon—let’s use BLOCK as our “shell” glyph
icon: MynahIcons.BLOCK,

buttons: [
{ id: 'run-bash-command', text: 'Run', icon: MynahIcons.PLAY, status: 'primary' },
{ id: 'reject-bash-command', text: 'Reject', icon: MynahIcons.CANCEL, status: 'error' },
{ id: 'modify-bash-command', text: 'Modify', icon: MynahIcons.PENCIL, status: 'clear' },
],
},
// these drive the little buttons that appear *in* the code block
codeBlockActions: {
'run-bash-command': {
id: 'run-bash-command',
label: 'Run', // ← was `text`
icon: MynahIcons.PLAY,
flash: 'infinite',
},
'reject-bash-command': {
id: 'reject-bash-command',
label: 'Reject', // ← was `text`
icon: MynahIcons.CANCEL,
},
},
};
Loading
Loading