mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 19:44:30 -07:00
feat(core): improve subagent result display (#20378)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { jsonToMarkdown, safeJsonToMarkdown } from './markdownUtils.js';
|
||||
|
||||
describe('markdownUtils', () => {
|
||||
describe('jsonToMarkdown', () => {
|
||||
it('should handle primitives', () => {
|
||||
expect(jsonToMarkdown('hello')).toBe('hello');
|
||||
expect(jsonToMarkdown(123)).toBe('123');
|
||||
expect(jsonToMarkdown(true)).toBe('true');
|
||||
expect(jsonToMarkdown(null)).toBe('null');
|
||||
expect(jsonToMarkdown(undefined)).toBe('undefined');
|
||||
});
|
||||
|
||||
it('should handle simple arrays', () => {
|
||||
const data = ['a', 'b', 'c'];
|
||||
expect(jsonToMarkdown(data)).toBe('- a\n- b\n- c');
|
||||
});
|
||||
|
||||
it('should handle simple objects and convert camelCase to Space Case', () => {
|
||||
const data = { userName: 'Alice', userAge: 30 };
|
||||
expect(jsonToMarkdown(data)).toBe(
|
||||
'- **User Name**: Alice\n- **User Age**: 30',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty structures', () => {
|
||||
expect(jsonToMarkdown([])).toBe('[]');
|
||||
expect(jsonToMarkdown({})).toBe('{}');
|
||||
});
|
||||
|
||||
it('should handle nested structures with proper indentation', () => {
|
||||
const data = {
|
||||
userInfo: {
|
||||
fullName: 'Bob Smith',
|
||||
userRoles: ['admin', 'user'],
|
||||
},
|
||||
isActive: true,
|
||||
};
|
||||
const result = jsonToMarkdown(data);
|
||||
expect(result).toBe(
|
||||
'- **User Info**:\n' +
|
||||
' - **Full Name**: Bob Smith\n' +
|
||||
' - **User Roles**:\n' +
|
||||
' - admin\n' +
|
||||
' - user\n' +
|
||||
'- **Is Active**: true',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render tables for arrays of similar objects with Space Case keys', () => {
|
||||
const data = [
|
||||
{ userId: 1, userName: 'Item 1' },
|
||||
{ userId: 2, userName: 'Item 2' },
|
||||
];
|
||||
const result = jsonToMarkdown(data);
|
||||
expect(result).toBe(
|
||||
'| User Id | User Name |\n| --- | --- |\n| 1 | Item 1 |\n| 2 | Item 2 |',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle pipe characters, backslashes, and newlines in table data', () => {
|
||||
const data = [
|
||||
{ colInfo: 'val|ue', otherInfo: 'line\nbreak', pathInfo: 'C:\\test' },
|
||||
];
|
||||
const result = jsonToMarkdown(data);
|
||||
expect(result).toBe(
|
||||
'| Col Info | Other Info | Path Info |\n| --- | --- | --- |\n| val\\|ue | line break | C:\\\\test |',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to lists for arrays with mixed objects', () => {
|
||||
const data = [
|
||||
{ userId: 1, userName: 'Item 1' },
|
||||
{ userId: 2, somethingElse: 'Item 2' },
|
||||
];
|
||||
const result = jsonToMarkdown(data);
|
||||
expect(result).toContain('- **User Id**: 1');
|
||||
expect(result).toContain('- **Something Else**: Item 2');
|
||||
});
|
||||
|
||||
it('should properly indent nested tables', () => {
|
||||
const data = {
|
||||
items: [
|
||||
{ id: 1, name: 'A' },
|
||||
{ id: 2, name: 'B' },
|
||||
],
|
||||
};
|
||||
const result = jsonToMarkdown(data);
|
||||
const lines = result.split('\n');
|
||||
expect(lines[0]).toBe('- **Items**:');
|
||||
expect(lines[1]).toBe(' | Id | Name |');
|
||||
expect(lines[2]).toBe(' | --- | --- |');
|
||||
expect(lines[3]).toBe(' | 1 | A |');
|
||||
expect(lines[4]).toBe(' | 2 | B |');
|
||||
});
|
||||
|
||||
it('should indent subsequent lines of multiline strings', () => {
|
||||
const data = {
|
||||
description: 'Line 1\nLine 2\nLine 3',
|
||||
};
|
||||
const result = jsonToMarkdown(data);
|
||||
expect(result).toBe('- **Description**: Line 1\n Line 2\n Line 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeJsonToMarkdown', () => {
|
||||
it('should convert valid JSON', () => {
|
||||
const json = JSON.stringify({ keyName: 'value' });
|
||||
expect(safeJsonToMarkdown(json)).toBe('- **Key Name**: value');
|
||||
});
|
||||
|
||||
it('should return original string for invalid JSON', () => {
|
||||
const notJson = 'Not a JSON string';
|
||||
expect(safeJsonToMarkdown(notJson)).toBe(notJson);
|
||||
});
|
||||
|
||||
it('should handle plain strings that look like numbers or booleans but are valid JSON', () => {
|
||||
expect(safeJsonToMarkdown('123')).toBe('123');
|
||||
expect(safeJsonToMarkdown('true')).toBe('true');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a camelCase string to a Space Case string.
|
||||
* e.g., "camelCaseString" -> "Camel Case String"
|
||||
*/
|
||||
function camelToSpace(text: string): string {
|
||||
const result = text.replace(/([A-Z])/g, ' $1');
|
||||
return result.charAt(0).toUpperCase() + result.slice(1).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSON-compatible value into a readable Markdown representation.
|
||||
*
|
||||
* @param data The data to convert.
|
||||
* @param indent The current indentation level (for internal recursion).
|
||||
* @returns A Markdown string representing the data.
|
||||
*/
|
||||
export function jsonToMarkdown(data: unknown, indent = 0): string {
|
||||
const spacing = ' '.repeat(indent);
|
||||
|
||||
if (data === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
|
||||
if (isArrayOfSimilarObjects(data)) {
|
||||
return renderTable(data, indent);
|
||||
}
|
||||
|
||||
return data
|
||||
.map((item) => {
|
||||
if (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
Object.keys(item).length > 0
|
||||
) {
|
||||
const rendered = jsonToMarkdown(item, indent + 1);
|
||||
return `${spacing}-\n${rendered}`;
|
||||
}
|
||||
const rendered = jsonToMarkdown(item, indent + 1).trimStart();
|
||||
return `${spacing}- ${rendered}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
const entries = Object.entries(data);
|
||||
if (entries.length === 0) {
|
||||
return '{}';
|
||||
}
|
||||
|
||||
return entries
|
||||
.map(([key, value]) => {
|
||||
const displayKey = camelToSpace(key);
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
Object.keys(value).length > 0
|
||||
) {
|
||||
const renderedValue = jsonToMarkdown(value, indent + 1);
|
||||
return `${spacing}- **${displayKey}**:\n${renderedValue}`;
|
||||
}
|
||||
const renderedValue = jsonToMarkdown(value, indent + 1).trimStart();
|
||||
return `${spacing}- **${displayKey}**: ${renderedValue}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data
|
||||
.split('\n')
|
||||
.map((line, i) => (i === 0 ? line : spacing + line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return String(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely attempts to parse a string as JSON and convert it to Markdown.
|
||||
* If parsing fails, returns the original string.
|
||||
*
|
||||
* @param text The text to potentially convert.
|
||||
* @returns The Markdown representation or the original text.
|
||||
*/
|
||||
export function safeJsonToMarkdown(text: string): string {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text);
|
||||
return jsonToMarkdown(parsed);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isArrayOfSimilarObjects(
|
||||
data: unknown[],
|
||||
): data is Array<Record<string, unknown>> {
|
||||
if (data.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!data.every(isRecord)) return false;
|
||||
const firstKeys = Object.keys(data[0]).sort().join(',');
|
||||
return data.every((item) => Object.keys(item).sort().join(',') === firstKeys);
|
||||
}
|
||||
|
||||
function renderTable(data: Array<Record<string, unknown>>, indent = 0): string {
|
||||
const spacing = ' '.repeat(indent);
|
||||
const keys = Object.keys(data[0]);
|
||||
const displayKeys = keys.map(camelToSpace);
|
||||
const header = `${spacing}| ${displayKeys.join(' | ')} |`;
|
||||
const separator = `${spacing}| ${keys.map(() => '---').join(' | ')} |`;
|
||||
const rows = data.map(
|
||||
(item) =>
|
||||
`${spacing}| ${keys
|
||||
.map((key) => {
|
||||
const val = item[key];
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
return JSON.stringify(val)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\|/g, '\\|');
|
||||
}
|
||||
return String(val)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\|/g, '\\|')
|
||||
.replace(/\n/g, ' ');
|
||||
})
|
||||
.join(' | ')} |`,
|
||||
);
|
||||
return [header, separator, ...rows].join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user