Skip to content

Commit ae3c83c

Browse files
authored
feat: support rendering file list as pills (#419)
* feat: support rendering file list as pills * add ui tests * refactor * fix empty list behavior * address comments * add image
1 parent 2d25c91 commit ae3c83c

File tree

14 files changed

+579
-11
lines changed

14 files changed

+579
-11
lines changed

docs/DATAMODEL.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,7 @@ interface ChatItemContent {
13431343
folderIcon?: MynahIcons | MynahIconsType | null;
13441344
collapsed?: boolean;
13451345
hideFileCount?: boolean;
1346+
renderAsPills?: boolean; // When true (header only), renders files as inline pills instead of tree
13461347
actions?: Record<string, FileNodeAction[]>;
13471348
details?: Record<string, TreeNodeDetails>;
13481349
} | null;
@@ -2486,10 +2487,80 @@ mynahUI.addChatItem(tabId, {
24862487

24872488
**NOTE 4:** In case you want a flat list, where all subfolders are not rendered but just all the files, you can pass `true` to the `flatList` prop.
24882489

2490+
**NOTE 5:** When using `renderAsPills: true` in a header's fileList, files will be displayed as inline pills instead of a traditional file tree. This is useful for showing a compact list of files that were processed or referenced.
2491+
24892492
<p align="center">
24902493
<img src="./img/data-model/chatItems/codeResult.png" alt="mainTitle" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
24912494
</p>
24922495

2496+
#### `renderAsPills` (default: `false`)
2497+
When set to `true` in a header's fileList, files will be rendered as inline pills next to the header text instead of a traditional file tree. This creates a compact display perfect for showing files that were read, processed, or referenced.
2498+
2499+
**Features:**
2500+
- Files appear as clickable pills inline with the header text and icon
2501+
- Uses `visibleName` from details as pill text (falls back to full file path)
2502+
- Deleted files get special styling with `mynah-chat-item-tree-file-pill-deleted` class
2503+
- Pills support hover tooltips when `description` is provided in details
2504+
- Header icon is automatically included in the custom renderer for proper alignment
2505+
- Pills dispatch `FILE_CLICK` events when clicked, same as regular file tree items
2506+
- Only works when `renderAsPills: true` is set in a header's fileList (not main fileList)
2507+
2508+
**Important Notes:**
2509+
- This feature only works within a `header.fileList`, not in the main chat item `fileList`
2510+
- When `renderAsPills: true`, the traditional file tree is replaced with inline pills
2511+
- The header's `body`, `icon`, and file pills are all rendered together in a custom renderer
2512+
- Empty `filePaths` array will result in no pills being rendered
2513+
2514+
```typescript
2515+
mynahUI.addChatItem('tab-1', {
2516+
type: ChatItemType.ANSWER,
2517+
header: {
2518+
icon: MynahIcons.EYE,
2519+
body: '5 files read',
2520+
fileList: {
2521+
filePaths: ['package.json', 'tsconfig.json', 'src/index.ts'],
2522+
renderAsPills: true,
2523+
details: {
2524+
'package.json': {
2525+
visibleName: 'package',
2526+
description: 'Project configuration'
2527+
},
2528+
'tsconfig.json': {
2529+
visibleName: 'tsconfig',
2530+
description: 'TypeScript configuration'
2531+
},
2532+
'src/index.ts': {
2533+
visibleName: 'index.ts',
2534+
description: 'Main entry point'
2535+
}
2536+
},
2537+
deletedFiles: ['src/index.ts'] // Will show with deleted styling
2538+
}
2539+
},
2540+
});
2541+
```
2542+
2543+
<p align="center">
2544+
<img src="./img/data-model/chatItems/renderAsPills.png" alt="renderAsPills" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
2545+
</p>
2546+
2547+
**Comparison with regular file tree:**
2548+
2549+
```typescript
2550+
// Regular file tree (renderAsPills: false or undefined)
2551+
mynahUI.addChatItem('tab-1', {
2552+
type: ChatItemType.ANSWER,
2553+
header: {
2554+
icon: MynahIcons.EYE,
2555+
body: 'Files analyzed',
2556+
fileList: {
2557+
filePaths: ['package.json', 'src/index.ts'],
2558+
renderAsPills: false // or omit this property
2559+
}
2560+
}
2561+
});
2562+
```
2563+
24932564

24942565
#### File `details`
24952566

25.1 KB
Loading

example/src/samples/sample-data.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,122 @@ mkdir -p src/ lalalaaaa sad fbnsafsdaf sdakjfsd sadf asdkljf basdkjfh ksajhf kjs
19651965
`,
19661966
codeBlockActions: { copy: null, 'insert-to-cursor': null },
19671967
},
1968+
{
1969+
type: ChatItemType.ANSWER,
1970+
fullWidth: true,
1971+
padding: false,
1972+
header: {
1973+
icon: 'progress',
1974+
body: 'Reading',
1975+
fileList: {
1976+
filePaths: ['package.json', 'README.md'],
1977+
details: {
1978+
'package.json': {
1979+
visibleName: 'package.json',
1980+
description: 'package.json'
1981+
},
1982+
'README.md': {
1983+
visibleName: 'README.md',
1984+
description: 'README.md'
1985+
}
1986+
},
1987+
renderAsPills: true
1988+
}
1989+
},
1990+
},
1991+
{
1992+
type: ChatItemType.ANSWER,
1993+
fullWidth: true,
1994+
padding: false,
1995+
header: {
1996+
icon: 'eye',
1997+
body: '5 files read',
1998+
fileList: {
1999+
filePaths: ['package.json', 'README.md', 'webpack.config.js', 'src/app.ts', 'src/components/Button/Button.tsx'],
2000+
details: {
2001+
'package.json': {
2002+
visibleName: 'package.json',
2003+
description: 'package.json'
2004+
},
2005+
'README.md': {
2006+
visibleName: 'README.md',
2007+
description: 'README.md'
2008+
},
2009+
'webpack.config.js': {
2010+
visibleName: 'webpack.config.js',
2011+
description: 'webpack.config.js'
2012+
},
2013+
'src/app.ts': {
2014+
visibleName: 'app.ts',
2015+
description: 'src/app.ts'
2016+
},
2017+
'src/components/Button/Button.tsx': {
2018+
visibleName: 'Button.tsx',
2019+
description: 'src/components/Button/Button.tsx'
2020+
}
2021+
},
2022+
renderAsPills: true
2023+
}
2024+
},
2025+
},
2026+
{
2027+
type: ChatItemType.ANSWER,
2028+
fullWidth: true,
2029+
padding: false,
2030+
header: {
2031+
icon: 'progress',
2032+
body: 'Listing',
2033+
fileList: {
2034+
filePaths: ['src/components/ui', 'src/components/forms'],
2035+
details: {
2036+
'src/components/ui': {
2037+
visibleName: 'ui',
2038+
description: 'src/components/ui'
2039+
},
2040+
'src/components/forms': {
2041+
visibleName: 'forms',
2042+
description: 'src/components/forms'
2043+
},
2044+
},
2045+
renderAsPills: true
2046+
}
2047+
}
2048+
},
2049+
{
2050+
type: ChatItemType.ANSWER,
2051+
fullWidth: true,
2052+
padding: false,
2053+
header: {
2054+
icon: 'check-list',
2055+
body: '5 directories listed',
2056+
fileList: {
2057+
filePaths: ['src/components/ui', 'src/components/forms', 'src/components/layout', 'src/utils/helpers', 'src/utils/validation'],
2058+
details: {
2059+
'src/components/ui': {
2060+
visibleName: 'ui',
2061+
description: 'src/components/ui',
2062+
},
2063+
'src/components/forms': {
2064+
visibleName: 'forms',
2065+
description: 'src/components/forms'
2066+
},
2067+
'src/components/layout': {
2068+
visibleName: 'layout',
2069+
description: 'src/components/layout'
2070+
},
2071+
'src/utils/helpers': {
2072+
visibleName: 'helpers',
2073+
description: 'src/components/helpers'
2074+
},
2075+
'src/utils/validation': {
2076+
visibleName: 'validation',
2077+
description: 'src/components/validation'
2078+
},
2079+
},
2080+
renderAsPills: true
2081+
}
2082+
}
2083+
}
19682084
];
19692085

19702086

src/components/__test__/chat-item/chat-item-card.spec.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,152 @@ describe('ChatItemCard', () => {
2525

2626
expect(card.render).toBeDefined();
2727
});
28+
29+
describe('Pills functionality', () => {
30+
it('should render pills when renderAsPills is true', () => {
31+
const card = new ChatItemCard({
32+
tabId: 'test-tab',
33+
chatItem: {
34+
type: ChatItemType.ANSWER,
35+
header: {
36+
icon: 'progress',
37+
body: 'Reading',
38+
fileList: {
39+
filePaths: [ 'file1.ts', 'file2.ts' ],
40+
renderAsPills: true
41+
}
42+
}
43+
}
44+
});
45+
46+
const pillElements = card.render.querySelectorAll('.mynah-chat-item-tree-file-pill');
47+
expect(pillElements.length).toBe(2);
48+
});
49+
50+
it('should include icon in customRenderer when pills are enabled', () => {
51+
const card = new ChatItemCard({
52+
tabId: 'test-tab',
53+
chatItem: {
54+
type: ChatItemType.ANSWER,
55+
header: {
56+
icon: 'eye',
57+
body: 'Files read',
58+
fileList: {
59+
filePaths: [ 'test.json' ],
60+
renderAsPills: true
61+
}
62+
}
63+
}
64+
});
65+
66+
const iconElement = card.render.querySelector('.mynah-ui-icon-eye');
67+
expect(iconElement).toBeTruthy();
68+
});
69+
70+
it('should render pills with correct file names', () => {
71+
const card = new ChatItemCard({
72+
tabId: 'test-tab',
73+
chatItem: {
74+
type: ChatItemType.ANSWER,
75+
header: {
76+
body: 'Processing',
77+
fileList: {
78+
filePaths: [ 'package.json', 'tsconfig.json' ],
79+
details: {
80+
'package.json': {
81+
visibleName: 'package'
82+
},
83+
'tsconfig.json': {
84+
visibleName: 'tsconfig'
85+
}
86+
},
87+
renderAsPills: true
88+
}
89+
}
90+
}
91+
});
92+
93+
const pillElements = card.render.querySelectorAll('.mynah-chat-item-tree-file-pill');
94+
expect(pillElements[0].textContent).toBe('package');
95+
expect(pillElements[1].textContent).toBe('tsconfig');
96+
});
97+
98+
it('should apply deleted styling to deleted files', () => {
99+
const card = new ChatItemCard({
100+
tabId: 'test-tab',
101+
chatItem: {
102+
type: ChatItemType.ANSWER,
103+
header: {
104+
body: 'Changes',
105+
fileList: {
106+
filePaths: [ 'deleted.ts', 'normal.ts' ],
107+
deletedFiles: [ 'deleted.ts' ],
108+
renderAsPills: true
109+
}
110+
}
111+
}
112+
});
113+
114+
const deletedPill = card.render.querySelector('.mynah-chat-item-tree-file-pill-deleted');
115+
expect(deletedPill).toBeTruthy();
116+
expect(deletedPill?.textContent).toBe('deleted.ts');
117+
});
118+
});
119+
120+
it('should not render pills when renderAsPills is false', () => {
121+
const card = new ChatItemCard({
122+
tabId: 'test-tab',
123+
chatItem: {
124+
type: ChatItemType.ANSWER,
125+
header: {
126+
body: 'Files',
127+
fileList: {
128+
filePaths: [ 'file1.ts' ],
129+
renderAsPills: false
130+
}
131+
}
132+
}
133+
});
134+
135+
const pillElements = card.render.querySelectorAll('.mynah-chat-item-tree-file-pill');
136+
expect(pillElements.length).toBe(0);
137+
});
138+
139+
it('should fall back to file path when visibleName is not provided', () => {
140+
const card = new ChatItemCard({
141+
tabId: 'test-tab',
142+
chatItem: {
143+
type: ChatItemType.ANSWER,
144+
header: {
145+
body: 'Files',
146+
fileList: {
147+
filePaths: [ 'src/components/test.ts' ],
148+
renderAsPills: true
149+
}
150+
}
151+
}
152+
});
153+
154+
const pillElement = card.render.querySelector('.mynah-chat-item-tree-file-pill');
155+
expect(pillElement?.textContent).toBe('src/components/test.ts');
156+
});
157+
158+
it('should handle empty filePaths array', () => {
159+
const card = new ChatItemCard({
160+
tabId: 'test-tab',
161+
chatItem: {
162+
type: ChatItemType.ANSWER,
163+
header: {
164+
body: 'No files',
165+
fileList: {
166+
filePaths: [],
167+
renderAsPills: true
168+
}
169+
}
170+
}
171+
});
172+
173+
const pillElements = card.render.querySelectorAll('.mynah-chat-item-tree-file-pill');
174+
expect(pillElements.length).toBe(0);
175+
});
28176
});

0 commit comments

Comments
 (0)