woopcode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +329 -0
- package/LICENSE +21 -0
- package/README.md +582 -0
- package/cli.ts +17 -0
- package/commands/agent.tsx +139 -0
- package/commands/agentController.ts +138 -0
- package/commands/models.ts +21 -0
- package/commands/providers/index.ts +12 -0
- package/commands/providers/listProviders.ts +28 -0
- package/commands/providers/login.ts +28 -0
- package/commands/providers/logout.ts +33 -0
- package/commands/providers/setProvider.ts +34 -0
- package/config/authProvider.ts +54 -0
- package/config/client.ts +163 -0
- package/config/config.ts +118 -0
- package/config/conversation.json +1 -0
- package/config/models.json +50 -0
- package/config/paths.ts +96 -0
- package/config/providers.json +21 -0
- package/config/runtime.ts +130 -0
- package/config/systemPrompt.ts +54 -0
- package/config/types.ts +88 -0
- package/onboarding/FLOW.md +291 -0
- package/onboarding/index.ts +56 -0
- package/onboarding/providers.ts +47 -0
- package/onboarding/setupWizard.tsx +193 -0
- package/onboarding/test-reset.ts +55 -0
- package/package.json +86 -0
- package/tools/createFile.ts +24 -0
- package/tools/editFile.ts +85 -0
- package/tools/findFiles.ts +94 -0
- package/tools/grep.ts +68 -0
- package/tools/index.ts +26 -0
- package/tools/listFiles.ts +49 -0
- package/tools/readFile.ts +53 -0
- package/tools/runTests.ts +29 -0
- package/tools/terminal.ts +37 -0
- package/tools/writeFile.ts +75 -0
- package/tui/package.json +0 -0
- package/tui/src/app.tsx +60 -0
- package/tui/src/components/ApprovalFooter.tsx +29 -0
- package/tui/src/components/AsciiLogo.tsx +78 -0
- package/tui/src/components/BootScreen.tsx +76 -0
- package/tui/src/components/CapabilityRow.tsx +17 -0
- package/tui/src/components/CodeBlock.tsx +70 -0
- package/tui/src/components/DiffPreview.tsx +46 -0
- package/tui/src/components/DiffViewer.tsx +36 -0
- package/tui/src/components/HomeFooter.tsx +11 -0
- package/tui/src/components/HomeScreen.tsx +71 -0
- package/tui/src/components/InlineCode.tsx +13 -0
- package/tui/src/components/LogoReveal.tsx +158 -0
- package/tui/src/components/Markdown.tsx +280 -0
- package/tui/src/components/MessageRenderer.tsx +84 -0
- package/tui/src/components/PromptCard.tsx +52 -0
- package/tui/src/components/StreamingCursor.tsx +13 -0
- package/tui/src/components/ThinkingIndicator.tsx +14 -0
- package/tui/src/components/ToolStatus.tsx +26 -0
- package/tui/src/header.tsx +25 -0
- package/tui/src/hooks/useBootAnimation.ts +67 -0
- package/tui/src/hooks/useLogoAnimation.ts +114 -0
- package/tui/src/index.ts +5 -0
- package/tui/src/prompt.tsx +66 -0
- package/tui/src/statusBar.tsx +83 -0
- package/tui/src/store/ui-store.ts +234 -0
- package/tui/src/store/useUIStore.ts +9 -0
- package/tui/src/timeline.tsx +96 -0
- package/tui/src/types.ts +40 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
|
|
3
|
+
export function ApprovalFooter() {
|
|
4
|
+
return (
|
|
5
|
+
<Box
|
|
6
|
+
borderStyle="round"
|
|
7
|
+
borderColor="yellow"
|
|
8
|
+
paddingX={1}
|
|
9
|
+
marginTop={1}
|
|
10
|
+
flexDirection="row"
|
|
11
|
+
gap={2}
|
|
12
|
+
>
|
|
13
|
+
<Text color="green" bold>
|
|
14
|
+
[A]
|
|
15
|
+
</Text>
|
|
16
|
+
<Text dimColor>Apply</Text>
|
|
17
|
+
|
|
18
|
+
<Text color="red" bold>
|
|
19
|
+
[R]
|
|
20
|
+
</Text>
|
|
21
|
+
<Text dimColor>Reject</Text>
|
|
22
|
+
|
|
23
|
+
<Text color="gray" bold>
|
|
24
|
+
[Esc]
|
|
25
|
+
</Text>
|
|
26
|
+
<Text dimColor>Cancel</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { useLogoAnimation } from "../hooks/useLogoAnimation";
|
|
3
|
+
|
|
4
|
+
const REVEAL_DURATION_MS = 500;
|
|
5
|
+
const WORD_SPACING = " ";
|
|
6
|
+
|
|
7
|
+
export interface AsciiLogoProps {
|
|
8
|
+
word: string;
|
|
9
|
+
revealDuration?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AsciiLogo({
|
|
13
|
+
word,
|
|
14
|
+
revealDuration = REVEAL_DURATION_MS,
|
|
15
|
+
}: AsciiLogoProps) {
|
|
16
|
+
const logoLines = createLogoLines(word);
|
|
17
|
+
const fullLogo = logoLines.map(({ woop, code }) => `${woop}${code}`).join("\n");
|
|
18
|
+
const { progress } = useLogoAnimation({
|
|
19
|
+
text: fullLogo,
|
|
20
|
+
duration: revealDuration,
|
|
21
|
+
mode: "typewriter",
|
|
22
|
+
});
|
|
23
|
+
let remainingCharacters = Math.floor(progress * fullLogo.length);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box flexDirection="column">
|
|
27
|
+
{logoLines.map(({ woop, code }, index) => {
|
|
28
|
+
const visibleWoop = takeVisible(woop, remainingCharacters);
|
|
29
|
+
remainingCharacters -= woop.length;
|
|
30
|
+
const visibleCode = takeVisible(code, remainingCharacters);
|
|
31
|
+
remainingCharacters -= code.length;
|
|
32
|
+
if (index < logoLines.length - 1) remainingCharacters -= 1;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Box key={index}>
|
|
36
|
+
<Text bold color="cyan">{visibleWoop}</Text>
|
|
37
|
+
<Text bold dimColor>{visibleCode}</Text>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createLogoLines(word: string): Array<{ woop: string; code: string }> {
|
|
46
|
+
if (word.toUpperCase() !== "WOOPCODE") {
|
|
47
|
+
throw new Error("AsciiLogo currently supports the WOOPCODE wordmark only.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return ANSI_SHADOW_WOOP.map((woop, index) => ({
|
|
51
|
+
woop,
|
|
52
|
+
code: `${WORD_SPACING}${ANSI_SHADOW_CODE[index] ?? ""}`,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function takeVisible(value: string, remainingCharacters: number) {
|
|
57
|
+
return value.slice(0, Math.max(0, remainingCharacters));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Generated with FIGlet's ANSI Shadow font, then kept as text so the TUI has
|
|
61
|
+
// no font runtime or package dependency.
|
|
62
|
+
const ANSI_SHADOW_WOOP = [
|
|
63
|
+
"██╗ ██╗ ██████╗ ██████╗ ██████╗ ",
|
|
64
|
+
"██║ ██║██╔═══██╗██╔═══██╗██╔══██╗",
|
|
65
|
+
"██║ █╗ ██║██║ ██║██║ ██║██████╔╝",
|
|
66
|
+
"██║███╗██║██║ ██║██║ ██║██╔═══╝ ",
|
|
67
|
+
"╚███╔███╔╝╚██████╔╝╚██████╔╝██║ ",
|
|
68
|
+
" ╚══╝╚══╝ ╚═════╝ ╚═════╝ ╚═╝ ",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
const ANSI_SHADOW_CODE = [
|
|
72
|
+
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
73
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
74
|
+
"██║ ██║ ██║██║ ██║█████╗ ",
|
|
75
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
76
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
77
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
78
|
+
] as const;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import Spinner from "ink-spinner";
|
|
3
|
+
import { useBootAnimation, STEPS } from "../hooks/useBootAnimation";
|
|
4
|
+
import { LogoReveal } from "./LogoReveal";
|
|
5
|
+
|
|
6
|
+
interface BootScreenProps {
|
|
7
|
+
onComplete: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function BootScreen({ onComplete }: BootScreenProps) {
|
|
11
|
+
const { logoText, loadingStep, doneSteps, phase } = useBootAnimation(onComplete);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
15
|
+
{/* Logo with premium animated reveal */}
|
|
16
|
+
<Box marginBottom={1}>
|
|
17
|
+
{phase === "logo" ? (
|
|
18
|
+
<LogoReveal
|
|
19
|
+
text="Woopcode"
|
|
20
|
+
duration={400}
|
|
21
|
+
mode="highlight"
|
|
22
|
+
onComplete={() => {}}
|
|
23
|
+
/>
|
|
24
|
+
) : (
|
|
25
|
+
<Text bold color="cyan">
|
|
26
|
+
Woopcode
|
|
27
|
+
</Text>
|
|
28
|
+
)}
|
|
29
|
+
</Box>
|
|
30
|
+
|
|
31
|
+
{/* Steps — only visible once logo is done */}
|
|
32
|
+
{phase !== "logo" && (
|
|
33
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
34
|
+
{STEPS.map((label, i) => (
|
|
35
|
+
<StepRow
|
|
36
|
+
key={label}
|
|
37
|
+
label={label}
|
|
38
|
+
done={doneSteps.has(i)}
|
|
39
|
+
loading={loadingStep === i}
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</Box>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{/* Launching line */}
|
|
46
|
+
{(phase === "launching" || phase === "done") && (
|
|
47
|
+
<Box marginTop={1}>
|
|
48
|
+
<Text dimColor>Launching Woopcode…</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
)}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface StepRowProps {
|
|
56
|
+
label: string;
|
|
57
|
+
done: boolean;
|
|
58
|
+
loading: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function StepRow({ label, done, loading }: StepRowProps) {
|
|
62
|
+
return (
|
|
63
|
+
<Box gap={1}>
|
|
64
|
+
<StepIcon done={done} loading={loading} />
|
|
65
|
+
<Text dimColor={!done && !loading} color={done ? "green" : loading ? "cyan" : undefined}>
|
|
66
|
+
{label}
|
|
67
|
+
</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function StepIcon({ done, loading }: { done: boolean; loading: boolean }) {
|
|
73
|
+
if (done) return <Text color="green">✓</Text>;
|
|
74
|
+
if (loading) return <Text color="cyan"><Spinner type="dots" /></Text>;
|
|
75
|
+
return <Text dimColor>○</Text>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
|
|
3
|
+
export interface CapabilityRowProps {
|
|
4
|
+
capabilities: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CapabilityRow({ capabilities }: CapabilityRowProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Box flexWrap="wrap" justifyContent="center">
|
|
10
|
+
{capabilities.map((capability, index) => (
|
|
11
|
+
<Box key={capability} marginRight={index === capabilities.length - 1 ? 0 : 2}>
|
|
12
|
+
<Text color="cyan" dimColor>#{capability}</Text>
|
|
13
|
+
</Box>
|
|
14
|
+
))}
|
|
15
|
+
</Box>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { highlight } from "cli-highlight";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
interface CodeBlockProps {
|
|
6
|
+
code: string;
|
|
7
|
+
language?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripAnsi(str: string): string {
|
|
11
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CodeBlock({ code, language }: CodeBlockProps) {
|
|
15
|
+
let highlighted: string;
|
|
16
|
+
try {
|
|
17
|
+
highlighted = highlight(code.trimEnd(), {
|
|
18
|
+
language: language || undefined,
|
|
19
|
+
ignoreIllegals: true,
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
highlighted = code.trimEnd();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lines = highlighted.split("\n");
|
|
26
|
+
const dim = chalk.dim;
|
|
27
|
+
|
|
28
|
+
const termWidth = Math.max((process.stdout.columns || 80) - 6, 30);
|
|
29
|
+
const maxContent = lines.reduce(
|
|
30
|
+
(max, l) => Math.max(max, stripAnsi(l).length),
|
|
31
|
+
0,
|
|
32
|
+
);
|
|
33
|
+
// box = │(1) + left pad(2) + content + right pad(1) + │(1) = content + 5
|
|
34
|
+
const boxWidth = Math.min(Math.max(maxContent + 6, 30), termWidth);
|
|
35
|
+
const inner = boxWidth - 5;
|
|
36
|
+
|
|
37
|
+
// ╭─ language ────────╮
|
|
38
|
+
const label = language ? `─ ${language} ` : "─";
|
|
39
|
+
const topFill = Math.max(0, boxWidth - label.length - 2);
|
|
40
|
+
const top = dim(`╭${label}${"─".repeat(topFill)}╮`);
|
|
41
|
+
|
|
42
|
+
// ╰────────────────────╯
|
|
43
|
+
const bottom = dim(`╰${"─".repeat(boxWidth - 2)}╯`);
|
|
44
|
+
|
|
45
|
+
// empty padding row
|
|
46
|
+
const empty = dim("│") + " ".repeat(boxWidth - 2) + dim("│");
|
|
47
|
+
|
|
48
|
+
// code rows — truncate long lines at inner width so the box never breaks
|
|
49
|
+
const rows = lines.map((line) => {
|
|
50
|
+
const vis = stripAnsi(line).length;
|
|
51
|
+
if (vis > inner) {
|
|
52
|
+
// Trim the raw string until its visible length fits, then add …
|
|
53
|
+
// We strip ANSI from the trimmed portion and use the highlighted version
|
|
54
|
+
// only when it fits — safest approach for ANSI-heavy strings.
|
|
55
|
+
const truncated = stripAnsi(line).slice(0, inner - 1) + "…";
|
|
56
|
+
const pad = inner - truncated.length;
|
|
57
|
+
return dim("│") + " " + truncated + " ".repeat(Math.max(0, pad)) + " " + dim("│");
|
|
58
|
+
}
|
|
59
|
+
const pad = inner - vis;
|
|
60
|
+
return dim("│") + " " + line + " ".repeat(pad) + " " + dim("│");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const output = [top, empty, ...rows, empty, bottom].join("\n");
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box marginY={1}>
|
|
67
|
+
<Text>{output}</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import type { PendingEdit } from "../types";
|
|
3
|
+
import { DiffViewer } from "./DiffViewer";
|
|
4
|
+
import { ApprovalFooter } from "./ApprovalFooter";
|
|
5
|
+
import { store } from "../store/ui-store";
|
|
6
|
+
|
|
7
|
+
interface DiffPreviewProps {
|
|
8
|
+
pendingEdit: PendingEdit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DiffPreview({ pendingEdit }: DiffPreviewProps) {
|
|
12
|
+
useInput((input, key) => {
|
|
13
|
+
if (key.escape) {
|
|
14
|
+
store.clearPendingEdit();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const lowerInput = input.toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (lowerInput === "a") {
|
|
21
|
+
store.approvePendingEdit();
|
|
22
|
+
} else if (lowerInput === "r") {
|
|
23
|
+
store.rejectPendingEdit();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Box flexDirection="column" paddingX={1}>
|
|
29
|
+
<Box
|
|
30
|
+
borderStyle="round"
|
|
31
|
+
borderColor="blue"
|
|
32
|
+
flexDirection="column"
|
|
33
|
+
paddingX={1}
|
|
34
|
+
>
|
|
35
|
+
<Text bold color="blue">
|
|
36
|
+
File Change Preview
|
|
37
|
+
</Text>
|
|
38
|
+
<Text dimColor>{pendingEdit.filePath}</Text>
|
|
39
|
+
|
|
40
|
+
<DiffViewer diff={pendingEdit.diff} />
|
|
41
|
+
</Box>
|
|
42
|
+
|
|
43
|
+
<ApprovalFooter />
|
|
44
|
+
</Box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
|
|
3
|
+
interface DiffViewerProps {
|
|
4
|
+
diff: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function DiffViewer({ diff }: DiffViewerProps) {
|
|
8
|
+
// Parse unified diff and render with colors
|
|
9
|
+
const lines = diff.split("\n");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
|
13
|
+
{lines.map((line, idx) => {
|
|
14
|
+
// Skip the first two lines (file headers)
|
|
15
|
+
if (idx < 2) return null;
|
|
16
|
+
|
|
17
|
+
let color: "green" | "red" | "cyan" | "dim" = "dim";
|
|
18
|
+
let content = line;
|
|
19
|
+
|
|
20
|
+
if (line.startsWith("@@")) {
|
|
21
|
+
color = "cyan";
|
|
22
|
+
} else if (line.startsWith("+")) {
|
|
23
|
+
color = "green";
|
|
24
|
+
} else if (line.startsWith("-")) {
|
|
25
|
+
color = "red";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Text key={idx} color={color}>
|
|
30
|
+
{content}
|
|
31
|
+
</Text>
|
|
32
|
+
);
|
|
33
|
+
})}
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
|
|
3
|
+
export interface HomeFooterProps {
|
|
4
|
+
repository: string;
|
|
5
|
+
branch: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function HomeFooter({ repository, branch, provider }: HomeFooterProps) {
|
|
10
|
+
return <Text dimColor>{repository} · {branch} · {provider}</Text>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Box, Text, useStdout } from "ink";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { AsciiLogo } from "./AsciiLogo";
|
|
4
|
+
import { CapabilityRow } from "./CapabilityRow";
|
|
5
|
+
import { HomeFooter } from "./HomeFooter";
|
|
6
|
+
import { PromptCard } from "./PromptCard";
|
|
7
|
+
|
|
8
|
+
export interface HomeScreenData {
|
|
9
|
+
logoWord: string;
|
|
10
|
+
subtitle: string;
|
|
11
|
+
promptExamples: readonly string[];
|
|
12
|
+
capabilities: readonly string[];
|
|
13
|
+
repository: string;
|
|
14
|
+
branch: string;
|
|
15
|
+
providerName: string;
|
|
16
|
+
provider: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HomeScreenProps extends HomeScreenData {
|
|
20
|
+
renderPrompt: (placeholder: string) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LAYOUT = {
|
|
24
|
+
contentSidePadding: 4,
|
|
25
|
+
preferredPromptWidth: 68,
|
|
26
|
+
minimumPromptWidth: 28,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/** The focused, pre-conversation experience for an agent workspace. */
|
|
30
|
+
export function HomeScreen({
|
|
31
|
+
logoWord,
|
|
32
|
+
subtitle,
|
|
33
|
+
promptExamples,
|
|
34
|
+
capabilities,
|
|
35
|
+
repository,
|
|
36
|
+
branch,
|
|
37
|
+
provider,
|
|
38
|
+
renderPrompt,
|
|
39
|
+
}: HomeScreenProps) {
|
|
40
|
+
const { stdout } = useStdout();
|
|
41
|
+
const terminalWidth = stdout.columns ?? LAYOUT.preferredPromptWidth;
|
|
42
|
+
const promptWidth = Math.min(
|
|
43
|
+
LAYOUT.preferredPromptWidth,
|
|
44
|
+
Math.max(LAYOUT.minimumPromptWidth, terminalWidth - LAYOUT.contentSidePadding),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Box flexDirection="column" flexGrow={1} justifyContent="center">
|
|
49
|
+
<Box flexDirection="column" alignItems="center">
|
|
50
|
+
<AsciiLogo word={logoWord} />
|
|
51
|
+
<Box marginTop={1} marginBottom={3}>
|
|
52
|
+
<Text dimColor>{subtitle}</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
</Box>
|
|
55
|
+
|
|
56
|
+
<Box justifyContent="center" width="100%" marginTop={3}>
|
|
57
|
+
<PromptCard width={promptWidth} examples={promptExamples}>
|
|
58
|
+
{renderPrompt}
|
|
59
|
+
</PromptCard>
|
|
60
|
+
</Box>
|
|
61
|
+
|
|
62
|
+
<Box justifyContent="center" width="100%" marginTop={1}>
|
|
63
|
+
<CapabilityRow capabilities={capabilities} />
|
|
64
|
+
</Box>
|
|
65
|
+
|
|
66
|
+
<Box justifyContent="center" width="100%" marginTop={3}>
|
|
67
|
+
<HomeFooter repository={repository} branch={branch} provider={provider} />
|
|
68
|
+
</Box>
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useLogoAnimation } from "../hooks/useLogoAnimation";
|
|
4
|
+
import type { AnimationMode } from "../hooks/useLogoAnimation";
|
|
5
|
+
|
|
6
|
+
export interface LogoRevealProps {
|
|
7
|
+
/** The text to animate */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Animation duration in milliseconds (default: 600) */
|
|
10
|
+
duration?: number;
|
|
11
|
+
/** Animation style (default: "highlight") */
|
|
12
|
+
mode?: AnimationMode;
|
|
13
|
+
/** Color for highlighted characters (default: "cyan") */
|
|
14
|
+
highlightColor?: string;
|
|
15
|
+
/** Whether to show bold text (default: true) */
|
|
16
|
+
bold?: boolean;
|
|
17
|
+
/** Callback when animation completes */
|
|
18
|
+
onComplete?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Premium animated logo reveal component for terminal UIs.
|
|
23
|
+
*
|
|
24
|
+
* Supports three animation modes:
|
|
25
|
+
* - "highlight" (default): Smooth left-to-right highlight sweep
|
|
26
|
+
* - "typewriter": Classic character-by-character reveal
|
|
27
|
+
* - "scan": Terminal boot-style scan line reveal
|
|
28
|
+
*/
|
|
29
|
+
export function LogoReveal({
|
|
30
|
+
text,
|
|
31
|
+
duration = 600,
|
|
32
|
+
mode = "highlight",
|
|
33
|
+
highlightColor = "cyan",
|
|
34
|
+
bold = true,
|
|
35
|
+
onComplete,
|
|
36
|
+
}: LogoRevealProps) {
|
|
37
|
+
const { visibleText, highlightPosition, isComplete } = useLogoAnimation({
|
|
38
|
+
text,
|
|
39
|
+
duration,
|
|
40
|
+
mode,
|
|
41
|
+
onComplete,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Render based on animation mode
|
|
45
|
+
switch (mode) {
|
|
46
|
+
case "typewriter":
|
|
47
|
+
return (
|
|
48
|
+
<Box>
|
|
49
|
+
<Text bold={bold} color={highlightColor}>
|
|
50
|
+
{visibleText}
|
|
51
|
+
</Text>
|
|
52
|
+
{!isComplete && <Text dimColor>_</Text>}
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
case "highlight":
|
|
57
|
+
return (
|
|
58
|
+
<Box>
|
|
59
|
+
{renderHighlightSweep(text, highlightPosition, highlightColor, bold, isComplete)}
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
case "scan":
|
|
64
|
+
return (
|
|
65
|
+
<Box>
|
|
66
|
+
{renderScanReveal(text, highlightPosition, highlightColor, bold, isComplete)}
|
|
67
|
+
</Box>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
return (
|
|
72
|
+
<Box>
|
|
73
|
+
<Text bold={bold} color={highlightColor}>
|
|
74
|
+
{text}
|
|
75
|
+
</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Render highlight sweep animation
|
|
83
|
+
* Full text visible, highlight sweeps left-to-right
|
|
84
|
+
*/
|
|
85
|
+
function renderHighlightSweep(
|
|
86
|
+
text: string,
|
|
87
|
+
position: number,
|
|
88
|
+
color: string,
|
|
89
|
+
bold: boolean,
|
|
90
|
+
isComplete: boolean
|
|
91
|
+
): React.ReactNode {
|
|
92
|
+
if (isComplete) {
|
|
93
|
+
// Final state: fully highlighted
|
|
94
|
+
return (
|
|
95
|
+
<Text bold={bold} color={color}>
|
|
96
|
+
{text}
|
|
97
|
+
</Text>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate which characters are behind the sweep
|
|
102
|
+
const sweepIndex = Math.floor(position * text.length);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
{/* Highlighted portion (characters behind sweep) */}
|
|
107
|
+
<Text bold={bold} color={color}>
|
|
108
|
+
{text.slice(0, sweepIndex)}
|
|
109
|
+
</Text>
|
|
110
|
+
{/* Dim portion (characters ahead of sweep) */}
|
|
111
|
+
<Text bold={bold} dimColor>
|
|
112
|
+
{text.slice(sweepIndex)}
|
|
113
|
+
</Text>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render scan reveal animation
|
|
120
|
+
* Characters begin dim, scan line reveals them
|
|
121
|
+
*/
|
|
122
|
+
function renderScanReveal(
|
|
123
|
+
text: string,
|
|
124
|
+
position: number,
|
|
125
|
+
color: string,
|
|
126
|
+
bold: boolean,
|
|
127
|
+
isComplete: boolean
|
|
128
|
+
): React.ReactNode {
|
|
129
|
+
if (isComplete) {
|
|
130
|
+
// Final state: fully revealed
|
|
131
|
+
return (
|
|
132
|
+
<Text bold={bold} color={color}>
|
|
133
|
+
{text}
|
|
134
|
+
</Text>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scanIndex = Math.floor(position * text.length);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<>
|
|
142
|
+
{/* Revealed portion */}
|
|
143
|
+
<Text bold={bold} color={color}>
|
|
144
|
+
{text.slice(0, scanIndex)}
|
|
145
|
+
</Text>
|
|
146
|
+
{/* Scan line character (if not at end) */}
|
|
147
|
+
{scanIndex < text.length && (
|
|
148
|
+
<Text bold={bold} color={color} backgroundColor="gray">
|
|
149
|
+
{text[scanIndex]}
|
|
150
|
+
</Text>
|
|
151
|
+
)}
|
|
152
|
+
{/* Unrevealed portion */}
|
|
153
|
+
<Text bold={bold} dimColor>
|
|
154
|
+
{text.slice(scanIndex + 1)}
|
|
155
|
+
</Text>
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|