diff --git a/example/src/commands.ts b/example/src/commands.ts
index b71dfbfba..dabde5767 100644
--- a/example/src/commands.ts
+++ b/example/src/commands.ts
@@ -28,5 +28,6 @@ export enum Commands {
CLEAR_CONTEXT_ITEMS = '/clear-context-items',
CLEAR_LOGS = '/clear-logs',
SHOW_CUSTOM_FORM = '/show-custom-form',
- VOTE = '/vote'
+ VOTE = '/vote',
+ MODIFIED_FILES_DEMO = '/modified-files-demo'
}
\ No newline at end of file
diff --git a/example/src/config.ts b/example/src/config.ts
index 4f5c8e020..f14c3d185 100644
--- a/example/src/config.ts
+++ b/example/src/config.ts
@@ -283,6 +283,11 @@ export const QuickActionCommands: QuickActionCommandGroup[] = [
icon: MynahIcons.FLASH,
description: 'Test streaming animation with different speeds. Use tab bar buttons to switch modes.',
},
+ {
+ command: Commands.MODIFIED_FILES_DEMO,
+ icon: MynahIcons.FILE,
+ description: 'Demo the modified files tracker component above the chat interface.',
+ },
],
},
];
diff --git a/example/src/main.ts b/example/src/main.ts
index 61096c7a0..116a01f76 100644
--- a/example/src/main.ts
+++ b/example/src/main.ts
@@ -1259,8 +1259,17 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
`);
},
onChatPrompt: (tabId: string, prompt: ChatPrompt) => {
-
+ // Clear modified files tracker only for commands that modify files
+ const fileModifyingCommands = [
+ Commands.MODIFIED_FILES_DEMO,
+ '/dev',
+ '/transform',
+ '/generate'
+ ];
+ if (prompt.command && fileModifyingCommands.includes(prompt.command)) {
+ mynahUI.clearModifiedFiles(tabId);
+ }
Log(`New prompt on tab: ${tabId}
prompt: ${prompt.prompt !== undefined && prompt.prompt !== '' ? prompt.prompt : '{command only}'}
@@ -1578,6 +1587,9 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
onMessageDismiss: (tabId, messageId) => {
Log(`Card dismissed: tabId: ${tabId}, messageId: ${messageId}`);
},
+ onModifiedFileClick: (tabId, filePath) => {
+ Log(`Modified file clicked on tab ${tabId}: ${filePath}`);
+ },
});
setTimeout(() => {
@@ -1625,6 +1637,116 @@ here to see if it gets cut off properly as expected, with an ellipsis through cs
mynahUI.addChatItem(tabId, exampleVoteChatItem);
mynahUI.addChatItem(tabId, defaultFollowUps);
break;
+ case Commands.MODIFIED_FILES_DEMO:
+ // Demo the modified files tracker
+ mynahUI.addChatItem(tabId, {
+ type: ChatItemType.ANSWER,
+ messageId: generateUID(),
+ body: 'Demonstrating the modified files tracker. Watch the component above the chat!',
+ });
+
+ // Make the component visible and set initial state
+ mynahUI.updateStore(tabId, {
+ modifiedFilesVisible: true,
+ modifiedFilesTitle: 'Work in progress...',
+ modifiedFilesList: {
+ filePaths: [],
+ flatList: true
+ }
+ });
+
+ // Simulate file modifications with delays
+ setTimeout(() => {
+ mynahUI.updateStore(tabId, {
+ modifiedFilesList: {
+ filePaths: ['src/components/chat-wrapper.ts'],
+ flatList: true
+ }
+ });
+ }, 1000);
+
+ setTimeout(() => {
+ mynahUI.updateStore(tabId, {
+ modifiedFilesList: {
+ filePaths: [
+ 'src/components/chat-wrapper.ts',
+ 'src/styles/components/_modified-files-tracker.scss'
+ ],
+ flatList: true
+ }
+ });
+ }, 2000);
+
+ setTimeout(() => {
+ mynahUI.updateStore(tabId, {
+ modifiedFilesList: {
+ filePaths: [
+ 'src/components/chat-wrapper.ts',
+ 'src/styles/components/_modified-files-tracker.scss',
+ 'src/main.ts'
+ ],
+ flatList: true
+ }
+ });
+ }, 3000);
+
+ setTimeout(() => {
+ mynahUI.updateStore(tabId, {
+ modifiedFilesList: {
+ filePaths: [
+ 'src/components/chat-wrapper.ts',
+ 'src/styles/components/_modified-files-tracker.scss',
+ 'src/main.ts',
+ 'example/src/main.ts'
+ ],
+ flatList: true,
+ actions: {
+ 'src/components/chat-wrapper.ts': [{ name: 'undo', icon: 'undo' }],
+ 'src/styles/components/_modified-files-tracker.scss': [{ name: 'undo', icon: 'undo' }],
+ 'src/main.ts': [{ name: 'undo', icon: 'undo' }],
+ 'example/src/main.ts': [{ name: 'undo', icon: 'undo' }]
+ }
+ }
+ });
+ }, 4000);
+
+ setTimeout(() => {
+ mynahUI.updateStore(tabId, {
+ modifiedFilesTitle: 'Work done!',
+ modifiedFilesList: {
+ filePaths: [
+ 'src/components/chat-wrapper.ts',
+ 'src/styles/components/_modified-files-tracker.scss',
+ 'src/main.ts',
+ 'example/src/main.ts'
+ ],
+ flatList: true,
+ actions: {
+ 'src/components/chat-wrapper.ts': [{ name: 'undo', icon: 'undo' }],
+ 'src/styles/components/_modified-files-tracker.scss': [{ name: 'undo', icon: 'undo' }],
+ 'src/main.ts': [{ name: 'undo', icon: 'undo' }],
+ 'example/src/main.ts': [{ name: 'undo', icon: 'undo' }]
+ }
+ }
+ });
+
+ // Add the undo all button as a separate chat item
+ mynahUI.addChatItem(tabId, {
+ type: ChatItemType.ANSWER,
+ messageId: generateUID(),
+ body: '',
+ buttons: [
+ { id: 'undo-all', text: 'Undo All', status: 'clear' }
+ ]
+ });
+ mynahUI.addChatItem(tabId, {
+ type: ChatItemType.ANSWER,
+ messageId: generateUID(),
+ body: 'Demo complete! The modified files tracker now shows "Work done!" status. Click on any file or use the undo buttons to see the callbacks in action.',
+ });
+ mynahUI.addChatItem(tabId, defaultFollowUps);
+ }, 5000);
+ break;
case Commands.CARD_WITH_MARKDOWN_LIST:
getGenerativeAIAnswer(tabId, sampleMarkdownList);
break;
diff --git a/example/src/samples/sample-data.ts b/example/src/samples/sample-data.ts
index a5b05be28..c59342b55 100644
--- a/example/src/samples/sample-data.ts
+++ b/example/src/samples/sample-data.ts
@@ -681,6 +681,10 @@ export const defaultFollowUps: ChatItem = {
pillText: 'Some auto reply',
prompt: 'Some random auto reply here.',
},
+ {
+ command: Commands.MODIFIED_FILES_DEMO,
+ pillText: 'Modified files demo',
+ },
],
},
};
@@ -2181,7 +2185,7 @@ export const mcpToolRunSampleCardInit:ChatItem = // Summary Card
filePaths: ['Running'],
details: {
'Running': {
- description: 'Work in progress!',
+ description: 'Work in progress...',
icon: null,
labelIcon: 'progress',
labelIconForegroundStatus: 'info',
diff --git a/src/__test__/components/feedback-form/feedback-form-integration.spec.ts b/src/__test__/components/feedback-form/feedback-form-integration.spec.ts
index 4823ae255..4054241bc 100644
--- a/src/__test__/components/feedback-form/feedback-form-integration.spec.ts
+++ b/src/__test__/components/feedback-form/feedback-form-integration.spec.ts
@@ -358,7 +358,7 @@ describe('FeedbackForm Integration Tests', () => {
describe('Error Handling', () => {
it('should handle missing tab data gracefully', () => {
// Override the mock for this specific test
- mockTabsStore.getTabDataStore.mockReturnValueOnce(undefined as any);
+ (mockTabsStore.getTabDataStore as jest.Mock).mockReturnValueOnce(undefined as any);
feedbackForm = new FeedbackForm();
diff --git a/src/__test__/components/feedback-form/feedback-form.spec.ts b/src/__test__/components/feedback-form/feedback-form.spec.ts
index 74352952d..658248471 100644
--- a/src/__test__/components/feedback-form/feedback-form.spec.ts
+++ b/src/__test__/components/feedback-form/feedback-form.spec.ts
@@ -347,7 +347,7 @@ describe('FeedbackForm Component', () => {
it('should return empty array when tab data store is undefined', () => {
// Override the mock for this specific test
- mockTabsStore.getTabDataStore.mockReturnValueOnce(undefined as any);
+ (mockTabsStore.getTabDataStore as jest.Mock).mockReturnValueOnce(undefined as any);
feedbackForm = new FeedbackForm();
diff --git a/src/components/__test__/chat-item/chat-item-card-content.spec.ts b/src/components/__test__/chat-item/chat-item-card-content.spec.ts
index 61535aa35..4d6e579b1 100644
--- a/src/components/__test__/chat-item/chat-item-card-content.spec.ts
+++ b/src/components/__test__/chat-item/chat-item-card-content.spec.ts
@@ -19,28 +19,26 @@ describe('ChatItemCardContent Animation Speed', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockGetInstance.mockReturnValue({
- // @ts-expect-error
+ (mockGetInstance as jest.Mock).mockReturnValue({
config: {
typewriterStackTime: 100,
typewriterMaxWordTime: 20,
disableTypewriterAnimation: false,
}
- });
+ } as any);
document.body.innerHTML = '
';
});
describe('Animation Configuration', () => {
it('should use fast animation settings', () => {
- mockGetInstance.mockReturnValue({
- // @ts-expect-error
+ (mockGetInstance as jest.Mock).mockReturnValue({
config: {
typewriterStackTime: 100,
typewriterMaxWordTime: 20,
disableTypewriterAnimation: false,
}
- });
+ } as any);
const props: ChatItemCardContentProps = {
body: 'Test content',
@@ -53,12 +51,11 @@ describe('ChatItemCardContent Animation Speed', () => {
});
it('should disable animation when configured', () => {
- mockGetInstance.mockReturnValue({
- // @ts-expect-error
+ (mockGetInstance as jest.Mock).mockReturnValue({
config: {
disableTypewriterAnimation: true,
}
- });
+ } as any);
const props: ChatItemCardContentProps = {
body: 'Test content',
@@ -89,10 +86,9 @@ describe('ChatItemCardContent Animation Speed', () => {
describe('Default Values', () => {
it('should use defaults when config is empty', () => {
- mockGetInstance.mockReturnValue({
- // @ts-expect-error
+ (mockGetInstance as jest.Mock).mockReturnValue({
config: {}
- });
+ } as any);
const props: ChatItemCardContentProps = {
body: 'Test content',
diff --git a/src/components/__test__/modified-files-tracker.spec.ts b/src/components/__test__/modified-files-tracker.spec.ts
new file mode 100644
index 000000000..c3a23215b
--- /dev/null
+++ b/src/components/__test__/modified-files-tracker.spec.ts
@@ -0,0 +1,467 @@
+import { ModifiedFilesTracker } from '../modified-files-tracker';
+import { MynahUIGlobalEvents } from '../../helper/events';
+import { ChatItem, ChatItemType } from '../../static';
+
+// Mock the tabs store
+jest.mock('../../helper/tabs-store', () => ({
+ MynahUITabsStore: {
+ getInstance: jest.fn(() => ({
+ getTabDataStore: jest.fn(() => ({
+ subscribe: jest.fn(),
+ getValue: jest.fn((key: string) => {
+ if (key === 'chatItems') {
+ return [];
+ }
+ return '';
+ })
+ }))
+ }))
+ }
+}));
+
+// Mock global events
+jest.mock('../../helper/events', () => ({
+ MynahUIGlobalEvents: {
+ getInstance: jest.fn(() => ({
+ dispatch: jest.fn()
+ }))
+ }
+}));
+
+// Mock CollapsibleContent
+jest.mock('../collapsible-content', () => ({
+ CollapsibleContent: jest.fn().mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => ({
+ innerHTML: '',
+ appendChild: jest.fn()
+ }))
+ },
+ updateTitle: jest.fn()
+ }))
+}));
+
+describe('ModifiedFilesTracker', () => {
+ let mockDispatch: jest.Mock;
+ let mockSubscribe: jest.Mock;
+ let mockGetValue: jest.Mock;
+ let mockGetTabDataStore: jest.Mock;
+
+ beforeEach(() => {
+ mockDispatch = jest.fn();
+ mockSubscribe = jest.fn();
+ mockGetValue = jest.fn((key: string) => {
+ if (key === 'chatItems') {
+ return [];
+ }
+ return '';
+ });
+ mockGetTabDataStore = jest.fn(() => ({
+ subscribe: mockSubscribe,
+ getValue: mockGetValue
+ }));
+
+ (MynahUIGlobalEvents.getInstance as jest.Mock).mockReturnValue({
+ dispatch: mockDispatch
+ });
+
+ const { MynahUITabsStore } = jest.requireMock('../../helper/tabs-store');
+ (MynahUITabsStore.getInstance as jest.Mock).mockReturnValue({
+ getTabDataStore: mockGetTabDataStore
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render with default props', () => {
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ expect(tracker.render).toBeDefined();
+ expect(tracker.render.classList.contains('mynah-modified-files-tracker-wrapper')).toBeTruthy();
+ expect(tracker.render.classList.contains('hidden')).toBeTruthy();
+ });
+
+ it('should render visible when visible prop is true', () => {
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab',
+ visible: true
+ });
+
+ expect(tracker.render.classList.contains('hidden')).toBeFalsy();
+ });
+
+ it('should subscribe to chatItems and modifiedFilesTitle', () => {
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ expect(tracker).toBeDefined();
+ expect(mockSubscribe).toHaveBeenCalledWith('chatItems', expect.any(Function));
+ expect(mockSubscribe).toHaveBeenCalledWith('modifiedFilesTitle', expect.any(Function));
+ });
+
+ describe('updateContent', () => {
+ let tracker: ModifiedFilesTracker;
+ let mockContentWrapper: any;
+
+ beforeEach(() => {
+ mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+ });
+
+ it('should show empty state when no modified files', () => {
+ mockGetValue.mockReturnValue([]);
+
+ (tracker as any).updateContent();
+
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+
+ it('should render file pills when modified files exist', () => {
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ fileList: {
+ filePaths: [ 'test.ts' ],
+ details: {
+ 'test.ts': {
+ changes: { added: 5, deleted: 2 },
+ visibleName: 'test.ts',
+ icon: 'ok-circled'
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ (tracker as any).updateContent();
+
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+ });
+
+ describe('title updates', () => {
+ it('should update title when modifiedFilesTitle changes', () => {
+ const mockUpdateTitle = jest.fn();
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => ({ innerHTML: '', appendChild: jest.fn() }))
+ },
+ updateTitle: mockUpdateTitle
+ }));
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ expect(tracker).toBeDefined();
+
+ const titleCallback = mockSubscribe.mock.calls.find(
+ (call: any) => call[0] === 'modifiedFilesTitle'
+ )?.[1];
+
+ titleCallback?.('New Title');
+
+ expect(mockUpdateTitle).toHaveBeenCalledWith('New Title');
+ });
+
+ it('should not update title when empty string is provided', () => {
+ const mockUpdateTitle = jest.fn();
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => ({ innerHTML: '', appendChild: jest.fn() }))
+ },
+ updateTitle: mockUpdateTitle
+ }));
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ expect(tracker).toBeDefined();
+
+ const titleCallback = mockSubscribe.mock.calls.find(
+ (call: any) => call[0] === 'modifiedFilesTitle'
+ )?.[1];
+
+ titleCallback?.('');
+
+ expect(mockUpdateTitle).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('setVisible method', () => {
+ it('should show tracker when setVisible(true) is called', () => {
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ tracker.render.removeClass = jest.fn();
+ tracker.setVisible(true);
+
+ expect(tracker.render.removeClass).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should hide tracker when setVisible(false) is called', () => {
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ tracker.render.addClass = jest.fn();
+ tracker.setVisible(false);
+
+ expect(tracker.render.addClass).toHaveBeenCalledWith('hidden');
+ });
+ });
+
+ describe('edge cases and error handling', () => {
+ it('should handle null contentWrapper gracefully', () => {
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => null)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ expect(() => (tracker as any).updateContent()).not.toThrow();
+ });
+
+ it('should handle files with deleted status', () => {
+ const mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ fileList: {
+ filePaths: [ 'deleted.ts' ],
+ deletedFiles: [ 'deleted.ts' ],
+ details: {
+ 'deleted.ts': {
+ changes: { added: 0, deleted: 10 },
+ visibleName: 'deleted.ts',
+ icon: 'trash'
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ (tracker as any).updateContent();
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+
+ it('should handle files with undo buttons', () => {
+ const mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ header: {
+ buttons: [ { id: 'undo-changes', text: 'Undo' } ]
+ },
+ fileList: {
+ filePaths: [ 'test.ts' ],
+ details: {
+ 'test.ts': {
+ changes: { added: 5, deleted: 2 },
+ visibleName: 'test.ts',
+ icon: 'ok-circled'
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ (tracker as any).updateContent();
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+
+ it('should handle undo-all-changes button', () => {
+ const mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ header: {
+ buttons: [ { id: 'undo-all-changes', text: 'Undo All Changes' } ]
+ },
+ fileList: {
+ filePaths: [ 'test.ts' ],
+ details: {
+ 'test.ts': {
+ changes: { added: 5, deleted: 2 },
+ visibleName: 'test.ts'
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ (tracker as any).updateContent();
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+
+ it('should handle files without icon', () => {
+ const mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ fileList: {
+ filePaths: [ 'test.ts' ],
+ details: {
+ 'test.ts': {
+ changes: { added: 5, deleted: 2 },
+ visibleName: 'test.ts'
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ (tracker as any).updateContent();
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+
+ it('should handle files with clickable false', () => {
+ const mockContentWrapper = {
+ innerHTML: '',
+ appendChild: jest.fn()
+ };
+
+ const { CollapsibleContent } = jest.requireMock('../collapsible-content');
+ (CollapsibleContent as jest.Mock).mockImplementation(() => ({
+ render: {
+ querySelector: jest.fn(() => mockContentWrapper)
+ },
+ updateTitle: jest.fn()
+ }));
+
+ const mockChatItems: ChatItem[] = [
+ {
+ type: ChatItemType.ANSWER,
+ messageId: 'msg-1',
+ fileList: {
+ filePaths: [ 'test.ts' ],
+ details: {
+ 'test.ts': {
+ changes: { added: 5, deleted: 2 },
+ visibleName: 'test.ts',
+ clickable: false
+ }
+ }
+ }
+ }
+ ];
+
+ mockGetValue.mockReturnValue(mockChatItems);
+
+ const tracker = new ModifiedFilesTracker({
+ tabId: 'test-tab'
+ });
+
+ (tracker as any).updateContent();
+ expect(mockContentWrapper.appendChild).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/components/chat-item/chat-item-card.ts b/src/components/chat-item/chat-item-card.ts
index d68e08358..55b4eda0e 100644
--- a/src/components/chat-item/chat-item-card.ts
+++ b/src/components/chat-item/chat-item-card.ts
@@ -320,11 +320,19 @@ export class ChatItemCard {
if (header.fileList?.details?.[filePath]?.clickable === false) {
return;
}
+ console.log('[ChatItemCard] File pill clicked:', {
+ tabId: this.props.tabId,
+ messageId: this.props.chatItem.messageId,
+ filePath,
+ deleted: isDeleted,
+ fileDetails: header.fileList?.details?.[filePath]
+ });
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_CLICK, {
tabId: this.props.tabId,
messageId: this.props.chatItem.messageId,
filePath,
- deleted: isDeleted
+ deleted: isDeleted,
+ fileDetails: header.fileList?.details?.[filePath]
});
},
...(description !== undefined
@@ -805,6 +813,13 @@ export class ChatItemCard {
formItems: this.chatFormItems,
buttons: [],
onActionClick: action => {
+ console.log('[ChatItemCard] Button clicked:', {
+ tabId: this.props.tabId,
+ messageId: this.props.chatItem.messageId,
+ actionId: action.id,
+ actionText: action.text,
+ formItemValues: this.chatFormItems !== null ? this.chatFormItems.getAllValues() : {}
+ });
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.BODY_ACTION_CLICKED, {
tabId: this.props.tabId,
messageId: this.props.chatItem.messageId,
diff --git a/src/components/chat-item/chat-item-tree-file.ts b/src/components/chat-item/chat-item-tree-file.ts
index facf4d15c..a049b12f2 100644
--- a/src/components/chat-item/chat-item-tree-file.ts
+++ b/src/components/chat-item/chat-item-tree-file.ts
@@ -42,9 +42,14 @@ export class ChatItemTreeFile {
click: () => {
this.hideTooltip();
if (this.props.details?.clickable !== false) {
+ const fileMessageId = this.props.details?.data?.messageId ?? this.props.messageId;
+ console.log('[ChatItemTreeFile] File clicked - originalFilePath:', this.props.originalFilePath);
+ console.log('[ChatItemTreeFile] File clicked - details.data.fullPath:', this.props.details?.data?.fullPath);
+ console.log('[ChatItemTreeFile] File clicked - details.description:', this.props.details?.description);
+ console.log('[ChatItemTreeFile] File clicked - using messageId:', fileMessageId);
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_CLICK, {
tabId: this.props.tabId,
- messageId: this.props.messageId,
+ messageId: fileMessageId,
filePath: this.props.originalFilePath,
deleted: this.props.deleted,
fileDetails: this.props.details
@@ -139,9 +144,17 @@ export class ChatItemTreeFile {
onClick: (e) => {
cancelEvent(e);
this.hideTooltip();
+ const fileMessageId = this.props.details?.data?.messageId ?? this.props.messageId;
+ console.log('[ChatItemTreeFile] File action clicked:', {
+ tabId: this.props.tabId,
+ messageId: fileMessageId,
+ filePath: this.props.originalFilePath,
+ actionName: action.name,
+ actionDetails: action
+ });
MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.FILE_ACTION_CLICK, {
tabId: this.props.tabId,
- messageId: this.props.messageId,
+ messageId: fileMessageId,
filePath: this.props.originalFilePath,
actionName: action.name,
});
diff --git a/src/components/chat-item/chat-wrapper.ts b/src/components/chat-item/chat-wrapper.ts
index ebcc1d4ee..d5c09312b 100644
--- a/src/components/chat-item/chat-wrapper.ts
+++ b/src/components/chat-item/chat-wrapper.ts
@@ -30,6 +30,7 @@ import { StyleLoader } from '../../helper/style-loader';
import { Icon } from '../icon';
import { cancelEvent, MynahUIGlobalEvents } from '../../helper/events';
import { TopBarButtonOverlayProps } from './prompt-input/prompt-top-bar/top-bar-button';
+import { ModifiedFilesTracker } from '../modified-files-tracker';
export const CONTAINER_GAP = 12;
export interface ChatWrapperProps {
@@ -58,6 +59,7 @@ export class ChatWrapper {
private readonly dragBlurOverlay: HTMLElement;
private dragOverlayVisibility: boolean = true;
private imageContextFeatureEnabled: boolean = false;
+ private readonly modifiedFilesTracker: ModifiedFilesTracker;
constructor (props: ChatWrapperProps) {
StyleLoader.getInstance().load('components/chat/_chat-wrapper.scss');
@@ -92,8 +94,18 @@ export class ChatWrapper {
this.imageContextFeatureEnabled = contextCommands.some(group =>
group.commands.some((cmd: QuickActionCommand) => cmd.command.toLowerCase() === 'image')
);
+
+ this.modifiedFilesTracker = new ModifiedFilesTracker({
+ tabId: this.props.tabId,
+ visible: false
+ });
+ MynahUITabsStore.getInstance().addListenerToDataStore(this.props.tabId, 'modifiedFilesVisible', (visible: boolean) => {
+ this.modifiedFilesTracker.setVisible(visible);
+ });
+
MynahUITabsStore.getInstance().addListenerToDataStore(this.props.tabId, 'chatItems', (chatItems: ChatItem[]) => {
const chatItemToInsert: ChatItem = chatItems[chatItems.length - 1];
+
if (Object.keys(this.allRenderedChatItems).length === chatItems.length) {
const lastItem = this.chatItemsContainer.children.item(Array.from(this.chatItemsContainer.children).length - 1);
if (lastItem != null && chatItemToInsert != null) {
@@ -310,6 +322,7 @@ export class ChatWrapper {
}
}).render,
this.promptStickyCard,
+ this.modifiedFilesTracker.render,
this.promptInputElement,
this.footerSpacer,
this.promptInfo,
@@ -537,4 +550,8 @@ export class ChatWrapper {
this.dragOverlayContent.style.display = visible ? 'flex' : 'none';
this.dragBlurOverlay.style.display = visible ? 'block' : 'none';
}
+
+ public setModifiedFilesTrackerVisible (visible: boolean): void {
+ this.modifiedFilesTracker.setVisible(visible);
+ }
}
diff --git a/src/components/collapsible-content.ts b/src/components/collapsible-content.ts
index fd1757b69..9d02d3b45 100644
--- a/src/components/collapsible-content.ts
+++ b/src/components/collapsible-content.ts
@@ -22,6 +22,7 @@ export class CollapsibleContent {
private readonly props: Required;
private readonly uid: string;
private icon: ExtendedHTMLElement;
+ private readonly titleTextElement: ExtendedHTMLElement;
constructor (props: CollapsibleContentProps) {
StyleLoader.getInstance().load('components/_collapsible-content.scss');
this.uid = generateUID();
@@ -71,11 +72,11 @@ export class CollapsibleContent {
classNames: [ 'mynah-collapsible-content-label-title-wrapper' ],
children: [
this.icon,
- {
+ this.titleTextElement = DomBuilder.getInstance().build({
type: 'span',
classNames: [ 'mynah-collapsible-content-label-title-text' ],
children: [ this.props.title ]
- }
+ })
]
},
{
@@ -88,4 +89,8 @@ export class CollapsibleContent {
],
});
}
+
+ public updateTitle (newTitle: string): void {
+ this.titleTextElement.update({ children: [ newTitle ] });
+ }
}
diff --git a/src/components/modified-files-tracker.ts b/src/components/modified-files-tracker.ts
new file mode 100644
index 000000000..4b99558e9
--- /dev/null
+++ b/src/components/modified-files-tracker.ts
@@ -0,0 +1,133 @@
+/*!
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DomBuilder, ExtendedHTMLElement } from '../helper/dom';
+import { StyleLoader } from '../helper/style-loader';
+import { CollapsibleContent } from './collapsible-content';
+import { ChatItemContent, ChatItemButton, MynahEventNames } from '../static';
+import testIds from '../helper/test-ids';
+import { MynahUITabsStore } from '../helper/tabs-store';
+import { ChatItemTreeViewWrapper } from './chat-item/chat-item-tree-view-wrapper';
+import { ChatItemButtonsWrapper } from './chat-item/chat-item-buttons';
+import { MynahUIGlobalEvents } from '../helper/events';
+
+export interface ModifiedFilesTrackerProps {
+ tabId: string;
+ visible?: boolean;
+}
+
+export class ModifiedFilesTracker {
+ render: ExtendedHTMLElement;
+ private readonly props: ModifiedFilesTrackerProps;
+ private readonly collapsibleContent: CollapsibleContent;
+ public titleText: string = 'No files modified!';
+
+ constructor (props: ModifiedFilesTrackerProps) {
+ StyleLoader.getInstance().load('components/_modified-files-tracker.scss');
+ this.props = { visible: false, ...props };
+
+ this.collapsibleContent = new CollapsibleContent({
+ title: this.titleText,
+ initialCollapsedState: true,
+ children: [],
+ classNames: [ 'mynah-modified-files-tracker' ],
+ testId: testIds.modifiedFilesTracker.wrapper
+ });
+
+ this.render = DomBuilder.getInstance().build({
+ type: 'div',
+ classNames: [
+ 'mynah-modified-files-tracker-wrapper',
+ ...(this.props.visible === true ? [] : [ 'hidden' ])
+ ],
+ testId: testIds.modifiedFilesTracker.container,
+ children: [ this.collapsibleContent.render ]
+ });
+
+ const tabDataStore = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId);
+
+ tabDataStore.subscribe('modifiedFilesList', (fileList: ChatItemContent['fileList'] | null) => {
+ this.renderModifiedFiles(fileList);
+ });
+
+ tabDataStore.subscribe('modifiedFilesTitle', (newTitle: string) => {
+ if (newTitle !== '') {
+ this.collapsibleContent.updateTitle(newTitle);
+ }
+ });
+
+ this.renderModifiedFiles(tabDataStore.getValue('modifiedFilesList'));
+ }
+
+ private renderModifiedFiles (fileList: ChatItemContent['fileList'] | null): void {
+ const contentWrapper = this.collapsibleContent.render.querySelector('.mynah-collapsible-content-label-content-wrapper');
+ if (contentWrapper == null) return;
+
+ contentWrapper.innerHTML = '';
+
+ if ((fileList?.filePaths?.length ?? 0) > 0 && fileList != null) {
+ this.renderFilePills(contentWrapper, fileList);
+ } else {
+ this.renderEmptyState(contentWrapper);
+ }
+ }
+
+ private renderEmptyState (contentWrapper: Element): void {
+ contentWrapper.appendChild(DomBuilder.getInstance().build({
+ type: 'div',
+ classNames: [ 'mynah-modified-files-empty-state' ],
+ children: [ 'No modified files' ]
+ }));
+ }
+
+ private renderFilePills (contentWrapper: Element, fileList: NonNullable & { messageId?: string }): void {
+ const messageId = fileList.messageId ?? `modified-files-tracker-${this.props.tabId}`;
+
+ // Render the file tree with actions and buttons as provided by the data
+ contentWrapper.appendChild(new ChatItemTreeViewWrapper({
+ tabId: this.props.tabId,
+ messageId,
+ files: fileList.filePaths ?? [],
+ cardTitle: '',
+ rootTitle: fileList.rootFolderTitle,
+ deletedFiles: fileList.deletedFiles ?? [],
+ flatList: fileList.flatList ?? true,
+ actions: (fileList as any).actions ?? {},
+ details: fileList.details ?? {},
+ hideFileCount: fileList.hideFileCount ?? true,
+ collapsed: fileList.collapsed ?? false,
+ referenceSuggestionLabel: '',
+ references: [],
+ onRootCollapsedStateChange: () => {}
+ }).render);
+
+ // Render buttons if they exist
+ const fileListWithButtons = fileList as ChatItemContent['fileList'] & { buttons?: ChatItemButton[] };
+ const buttons: ChatItemButton[] | undefined = fileListWithButtons.buttons;
+ if (Array.isArray(buttons) && buttons.length > 0) {
+ const buttonsWrapper = new ChatItemButtonsWrapper({
+ tabId: this.props.tabId,
+ buttons,
+ onActionClick: (action: ChatItemButton) => {
+ MynahUIGlobalEvents.getInstance().dispatch(MynahEventNames.BODY_ACTION_CLICKED, {
+ tabId: this.props.tabId,
+ messageId: (action as any).messageId != null ? (action as any).messageId : messageId,
+ actionId: action.id,
+ actionText: action.text
+ });
+ }
+ });
+ contentWrapper.appendChild(buttonsWrapper.render);
+ }
+ }
+
+ public setVisible (visible: boolean): void {
+ if (visible) {
+ this.render.removeClass('hidden');
+ } else {
+ this.render.addClass('hidden');
+ }
+ }
+}
diff --git a/src/helper/store.ts b/src/helper/store.ts
index e6edae95b..33bd41b1f 100644
--- a/src/helper/store.ts
+++ b/src/helper/store.ts
@@ -43,7 +43,11 @@ const emptyDataModelObject: Required = {
compactMode: false,
tabHeaderDetails: null,
tabMetadata: {},
- customContextCommand: []
+ customContextCommand: [],
+ modifiedFilesTitle: 'No files modified!',
+ modifiedFilesVisible: false,
+ modifiedFilesList: null,
+ newConversation: false
};
const dataModelKeys = Object.keys(emptyDataModelObject);
export class EmptyMynahUIDataModel {
diff --git a/src/helper/test-ids.ts b/src/helper/test-ids.ts
index f399b8ef3..a218a5a0b 100644
--- a/src/helper/test-ids.ts
+++ b/src/helper/test-ids.ts
@@ -175,5 +175,13 @@ export default {
option: 'dropdown-list-option',
optionLabel: 'dropdown-list-option-label',
checkIcon: 'dropdown-list-check-icon'
+ },
+ modifiedFilesTracker: {
+ container: 'modified-files-tracker-container',
+ wrapper: 'modified-files-tracker-wrapper',
+ emptyState: 'modified-files-tracker-empty-state',
+ fileItem: 'modified-files-tracker-file-item',
+ fileItemAccept: 'modified-files-tracker-file-item-accept',
+ fileItemUndo: 'modified-files-tracker-file-item-undo'
}
};
diff --git a/src/main.ts b/src/main.ts
index 64e56233b..c65f7e431 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -50,6 +50,7 @@ import { StyleLoader } from './helper/style-loader';
import { Icon } from './components/icon';
import { Button } from './components/button';
import { TopBarButtonOverlayProps } from './components/chat-item/prompt-input/prompt-top-bar/top-bar-button';
+// TrackedFile interface removed - now using data-driven approach
export { generateUID } from './helper/guid';
export {
@@ -96,6 +97,10 @@ export {
ChatItemCardContent,
ChatItemCardContentProps
} from './components/chat-item/chat-item-card-content';
+export {
+ ModifiedFilesTracker,
+ ModifiedFilesTrackerProps
+} from './components/modified-files-tracker';
export { default as MynahUITestIds } from './helper/test-ids';
export interface MynahUIProps {
@@ -337,6 +342,11 @@ export interface MynahUIProps {
files: FileList,
insertPosition: number
) => void;
+ onModifiedFileClick?: (
+ tabId: string,
+ filePath: string,
+ eventId?: string
+ ) => void;
}
export class MynahUI {
@@ -352,6 +362,17 @@ export class MynahUI {
private readonly sheet?: Sheet;
private readonly chatWrappers: Record = {};
+ private logToStorage (message: string): void {
+ try {
+ const timestamp = new Date().toISOString();
+ const logEntry = `[${timestamp}] ${message}`;
+ const existingLogs = localStorage.getItem('mynah-modified-files-logs') ?? '';
+ localStorage.setItem('mynah-modified-files-logs', existingLogs + logEntry + '\n');
+ } catch (error) {
+ // Ignore storage errors
+ }
+ }
+
constructor (props: MynahUIProps) {
StyleLoader.getInstance(props.loadStyles !== false).load('styles.scss');
configureMarked();
@@ -387,7 +408,7 @@ export class MynahUI {
props.onStopChatResponse(tabId, this.getUserEventId());
}
}
- : undefined,
+ : undefined
});
return this.chatWrappers[tabId].render;
})
@@ -466,7 +487,7 @@ export class MynahUI {
props.onStopChatResponse(tabId, this.getUserEventId());
}
}
- : undefined,
+ : undefined
});
this.tabContentsWrapper.appendChild(this.chatWrappers[tabId].render);
this.focusToInput(tabId);
@@ -535,11 +556,7 @@ export class MynahUI {
};
private readonly addGlobalListeners = (): void => {
- MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CHAT_PROMPT, (data: { tabId: string; prompt: ChatPrompt }) => {
- if (this.props.onChatPrompt !== undefined) {
- this.props.onChatPrompt(data.tabId, data.prompt, this.getUserEventId());
- }
- });
+ MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.CHAT_PROMPT, this.handleChatPrompt);
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FOLLOW_UP_CLICKED, (data: {
tabId: string;
@@ -601,6 +618,7 @@ export class MynahUI {
actionText?: string;
formItemValues?: Record;
}) => {
+ console.log('[MynahUI] BODY_ACTION_CLICKED event received:', data);
if (this.props.onInBodyButtonClicked !== undefined) {
this.props.onInBodyButtonClicked(data.tabId, data.messageId, {
id: data.actionId,
@@ -792,6 +810,7 @@ export class MynahUI {
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FILE_CLICK, (data) => {
+ console.log('[MynahUI] FILE_CLICK event received:', data);
if (this.props.onFileClick !== undefined) {
this.props.onFileClick(
data.tabId,
@@ -821,6 +840,7 @@ export class MynahUI {
});
MynahUIGlobalEvents.getInstance().addListener(MynahEventNames.FILE_ACTION_CLICK, (data) => {
+ console.log('[MynahUI] FILE_ACTION_CLICK event received:', data);
if (this.props.onFileActionClick !== undefined) {
this.props.onFileActionClick(
data.tabId,
@@ -1208,6 +1228,147 @@ export class MynahUI {
};
};
+ /**
+ * Adds a file to the modified files tracker for the specified tab with file type
+ * @param tabId The tab ID
+ * @param filePath The path of the file
+ * @param fileType The type of file change ('created', 'modified', 'deleted')
+ * @param fullPath Optional full path of the file
+ * @param toolUseId Optional tool use ID for undo operations
+ */
+ public addFile = (tabId: string, filePath: string, fileType: 'created' | 'modified' | 'deleted' = 'modified', fullPath?: string, toolUseId?: string): void => {
+ // No-op: now handled by data-driven approach through ChatItem.fileList
+ };
+
+ /**
+ * Adds a file to the modified files tracker for the specified tab
+ * @deprecated Use addFile() instead
+ * @param tabId The tab ID
+ * @param filePath The path of the modified file
+ * @param toolUseId Optional tool use ID for undo operations
+ */
+ public addModifiedFile = (tabId: string, filePath: string, toolUseId?: string): void => {
+ this.addFile(tabId, filePath, 'modified', toolUseId);
+ };
+
+ /**
+ * Removes a file from the modified files tracker for the specified tab
+ * @param tabId The tab ID
+ * @param filePath The path of the file to remove
+ */
+ public removeFile = (tabId: string, filePath: string): void => {
+ // No-op: now handled by data-driven approach
+ };
+
+ /**
+ * Removes a file from the modified files tracker for the specified tab
+ * @deprecated Use removeFile() instead
+ * @param tabId The tab ID
+ * @param filePath The path of the file to remove
+ */
+ public removeModifiedFile = (tabId: string, filePath: string): void => {
+ this.removeFile(tabId, filePath);
+ };
+
+ /**
+ * Sets the work in progress status for the files tracker
+ * @param tabId The tab ID
+ * @param inProgress Whether work is in progress
+ */
+ public setFilesWorkInProgress = (tabId: string, inProgress: boolean): void => {
+ // No-op: work in progress functionality removed
+ };
+
+ /**
+ * Sets the work in progress status for the modified files tracker
+ * @deprecated Use setFilesWorkInProgress() instead
+ * @param tabId The tab ID
+ * @param inProgress Whether work is in progress
+ */
+ public setModifiedFilesWorkInProgress = (tabId: string, inProgress: boolean): void => {
+ this.setFilesWorkInProgress(tabId, inProgress);
+ };
+
+ /**
+ * Clears all files for the specified tab
+ * @param tabId The tab ID
+ */
+ public clearFiles = (tabId: string): void => {
+ // No-op: now handled by data-driven approach
+ };
+
+ /**
+ * Clears all modified files for the specified tab
+ * @deprecated Use clearFiles() instead
+ * @param tabId The tab ID
+ */
+ public clearModifiedFiles = (tabId: string): void => {
+ this.clearFiles(tabId);
+ };
+
+ private readonly handleChatPrompt = (data: { tabId: string; prompt: ChatPrompt }): void => {
+ // Clear modified files for file-modifying commands
+ const fileModifyingCommands = [ '/dev', '/transform', '/generate' ];
+ if (data.prompt.command !== null && data.prompt.command !== undefined && fileModifyingCommands.includes(data.prompt.command)) {
+ this.clearModifiedFiles(data.tabId);
+ }
+
+ if (this.props.onChatPrompt !== undefined) {
+ this.props.onChatPrompt(data.tabId, data.prompt, this.getUserEventId());
+ }
+ };
+
+ /**
+ * Gets the list of tracked files for the specified tab
+ * @param tabId The tab ID
+ * @returns Array of tracked files with types
+ */
+ public getTrackedFiles = (tabId: string): any[] => {
+ // Return empty array: now handled by data-driven approach
+ return [];
+ };
+
+ /**
+ * Gets the list of modified files for the specified tab
+ * @deprecated Use getTrackedFiles() instead
+ * @param tabId The tab ID
+ * @returns Array of modified file paths
+ */
+ public getModifiedFiles = (tabId: string): string[] => {
+ // Return empty array: now handled by data-driven approach
+ return [];
+ };
+
+ /**
+ * Sets the visibility of the files tracker for the specified tab
+ * @param tabId The tab ID
+ * @param visible Whether the tracker should be visible
+ */
+ public setFilesTrackerVisible = (tabId: string, visible: boolean): void => {
+ if (this.chatWrappers[tabId] != null) {
+ this.chatWrappers[tabId].setModifiedFilesTrackerVisible(visible);
+ }
+ };
+
+ /**
+ * Sets the visibility of the modified files tracker for the specified tab
+ * @deprecated Use setFilesTrackerVisible() instead
+ * @param tabId The tab ID
+ * @param visible Whether the tracker should be visible
+ */
+ public setModifiedFilesTrackerVisible = (tabId: string, visible: boolean): void => {
+ this.setFilesTrackerVisible(tabId, visible);
+ };
+
+ /**
+ * Sets the message ID for the modified files tracker to enable diff mode functionality
+ * @param tabId The tab ID
+ * @param messageId The message ID to associate with file clicks
+ */
+ public setMessageId = (tabId: string, messageId: string): void => {
+ // No-op: now handled by data-driven approach
+ };
+
public destroy = (): void => {
// Destroy all chat wrappers
Object.values(this.chatWrappers).forEach(chatWrapper => {
diff --git a/src/static.ts b/src/static.ts
index beaa86040..808946f83 100644
--- a/src/static.ts
+++ b/src/static.ts
@@ -192,6 +192,22 @@ export interface MynahUIDataModel {
* Custom context commands to be inserted into the prompt input.
*/
customContextCommand?: QuickActionCommand[];
+ /**
+ * Title for the modified files tracker component
+ */
+ modifiedFilesTitle?: string;
+ /**
+ * Visibility state for the modified files tracker component
+ */
+ modifiedFilesVisible?: boolean;
+ /**
+ * Flag to indicate when a new conversation starts
+ */
+ newConversation?: boolean;
+ /**
+ * Modified files data for the tracker component (push mechanism)
+ */
+ modifiedFilesList?: ChatItemContent['fileList'];
}
export interface MynahUITabStoreTab {
diff --git a/src/styles/components/_modified-files-tracker.scss b/src/styles/components/_modified-files-tracker.scss
new file mode 100644
index 000000000..3860d99bd
--- /dev/null
+++ b/src/styles/components/_modified-files-tracker.scss
@@ -0,0 +1,42 @@
+/*!
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+.mynah-modified-files-tracker-wrapper {
+ border: var(--mynah-border-width) solid var(--mynah-color-border-default);
+ border-bottom: none;
+ border-radius: var(--mynah-input-radius) var(--mynah-input-radius) var(--mynah-card-radius-corner)
+ var(--mynah-card-radius-corner);
+ margin: var(--mynah-card-radius-corner) calc(var(--mynah-chat-wrapper-spacing) + var(--mynah-sizing-2));
+
+ &.hidden {
+ display: none;
+ }
+
+ .mynah-collapsible-content-label-content-wrapper {
+ pointer-events: none;
+
+ * {
+ pointer-events: auto;
+ }
+ }
+
+ .mynah-modified-files-empty-state {
+ color: var(--mynah-color-text-weak);
+ font-style: italic;
+ padding: var(--mynah-sizing-2) var(--mynah-card-radius-corner);
+ }
+}
+
+// Remove top border radius and padding from chat prompt when modified files tracker is visible
+.mynah-modified-files-tracker-wrapper:not(.hidden) + .mynah-chat-prompt-wrapper {
+ padding-top: var(--mynah-card-radius-corner);
+ margin-top: calc(var(--mynah-sizing-negative-2, calc(-1 * var(--mynah-sizing-2))));
+
+ > .mynah-chat-prompt {
+ border-top-left-radius: var(--mynah-card-radius-corner);
+ border-top-right-radius: var(--mynah-card-radius-corner);
+ border-top: none;
+ }
+}
diff --git a/src/styles/components/chat/_chat-wrapper.scss b/src/styles/components/chat/_chat-wrapper.scss
index 68199e473..d53277b34 100644
--- a/src/styles/components/chat/_chat-wrapper.scss
+++ b/src/styles/components/chat/_chat-wrapper.scss
@@ -144,6 +144,14 @@
}
}
+ > .mynah-modified-files-tracker-wrapper {
+ position: relative;
+ flex-shrink: 0;
+ padding: 0 var(--mynah-sizing-4);
+ margin-bottom: var(--mynah-sizing-2);
+ z-index: 1;
+ }
+
&:not(.with-background) {
> .mynah-ui-gradient-background {
opacity: 0;