- Exécution de commandes shell inline avec `!commande` ou `!`commande args``
- Support dans les prompts directs et les fichiers .claude/commands/**/*.md
- Exécution parallèle de multiples commandes
- Gestion robuste des erreurs et timeouts
- Intégration avec les variables d'environnement de settings.json
Nouveaux fichiers:
- packages/cli/src/user-shell.ts
- packages/core/src/shell.ts et shell.test.ts
- packages/ext-claudecode/src/tools/todowrite.test.ts et webfetch.test.ts
- packages/ui/src/banner.tsx, pixels.ts et pixels.test.ts
Tests: ✅ Tous les tests passent
Breaking Changes: Mineur - SlashCommand.prompt() async
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import React, { useMemo } from "react";
|
|
import { Box, Text, useStdout } from "ink";
|
|
import { rainbowColors } from "./colors";
|
|
import {
|
|
MASK_WIDTH,
|
|
buildMaskGrid,
|
|
buildTitleGrid,
|
|
canRenderTitle,
|
|
pixelsToRuns,
|
|
titleWidth,
|
|
type Run,
|
|
} from "./pixels";
|
|
|
|
const GREEN = "#7BD74F";
|
|
const PINK = "#E58AFF";
|
|
const GREEN_LINES = ["Assistant IA en ligne de commande,", "propulsé par Ink et vos modèles de langage."];
|
|
const PINK_LINES = ["Un seul agent, plusieurs modes :", "le modèle de votre choix."];
|
|
|
|
const MASK_LINES = pixelsToRuns(buildMaskGrid());
|
|
const GAP = 3;
|
|
|
|
function PixelBlock({ lines }: { lines: Run[][] }) {
|
|
return (
|
|
<Box flexDirection="column">
|
|
{lines.map((runs, y) => (
|
|
<Text key={y}>
|
|
{runs.map((r, i) => (
|
|
<Text key={i} color={r.fg} backgroundColor={r.bg}>
|
|
{r.text}
|
|
</Text>
|
|
))}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/** Repli pour un terminal étroit : cadre à double bordure, titre centré en dégradé arc-en-ciel. */
|
|
function CompactBanner({ title, columns }: { title: string; columns: number }) {
|
|
const chars = [...title];
|
|
const colors = rainbowColors(chars.length);
|
|
return (
|
|
<Box borderStyle="double" justifyContent="center" paddingX={1} width={Math.max(columns, chars.length + 6)}>
|
|
<Text bold>
|
|
{chars.map((ch, i) => (
|
|
<Text key={i} color={colors[i]}>
|
|
{ch}
|
|
</Text>
|
|
))}
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bannière d'accueil : masque pixel-art à gauche, titre pixel arc-en-ciel à droite, puis deux textes
|
|
* (vert, rose). Selon la largeur du terminal : complète, sans masque, ou cadre compact.
|
|
*/
|
|
export function Banner({ title }: { title: string }) {
|
|
const { stdout } = useStdout();
|
|
const columns = stdout?.columns ?? 80;
|
|
const titleLines = useMemo(
|
|
() => (canRenderTitle(title) ? pixelsToRuns(buildTitleGrid(title)) : null),
|
|
[title],
|
|
);
|
|
|
|
if (!titleLines || columns < titleWidth(title) + 2) {
|
|
return <CompactBanner title={title} columns={columns} />;
|
|
}
|
|
|
|
const right = (
|
|
<Box flexDirection="column">
|
|
<PixelBlock lines={titleLines} />
|
|
<Box marginTop={1} flexDirection="column">
|
|
{GREEN_LINES.map((line) => (
|
|
<Text key={line} color={GREEN} wrap="truncate">
|
|
{line}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
<Box marginTop={1} flexDirection="column">
|
|
{PINK_LINES.map((line) => (
|
|
<Text key={line} color={PINK} wrap="truncate">
|
|
{line}
|
|
</Text>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
if (columns < MASK_WIDTH + GAP + titleWidth(title)) return right;
|
|
return (
|
|
<Box>
|
|
<PixelBlock lines={MASK_LINES} />
|
|
<Box marginLeft={GAP}>{right}</Box>
|
|
</Box>
|
|
);
|
|
}
|