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.
Files changed (67) hide show
  1. package/CONTRIBUTING.md +329 -0
  2. package/LICENSE +21 -0
  3. package/README.md +582 -0
  4. package/cli.ts +17 -0
  5. package/commands/agent.tsx +139 -0
  6. package/commands/agentController.ts +138 -0
  7. package/commands/models.ts +21 -0
  8. package/commands/providers/index.ts +12 -0
  9. package/commands/providers/listProviders.ts +28 -0
  10. package/commands/providers/login.ts +28 -0
  11. package/commands/providers/logout.ts +33 -0
  12. package/commands/providers/setProvider.ts +34 -0
  13. package/config/authProvider.ts +54 -0
  14. package/config/client.ts +163 -0
  15. package/config/config.ts +118 -0
  16. package/config/conversation.json +1 -0
  17. package/config/models.json +50 -0
  18. package/config/paths.ts +96 -0
  19. package/config/providers.json +21 -0
  20. package/config/runtime.ts +130 -0
  21. package/config/systemPrompt.ts +54 -0
  22. package/config/types.ts +88 -0
  23. package/onboarding/FLOW.md +291 -0
  24. package/onboarding/index.ts +56 -0
  25. package/onboarding/providers.ts +47 -0
  26. package/onboarding/setupWizard.tsx +193 -0
  27. package/onboarding/test-reset.ts +55 -0
  28. package/package.json +86 -0
  29. package/tools/createFile.ts +24 -0
  30. package/tools/editFile.ts +85 -0
  31. package/tools/findFiles.ts +94 -0
  32. package/tools/grep.ts +68 -0
  33. package/tools/index.ts +26 -0
  34. package/tools/listFiles.ts +49 -0
  35. package/tools/readFile.ts +53 -0
  36. package/tools/runTests.ts +29 -0
  37. package/tools/terminal.ts +37 -0
  38. package/tools/writeFile.ts +75 -0
  39. package/tui/package.json +0 -0
  40. package/tui/src/app.tsx +60 -0
  41. package/tui/src/components/ApprovalFooter.tsx +29 -0
  42. package/tui/src/components/AsciiLogo.tsx +78 -0
  43. package/tui/src/components/BootScreen.tsx +76 -0
  44. package/tui/src/components/CapabilityRow.tsx +17 -0
  45. package/tui/src/components/CodeBlock.tsx +70 -0
  46. package/tui/src/components/DiffPreview.tsx +46 -0
  47. package/tui/src/components/DiffViewer.tsx +36 -0
  48. package/tui/src/components/HomeFooter.tsx +11 -0
  49. package/tui/src/components/HomeScreen.tsx +71 -0
  50. package/tui/src/components/InlineCode.tsx +13 -0
  51. package/tui/src/components/LogoReveal.tsx +158 -0
  52. package/tui/src/components/Markdown.tsx +280 -0
  53. package/tui/src/components/MessageRenderer.tsx +84 -0
  54. package/tui/src/components/PromptCard.tsx +52 -0
  55. package/tui/src/components/StreamingCursor.tsx +13 -0
  56. package/tui/src/components/ThinkingIndicator.tsx +14 -0
  57. package/tui/src/components/ToolStatus.tsx +26 -0
  58. package/tui/src/header.tsx +25 -0
  59. package/tui/src/hooks/useBootAnimation.ts +67 -0
  60. package/tui/src/hooks/useLogoAnimation.ts +114 -0
  61. package/tui/src/index.ts +5 -0
  62. package/tui/src/prompt.tsx +66 -0
  63. package/tui/src/statusBar.tsx +83 -0
  64. package/tui/src/store/ui-store.ts +234 -0
  65. package/tui/src/store/useUIStore.ts +9 -0
  66. package/tui/src/timeline.tsx +96 -0
  67. package/tui/src/types.ts +40 -0
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "woopcode",
3
+ "version": "0.1.0",
4
+ "description": "An autonomous AI coding assistant built for the terminal",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Manas Raghuwanshi",
8
+ "url": "https://github.com/mangit955"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mangit955/woop-code.git"
13
+ },
14
+ "homepage": "https://github.com/mangit955/woop-code#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/mangit955/woop-code/issues"
17
+ },
18
+ "keywords": [
19
+ "ai",
20
+ "coding-assistant",
21
+ "cli",
22
+ "terminal",
23
+ "autonomous-agent",
24
+ "llm",
25
+ "gemini",
26
+ "bun",
27
+ "typescript",
28
+ "developer-tools",
29
+ "code-generation",
30
+ "ai-agent"
31
+ ],
32
+ "bin": {
33
+ "woopcode": "./cli.ts"
34
+ },
35
+ "main": "./cli.ts",
36
+ "type": "module",
37
+ "files": [
38
+ "cli.ts",
39
+ "commands/",
40
+ "config/",
41
+ "tools/",
42
+ "tui/",
43
+ "onboarding/",
44
+ "README.md",
45
+ "LICENSE",
46
+ "CONTRIBUTING.md"
47
+ ],
48
+ "scripts": {
49
+ "start": "bun ./cli.ts",
50
+ "test": "bun test",
51
+ "test:watch": "bun test --watch",
52
+ "prepublishOnly": "bun test"
53
+ },
54
+ "dependencies": {
55
+ "@google/genai": "^2.12.0",
56
+ "@inkjs/ui": "^2.0.0",
57
+ "chalk": "^5.6.2",
58
+ "cli-highlight": "^2.1.11",
59
+ "commander": "^12.0.0",
60
+ "diff": "^9.0.0",
61
+ "ink": "^7.1.1",
62
+ "ink-spinner": "^5.0.0",
63
+ "ink-text-input": "^6.0.0",
64
+ "marked": "^18.0.7",
65
+ "react": "^19.2.7",
66
+ "woopcode": "file:woopcode-0.1.0.tgz"
67
+ },
68
+ "devDependencies": {
69
+ "@stryker-mutator/core": "^9.6.1",
70
+ "@stryker-mutator/typescript-checker": "^9.6.1",
71
+ "@types/bun": "latest",
72
+ "@types/commander": "^2.12.5",
73
+ "@types/react": "^19.2.17",
74
+ "fast-check": "^4.9.0"
75
+ },
76
+ "peerDependencies": {
77
+ "typescript": "^5.0.0"
78
+ },
79
+ "engines": {
80
+ "bun": ">=1.0.0"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public",
84
+ "registry": "https://registry.npmjs.org/"
85
+ }
86
+ }
@@ -0,0 +1,24 @@
1
+ import type { Tool } from "../config/types";
2
+ import { existsSync } from "fs";
3
+
4
+ export const createFileTool: Tool = {
5
+ name: "create_file",
6
+ description: "Creates a new file with the provided content.",
7
+ parameters: [
8
+ { name: "path", description: "File path", required: true },
9
+ { name: "content", description: "File content", required: true },
10
+ ],
11
+ async execute(args) {
12
+ const path = args.path as string;
13
+ const content = args.content as string;
14
+
15
+ if (!path) throw new Error("Missing required argument: path");
16
+ if (!content) throw new Error("Missing required argument: content");
17
+
18
+ if (existsSync(path)) {
19
+ throw new Error(`File already exists: ${path}`);
20
+ }
21
+ await Bun.write(path, content);
22
+ return `Created file: ${path}`;
23
+ },
24
+ };
@@ -0,0 +1,85 @@
1
+ import type { Tool } from "../config/types";
2
+ import { createTwoFilesPatch } from "diff";
3
+ import { store } from "../tui/src/store/ui-store";
4
+ import type { PendingEdit } from "../tui/src/types";
5
+
6
+ export const editFileTool: Tool = {
7
+ name: "edit_file",
8
+ description: "Replace text inside an existing file.",
9
+
10
+ parameters: [
11
+ {
12
+ name: "path",
13
+ description: "File path",
14
+ required: true,
15
+ },
16
+ {
17
+ name: "oldText",
18
+ description: "Text to replace",
19
+ required: true,
20
+ },
21
+ {
22
+ name: "newText",
23
+ description: "Replacement text",
24
+ required: true,
25
+ },
26
+ ],
27
+
28
+ async execute(args) {
29
+ const path = args.path as string;
30
+ const oldText = args.oldText as string;
31
+ const newText = args.newText as string;
32
+
33
+ const file = Bun.file(path);
34
+
35
+ if (!(await file.exists())) {
36
+ throw new Error(`File not found: ${path}`);
37
+ }
38
+
39
+ const content = await file.text();
40
+
41
+ if (!content.includes(oldText)) {
42
+ throw new Error("Text to replace not found.");
43
+ }
44
+
45
+ const updated = content.replace(oldText, newText);
46
+
47
+ // If content is identical, skip diff preview
48
+ if (content === updated) {
49
+ return `No changes needed for ${path}`;
50
+ }
51
+
52
+ // Generate unified diff
53
+ const diff = createTwoFilesPatch(path, path, content, updated, "", "", {
54
+ context: 3,
55
+ });
56
+
57
+ // Create pending edit
58
+ const pendingEdit: PendingEdit = {
59
+ id: crypto.randomUUID(),
60
+ filePath: path,
61
+ oldContent: content,
62
+ newContent: updated,
63
+ diff,
64
+ toolCallId: crypto.randomUUID(),
65
+ };
66
+
67
+ // Request approval from UI
68
+ let approved: boolean;
69
+ try {
70
+ approved = await store.setPendingEdit(pendingEdit);
71
+ } catch (error) {
72
+ // User cancelled with Esc
73
+ return `Edit cancelled for ${path}`;
74
+ }
75
+
76
+ if (!approved) {
77
+ return `Edit rejected for ${path}`;
78
+ }
79
+
80
+ // Write file after approval
81
+ await Bun.write(path, updated);
82
+
83
+ return `Edited ${path}`;
84
+ },
85
+ };
@@ -0,0 +1,94 @@
1
+ import type { Tool } from "../config/types";
2
+
3
+ export const findFilesTool: Tool = {
4
+ name: "find_files",
5
+ description: "Finds files by name or partial filename.",
6
+ parameters: [
7
+ {
8
+ name: "query",
9
+ required: true,
10
+ description: "filename or partial filename",
11
+ },
12
+ {
13
+ name: "path",
14
+ required: false,
15
+ description: "root directory, default process.cwd()",
16
+ },
17
+ ],
18
+ async execute(args) {
19
+ const query = args.query;
20
+ if (!query || typeof query !== "string") {
21
+ throw new Error("Parameter 'query' is required and must be a string.");
22
+ }
23
+ const rootPath =
24
+ args.path && typeof args.path === "string" ? args.path : process.cwd();
25
+
26
+ const matches: string[] = [];
27
+ const lowerQuery = query.toLowerCase();
28
+
29
+ const glob = new Bun.Glob("**/*");
30
+ for await (const entry of glob.scan(rootPath)) {
31
+ const parts = entry.split("/");
32
+ if (
33
+ parts.some((part) =>
34
+ [
35
+ ".git",
36
+ "node_modules",
37
+ "dist",
38
+ "build",
39
+ ".next",
40
+ "coverage",
41
+ ].includes(part),
42
+ )
43
+ ) {
44
+ continue;
45
+ }
46
+ const basename = parts.at(-1);
47
+ if (!basename) {
48
+ continue;
49
+ }
50
+ if (
51
+ [
52
+ "bun.lock",
53
+ "package-lock.json",
54
+ "pnpm-lock.yaml",
55
+ "yarn.lock",
56
+ ].includes(basename)
57
+ ) {
58
+ continue;
59
+ }
60
+ if (
61
+ basename.toLowerCase().includes(lowerQuery) ||
62
+ entry.toLowerCase().includes(lowerQuery)
63
+ ) {
64
+ // Only include files, not directories
65
+ const fullPath = `${rootPath}/${entry}`;
66
+ try {
67
+ const stat = await Bun.file(fullPath).stat();
68
+ if (!stat.isDirectory) {
69
+ matches.push(entry);
70
+ }
71
+ } catch {
72
+ // If we can't stat it, skip it
73
+ continue;
74
+ }
75
+ }
76
+ }
77
+
78
+ if (matches.length === 0) {
79
+ return "No matching files found.";
80
+ }
81
+
82
+ const output = matches.join("\n");
83
+ const MAX_OUTPUT_SIZE = 8 * 1024; // 8 KB
84
+
85
+ if (output.length <= MAX_OUTPUT_SIZE) {
86
+ return output;
87
+ }
88
+
89
+ return (
90
+ output.slice(0, MAX_OUTPUT_SIZE) +
91
+ `\n\n... output truncated (${matches.length} matches found)`
92
+ );
93
+ },
94
+ };
package/tools/grep.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { Tool } from "../config/types";
2
+
3
+ export const grepTool: Tool = {
4
+ name: "grep",
5
+ description: "Searches recursively for text in project files.",
6
+ parameters: [
7
+ { name: "pattern", required: true, description: "text to search for" },
8
+ {
9
+ name: "path",
10
+ required: false,
11
+ description: "directory to search, default process.cwd()",
12
+ },
13
+ ],
14
+ async execute(args) {
15
+ const pattern = args.pattern;
16
+ if (!pattern || typeof pattern !== "string") {
17
+ throw new Error("Parameter 'pattern' is required and must be a string.");
18
+ }
19
+
20
+ const path =
21
+ args.path && typeof args.path === "string" ? args.path : process.cwd();
22
+
23
+ const proc = Bun.spawn({
24
+ cmd: [
25
+ "grep",
26
+ "-RIn",
27
+ "--exclude-dir=node_modules",
28
+ "--exclude-dir=.git",
29
+ "--exclude-dir=dist",
30
+ "--exclude-dir=build",
31
+ "--exclude-dir=.next",
32
+ "--exclude-dir=coverage",
33
+ "--exclude=bun.lock",
34
+ "--exclude=package-lock.json",
35
+ "--exclude=pnpm-lock.yaml",
36
+ "--exclude=yarn.lock",
37
+ pattern,
38
+ path,
39
+ ],
40
+ stdout: "pipe",
41
+ stderr: "pipe",
42
+ });
43
+
44
+ const [stdout, stderr, exitCode] = await Promise.all([
45
+ proc.stdout.text(),
46
+ proc.stderr.text(),
47
+ proc.exited,
48
+ ]);
49
+
50
+ if (exitCode === 1) {
51
+ return "No matches found.";
52
+ }
53
+ if (exitCode > 1) {
54
+ throw new Error(stderr);
55
+ }
56
+ const MAX_OUTPUT_SIZE = 8 * 1024;
57
+
58
+ if (stdout.length <= MAX_OUTPUT_SIZE) {
59
+ return stdout;
60
+ }
61
+
62
+ return (
63
+ stdout.slice(0, MAX_OUTPUT_SIZE) +
64
+ `\n\n... Output truncated (${stdout.length - MAX_OUTPUT_SIZE} more bytes). ` +
65
+ `Refine your search or use read_file on a specific file.`
66
+ );
67
+ },
68
+ };
package/tools/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { terminalTool } from "./terminal";
2
+ import { readFileTool } from "./readFile";
3
+ import { listFilesTool } from "./listFiles";
4
+ import type { Tool } from "../config/types";
5
+ import { createFileTool } from "./createFile";
6
+ import { writeFileTool } from "./writeFile";
7
+ import { editFileTool } from "./editFile";
8
+ import { grepTool } from "./grep";
9
+ import { runTestsTool } from "./runTests";
10
+ import { findFilesTool } from "./findFiles";
11
+
12
+ export const toolRegistery: Tool[] = [
13
+ listFilesTool,
14
+ readFileTool,
15
+ terminalTool,
16
+ createFileTool,
17
+ writeFileTool,
18
+ editFileTool,
19
+ grepTool,
20
+ runTestsTool,
21
+ findFilesTool,
22
+ ];
23
+
24
+ export function getTool(name: string): Tool | undefined {
25
+ return toolRegistery.find((tool) => tool.name === name);
26
+ }
@@ -0,0 +1,49 @@
1
+ import type { Tool } from "../config/types";
2
+
3
+ export const listFilesTool: Tool = {
4
+ name: "list_Files",
5
+ description: "List all the files in the directory",
6
+ parameters: [
7
+ {
8
+ name: "path",
9
+ description: "Directory path",
10
+ required: false,
11
+ },
12
+ ],
13
+
14
+ async execute(args) {
15
+ const path = (args.path as string) || process.cwd();
16
+
17
+ const files: string[] = [];
18
+
19
+ for await (const file of new Bun.Glob("**/*").scan(path)) {
20
+ const ignoredDirs = [
21
+ "node_modules",
22
+ ".git",
23
+ "dist",
24
+ "build",
25
+ ".next",
26
+ "coverage",
27
+ ];
28
+
29
+ if (
30
+ ignoredDirs.some((dir) => file === dir || file.startsWith(`${dir}/`))
31
+ ) {
32
+ continue;
33
+ }
34
+
35
+ files.push(file);
36
+ }
37
+ const MAX_TOOL_OUTPUT = 8 * 1024; // 8 KB
38
+ const output = files.join("\n");
39
+
40
+ if (output.length <= MAX_TOOL_OUTPUT) {
41
+ return output;
42
+ }
43
+
44
+ return (
45
+ output.slice(0, MAX_TOOL_OUTPUT) +
46
+ `\n\n... Output truncated (${output.length - MAX_TOOL_OUTPUT} more characters)`
47
+ );
48
+ },
49
+ };
@@ -0,0 +1,53 @@
1
+ import type { Tool } from "../config/types";
2
+ import { statSync } from "fs";
3
+
4
+ export const readFileTool: Tool = {
5
+ name: "read_file",
6
+ description: "Reads the contents of a file.",
7
+ parameters: [
8
+ {
9
+ name: "path",
10
+ description: "Path to the file",
11
+ required: true,
12
+ },
13
+ ],
14
+
15
+ async execute(args) {
16
+ const path = args.path as string;
17
+
18
+ if (!path) {
19
+ throw Error("File path is required");
20
+ }
21
+
22
+ const file = Bun.file(path);
23
+
24
+ if (!(await file.exists())) {
25
+ throw Error(`File ${path} does not exist`);
26
+ }
27
+
28
+ // Check if path is a directory
29
+ try {
30
+ const stats = statSync(path);
31
+ if (stats.isDirectory()) {
32
+ throw Error(`Cannot read ${path}: it is a directory. Use list_files to see directory contents.`);
33
+ }
34
+ } catch (err) {
35
+ if (err instanceof Error && err.message.includes('is a directory')) {
36
+ throw err;
37
+ }
38
+ // If stat fails for other reasons, continue trying to read
39
+ }
40
+
41
+ const MAX_OUTPUT = 16 * 1024; // 16 KB
42
+ const content = await file.text();
43
+
44
+ if (content.length > MAX_OUTPUT) {
45
+ return (
46
+ content.slice(0, MAX_OUTPUT) +
47
+ `\n\n... File truncated. Showing first ${MAX_OUTPUT} characters of ${content.length}.`
48
+ );
49
+ }
50
+
51
+ return content;
52
+ },
53
+ };
@@ -0,0 +1,29 @@
1
+ import type { Tool } from "../config/types";
2
+
3
+ export const runTestsTool: Tool = {
4
+ name: "run_tests",
5
+ description: "Runs the project's test command.",
6
+ parameters: [
7
+ { name: "command", required: false, description: "defaults to bun test" },
8
+ ],
9
+ async execute(args) {
10
+ const command =
11
+ args.command && typeof args.command === "string"
12
+ ? args.command
13
+ : "bun test";
14
+
15
+ const proc = Bun.spawn({
16
+ cmd: ["sh", "-c", command],
17
+ stdout: "pipe",
18
+ stderr: "pipe",
19
+ });
20
+
21
+ const [stdout, stderr, exitCode] = await Promise.all([
22
+ proc.stdout.text(),
23
+ proc.stderr.text(),
24
+ proc.exited,
25
+ ]);
26
+
27
+ return `Exit code: ${exitCode}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`;
28
+ },
29
+ };
@@ -0,0 +1,37 @@
1
+ import type { Tool } from "../config/types";
2
+
3
+ export const terminalTool: Tool = {
4
+ name: "run_terminal",
5
+ description: "Runs a terminal command in the current project.",
6
+ parameters: [
7
+ {
8
+ name: "command",
9
+ description: "Command to execute",
10
+ required: true,
11
+ },
12
+ ],
13
+
14
+ async execute(args) {
15
+ const command = args.command as string;
16
+
17
+ if (!command) {
18
+ throw Error("command is required");
19
+ }
20
+
21
+ const proc = Bun.spawn({
22
+ cmd: ["sh", "-c", command],
23
+ stdout: "pipe",
24
+ stderr: "pipe",
25
+ });
26
+
27
+ const stdout = await new Response(proc.stdout).text();
28
+ const stderr = await new Response(proc.stderr).text();
29
+
30
+ await proc.exited;
31
+
32
+ if (stderr) {
33
+ return stderr;
34
+ }
35
+ return stdout;
36
+ },
37
+ };
@@ -0,0 +1,75 @@
1
+ import type { Tool } from "../config/types";
2
+ import { createTwoFilesPatch } from "diff";
3
+ import { store } from "../tui/src/store/ui-store";
4
+ import type { PendingEdit } from "../tui/src/types";
5
+
6
+ export const writeFileTool: Tool = {
7
+ name: "write_file",
8
+
9
+ description: "Overwrite an existing file.",
10
+
11
+ parameters: [
12
+ {
13
+ name: "path",
14
+ description: "File path",
15
+ required: true,
16
+ },
17
+ {
18
+ name: "content",
19
+ description: "New file contents",
20
+ required: true,
21
+ },
22
+ ],
23
+
24
+ async execute(args) {
25
+ const path = args.path as string;
26
+ const content = args.content as string;
27
+
28
+ const file = Bun.file(path);
29
+
30
+ if (!(await file.exists())) {
31
+ throw new Error(`File not found: ${path}`);
32
+ }
33
+
34
+ // Read current content
35
+ const oldContent = await file.text();
36
+
37
+ // If content is identical, skip diff preview
38
+ if (oldContent === content) {
39
+ return `No changes needed for ${path}`;
40
+ }
41
+
42
+ // Generate unified diff
43
+ const diff = createTwoFilesPatch(path, path, oldContent, content, "", "", {
44
+ context: 3,
45
+ });
46
+
47
+ // Create pending edit
48
+ const pendingEdit: PendingEdit = {
49
+ id: crypto.randomUUID(),
50
+ filePath: path,
51
+ oldContent,
52
+ newContent: content,
53
+ diff,
54
+ toolCallId: crypto.randomUUID(),
55
+ };
56
+
57
+ // Request approval from UI
58
+ let approved: boolean;
59
+ try {
60
+ approved = await store.setPendingEdit(pendingEdit);
61
+ } catch (error) {
62
+ // User cancelled with Esc
63
+ return `Edit cancelled for ${path}`;
64
+ }
65
+
66
+ if (!approved) {
67
+ return `Edit rejected for ${path}`;
68
+ }
69
+
70
+ // Write file after approval
71
+ await Bun.write(path, content);
72
+
73
+ return `Updated ${path}`;
74
+ },
75
+ };
File without changes
@@ -0,0 +1,60 @@
1
+ import { Box } from "ink";
2
+ import { Header } from "./header";
3
+ import { Timeline } from "./timeline";
4
+ import { ConnectedStatusBar } from "./statusBar";
5
+ import { Prompt } from "./prompt";
6
+ import { useUIStore } from "./store/useUIStore";
7
+ import { HomeScreen, type HomeScreenData } from "./components/HomeScreen";
8
+ import { DiffPreview } from "./components/DiffPreview";
9
+ import type { AgentController } from "../../commands/agentController";
10
+ import { useState } from "react";
11
+
12
+ interface AppProps {
13
+ controller: AgentController;
14
+ onExit: () => Promise<void>;
15
+ homeScreen: HomeScreenData;
16
+ }
17
+
18
+ export function App({ controller, onExit, homeScreen }: AppProps) {
19
+ const state = useUIStore();
20
+ const [promptValue, setPromptValue] = useState("");
21
+ const showHome = state.timeline.length === 0;
22
+ const hasPendingEdit = state.pendingEdit !== null;
23
+
24
+ const promptProps = {
25
+ controller,
26
+ onExit,
27
+ value: promptValue,
28
+ onValueChange: setPromptValue,
29
+ };
30
+
31
+ return (
32
+ <Box flexDirection="column" height="100%" paddingX={1}>
33
+ <Box marginBottom={1}>
34
+ <Header branch={homeScreen.branch} provider={homeScreen.providerName} />
35
+ </Box>
36
+
37
+ <Box flexDirection="column" flexGrow={1} paddingX={1}>
38
+ {hasPendingEdit ? (
39
+ <DiffPreview pendingEdit={state.pendingEdit!} />
40
+ ) : showHome ? (
41
+ <HomeScreen
42
+ {...homeScreen}
43
+ renderPrompt={(placeholder) => (
44
+ <Prompt {...promptProps} placeholder={placeholder} />
45
+ )}
46
+ />
47
+ ) : (
48
+ <Timeline items={state.timeline} isThinking={state.isThinking} />
49
+ )}
50
+ </Box>
51
+
52
+ {!showHome && !hasPendingEdit && (
53
+ <Box flexDirection="column" marginTop={1}>
54
+ <ConnectedStatusBar />
55
+ <Prompt {...promptProps} />
56
+ </Box>
57
+ )}
58
+ </Box>
59
+ );
60
+ }