mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(cli): implement logical component tracking for visual journey testing
- Expose Ink rootNode in AppRig and render utilities for tree traversal - Add toVisuallyContain matcher to verify component presence in the Ink DOM - Inject component metadata as comments into generated SVGs for auditing - Add waitForComponent to AppRig for deterministic UI state synchronization - Implement visual journey test for SuggestionsDisplay
This commit is contained in:
@@ -693,6 +693,10 @@ export class AppRig {
|
|||||||
return stripAnsi(this.renderResult.stdout.lastFrame() || '');
|
return stripAnsi(this.renderResult.stdout.lastFrame() || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateSvg(): string {
|
||||||
|
return this.renderResult?.generateSvg() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
async waitForOutput(pattern: string | RegExp, timeout = 30000) {
|
async waitForOutput(pattern: string | RegExp, timeout = 30000) {
|
||||||
await this.waitUntil(
|
await this.waitUntil(
|
||||||
() => {
|
() => {
|
||||||
@@ -708,6 +712,33 @@ export class AppRig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForComponent(componentName: string, timeout = 30000) {
|
||||||
|
await this.waitUntil(
|
||||||
|
() => {
|
||||||
|
const rootNode = this.renderResult?.rootNode;
|
||||||
|
if (!rootNode) return false;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const find = (node: any): boolean => {
|
||||||
|
if (node.internal_componentName === componentName) return true;
|
||||||
|
if (node.internal_testId === componentName) return true;
|
||||||
|
if (node.childNodes) {
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.nodeName !== '#text' && find(child)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return find(rootNode);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout,
|
||||||
|
message: `Timed out waiting for component: ${componentName}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async waitForIdle(timeout = 20000) {
|
async waitForIdle(timeout = 20000) {
|
||||||
await this.waitForOutput('Type your message', timeout);
|
await this.waitForOutput('Type your message', timeout);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,81 @@ import { expect, type Assertion } from 'vitest';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
|
import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
|
||||||
|
import { type DOMElement as _DOMElement, type DOMNode } from 'ink';
|
||||||
|
|
||||||
|
export interface CustomMatchers<R = unknown> {
|
||||||
|
toMatchSvgSnapshot(options?: {
|
||||||
|
allowEmpty?: boolean;
|
||||||
|
name?: string;
|
||||||
|
}): Promise<R>;
|
||||||
|
toVisuallyContain(componentName: string): R;
|
||||||
|
toHaveOnlyValidCharacters(): R;
|
||||||
|
}
|
||||||
|
|
||||||
// RegExp to detect invalid characters: backspace, and ANSI escape codes
|
// RegExp to detect invalid characters: backspace, and ANSI escape codes
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
const invalidCharsRegex = /[\b\x1b]/;
|
const invalidCharsRegex = /[\b\x1b]/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverses the Ink tree to find a node matching a predicate.
|
||||||
|
*/
|
||||||
|
function findInTree(
|
||||||
|
node: DOMNode,
|
||||||
|
predicate: (node: DOMNode) => boolean,
|
||||||
|
): DOMNode | undefined {
|
||||||
|
if (predicate(node)) return node;
|
||||||
|
if (node.nodeName !== '#text') {
|
||||||
|
for (const child of (node as _DOMElement).childNodes) {
|
||||||
|
const found = findInTree(child, predicate);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the Ink DOM tree contains a specific component by name or testId.
|
||||||
|
*/
|
||||||
|
export function toVisuallyContain(
|
||||||
|
this: Assertion,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
received: any,
|
||||||
|
componentName: string,
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const { isNot } = this as any;
|
||||||
|
|
||||||
|
const rootNode = received.rootNode || received.renderResult?.rootNode;
|
||||||
|
const svg =
|
||||||
|
typeof received.generateSvg === 'function' ? received.generateSvg() : '';
|
||||||
|
|
||||||
|
// 1. Check logical tree presence (Automatic via Ink Root)
|
||||||
|
const isTreePresent = rootNode
|
||||||
|
? !!findInTree(rootNode, (node) => {
|
||||||
|
if (node.nodeName === '#text') return false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const el = node as any;
|
||||||
|
const match =
|
||||||
|
el.internal_componentName === componentName ||
|
||||||
|
el.internal_testId === componentName;
|
||||||
|
return match;
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// 2. Check physical presence in the SVG audit trail (Fallback)
|
||||||
|
const isPhysicallyPresent = svg.includes(
|
||||||
|
`<!-- component: ${componentName} -->`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pass = isTreePresent || isPhysicallyPresent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
`Expected component "${componentName}" ${isNot ? 'NOT ' : ''}to be present in the Ink tree or SVG metadata.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const callCountByTest = new Map<string, number>();
|
const callCountByTest = new Map<string, number>();
|
||||||
|
|
||||||
export async function toMatchSvgSnapshot(
|
export async function toMatchSvgSnapshot(
|
||||||
@@ -101,7 +171,7 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Expected buffer ${isNot ? 'not ' : ''}to have only valid characters, but found invalid characters in lines:\n${invalidLines
|
`Expected buffer ${isNot ? 'not ' : ''}to have only valid characters, but found invalid characters in lines:\n${invalidLines
|
||||||
.map((l) => ` [${l.line}]: "${l.content}"`) /* This line was changed */
|
.map((l) => ` [${l.line}]: "${l.content}"`)
|
||||||
.join('\n')}`,
|
.join('\n')}`,
|
||||||
actual: buffer.lines,
|
actual: buffer.lines,
|
||||||
expected: 'Lines with no line breaks, backspaces, or escape codes.',
|
expected: 'Lines with no line breaks, backspaces, or escape codes.',
|
||||||
@@ -112,6 +182,7 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
|
|||||||
expect.extend({
|
expect.extend({
|
||||||
toHaveOnlyValidCharacters,
|
toHaveOnlyValidCharacters,
|
||||||
toMatchSvgSnapshot,
|
toMatchSvgSnapshot,
|
||||||
|
toVisuallyContain,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
@@ -128,5 +199,6 @@ declare module 'vitest' {
|
|||||||
allowEmpty?: boolean;
|
allowEmpty?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
toVisuallyContain(componentName: string): T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
render as inkRenderDirect,
|
render as inkRenderDirect,
|
||||||
type Instance as InkInstance,
|
type Instance as InkInstance,
|
||||||
type RenderOptions,
|
type RenderOptions,
|
||||||
|
type DOMElement,
|
||||||
} from 'ink';
|
} from 'ink';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
@@ -377,6 +378,7 @@ export type RenderInstance = {
|
|||||||
waitUntilReady: () => Promise<void>;
|
waitUntilReady: () => Promise<void>;
|
||||||
capturedOverflowState: OverflowState | undefined;
|
capturedOverflowState: OverflowState | undefined;
|
||||||
capturedOverflowActions: OverflowActions | undefined;
|
capturedOverflowActions: OverflowActions | undefined;
|
||||||
|
rootNode: DOMElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const instances: InkInstance[] = [];
|
const instances: InkInstance[] = [];
|
||||||
@@ -460,8 +462,14 @@ export const render = (
|
|||||||
lastFrameRaw: stdout.lastFrameRaw,
|
lastFrameRaw: stdout.lastFrameRaw,
|
||||||
generateSvg: stdout.generateSvg,
|
generateSvg: stdout.generateSvg,
|
||||||
terminal: state.terminal,
|
terminal: state.terminal,
|
||||||
|
get rootNode() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return (instance as any).rootNode as DOMElement;
|
||||||
|
},
|
||||||
waitUntilReady: () => stdout.waitUntilReady(),
|
waitUntilReady: () => stdout.waitUntilReady(),
|
||||||
};
|
capturedOverflowState: undefined,
|
||||||
|
capturedOverflowActions: undefined,
|
||||||
|
} as RenderInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanup = () => {
|
export const cleanup = () => {
|
||||||
|
|||||||
@@ -5,9 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Terminal } from '@xterm/headless';
|
import type { Terminal } from '@xterm/headless';
|
||||||
|
import { type DOMElement } from 'ink';
|
||||||
|
|
||||||
export const generateSvgForTerminal = (terminal: Terminal): string => {
|
export const generateSvgForTerminal = (
|
||||||
|
terminal: Terminal,
|
||||||
|
options: { rootNode?: DOMElement } = {},
|
||||||
|
): string => {
|
||||||
const activeBuffer = terminal.buffer.active;
|
const activeBuffer = terminal.buffer.active;
|
||||||
|
const { rootNode } = options;
|
||||||
|
|
||||||
const getHexColor = (
|
const getHexColor = (
|
||||||
isRGB: boolean,
|
isRGB: boolean,
|
||||||
@@ -208,6 +213,24 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
|||||||
finalizeBlock(line.length);
|
finalizeBlock(line.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject unique component names from the tree as comments for auditing
|
||||||
|
if (rootNode) {
|
||||||
|
const components = new Set<string>();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const collect = (node: any) => {
|
||||||
|
if (node.internal_componentName)
|
||||||
|
components.add(node.internal_componentName);
|
||||||
|
if (node.internal_testId) components.add(node.internal_testId);
|
||||||
|
if (node.childNodes) {
|
||||||
|
for (const child of node.childNodes) collect(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
collect(rootNode);
|
||||||
|
for (const name of components) {
|
||||||
|
svg += ` <!-- component: ${name} -->\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg += ` </g>\n</svg>`;
|
svg += ` </g>\n</svg>`;
|
||||||
return svg;
|
return svg;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { AppRig } from '../../test-utils/AppRig.js';
|
||||||
|
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
describe('SuggestionsDisplay UX Journey', () => {
|
||||||
|
let rig: AppRig;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const fakeResponsesPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'test-utils',
|
||||||
|
'fixtures',
|
||||||
|
'simple.responses',
|
||||||
|
);
|
||||||
|
rig = new AppRig({ fakeResponsesPath });
|
||||||
|
await rig.initialize();
|
||||||
|
rig.render();
|
||||||
|
await rig.waitForIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rig.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should visually show the suggestions display when / is typed', async () => {
|
||||||
|
// Initially should not have suggestions
|
||||||
|
expect(rig).not.toVisuallyContain(SuggestionsDisplay.name);
|
||||||
|
|
||||||
|
// Type '/' to trigger suggestions
|
||||||
|
await rig.type('/');
|
||||||
|
|
||||||
|
// Wait for SuggestionsDisplay to appear (Automatic lookup!)
|
||||||
|
await rig.waitForComponent(SuggestionsDisplay.name);
|
||||||
|
|
||||||
|
// Assert that the component is now present in the tree
|
||||||
|
expect(rig).toVisuallyContain(SuggestionsDisplay.name);
|
||||||
|
|
||||||
|
// Also verify text for sanity
|
||||||
|
expect(rig.lastFrame).toContain('about');
|
||||||
|
|
||||||
|
// Capture the state for manual inspection if needed
|
||||||
|
await expect(rig).toMatchSvgSnapshot({ name: 'suggestions-opened' });
|
||||||
|
});
|
||||||
|
});
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="581" viewBox="0 0 1100 581">
|
||||||
|
<style>
|
||||||
|
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||||
|
</style>
|
||||||
|
<rect width="1100" height="581" fill="#000000" />
|
||||||
|
<g transform="translate(10, 10)">
|
||||||
|
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs">▝</text>
|
||||||
|
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs">▜</text>
|
||||||
|
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs">▄</text>
|
||||||
|
<text x="63" y="19" fill="#ffffff" textLength="1017" lengthAdjust="spacingAndGlyphs"> Gemini CLI vtest-version </text>
|
||||||
|
<text x="36" y="36" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs">▝</text>
|
||||||
|
<text x="45" y="36" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs">▜</text>
|
||||||
|
<text x="54" y="36" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs">▄</text>
|
||||||
|
<text x="27" y="53" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs">▗</text>
|
||||||
|
<text x="36" y="53" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs">▟</text>
|
||||||
|
<text x="45" y="53" fill="#a471a7" textLength="9" lengthAdjust="spacingAndGlyphs">▀</text>
|
||||||
|
<text x="63" y="53" fill="#ffffff" textLength="1017" lengthAdjust="spacingAndGlyphs"> Authenticated with gemini-api-key /auth </text>
|
||||||
|
<text x="18" y="70" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs">▝</text>
|
||||||
|
<text x="27" y="70" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs">▀</text>
|
||||||
|
<text x="0" y="121" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">Tips for getting started: </text>
|
||||||
|
<text x="0" y="138" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">1. Create GEMINI.md files to customize your interactions </text>
|
||||||
|
<text x="0" y="155" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">2. /help for more information </text>
|
||||||
|
<text x="0" y="172" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">3. Ask coding questions, edit code or run commands </text>
|
||||||
|
<text x="0" y="189" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">4. Be specific for the best results </text>
|
||||||
|
<text x="0" y="240" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">──────────────────────────────────────────────────────────── </text>
|
||||||
|
<text x="0" y="257" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> Shift+Tab to accept edits </text>
|
||||||
|
<text x="0" y="308" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ </text>
|
||||||
|
<text x="0" y="325" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> > /</text>
|
||||||
|
<rect x="36" y="323" width="9" height="17" fill="#ffffff" />
|
||||||
|
<text x="0" y="342" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ </text>
|
||||||
|
<text x="0" y="359" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> about Show version info </text>
|
||||||
|
<text x="0" y="376" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> agents Manage agents </text>
|
||||||
|
<text x="0" y="393" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> auth Manage authentication </text>
|
||||||
|
<text x="0" y="410" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> bug Submit a bug report </text>
|
||||||
|
<text x="0" y="427" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> chat Browse auto-saved conversations and ma… </text>
|
||||||
|
<text x="0" y="444" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> clear Clear the screen and conversation hist… </text>
|
||||||
|
<text x="0" y="461" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> commands Manage custom slash commands. Usage: /… </text>
|
||||||
|
<text x="0" y="478" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> compress Compresses the context by replacing it… </text>
|
||||||
|
<text x="0" y="495" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> ▼ </text>
|
||||||
|
<text x="0" y="512" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> (1/38) </text>
|
||||||
|
<text x="0" y="529" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> workspace (/directory) sandbox /model </text>
|
||||||
|
<text x="0" y="546" fill="#ffffff" textLength="1080" lengthAdjust="spacingAndGlyphs"> ~ no sandbox test-model </text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`SuggestionsDisplay UX Journey > should visually show the suggestions display when / is typed 1`] = `
|
||||||
|
"
|
||||||
|
▝▜▄ Gemini CLI vtest-version
|
||||||
|
▝▜▄
|
||||||
|
▗▟▀ Authenticated with gemini-api-key /auth
|
||||||
|
▝▀
|
||||||
|
|
||||||
|
|
||||||
|
Tips for getting started:
|
||||||
|
1. Create GEMINI.md files to customize your interactions
|
||||||
|
2. /help for more information
|
||||||
|
3. Ask coding questions, edit code or run commands
|
||||||
|
4. Be specific for the best results
|
||||||
|
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
Shift+Tab to accept edits
|
||||||
|
|
||||||
|
|
||||||
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
> /
|
||||||
|
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||||
|
about Show version info
|
||||||
|
agents Manage agents
|
||||||
|
auth Manage authentication
|
||||||
|
bug Submit a bug report
|
||||||
|
chat Browse auto-saved conversations and ma…
|
||||||
|
clear Clear the screen and conversation hist…
|
||||||
|
commands Manage custom slash commands. Usage: /…
|
||||||
|
compress Compresses the context by replacing it…
|
||||||
|
▼
|
||||||
|
(1/38)
|
||||||
|
workspace (/directory) sandbox /model
|
||||||
|
~ no sandbox test-model
|
||||||
|
"
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user