openreport 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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/openreport.ts +6 -0
- package/package.json +61 -0
- package/src/agents/api-documentation.ts +66 -0
- package/src/agents/architecture-analyst.ts +46 -0
- package/src/agents/code-quality-reviewer.ts +59 -0
- package/src/agents/dependency-analyzer.ts +51 -0
- package/src/agents/onboarding-guide.ts +59 -0
- package/src/agents/orchestrator.ts +41 -0
- package/src/agents/performance-analyzer.ts +57 -0
- package/src/agents/registry.ts +50 -0
- package/src/agents/security-auditor.ts +61 -0
- package/src/agents/test-coverage-analyst.ts +58 -0
- package/src/agents/todo-generator.ts +50 -0
- package/src/app/App.tsx +151 -0
- package/src/app/theme.ts +54 -0
- package/src/cli.ts +145 -0
- package/src/commands/init.ts +81 -0
- package/src/commands/interactive.tsx +29 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/run.ts +168 -0
- package/src/commands/view.tsx +52 -0
- package/src/components/generation/AgentStatusItem.tsx +125 -0
- package/src/components/generation/AgentStatusList.tsx +70 -0
- package/src/components/generation/ProgressSummary.tsx +107 -0
- package/src/components/generation/StreamingOutput.tsx +154 -0
- package/src/components/layout/Container.tsx +24 -0
- package/src/components/layout/Footer.tsx +52 -0
- package/src/components/layout/Header.tsx +50 -0
- package/src/components/report/MarkdownRenderer.tsx +50 -0
- package/src/components/report/ReportCard.tsx +31 -0
- package/src/components/report/ScrollableView.tsx +164 -0
- package/src/config/cli-detection.ts +130 -0
- package/src/config/cli-model.ts +397 -0
- package/src/config/cli-prompt-formatter.ts +129 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +168 -0
- package/src/config/ollama.ts +48 -0
- package/src/config/providers.ts +199 -0
- package/src/config/resolve-provider.ts +62 -0
- package/src/config/saver.ts +50 -0
- package/src/config/schema.ts +51 -0
- package/src/errors.ts +34 -0
- package/src/hooks/useReportGeneration.ts +199 -0
- package/src/hooks/useTerminalSize.ts +35 -0
- package/src/ingestion/context-selector.ts +247 -0
- package/src/ingestion/file-tree.ts +227 -0
- package/src/ingestion/token-budget.ts +52 -0
- package/src/pipeline/agent-runner.ts +360 -0
- package/src/pipeline/combiner.ts +199 -0
- package/src/pipeline/context.ts +108 -0
- package/src/pipeline/extraction.ts +153 -0
- package/src/pipeline/progress.ts +192 -0
- package/src/pipeline/runner.ts +526 -0
- package/src/report/html-renderer.ts +294 -0
- package/src/report/html-script.ts +123 -0
- package/src/report/html-styles.ts +1127 -0
- package/src/report/md-to-html.ts +153 -0
- package/src/report/open-browser.ts +22 -0
- package/src/schemas/findings.ts +48 -0
- package/src/schemas/report.ts +64 -0
- package/src/screens/ConfigScreen.tsx +271 -0
- package/src/screens/GenerationScreen.tsx +278 -0
- package/src/screens/HistoryScreen.tsx +108 -0
- package/src/screens/HomeScreen.tsx +143 -0
- package/src/screens/ViewerScreen.tsx +82 -0
- package/src/storage/metadata.ts +69 -0
- package/src/storage/report-store.ts +128 -0
- package/src/tools/get-file-tree.ts +157 -0
- package/src/tools/get-git-info.ts +123 -0
- package/src/tools/glob.ts +48 -0
- package/src/tools/grep.ts +149 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-directory.ts +57 -0
- package/src/tools/read-file.ts +52 -0
- package/src/tools/read-package-json.ts +48 -0
- package/src/tools/run-command.ts +154 -0
- package/src/tools/shared-ignore.ts +58 -0
- package/src/types/index.ts +127 -0
- package/src/types/marked-terminal.d.ts +17 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/file-utils.ts +77 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/grade-colors.ts +43 -0
- package/src/utils/project-detector.ts +296 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const HTML_ESC_MAP: Record<string, string> = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
'"': """,
|
|
6
|
+
"'": "'",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function esc(str: string): string {
|
|
10
|
+
return str.replace(/[&<>"']/g, (c) => HTML_ESC_MAP[c] ?? c);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Minimal markdown-to-HTML for section content — line-based state machine */
|
|
14
|
+
export function mdToHtml(text: string): string {
|
|
15
|
+
const lines = text.split("\n");
|
|
16
|
+
const out: string[] = [];
|
|
17
|
+
let i = 0;
|
|
18
|
+
|
|
19
|
+
while (i < lines.length) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
|
|
23
|
+
// Skip blank lines
|
|
24
|
+
if (!trimmed) { i++; continue; }
|
|
25
|
+
|
|
26
|
+
// Code block (fenced)
|
|
27
|
+
if (trimmed.startsWith("```")) {
|
|
28
|
+
const lang = trimmed.slice(3).trim();
|
|
29
|
+
const codeLines: string[] = [];
|
|
30
|
+
i++;
|
|
31
|
+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
|
32
|
+
codeLines.push(lines[i]);
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
i++; // skip closing ```
|
|
36
|
+
out.push(`<pre><code${lang ? ` class="lang-${esc(lang)}"` : ""}>${esc(codeLines.join("\n"))}</code></pre>`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Heading
|
|
41
|
+
const hMatch = trimmed.match(/^(#{1,4})\s+(.+)/);
|
|
42
|
+
if (hMatch) {
|
|
43
|
+
const level = Math.min(hMatch[1].length + 2, 6);
|
|
44
|
+
out.push(`<h${level}>${inlineFormat(hMatch[2])}</h${level}>`);
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Table: line starts with |, and next line is a separator (|---|)
|
|
50
|
+
if (trimmed.startsWith("|") && i + 1 < lines.length && /^\|[\s-:|]+\|$/.test(lines[i + 1].trim())) {
|
|
51
|
+
const tableLines: string[] = [trimmed];
|
|
52
|
+
i++;
|
|
53
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
|
54
|
+
tableLines.push(lines[i].trim());
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
out.push(renderTable(tableLines.join("\n")));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Checkbox list (- [ ] or - [x])
|
|
62
|
+
if (/^[-*]\s\[[ xX]\]\s/.test(trimmed)) {
|
|
63
|
+
const items: string[] = [];
|
|
64
|
+
while (i < lines.length && /^[-*]\s\[[ xX]\]\s/.test(lines[i].trim())) {
|
|
65
|
+
items.push(lines[i].trim());
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
out.push(`<ul class="checklist">${items.map((t) => {
|
|
69
|
+
const checked = /^[-*]\s\[[xX]\]/.test(t);
|
|
70
|
+
const text = t.replace(/^[-*]\s\[[ xX]\]\s+/, "");
|
|
71
|
+
return `<li class="checklist-item${checked ? " checked" : ""}" onclick="toggleCheck(this)"><span class="checkbox${checked ? " checkbox-checked" : ""}">${checked ? "✓" : ""}</span><span class="checklist-text">${inlineFormat(text)}</span></li>`;
|
|
72
|
+
}).join("")}</ul>`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Unordered list
|
|
77
|
+
if (/^[-*]\s/.test(trimmed)) {
|
|
78
|
+
const items: string[] = [];
|
|
79
|
+
while (i < lines.length && /^[-*]\s/.test(lines[i].trim())) {
|
|
80
|
+
items.push(lines[i].trim().replace(/^[-*]\s+/, ""));
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
out.push(`<ul>${items.map((t) => `<li>${inlineFormat(t)}</li>`).join("")}</ul>`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ordered list
|
|
88
|
+
if (/^\d+\.\s/.test(trimmed)) {
|
|
89
|
+
const items: string[] = [];
|
|
90
|
+
while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
|
|
91
|
+
items.push(lines[i].trim().replace(/^\d+\.\s+/, ""));
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
out.push(`<ol>${items.map((t) => `<li>${inlineFormat(t)}</li>`).join("")}</ol>`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Paragraph: collect consecutive non-blank, non-special lines
|
|
99
|
+
const paraLines: string[] = [trimmed];
|
|
100
|
+
i++;
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const next = lines[i].trim();
|
|
103
|
+
if (!next || next.startsWith("```") || next.startsWith("#") || next.startsWith("|") || /^[-*]\s/.test(next) || /^\d+\.\s/.test(next)) break;
|
|
104
|
+
paraLines.push(next);
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
out.push(`<p>${inlineFormat(paraLines.join(" "))}</p>`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return out.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isSafeUrl(url: string): boolean {
|
|
114
|
+
const decoded = url.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').trim();
|
|
115
|
+
const scheme = decoded.toLowerCase().split(":")[0];
|
|
116
|
+
if (!decoded.includes(":")) return true; // relative URLs are safe
|
|
117
|
+
return scheme === "http" || scheme === "https" || scheme === "mailto";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inlineFormat(text: string): string {
|
|
121
|
+
return esc(text)
|
|
122
|
+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
123
|
+
.replace(/\*(.+?)\*/g, "<em>$1</em>")
|
|
124
|
+
.replace(/`([^`]+)`/g, '<code class="inline">$1</code>')
|
|
125
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => {
|
|
126
|
+
if (isSafeUrl(url)) {
|
|
127
|
+
return `<a href="${esc(url)}">${label}</a>`;
|
|
128
|
+
}
|
|
129
|
+
return label;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderTable(text: string): string {
|
|
134
|
+
const rows = text
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((r) => r.trim())
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
if (rows.length < 2) return `<p>${esc(text)}</p>`;
|
|
139
|
+
|
|
140
|
+
const parseRow = (row: string) =>
|
|
141
|
+
row
|
|
142
|
+
.split("|")
|
|
143
|
+
.map((c) => c.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
|
|
146
|
+
const headers = parseRow(rows[0]);
|
|
147
|
+
const bodyRows = rows.slice(2).map(parseRow); // skip separator
|
|
148
|
+
|
|
149
|
+
return `<div class="table-wrap"><table>
|
|
150
|
+
<thead><tr>${headers.map((h) => `<th>${inlineFormat(h)}</th>`).join("")}</tr></thead>
|
|
151
|
+
<tbody>${bodyRows.map((cells) => `<tr>${cells.map((c) => `<td>${inlineFormat(c)}</td>`).join("")}</tr>`).join("")}</tbody>
|
|
152
|
+
</table></div>`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import { debugLog } from "../utils/debug.js";
|
|
4
|
+
|
|
5
|
+
const SAFE_PATH = /^[a-zA-Z0-9_\-./\\: ]+$/;
|
|
6
|
+
|
|
7
|
+
export function openInBrowser(filePath: string): void {
|
|
8
|
+
if (!SAFE_PATH.test(filePath)) return;
|
|
9
|
+
|
|
10
|
+
const platform = os.platform();
|
|
11
|
+
|
|
12
|
+
let child;
|
|
13
|
+
if (platform === "win32") {
|
|
14
|
+
child = spawn("cmd", ["/c", "start", "", filePath], { shell: false });
|
|
15
|
+
} else if (platform === "darwin") {
|
|
16
|
+
child = spawn("open", [filePath]);
|
|
17
|
+
} else {
|
|
18
|
+
child = spawn("xdg-open", [filePath]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
child.on("error", (err) => debugLog("browser:open", `Failed to open browser: ${err.message}`));
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ── Normalization functions ─────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export function normalizeSeverity(s: string): "critical" | "warning" | "info" | "suggestion" {
|
|
6
|
+
const lower = s.toLowerCase().trim();
|
|
7
|
+
if (["critical", "error", "high", "severe"].includes(lower)) return "critical";
|
|
8
|
+
if (["warning", "warn", "medium", "moderate"].includes(lower)) return "warning";
|
|
9
|
+
if (["info", "information", "low", "note"].includes(lower)) return "info";
|
|
10
|
+
if (["suggestion", "hint", "improvement", "enhancement"].includes(lower)) return "suggestion";
|
|
11
|
+
return "info";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeEffort(s: string): "low" | "medium" | "high" {
|
|
15
|
+
const lower = s.toLowerCase().trim();
|
|
16
|
+
if (["low", "easy", "trivial", "simple"].includes(lower)) return "low";
|
|
17
|
+
if (["high", "hard", "complex", "difficult"].includes(lower)) return "high";
|
|
18
|
+
return "medium";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Accept any string for category — AI agents produce varied labels
|
|
22
|
+
export const CategorySchema = z.string();
|
|
23
|
+
|
|
24
|
+
export const FindingSchema = z.object({
|
|
25
|
+
id: z.string(),
|
|
26
|
+
severity: z.string().transform(normalizeSeverity),
|
|
27
|
+
category: CategorySchema,
|
|
28
|
+
title: z.string(),
|
|
29
|
+
description: z.string(),
|
|
30
|
+
files: z.array(z.string()).default([]),
|
|
31
|
+
recommendation: z.string().default(""),
|
|
32
|
+
effort: z.string().transform(normalizeEffort).default("medium"),
|
|
33
|
+
codeSnippet: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const SEVERITY_ORDER: Record<string, number> = { critical: 0, warning: 1, info: 2, suggestion: 3 };
|
|
37
|
+
|
|
38
|
+
export const SEVERITY_EMOJI: Record<string, string> = {
|
|
39
|
+
critical: "\u{1F534}",
|
|
40
|
+
warning: "\u{1F7E0}",
|
|
41
|
+
info: "\u{1F535}",
|
|
42
|
+
suggestion: "\u{1F4A1}",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type Severity = "critical" | "warning" | "info" | "suggestion";
|
|
46
|
+
export type Effort = "low" | "medium" | "high";
|
|
47
|
+
export type Category = z.infer<typeof CategorySchema>;
|
|
48
|
+
export type Finding = z.infer<typeof FindingSchema>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FindingSchema } from "./findings.js";
|
|
3
|
+
|
|
4
|
+
export { FindingSchema } from "./findings.js";
|
|
5
|
+
|
|
6
|
+
export const SubReportSchema = z.object({
|
|
7
|
+
agentId: z.string(),
|
|
8
|
+
agentName: z.string(),
|
|
9
|
+
title: z.string(),
|
|
10
|
+
summary: z.string(),
|
|
11
|
+
sections: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
heading: z.string(),
|
|
14
|
+
content: z.string(),
|
|
15
|
+
})
|
|
16
|
+
),
|
|
17
|
+
findings: z.array(FindingSchema),
|
|
18
|
+
metadata: z
|
|
19
|
+
.object({
|
|
20
|
+
filesAnalyzed: z.number().default(0),
|
|
21
|
+
tokensUsed: z
|
|
22
|
+
.object({ input: z.number(), output: z.number() })
|
|
23
|
+
.default({ input: 0, output: 0 }),
|
|
24
|
+
duration: z.number().default(0),
|
|
25
|
+
})
|
|
26
|
+
.default({}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const FindingSummarySchema = z.object({
|
|
30
|
+
critical: z.number().default(0),
|
|
31
|
+
warning: z.number().default(0),
|
|
32
|
+
info: z.number().default(0),
|
|
33
|
+
suggestion: z.number().default(0),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const ReportMetadataSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
projectName: z.string(),
|
|
40
|
+
projectPath: z.string(),
|
|
41
|
+
createdAt: z.string(),
|
|
42
|
+
duration: z.number(),
|
|
43
|
+
model: z.string(),
|
|
44
|
+
tokens: z.object({ input: z.number(), output: z.number() }),
|
|
45
|
+
agentStatuses: z.array(
|
|
46
|
+
z.object({
|
|
47
|
+
agentId: z.string(),
|
|
48
|
+
status: z.enum(["completed", "failed", "skipped"]),
|
|
49
|
+
duration: z.number().optional(),
|
|
50
|
+
})
|
|
51
|
+
),
|
|
52
|
+
grade: z.string().optional(),
|
|
53
|
+
sections: z.array(z.string()),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const FullReportSchema = z.object({
|
|
57
|
+
metadata: ReportMetadataSchema,
|
|
58
|
+
executiveSummary: z.string(),
|
|
59
|
+
findingSummary: FindingSummarySchema,
|
|
60
|
+
subReports: z.array(SubReportSchema),
|
|
61
|
+
allFindings: z.array(FindingSchema).optional(),
|
|
62
|
+
markdownContent: z.string(),
|
|
63
|
+
});
|
|
64
|
+
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { Header } from "../components/layout/Header.js";
|
|
5
|
+
import { Footer } from "../components/layout/Footer.js";
|
|
6
|
+
import { Container } from "../components/layout/Container.js";
|
|
7
|
+
import {
|
|
8
|
+
detectInstalledClis,
|
|
9
|
+
type DetectedCli,
|
|
10
|
+
} from "../config/cli-detection.js";
|
|
11
|
+
import { useTerminalSize } from "../hooks/useTerminalSize.js";
|
|
12
|
+
import type { OpenReportConfig } from "../config/schema.js";
|
|
13
|
+
import type { Screen } from "../types/index.js";
|
|
14
|
+
|
|
15
|
+
export interface ConfigChanges {
|
|
16
|
+
defaultProvider: string;
|
|
17
|
+
defaultModel: string;
|
|
18
|
+
maxConcurrency: number;
|
|
19
|
+
temperature: number;
|
|
20
|
+
todoList: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ConfigScreenProps {
|
|
24
|
+
config: OpenReportConfig;
|
|
25
|
+
onNavigate: (screen: Screen, params?: Record<string, string>) => void;
|
|
26
|
+
onSave: (changes: ConfigChanges) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Models available per CLI agent
|
|
30
|
+
const CLI_MODELS: Record<string, string[]> = {
|
|
31
|
+
"claude-code": ["sonnet", "opus", "haiku"],
|
|
32
|
+
"gemini-cli": ["gemini-cli"],
|
|
33
|
+
"codex-cli": ["codex-cli"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ConfigField = "provider" | "model" | "concurrency" | "temperature" | "todoList";
|
|
37
|
+
|
|
38
|
+
export function ConfigScreen({
|
|
39
|
+
config,
|
|
40
|
+
onNavigate,
|
|
41
|
+
onSave,
|
|
42
|
+
}: ConfigScreenProps) {
|
|
43
|
+
const [selectedField, setSelectedField] = useState<number>(0);
|
|
44
|
+
const [currentProvider, setCurrentProvider] = useState(
|
|
45
|
+
config.defaultProvider
|
|
46
|
+
);
|
|
47
|
+
const [currentModel, setCurrentModel] = useState(config.defaultModel);
|
|
48
|
+
const [concurrency, setConcurrency] = useState(config.agents.maxConcurrency);
|
|
49
|
+
const [temperature, setTemperature] = useState(config.agents.temperature);
|
|
50
|
+
const [todoList, setTodoList] = useState(config.features?.todoList ?? false);
|
|
51
|
+
const [saved, setSaved] = useState(false);
|
|
52
|
+
|
|
53
|
+
// CLI tools state
|
|
54
|
+
const [detectedClis, setDetectedClis] = useState<DetectedCli[]>([]);
|
|
55
|
+
const [cliDetecting, setCliDetecting] = useState(true);
|
|
56
|
+
|
|
57
|
+
// Detect installed CLI tools on mount
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setCliDetecting(true);
|
|
60
|
+
detectInstalledClis().then((clis) => {
|
|
61
|
+
setDetectedClis(clis);
|
|
62
|
+
setCliDetecting(false);
|
|
63
|
+
|
|
64
|
+
// Auto-switch to first available agent if current isn't detected
|
|
65
|
+
const currentIsAvailable = clis.some((c) => c.id === currentProvider);
|
|
66
|
+
if (!currentIsAvailable && clis.length > 0) {
|
|
67
|
+
setCurrentProvider(clis[0].id);
|
|
68
|
+
const models = CLI_MODELS[clis[0].id] || [];
|
|
69
|
+
if (models.length > 0) {
|
|
70
|
+
setCurrentModel(models[0]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// Only CLI agents are available for analysis
|
|
77
|
+
const allProviders = detectedClis.map((c) => c.id);
|
|
78
|
+
|
|
79
|
+
const getModelsForProvider = (provider: string): string[] => {
|
|
80
|
+
return CLI_MODELS[provider] || [];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getProviderLabel = (id: string): string => {
|
|
84
|
+
const cli = detectedClis.find((c) => c.id === id);
|
|
85
|
+
if (cli) return cli.name;
|
|
86
|
+
return id;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const isDetected = detectedClis.some((c) => c.id === currentProvider);
|
|
90
|
+
|
|
91
|
+
const hasChanges =
|
|
92
|
+
currentProvider !== config.defaultProvider ||
|
|
93
|
+
currentModel !== config.defaultModel ||
|
|
94
|
+
concurrency !== config.agents.maxConcurrency ||
|
|
95
|
+
temperature !== config.agents.temperature ||
|
|
96
|
+
todoList !== (config.features?.todoList ?? false);
|
|
97
|
+
|
|
98
|
+
const fields: Array<{ id: ConfigField; label: string; value: string }> = [
|
|
99
|
+
{
|
|
100
|
+
id: "provider",
|
|
101
|
+
label: "Agent",
|
|
102
|
+
value: getProviderLabel(currentProvider),
|
|
103
|
+
},
|
|
104
|
+
{ id: "model", label: "Model", value: currentModel },
|
|
105
|
+
{
|
|
106
|
+
id: "concurrency",
|
|
107
|
+
label: "Max Concurrency",
|
|
108
|
+
value: String(concurrency),
|
|
109
|
+
},
|
|
110
|
+
{ id: "temperature", label: "Temperature", value: String(temperature) },
|
|
111
|
+
{ id: "todoList", label: "Todo List", value: todoList ? "ON" : "OFF" },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
useInput((input, key) => {
|
|
115
|
+
if (key.upArrow) {
|
|
116
|
+
setSelectedField((prev) => Math.max(0, prev - 1));
|
|
117
|
+
setSaved(false);
|
|
118
|
+
} else if (key.downArrow) {
|
|
119
|
+
setSelectedField((prev) => Math.min(fields.length - 1, prev + 1));
|
|
120
|
+
setSaved(false);
|
|
121
|
+
} else if (key.leftArrow || key.rightArrow) {
|
|
122
|
+
setSaved(false);
|
|
123
|
+
const field = fields[selectedField];
|
|
124
|
+
const direction = key.rightArrow ? 1 : -1;
|
|
125
|
+
|
|
126
|
+
switch (field.id) {
|
|
127
|
+
case "provider": {
|
|
128
|
+
if (allProviders.length === 0) break;
|
|
129
|
+
const idx = allProviders.indexOf(currentProvider);
|
|
130
|
+
const newIdx =
|
|
131
|
+
(idx + direction + allProviders.length) % allProviders.length;
|
|
132
|
+
const newProvider = allProviders[newIdx];
|
|
133
|
+
setCurrentProvider(newProvider);
|
|
134
|
+
// Auto-select first model
|
|
135
|
+
const models = getModelsForProvider(newProvider);
|
|
136
|
+
if (models.length > 0 && !models.includes(currentModel)) {
|
|
137
|
+
setCurrentModel(models[0]);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "model": {
|
|
142
|
+
const models = getModelsForProvider(currentProvider);
|
|
143
|
+
if (models.length === 0) break;
|
|
144
|
+
const idx = models.indexOf(currentModel);
|
|
145
|
+
const newIdx = (idx + direction + models.length) % models.length;
|
|
146
|
+
setCurrentModel(models[newIdx]);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "concurrency": {
|
|
150
|
+
setConcurrency((prev) =>
|
|
151
|
+
Math.min(10, Math.max(1, prev + direction))
|
|
152
|
+
);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "temperature": {
|
|
156
|
+
setTemperature((prev) => {
|
|
157
|
+
const next = prev + direction * 0.1;
|
|
158
|
+
return Math.round(Math.min(1, Math.max(0, next)) * 10) / 10;
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "todoList": {
|
|
163
|
+
setTodoList((prev) => !prev);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else if (key.return || input === "s") {
|
|
168
|
+
onSave({
|
|
169
|
+
defaultProvider: currentProvider,
|
|
170
|
+
defaultModel: currentModel,
|
|
171
|
+
maxConcurrency: concurrency,
|
|
172
|
+
temperature,
|
|
173
|
+
todoList,
|
|
174
|
+
});
|
|
175
|
+
setSaved(true);
|
|
176
|
+
} else if (key.escape || input === "q") {
|
|
177
|
+
onNavigate("home");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { columns, rows } = useTerminalSize();
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Box flexDirection="column" width={columns} height={rows}>
|
|
185
|
+
<Header modelName={currentModel} />
|
|
186
|
+
|
|
187
|
+
<Container>
|
|
188
|
+
<Text bold>Configuration</Text>
|
|
189
|
+
<Text color="gray">
|
|
190
|
+
Use arrows to change values, Enter to save
|
|
191
|
+
</Text>
|
|
192
|
+
|
|
193
|
+
<Box flexDirection="column" paddingY={1}>
|
|
194
|
+
{fields.map((field, i) => (
|
|
195
|
+
<Box key={field.id} flexDirection="row" gap={2}>
|
|
196
|
+
<Text color={i === selectedField ? "yellow" : "gray"}>
|
|
197
|
+
{i === selectedField ? ">" : " "}
|
|
198
|
+
</Text>
|
|
199
|
+
<Box width={20}>
|
|
200
|
+
<Text bold={i === selectedField}>{field.label}</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
<Text color="cyan">
|
|
203
|
+
{i === selectedField ? "◀ " : " "}
|
|
204
|
+
{field.value}
|
|
205
|
+
{i === selectedField ? " ▶" : " "}
|
|
206
|
+
</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
))}
|
|
209
|
+
</Box>
|
|
210
|
+
|
|
211
|
+
{/* Agent status */}
|
|
212
|
+
<Box flexDirection="column" paddingY={0} gap={0}>
|
|
213
|
+
{cliDetecting ? (
|
|
214
|
+
<Box gap={1}>
|
|
215
|
+
<Spinner type="dots" />
|
|
216
|
+
<Text color="gray">Detecting installed agents...</Text>
|
|
217
|
+
</Box>
|
|
218
|
+
) : detectedClis.length === 0 ? (
|
|
219
|
+
<Box flexDirection="column">
|
|
220
|
+
<Text color="red">
|
|
221
|
+
No code agents detected on your system
|
|
222
|
+
</Text>
|
|
223
|
+
<Text color="gray">
|
|
224
|
+
Install one of: claude, gemini, codex
|
|
225
|
+
</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
) : (
|
|
228
|
+
<Box flexDirection="column" gap={0}>
|
|
229
|
+
{isDetected && (
|
|
230
|
+
<Text color="green">
|
|
231
|
+
{getProviderLabel(currentProvider)} detected - uses your existing authentication
|
|
232
|
+
</Text>
|
|
233
|
+
)}
|
|
234
|
+
{!isDetected && (
|
|
235
|
+
<Text color="yellow">
|
|
236
|
+
{currentProvider} not found - use arrows to select an installed agent
|
|
237
|
+
</Text>
|
|
238
|
+
)}
|
|
239
|
+
<Text color="gray" dimColor>
|
|
240
|
+
Available: {detectedClis.map((c) => c.name).join(", ")}
|
|
241
|
+
</Text>
|
|
242
|
+
</Box>
|
|
243
|
+
)}
|
|
244
|
+
</Box>
|
|
245
|
+
|
|
246
|
+
<Box paddingY={1}>
|
|
247
|
+
{saved ? (
|
|
248
|
+
<Text color="green" bold>
|
|
249
|
+
Configuration saved!
|
|
250
|
+
</Text>
|
|
251
|
+
) : hasChanges ? (
|
|
252
|
+
<Text color="yellow">
|
|
253
|
+
Unsaved changes - press Enter or [s] to save
|
|
254
|
+
</Text>
|
|
255
|
+
) : (
|
|
256
|
+
<Text color="gray">No changes</Text>
|
|
257
|
+
)}
|
|
258
|
+
</Box>
|
|
259
|
+
</Container>
|
|
260
|
+
|
|
261
|
+
<Footer
|
|
262
|
+
actions={[
|
|
263
|
+
{ key: "←→", label: "Change" },
|
|
264
|
+
{ key: "↑↓", label: "Navigate" },
|
|
265
|
+
{ key: "enter", label: "Save" },
|
|
266
|
+
{ key: "esc", label: "Back" },
|
|
267
|
+
]}
|
|
268
|
+
/>
|
|
269
|
+
</Box>
|
|
270
|
+
);
|
|
271
|
+
}
|