@mrace07/kairo 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/README.md +141 -0
- package/dist/application/coding-agent.d.ts +85 -0
- package/dist/application/coding-agent.js +765 -0
- package/dist/application/context-manager.d.ts +22 -0
- package/dist/application/context-manager.js +174 -0
- package/dist/application/context-selector.d.ts +11 -0
- package/dist/application/context-selector.js +74 -0
- package/dist/application/evaluated-agent.d.ts +7 -0
- package/dist/application/evaluated-agent.js +16 -0
- package/dist/application/evaluation-comparison.d.ts +34 -0
- package/dist/application/evaluation-comparison.js +91 -0
- package/dist/application/evaluation-harness.d.ts +19 -0
- package/dist/application/evaluation-harness.js +217 -0
- package/dist/application/failure-analyzer.d.ts +5 -0
- package/dist/application/failure-analyzer.js +37 -0
- package/dist/application/interaction-routing.d.ts +9 -0
- package/dist/application/interaction-routing.js +20 -0
- package/dist/application/live-evaluation.d.ts +8 -0
- package/dist/application/live-evaluation.js +185 -0
- package/dist/application/model-routing.d.ts +12 -0
- package/dist/application/model-routing.js +40 -0
- package/dist/application/model-system-instruction.d.ts +4 -0
- package/dist/application/model-system-instruction.js +4 -0
- package/dist/application/self-evaluation.d.ts +26 -0
- package/dist/application/self-evaluation.js +394 -0
- package/dist/application/task-metrics.d.ts +31 -0
- package/dist/application/task-metrics.js +42 -0
- package/dist/application/verification-planner.d.ts +12 -0
- package/dist/application/verification-planner.js +97 -0
- package/dist/domain/models.d.ts +247 -0
- package/dist/domain/models.js +1 -0
- package/dist/domain/ports.d.ts +87 -0
- package/dist/domain/ports.js +1 -0
- package/dist/domain/provider-error.d.ts +18 -0
- package/dist/domain/provider-error.js +17 -0
- package/dist/infrastructure/configuration/config.d.ts +24 -0
- package/dist/infrastructure/configuration/config.js +79 -0
- package/dist/infrastructure/filesystem/platform-paths.d.ts +8 -0
- package/dist/infrastructure/filesystem/platform-paths.js +18 -0
- package/dist/infrastructure/persistence/sqlite-session-store.d.ts +82 -0
- package/dist/infrastructure/persistence/sqlite-session-store.js +447 -0
- package/dist/infrastructure/providers/gemini-provider.d.ts +14 -0
- package/dist/infrastructure/providers/gemini-provider.js +90 -0
- package/dist/infrastructure/providers/groq-provider.d.ts +16 -0
- package/dist/infrastructure/providers/groq-provider.js +101 -0
- package/dist/infrastructure/providers/jev-safety-advisor.d.ts +18 -0
- package/dist/infrastructure/providers/jev-safety-advisor.js +95 -0
- package/dist/infrastructure/providers/mistral-provider.d.ts +15 -0
- package/dist/infrastructure/providers/mistral-provider.js +137 -0
- package/dist/infrastructure/providers/openrouter-provider.d.ts +15 -0
- package/dist/infrastructure/providers/openrouter-provider.js +104 -0
- package/dist/infrastructure/providers/provider-recovery.d.ts +10 -0
- package/dist/infrastructure/providers/provider-recovery.js +108 -0
- package/dist/infrastructure/providers/provider-registry.d.ts +22 -0
- package/dist/infrastructure/providers/provider-registry.js +67 -0
- package/dist/infrastructure/repository/repository-awareness.d.ts +12 -0
- package/dist/infrastructure/repository/repository-awareness.js +25 -0
- package/dist/infrastructure/repository/repository-profiler.d.ts +35 -0
- package/dist/infrastructure/repository/repository-profiler.js +498 -0
- package/dist/infrastructure/security/macos-keychain-store.d.ts +17 -0
- package/dist/infrastructure/security/macos-keychain-store.js +73 -0
- package/dist/infrastructure/tools/workspace-tools.d.ts +30 -0
- package/dist/infrastructure/tools/workspace-tools.js +321 -0
- package/dist/interface/cli/evaluation-comparison-report.d.ts +6 -0
- package/dist/interface/cli/evaluation-comparison-report.js +46 -0
- package/dist/interface/cli/evaluation-report.d.ts +14 -0
- package/dist/interface/cli/evaluation-report.js +122 -0
- package/dist/interface/cli/index.d.ts +2 -0
- package/dist/interface/cli/index.js +238 -0
- package/dist/interface/cli/provider-setup.d.ts +16 -0
- package/dist/interface/cli/provider-setup.js +86 -0
- package/dist/interface/cli/repl.d.ts +7 -0
- package/dist/interface/cli/repl.js +19 -0
- package/dist/interface/cli/task-trace.d.ts +7 -0
- package/dist/interface/cli/task-trace.js +48 -0
- package/dist/interface/cli/tui.d.ts +147 -0
- package/dist/interface/cli/tui.js +910 -0
- package/package.json +61 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { basename, relative, resolve } from "node:path";
|
|
5
|
+
import { Box, render, Text, useApp, useInput } from "ink";
|
|
6
|
+
import TextInput from "ink-text-input";
|
|
7
|
+
import parseDiff from "parse-diff";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { ProviderError } from "../../domain/provider-error.js";
|
|
10
|
+
import { autoInteractionMode, greetingResponse, interactionIntentState, } from "../../application/interaction-routing.js";
|
|
11
|
+
import { setAutoModelRoutingEnabled, setJevEnabled, setJevFeature, setModelSelection, } from "../../infrastructure/configuration/config.js";
|
|
12
|
+
import { JevDecisionProvider } from "../../infrastructure/providers/jev-safety-advisor.js";
|
|
13
|
+
import { modelRoutingState, quotaFallbackModels, selectAutoModel, } from "../../application/model-routing.js";
|
|
14
|
+
import { isProviderId, providerById, providerRegistry, } from "../../infrastructure/providers/provider-registry.js";
|
|
15
|
+
import { formatMetrics, formatPlan, formatTrace } from "./task-trace.js";
|
|
16
|
+
const colors = {
|
|
17
|
+
accent: "#7dd3fc",
|
|
18
|
+
accentSoft: "#a5b4fc",
|
|
19
|
+
muted: "#64748b",
|
|
20
|
+
textSoft: "#94a3b8",
|
|
21
|
+
success: "#4ade80",
|
|
22
|
+
warning: "#fbbf24",
|
|
23
|
+
danger: "#fb7185",
|
|
24
|
+
jev: "#c084fc",
|
|
25
|
+
};
|
|
26
|
+
export const slashCommands = [
|
|
27
|
+
{ name: "/help", description: "Show commands and shortcuts" },
|
|
28
|
+
{ name: "/plan", description: "Toggle read-only planning mode" },
|
|
29
|
+
{ name: "/build", description: "Build the latest saved plan" },
|
|
30
|
+
{ name: "/models", description: "Choose a model" },
|
|
31
|
+
{ name: "/auto", description: "Toggle Jev automatic model routing" },
|
|
32
|
+
{ name: "/jev", description: "Manage the Jev safety advisor" },
|
|
33
|
+
{ name: "/new", description: "Start a new session" },
|
|
34
|
+
{ name: "/resume", description: "Continue a task or open a session", acceptsArgument: true },
|
|
35
|
+
{ name: "/history", description: "List saved sessions" },
|
|
36
|
+
{ name: "/status", description: "Show the latest task status" },
|
|
37
|
+
{ name: "/trace", description: "Show task trace", acceptsArgument: true },
|
|
38
|
+
{ name: "/changes", description: "Show changed files" },
|
|
39
|
+
{ name: "/verify", description: "Run a verification command", acceptsArgument: true },
|
|
40
|
+
{ name: "/compact", description: "Save a context checkpoint" },
|
|
41
|
+
{ name: "/cancel", description: "Cancel the current task" },
|
|
42
|
+
{ name: "/logout", description: "Remove the active provider credential" },
|
|
43
|
+
{ name: "/quit", description: "Exit Kairo" },
|
|
44
|
+
];
|
|
45
|
+
/** Reads only compact repository metadata for the header. */
|
|
46
|
+
export function repositoryStatus(workspace) {
|
|
47
|
+
const git = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
48
|
+
const run = (args) => execFileSync(git, args, {
|
|
49
|
+
cwd: workspace,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
52
|
+
timeout: 2_000,
|
|
53
|
+
}).trim();
|
|
54
|
+
try {
|
|
55
|
+
const branch = run(["branch", "--show-current"]) || undefined;
|
|
56
|
+
const status = run(["status", "--short", "--untracked-files=normal"]);
|
|
57
|
+
return { branch, changedFiles: status ? status.split("\n").length : 0 };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { changedFiles: 0 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Returns an exit-code-tolerant Git command result; `git diff` uses exit code 1 for changes. */
|
|
64
|
+
function gitOutput(workspace, args) {
|
|
65
|
+
const git = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
66
|
+
try {
|
|
67
|
+
return execFileSync(git, args, {
|
|
68
|
+
cwd: workspace,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
timeout: 2_000,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const output = error.stdout;
|
|
76
|
+
return typeof output === "string" ? output : output?.toString();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function gitSucceeds(workspace, args) {
|
|
80
|
+
const git = process.platform === "darwin" ? "/usr/bin/git" : "git";
|
|
81
|
+
try {
|
|
82
|
+
execFileSync(git, args, {
|
|
83
|
+
cwd: workspace,
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
timeout: 2_000,
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Builds a reviewable working-tree patch for a file Kairo touched. This deliberately uses the
|
|
95
|
+
* current Git state rather than claiming to isolate just the agent's edits.
|
|
96
|
+
*/
|
|
97
|
+
export function changedFileReview(workspace, path) {
|
|
98
|
+
const absolute = resolve(workspace, path);
|
|
99
|
+
const workspaceRelative = relative(workspace, absolute);
|
|
100
|
+
if (workspaceRelative.startsWith("..") ||
|
|
101
|
+
workspaceRelative === "" ||
|
|
102
|
+
workspaceRelative.includes("../"))
|
|
103
|
+
return { path, diff: "", unavailable: "This path is outside the active workspace." };
|
|
104
|
+
const tracked = gitSucceeds(workspace, ["ls-files", "--error-unmatch", "--", path]);
|
|
105
|
+
if (tracked && gitSucceeds(workspace, ["rev-parse", "--verify", "HEAD"])) {
|
|
106
|
+
const diff = gitOutput(workspace, ["diff", "--no-ext-diff", "--unified=3", "HEAD", "--", path]);
|
|
107
|
+
if (diff !== undefined)
|
|
108
|
+
return { path, diff };
|
|
109
|
+
}
|
|
110
|
+
if (existsSync(absolute)) {
|
|
111
|
+
const diff = gitOutput(workspace, [
|
|
112
|
+
"diff",
|
|
113
|
+
"--no-index",
|
|
114
|
+
"--unified=3",
|
|
115
|
+
"--",
|
|
116
|
+
"/dev/null",
|
|
117
|
+
path,
|
|
118
|
+
]);
|
|
119
|
+
if (diff)
|
|
120
|
+
return { path, diff };
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
path,
|
|
124
|
+
diff: readFileSync(absolute, "utf8"),
|
|
125
|
+
unavailable: "No Git diff is available; showing the current file instead.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Fall through to a short, actionable empty state.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { path, diff: "", unavailable: "No current Git diff is available for this file." };
|
|
133
|
+
}
|
|
134
|
+
/** Finds palette entries while the user is typing a slash-command name. */
|
|
135
|
+
export function matchingSlashCommands(input) {
|
|
136
|
+
if (!input.startsWith("/") || input.includes(" "))
|
|
137
|
+
return [];
|
|
138
|
+
return slashCommands.filter((command) => command.name.startsWith(input.toLowerCase()));
|
|
139
|
+
}
|
|
140
|
+
function commandInput(command) {
|
|
141
|
+
return `${command.name}${"acceptsArgument" in command && command.acceptsArgument ? " " : ""}`;
|
|
142
|
+
}
|
|
143
|
+
/** Flattens the supported provider registry into the options shown by `/models`. */
|
|
144
|
+
export function modelOptions() {
|
|
145
|
+
return providerRegistry.flatMap((provider) => provider.models.map((model) => ({
|
|
146
|
+
provider: provider.id,
|
|
147
|
+
model: model.id,
|
|
148
|
+
label: `${provider.name} — ${model.label}${model.recommended ? " (recommended)" : ""}`,
|
|
149
|
+
})));
|
|
150
|
+
}
|
|
151
|
+
/** Bridges the agent's approval port to the active Ink application. */
|
|
152
|
+
export class TuiApproval {
|
|
153
|
+
request;
|
|
154
|
+
constructor(request) {
|
|
155
|
+
this.request = request;
|
|
156
|
+
}
|
|
157
|
+
async approve(call, description) {
|
|
158
|
+
return new Promise((resolve) => this.request({ call, description, resolve }));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** `/plan` is intentionally a live toggle, never a one-shot planning command. */
|
|
162
|
+
export function interactionModeAfterCommand(mode, command) {
|
|
163
|
+
return command === "/plan" ? (mode === "build" ? "plan" : "build") : mode;
|
|
164
|
+
}
|
|
165
|
+
/** Reflects a completed Jev planning route in the persistent TUI interaction mode. */
|
|
166
|
+
export function interactionModeAfterTask(mode, taskMode) {
|
|
167
|
+
return taskMode === "planning" ? "plan" : mode;
|
|
168
|
+
}
|
|
169
|
+
export function ModeBadge({ mode }) {
|
|
170
|
+
return (_jsx(Text, { bold: true, color: mode === "plan" ? "yellow" : "green", children: mode === "plan" ? "PLAN" : "BUILD" }));
|
|
171
|
+
}
|
|
172
|
+
function entryColor(entry) {
|
|
173
|
+
if (entry.kind === "user")
|
|
174
|
+
return "cyan";
|
|
175
|
+
if (entry.kind === "error")
|
|
176
|
+
return "red";
|
|
177
|
+
if (entry.kind === "system")
|
|
178
|
+
return "yellow";
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
function entryLabel(kind) {
|
|
182
|
+
return { user: "YOU", assistant: "KAIRO", system: "SYSTEM", error: "ERROR" }[kind];
|
|
183
|
+
}
|
|
184
|
+
/** A compact chat row that keeps requests, answers, and operational notices distinct. */
|
|
185
|
+
export function TranscriptRow({ entry }) {
|
|
186
|
+
const color = entryColor(entry);
|
|
187
|
+
const isConversation = entry.kind === "user" || entry.kind === "assistant";
|
|
188
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: isConversation ? 1 : 0, children: [_jsx(Text, { bold: true, color: color, dimColor: entry.kind === "system", children: entryLabel(entry.kind) }), _jsx(Box, { paddingLeft: 1, children: entry.kind === "assistant" && !entry.text ? (_jsx(LoadingIndicator, { label: "Generating response" })) : (_jsx(Text, { color: color, wrap: "wrap", children: entry.text })) })] }));
|
|
189
|
+
}
|
|
190
|
+
function LoadingIndicator({ label, color = "yellow" }) {
|
|
191
|
+
const frames = ["◐", "◓", "◑", "◒"];
|
|
192
|
+
const [frame, setFrame] = useState(0);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const timer = setInterval(() => setFrame((current) => (current + 1) % frames.length), 120);
|
|
195
|
+
timer.unref();
|
|
196
|
+
return () => clearInterval(timer);
|
|
197
|
+
}, []);
|
|
198
|
+
return (_jsxs(Text, { color: color, children: [frames[frame], " ", label, "\u2026"] }));
|
|
199
|
+
}
|
|
200
|
+
/** Converts a Git patch into the line-oriented model used by Kairo's terminal review view. */
|
|
201
|
+
export function inlineDiff(review) {
|
|
202
|
+
const file = parseDiff(review.diff)[0];
|
|
203
|
+
if (!file)
|
|
204
|
+
return undefined;
|
|
205
|
+
return {
|
|
206
|
+
path: review.path,
|
|
207
|
+
additions: file.additions,
|
|
208
|
+
deletions: file.deletions,
|
|
209
|
+
hunks: file.chunks.map((chunk) => ({
|
|
210
|
+
header: chunk.content,
|
|
211
|
+
lines: chunk.changes.map((change) => {
|
|
212
|
+
if (change.type === "add")
|
|
213
|
+
return { kind: "addition", newLine: change.ln, text: change.content.slice(1) };
|
|
214
|
+
if (change.type === "del")
|
|
215
|
+
return { kind: "deletion", oldLine: change.ln, text: change.content.slice(1) };
|
|
216
|
+
return {
|
|
217
|
+
kind: "context",
|
|
218
|
+
oldLine: change.ln1,
|
|
219
|
+
newLine: change.ln2,
|
|
220
|
+
text: change.content.slice(1),
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
223
|
+
})),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function lineNumberWidth(diff) {
|
|
227
|
+
const largest = Math.max(1, ...diff.hunks.flatMap((hunk) => hunk.lines.flatMap((line) => [line.oldLine ?? 0, line.newLine ?? 0])));
|
|
228
|
+
return String(largest).length;
|
|
229
|
+
}
|
|
230
|
+
function InlineDiffLineRow({ line, width, }) {
|
|
231
|
+
const isAddition = line.kind === "addition";
|
|
232
|
+
const isDeletion = line.kind === "deletion";
|
|
233
|
+
const marker = isAddition ? "+" : isDeletion ? "-" : " ";
|
|
234
|
+
const number = isDeletion ? line.oldLine : line.newLine;
|
|
235
|
+
const foreground = isAddition ? "#86efac" : isDeletion ? "#fca5a5" : undefined;
|
|
236
|
+
const background = isAddition ? "#14532d" : isDeletion ? "#450a0a" : undefined;
|
|
237
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: foreground, children: marker }), _jsxs(Text, { color: colors.muted, children: [" ", String(number ?? "").padStart(width), " "] }), _jsx(Text, { color: foreground, backgroundColor: background, wrap: "wrap", children: line.text || " " })] }));
|
|
238
|
+
}
|
|
239
|
+
function DiffPreview({ review }) {
|
|
240
|
+
const diff = inlineDiff(review);
|
|
241
|
+
return (_jsxs(Box, { flexDirection: "column", children: [review.unavailable ? _jsx(Text, { color: colors.warning, children: review.unavailable }) : null, diff ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: colors.textSoft, children: ["Edited ", diff.path, " ", _jsxs(Text, { color: colors.success, children: ["(+", diff.additions] }), " ", _jsxs(Text, { color: colors.danger, children: ["-", diff.deletions, ")"] })] }), diff.hunks.map((hunk, hunkIndex) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: hunk.header }), hunk.lines.map((line, lineIndex) => (_jsx(InlineDiffLineRow, { line: line, width: lineNumberWidth(diff) }, `${hunkIndex}-${lineIndex}-${line.text}`)))] }, `${hunkIndex}-${hunk.header}`)))] })) : review.diff ? (_jsx(Text, { children: review.diff })) : null] }));
|
|
242
|
+
}
|
|
243
|
+
function ChangesPanel({ workspace, files, selected, }) {
|
|
244
|
+
const selectedPath = files[selected];
|
|
245
|
+
const review = selectedPath ? changedFileReview(workspace, selectedPath) : undefined;
|
|
246
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.accent, flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: colors.accent, children: ["CHANGES \u00B7 ", files.length] }), _jsx(Text, { color: colors.muted, children: "working-tree diff" })] }), _jsx(Text, { color: colors.textSoft, children: "Files Kairo touched in this task. Git may include earlier local edits." }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: files.map((file, index) => (_jsxs(Text, { color: index === selected ? colors.accent : colors.textSoft, bold: index === selected, children: [index === selected ? "›" : " ", " ", file] }, file))) }), review ? (_jsxs(Box, { borderStyle: "single", borderColor: colors.muted, flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsx(Text, { bold: true, children: review.path }), _jsx(DiffPreview, { review: review })] })) : null, _jsx(Text, { color: colors.muted, children: "\u2191/\u2193 select \u00B7 n/p next file \u00B7 Esc close" })] }));
|
|
247
|
+
}
|
|
248
|
+
export function BrandMark() {
|
|
249
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(Text, { bold: true, color: colors.accent, children: "██╗ ██╗ █████╗ ██╗██████╗ ██████╗" }), _jsx(Text, { bold: true, color: colors.accent, children: "██║ ██╔╝██╔══██╗██║██╔══██╗██╔═══██╗" }), _jsx(Text, { bold: true, color: colors.accentSoft, children: "█████╔╝ ███████║██║██████╔╝██║ ██║" }), _jsx(Text, { bold: true, color: colors.accentSoft, children: "██╔═██╗ ██╔══██║██║██╔══██╗██║ ██║" }), _jsx(Text, { bold: true, color: colors.accentSoft, children: "██║ ██╗██║ ██║██║██║ ██║╚██████╔╝" }), _jsx(Text, { color: colors.muted, children: "╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝" })] }));
|
|
250
|
+
}
|
|
251
|
+
function EmptyState({ mode, workspace, repo, }) {
|
|
252
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginTop: 2, marginBottom: 1, children: [_jsx(BrandMark, {}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: colors.textSoft, children: "dependable coding, right in your terminal" }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: basename(workspace) }), repo.branch ? _jsxs(Text, { color: colors.muted, children: [" \u00B7 ", repo.branch] }) : null, repo.changedFiles ? (_jsxs(Text, { color: colors.warning, children: [" \u00B7 ", repo.changedFiles, " changed"] })) : null] }), _jsx(Box, { flexDirection: "column", marginTop: 2, children: [
|
|
253
|
+
["/plan", "inspect first, change nothing"],
|
|
254
|
+
["/models", "choose the coding model"],
|
|
255
|
+
["/resume", "continue previous work"],
|
|
256
|
+
["/help", "see every command"],
|
|
257
|
+
].map(([command, description]) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: colors.accent, children: command.padEnd(11) }), _jsx(Text, { color: colors.textSoft, children: description })] }, command))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: mode === "plan" ? "Read-only planning is active" : "Describe an outcome to begin" }) })] }));
|
|
258
|
+
}
|
|
259
|
+
function WorkspaceBar({ workspace, repo, model, }) {
|
|
260
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.muted, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: colors.accent, children: "KAIRO" }), _jsxs(Text, { color: colors.textSoft, children: [" \u00B7 ", basename(workspace)] }), repo.branch ? _jsxs(Text, { color: colors.muted, children: [" \u00B7 ", repo.branch] }) : null, repo.changedFiles ? (_jsxs(Text, { color: colors.warning, children: [" \u00B7 ", repo.changedFiles, " changed"] })) : null] }), _jsxs(Text, { color: colors.muted, children: [model.provider, "/", model.model] })] }));
|
|
261
|
+
}
|
|
262
|
+
function ApprovalCard({ pending }) {
|
|
263
|
+
const [summary, ...details] = pending.description.split("\n");
|
|
264
|
+
const command = summary.startsWith("Run command");
|
|
265
|
+
const scopedWrite = (pending.call.name === "write_file" || pending.call.name === "edit_file") &&
|
|
266
|
+
typeof pending.call.args.path === "string" &&
|
|
267
|
+
Boolean(pending.call.args.path.trim());
|
|
268
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.warning, flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: colors.warning, children: "REVIEW ACTION" }), _jsx(Text, { bold: true, color: colors.danger, children: command ? "COMMAND" : "WRITE" })] }), _jsx(Text, { color: colors.textSoft, children: summary }), details.length ? _jsx(Text, { children: details.join("\n") }) : null, _jsx(Box, { marginTop: 1, children: scopedWrite ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: colors.success, children: "y / Enter allow this file for task" }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { color: colors.textSoft, children: "o allow once" }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { bold: true, color: colors.danger, children: "n / Esc deny" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: colors.success, children: "y / Enter allow" }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { bold: true, color: colors.danger, children: "n / Esc deny" })] })) })] }));
|
|
269
|
+
}
|
|
270
|
+
/** Renders one bounded Kairo session and dispatches terminal commands without readline. */
|
|
271
|
+
export function KairoTui(props) {
|
|
272
|
+
const { exit } = useApp();
|
|
273
|
+
const [active, setActive] = useState(props.session);
|
|
274
|
+
const [repo, setRepo] = useState(() => repositoryStatus(props.session.workspace));
|
|
275
|
+
const [selection, setSelection] = useState(props.initialSelection);
|
|
276
|
+
const [apiKey, setApiKey] = useState(props.initialApiKey);
|
|
277
|
+
const [jevKey, setJevKey] = useState(props.initialJevKey);
|
|
278
|
+
const [jevEnabled, setJevEnabledState] = useState(props.initialJevEnabled);
|
|
279
|
+
const [jevFeatures, setJevFeatures] = useState(props.initialJevFeatures);
|
|
280
|
+
const [autoModelRouting, setAutoModelRouting] = useState(props.initialAutoModelRoutingEnabled);
|
|
281
|
+
const [routedSelection, setRoutedSelection] = useState();
|
|
282
|
+
const [mode, setMode] = useState("build");
|
|
283
|
+
const [input, setInput] = useState("");
|
|
284
|
+
const [entries, setEntries] = useState([]);
|
|
285
|
+
const [busy, setBusy] = useState("idle");
|
|
286
|
+
const [changesTask, setChangesTask] = useState();
|
|
287
|
+
const [changesIndex, setChangesIndex] = useState(0);
|
|
288
|
+
const [pendingApproval, setPendingApproval] = useState();
|
|
289
|
+
const [modelPicker, setModelPicker] = useState(false);
|
|
290
|
+
const [modelIndex, setModelIndex] = useState(0);
|
|
291
|
+
const [credentialModel, setCredentialModel] = useState();
|
|
292
|
+
const [credentialInput, setCredentialInput] = useState("");
|
|
293
|
+
const [credentialError, setCredentialError] = useState();
|
|
294
|
+
const [savingCredential, setSavingCredential] = useState(false);
|
|
295
|
+
const [jevPanel, setJevPanel] = useState(false);
|
|
296
|
+
const [jevIndex, setJevIndex] = useState(0);
|
|
297
|
+
const [jevCredentialInput, setJevCredentialInput] = useState("");
|
|
298
|
+
const [jevCredentialError, setJevCredentialError] = useState();
|
|
299
|
+
const [jevCredentialMode, setJevCredentialMode] = useState(false);
|
|
300
|
+
const [savingJevCredential, setSavingJevCredential] = useState(false);
|
|
301
|
+
const [commandIndex, setCommandIndex] = useState(0);
|
|
302
|
+
const models = useMemo(modelOptions, []);
|
|
303
|
+
const commandMatches = useMemo(() => matchingSlashCommands(input), [input]);
|
|
304
|
+
const approval = useMemo(() => new TuiApproval((pending) => {
|
|
305
|
+
setBusy("acting");
|
|
306
|
+
setPendingApproval(pending);
|
|
307
|
+
}), []);
|
|
308
|
+
const agent = useMemo(() => apiKey
|
|
309
|
+
? props.createAgent(approval, selection, apiKey, jevEnabled && jevKey ? new JevDecisionProvider(jevKey) : undefined, jevFeatures)
|
|
310
|
+
: undefined, [apiKey, approval, jevEnabled, jevFeatures, jevKey, props.createAgent, selection]);
|
|
311
|
+
const resolveTaskAgent = useCallback(async (request) => {
|
|
312
|
+
if (!autoModelRouting || !jevEnabled || !jevKey || !agent) {
|
|
313
|
+
setRoutedSelection(undefined);
|
|
314
|
+
return agent && { agent, selection, fallbacks: [] };
|
|
315
|
+
}
|
|
316
|
+
const providerKeys = new Map(await Promise.all(providerRegistry.map(async (provider) => [provider.id, await props.credentials.get(provider.id)])));
|
|
317
|
+
const available = models.flatMap((model) => {
|
|
318
|
+
const apiKey = providerKeys.get(model.provider);
|
|
319
|
+
const tier = providerById(model.provider).models.find((item) => item.id === model.model)?.tier;
|
|
320
|
+
return apiKey && tier ? [{ ...model, apiKey, tier }] : [];
|
|
321
|
+
});
|
|
322
|
+
if (!available.length) {
|
|
323
|
+
setRoutedSelection(undefined);
|
|
324
|
+
return { agent, selection, fallbacks: [] };
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const decision = await new JevDecisionProvider(jevKey).modelTier(modelRoutingState(request, available));
|
|
328
|
+
if (decision.confidence < 0.85) {
|
|
329
|
+
setRoutedSelection(undefined);
|
|
330
|
+
return { agent, selection, fallbacks: [] };
|
|
331
|
+
}
|
|
332
|
+
const routed = selectAutoModel(available, selection, decision.value);
|
|
333
|
+
if (!routed) {
|
|
334
|
+
setRoutedSelection(undefined);
|
|
335
|
+
return { agent, selection, fallbacks: [] };
|
|
336
|
+
}
|
|
337
|
+
setRoutedSelection({ provider: routed.provider, model: routed.model });
|
|
338
|
+
const routedSelection = { provider: routed.provider, model: routed.model };
|
|
339
|
+
return {
|
|
340
|
+
agent: props.createAgent(approval, routedSelection, routed.apiKey, new JevDecisionProvider(jevKey), jevFeatures),
|
|
341
|
+
selection: routedSelection,
|
|
342
|
+
fallbacks: quotaFallbackModels(available, routedSelection, selection, decision.value),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
setRoutedSelection(undefined);
|
|
347
|
+
return { agent, selection, fallbacks: [] };
|
|
348
|
+
}
|
|
349
|
+
}, [agent, approval, autoModelRouting, jevEnabled, jevFeatures, jevKey, models, props, selection]);
|
|
350
|
+
const classifyInteraction = useCallback(async (request) => {
|
|
351
|
+
const localResponse = greetingResponse(request);
|
|
352
|
+
if (localResponse)
|
|
353
|
+
return { intent: "conversation", localResponse };
|
|
354
|
+
if (!jevEnabled || !jevKey || !jevFeatures.routing)
|
|
355
|
+
return { intent: "repository_task" };
|
|
356
|
+
try {
|
|
357
|
+
const decision = await new JevDecisionProvider(jevKey).intent(interactionIntentState(request));
|
|
358
|
+
return decision.confidence >= 0.85
|
|
359
|
+
? { intent: decision.value }
|
|
360
|
+
: { intent: "repository_task" };
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return { intent: "repository_task" };
|
|
364
|
+
}
|
|
365
|
+
}, [jevEnabled, jevFeatures.routing, jevKey]);
|
|
366
|
+
const append = useCallback((kind, text) => {
|
|
367
|
+
setEntries((current) => [...current, { id: current.length, kind, text }]);
|
|
368
|
+
}, []);
|
|
369
|
+
const appendStream = useCallback((entryId, chunk) => {
|
|
370
|
+
setEntries((current) => current.map((entry) => entry.id === entryId ? { ...entry, text: entry.text + chunk } : entry));
|
|
371
|
+
}, []);
|
|
372
|
+
const answerApproval = useCallback((decision) => {
|
|
373
|
+
if (!pendingApproval)
|
|
374
|
+
return;
|
|
375
|
+
setPendingApproval(undefined);
|
|
376
|
+
append("system", decision === "task_file"
|
|
377
|
+
? `Approved writes to ${String(pendingApproval.call.args.path)} for this task.`
|
|
378
|
+
: decision
|
|
379
|
+
? "Approved action."
|
|
380
|
+
: "Denied action.");
|
|
381
|
+
pendingApproval.resolve(decision);
|
|
382
|
+
}, [append, pendingApproval]);
|
|
383
|
+
useInput((inputKey, key) => {
|
|
384
|
+
if (!pendingApproval)
|
|
385
|
+
return;
|
|
386
|
+
const scopedWrite = (pendingApproval.call.name === "write_file" || pendingApproval.call.name === "edit_file") &&
|
|
387
|
+
typeof pendingApproval.call.args.path === "string" &&
|
|
388
|
+
Boolean(pendingApproval.call.args.path.trim());
|
|
389
|
+
if (inputKey.toLowerCase() === "y" || key.return)
|
|
390
|
+
answerApproval(scopedWrite ? "task_file" : true);
|
|
391
|
+
if (inputKey.toLowerCase() === "o" && scopedWrite)
|
|
392
|
+
answerApproval(true);
|
|
393
|
+
if (inputKey.toLowerCase() === "n" || key.escape)
|
|
394
|
+
answerApproval(false);
|
|
395
|
+
});
|
|
396
|
+
useInput((inputKey, key) => {
|
|
397
|
+
if (!changesTask)
|
|
398
|
+
return;
|
|
399
|
+
if (key.escape)
|
|
400
|
+
return setChangesTask(undefined);
|
|
401
|
+
const fileCount = changesTask.changedFiles.length;
|
|
402
|
+
if (key.upArrow || inputKey.toLowerCase() === "p")
|
|
403
|
+
return setChangesIndex((current) => Math.max(0, current - 1));
|
|
404
|
+
if (key.downArrow || inputKey.toLowerCase() === "n")
|
|
405
|
+
return setChangesIndex((current) => Math.min(fileCount - 1, current + 1));
|
|
406
|
+
});
|
|
407
|
+
const saveJevCredential = useCallback(async (value) => {
|
|
408
|
+
if (savingJevCredential)
|
|
409
|
+
return;
|
|
410
|
+
const key = value.trim();
|
|
411
|
+
if (!key)
|
|
412
|
+
return setJevCredentialError("API key cannot be empty.");
|
|
413
|
+
setSavingJevCredential(true);
|
|
414
|
+
setJevCredentialError(undefined);
|
|
415
|
+
try {
|
|
416
|
+
await new JevDecisionProvider(key).validate();
|
|
417
|
+
await props.credentials.save("jev", key);
|
|
418
|
+
await setJevEnabled(true);
|
|
419
|
+
setJevKey(key);
|
|
420
|
+
setJevEnabledState(true);
|
|
421
|
+
setJevCredentialInput("");
|
|
422
|
+
setJevCredentialMode(false);
|
|
423
|
+
setJevPanel(false);
|
|
424
|
+
append("system", "Jev safety advisor enabled.");
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
setJevCredentialError(error.message);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
setSavingJevCredential(false);
|
|
431
|
+
}
|
|
432
|
+
}, [append, props.credentials, savingJevCredential]);
|
|
433
|
+
useInput((inputKey, key) => {
|
|
434
|
+
if (!jevPanel || savingJevCredential)
|
|
435
|
+
return;
|
|
436
|
+
if (jevCredentialMode) {
|
|
437
|
+
if (key.escape) {
|
|
438
|
+
setJevCredentialInput("");
|
|
439
|
+
setJevCredentialError(undefined);
|
|
440
|
+
setJevCredentialMode(false);
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (key.escape)
|
|
445
|
+
return setJevPanel(false);
|
|
446
|
+
if (key.upArrow)
|
|
447
|
+
return setJevIndex((current) => Math.max(0, current - 1));
|
|
448
|
+
if (key.downArrow)
|
|
449
|
+
return setJevIndex((current) => Math.min(6, current + 1));
|
|
450
|
+
if (key.return || /^[1-7]$/.test(inputKey)) {
|
|
451
|
+
const action = /^[1-7]$/.test(inputKey) ? Number(inputKey) - 1 : jevIndex;
|
|
452
|
+
if (action === 0) {
|
|
453
|
+
if (!jevKey) {
|
|
454
|
+
setJevCredentialError("Add a TypeSafe API key before enabling Jev.");
|
|
455
|
+
setJevCredentialMode(true);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const next = !jevEnabled;
|
|
459
|
+
void setJevEnabled(next).then(() => {
|
|
460
|
+
setJevEnabledState(next);
|
|
461
|
+
append("system", `Jev safety advisor ${next ? "enabled" : "disabled"}.`);
|
|
462
|
+
});
|
|
463
|
+
return setJevPanel(false);
|
|
464
|
+
}
|
|
465
|
+
if (action === 1) {
|
|
466
|
+
const next = !jevFeatures.routing;
|
|
467
|
+
void setJevFeature("routing", next).then(() => setJevFeatures((current) => ({ ...current, routing: next })));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (action === 2) {
|
|
471
|
+
const next = !jevFeatures.safety;
|
|
472
|
+
void setJevFeature("safety", next).then(() => setJevFeatures((current) => ({ ...current, safety: next })));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (action === 3) {
|
|
476
|
+
const next = !jevFeatures.recovery;
|
|
477
|
+
void setJevFeature("recovery", next).then(() => setJevFeatures((current) => ({ ...current, recovery: next })));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (action === 4) {
|
|
481
|
+
const next = !jevFeatures.autonomy;
|
|
482
|
+
void setJevFeature("autonomy", next).then(() => setJevFeatures((current) => ({ ...current, autonomy: next })));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (action === 5) {
|
|
486
|
+
setJevCredentialInput("");
|
|
487
|
+
setJevCredentialError(undefined);
|
|
488
|
+
setJevCredentialMode(true);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (action === 6) {
|
|
492
|
+
void props.credentials.clear("jev").then(async () => {
|
|
493
|
+
await setJevEnabled(false);
|
|
494
|
+
setJevKey(undefined);
|
|
495
|
+
setJevEnabledState(false);
|
|
496
|
+
setJevPanel(false);
|
|
497
|
+
append("system", "Jev credential removed.");
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
const chooseModel = useCallback(async (next) => {
|
|
503
|
+
const nextKey = await props.credentials.get(next.provider);
|
|
504
|
+
if (!nextKey) {
|
|
505
|
+
setCredentialInput("");
|
|
506
|
+
setCredentialError(undefined);
|
|
507
|
+
setCredentialModel(next);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
await setModelSelection(next);
|
|
511
|
+
await setAutoModelRoutingEnabled(false);
|
|
512
|
+
setSelection(next);
|
|
513
|
+
setApiKey(nextKey);
|
|
514
|
+
setAutoModelRouting(false);
|
|
515
|
+
setRoutedSelection(undefined);
|
|
516
|
+
append("system", `Using ${next.provider}/${next.model}.`);
|
|
517
|
+
}, [append, props.credentials]);
|
|
518
|
+
const saveCredential = useCallback(async (value) => {
|
|
519
|
+
if (!credentialModel || savingCredential)
|
|
520
|
+
return;
|
|
521
|
+
const key = value.trim();
|
|
522
|
+
if (!key)
|
|
523
|
+
return setCredentialError("API key cannot be empty.");
|
|
524
|
+
setSavingCredential(true);
|
|
525
|
+
setCredentialError(undefined);
|
|
526
|
+
try {
|
|
527
|
+
await providerById(credentialModel.provider).validate(key);
|
|
528
|
+
await props.credentials.save(credentialModel.provider, key);
|
|
529
|
+
await setModelSelection(credentialModel);
|
|
530
|
+
await setAutoModelRoutingEnabled(false);
|
|
531
|
+
setSelection(credentialModel);
|
|
532
|
+
setApiKey(key);
|
|
533
|
+
setAutoModelRouting(false);
|
|
534
|
+
setRoutedSelection(undefined);
|
|
535
|
+
setCredentialInput("");
|
|
536
|
+
setCredentialModel(undefined);
|
|
537
|
+
append("system", `Using ${credentialModel.provider}/${credentialModel.model}.`);
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
setCredentialError(error.message);
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
setSavingCredential(false);
|
|
544
|
+
}
|
|
545
|
+
}, [append, credentialModel, props.credentials, savingCredential]);
|
|
546
|
+
useInput((inputKey, key) => {
|
|
547
|
+
if (!credentialModel || savingCredential)
|
|
548
|
+
return;
|
|
549
|
+
if (key.escape) {
|
|
550
|
+
setCredentialInput("");
|
|
551
|
+
setCredentialError(undefined);
|
|
552
|
+
setCredentialModel(undefined);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
useInput((inputKey, key) => {
|
|
556
|
+
if (!modelPicker)
|
|
557
|
+
return;
|
|
558
|
+
if (key.escape)
|
|
559
|
+
return setModelPicker(false);
|
|
560
|
+
if (key.upArrow)
|
|
561
|
+
return setModelIndex((current) => Math.max(0, current - 1));
|
|
562
|
+
if (key.downArrow)
|
|
563
|
+
return setModelIndex((current) => Math.min(models.length - 1, current + 1));
|
|
564
|
+
if (key.return) {
|
|
565
|
+
const next = models[modelIndex];
|
|
566
|
+
setModelPicker(false);
|
|
567
|
+
if (next)
|
|
568
|
+
void chooseModel(next);
|
|
569
|
+
}
|
|
570
|
+
if (/^[1-9]$/.test(inputKey)) {
|
|
571
|
+
const next = models[Number(inputKey) - 1];
|
|
572
|
+
if (next) {
|
|
573
|
+
setModelPicker(false);
|
|
574
|
+
void chooseModel(next);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
const runTask = useCallback(async (request, requestedMode) => {
|
|
579
|
+
// Render optimistically before routing can make a network request through Jev.
|
|
580
|
+
append("user", request);
|
|
581
|
+
setBusy(requestedMode === "plan" ? "planning" : "acting");
|
|
582
|
+
const interaction = requestedMode === "build" || autoModelRouting
|
|
583
|
+
? await classifyInteraction(request)
|
|
584
|
+
: undefined;
|
|
585
|
+
const taskMode = autoModelRouting && interaction ? autoInteractionMode(interaction.intent) : requestedMode;
|
|
586
|
+
if (autoModelRouting && taskMode !== mode)
|
|
587
|
+
setMode(taskMode);
|
|
588
|
+
setBusy(taskMode === "plan" ? "planning" : "acting");
|
|
589
|
+
if (interaction?.intent === "conversation") {
|
|
590
|
+
setBusy("idle");
|
|
591
|
+
return append("assistant", interaction.localResponse ?? "How can I help?");
|
|
592
|
+
}
|
|
593
|
+
if (!agent) {
|
|
594
|
+
setBusy("idle");
|
|
595
|
+
return append("error", "No active credential. Use /models to add a provider API key.");
|
|
596
|
+
}
|
|
597
|
+
const entryId = entries.length + 1;
|
|
598
|
+
setEntries((current) => [...current, { id: entryId, kind: "assistant", text: "" }]);
|
|
599
|
+
const onAgentText = (chunk) => {
|
|
600
|
+
if (chunk === "\n[Jev routed this request to a one-time read-only plan]\n") {
|
|
601
|
+
setMode("plan");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (/^\n\[Tool] [^\n]+\n$/.test(chunk) ||
|
|
605
|
+
chunk === "\n[Plan saved]\n" ||
|
|
606
|
+
chunk === "\n[Verification passed]\n" ||
|
|
607
|
+
chunk === "\n[Verification failed]\n")
|
|
608
|
+
return;
|
|
609
|
+
appendStream(entryId, chunk);
|
|
610
|
+
};
|
|
611
|
+
try {
|
|
612
|
+
const route = taskMode === "build" && interaction?.intent === "repository_task"
|
|
613
|
+
? await resolveTaskAgent(request)
|
|
614
|
+
: agent && { agent, selection, fallbacks: [] };
|
|
615
|
+
if (!route)
|
|
616
|
+
throw new Error("No credential is available for the selected model.");
|
|
617
|
+
let taskAgent = route.agent;
|
|
618
|
+
if (taskMode === "plan")
|
|
619
|
+
await taskAgent.plan(active.id, request, onAgentText);
|
|
620
|
+
else if (interaction?.intent === "answer")
|
|
621
|
+
await taskAgent.answer(active.id, request, onAgentText);
|
|
622
|
+
else {
|
|
623
|
+
try {
|
|
624
|
+
await taskAgent.run(active.id, request, onAgentText);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (!(error instanceof ProviderError) || error.category !== "quota")
|
|
628
|
+
throw error;
|
|
629
|
+
let latestError = error;
|
|
630
|
+
for (const fallback of route.fallbacks) {
|
|
631
|
+
append("system", `Automatic fallback: ${route.selection.provider}/${route.selection.model} reached its quota; switching to ${fallback.provider}/${fallback.model}.`);
|
|
632
|
+
taskAgent = props.createAgent(approval, { provider: fallback.provider, model: fallback.model }, fallback.apiKey, new JevDecisionProvider(jevKey), jevFeatures);
|
|
633
|
+
try {
|
|
634
|
+
await taskAgent.retryAfterProviderQuota(active.id, onAgentText);
|
|
635
|
+
latestError = undefined;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
catch (fallbackError) {
|
|
639
|
+
latestError = fallbackError;
|
|
640
|
+
if (!(fallbackError instanceof ProviderError) || fallbackError.category !== "quota")
|
|
641
|
+
throw fallbackError;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (latestError)
|
|
645
|
+
throw latestError;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const task = taskAgent.status(active.id);
|
|
649
|
+
setMode((current) => interactionModeAfterTask(current, task?.mode));
|
|
650
|
+
if (task?.changedFiles.length) {
|
|
651
|
+
setChangesIndex(0);
|
|
652
|
+
setChangesTask(task);
|
|
653
|
+
}
|
|
654
|
+
if (task?.mode === "planning" && task.plan)
|
|
655
|
+
append("system", formatPlan(task.plan));
|
|
656
|
+
if (task?.status === "cancelled")
|
|
657
|
+
append("system", "Task cancelled.");
|
|
658
|
+
setRepo(repositoryStatus(active.workspace));
|
|
659
|
+
setBusy("idle");
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
setRepo(repositoryStatus(active.workspace));
|
|
663
|
+
setBusy("idle");
|
|
664
|
+
// CodingAgent already rendered and persisted a terminal task failure. Avoid echoing it.
|
|
665
|
+
if (agent.status(active.id)?.status !== "failed")
|
|
666
|
+
append("error", `Kairo: ${error.message}`);
|
|
667
|
+
}
|
|
668
|
+
}, [
|
|
669
|
+
active.id,
|
|
670
|
+
active.workspace,
|
|
671
|
+
agent,
|
|
672
|
+
append,
|
|
673
|
+
appendStream,
|
|
674
|
+
approval,
|
|
675
|
+
autoModelRouting,
|
|
676
|
+
classifyInteraction,
|
|
677
|
+
entries.length,
|
|
678
|
+
jevFeatures,
|
|
679
|
+
jevKey,
|
|
680
|
+
props,
|
|
681
|
+
resolveTaskAgent,
|
|
682
|
+
selection,
|
|
683
|
+
mode,
|
|
684
|
+
]);
|
|
685
|
+
const submit = useCallback(async (value) => {
|
|
686
|
+
let line = value.trim();
|
|
687
|
+
if (!line || busy !== "idle" || pendingApproval)
|
|
688
|
+
return;
|
|
689
|
+
const selectedCommand = commandMatches[commandIndex];
|
|
690
|
+
if (selectedCommand && line !== selectedCommand.name) {
|
|
691
|
+
if ("acceptsArgument" in selectedCommand && selectedCommand.acceptsArgument) {
|
|
692
|
+
setInput(commandInput(selectedCommand));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
line = selectedCommand.name;
|
|
696
|
+
}
|
|
697
|
+
setInput("");
|
|
698
|
+
if (line === "/help") {
|
|
699
|
+
return append("system", slashCommands
|
|
700
|
+
.map((command) => `${command.name.padEnd(10)} ${command.description}`)
|
|
701
|
+
.join("\n"));
|
|
702
|
+
}
|
|
703
|
+
if (line === "/plan") {
|
|
704
|
+
const next = interactionModeAfterCommand(mode, line);
|
|
705
|
+
setMode(next);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (line === "/build") {
|
|
709
|
+
setMode("build");
|
|
710
|
+
const latest = agent?.status(active.id);
|
|
711
|
+
if (latest?.status === "planned" && latest.plan) {
|
|
712
|
+
append("system", "Building the saved plan.");
|
|
713
|
+
await runTask("Implement the saved plan and verify the result.", "build");
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (line === "/quit" || line === "/exit")
|
|
718
|
+
return exit();
|
|
719
|
+
if (line === "/new") {
|
|
720
|
+
const next = props.store.create(active.workspace);
|
|
721
|
+
setActive(next);
|
|
722
|
+
setMode("build");
|
|
723
|
+
setEntries([]);
|
|
724
|
+
setChangesTask(undefined);
|
|
725
|
+
setRepo(repositoryStatus(next.workspace));
|
|
726
|
+
return append("system", `New session: ${next.id} — BUILD mode.`);
|
|
727
|
+
}
|
|
728
|
+
if (line === "/history") {
|
|
729
|
+
return append("system", props.store
|
|
730
|
+
.list()
|
|
731
|
+
.map((item) => `${item.id} ${item.workspace}`)
|
|
732
|
+
.join("\n") || "No sessions.");
|
|
733
|
+
}
|
|
734
|
+
if (line === "/status" || line === "/changes") {
|
|
735
|
+
const task = agent?.status(active.id);
|
|
736
|
+
if (line === "/changes" && task?.changedFiles.length) {
|
|
737
|
+
setChangesIndex(0);
|
|
738
|
+
return setChangesTask(task);
|
|
739
|
+
}
|
|
740
|
+
return append("system", !task
|
|
741
|
+
? "No task has run in this session."
|
|
742
|
+
: line === "/changes"
|
|
743
|
+
? task.changedFiles.join("\n") || "No files changed."
|
|
744
|
+
: `${task.status} (${task.mode}): ${task.prompt}\n${formatMetrics(props.store.taskEvents(task.id))}`);
|
|
745
|
+
}
|
|
746
|
+
if (line === "/trace" || line.startsWith("/trace ")) {
|
|
747
|
+
const task = line === "/trace" ? agent?.status(active.id) : props.store.task(line.slice(7).trim());
|
|
748
|
+
return append("system", task && task.sessionId === active.id
|
|
749
|
+
? formatTrace(task, props.store.taskEvents(task.id))
|
|
750
|
+
: "Task not found in this session.");
|
|
751
|
+
}
|
|
752
|
+
if (line === "/compact")
|
|
753
|
+
return append("system", agent?.compact(active.id) ? "Context checkpoint saved." : "No task to compact.");
|
|
754
|
+
if (line === "/cancel") {
|
|
755
|
+
agent?.cancel(active.id);
|
|
756
|
+
setBusy("acting");
|
|
757
|
+
return append("system", "Task cancelled.");
|
|
758
|
+
}
|
|
759
|
+
if (line === "/resume" || line.startsWith("/resume ")) {
|
|
760
|
+
const sessionId = line.slice(7).trim();
|
|
761
|
+
if (sessionId) {
|
|
762
|
+
const next = props.store.get(sessionId);
|
|
763
|
+
if (!next)
|
|
764
|
+
return append("error", "Session not found.");
|
|
765
|
+
setActive(next);
|
|
766
|
+
setMode("build");
|
|
767
|
+
setRepo(repositoryStatus(next.workspace));
|
|
768
|
+
return append("system", `Resumed ${next.id} — BUILD mode.`);
|
|
769
|
+
}
|
|
770
|
+
if (!agent)
|
|
771
|
+
return append("error", "No active credential.");
|
|
772
|
+
setMode("build");
|
|
773
|
+
setBusy("acting");
|
|
774
|
+
try {
|
|
775
|
+
await agent.resume(active.id, (chunk) => append("assistant", chunk));
|
|
776
|
+
setBusy("idle");
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
setBusy("idle");
|
|
780
|
+
append("error", `Kairo: ${error.message}`);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (line.startsWith("/verify ")) {
|
|
785
|
+
if (!agent)
|
|
786
|
+
return append("error", "No active credential.");
|
|
787
|
+
setBusy("verifying");
|
|
788
|
+
try {
|
|
789
|
+
await agent.verify(active.id, line.slice(8).trim(), (chunk) => append("assistant", chunk));
|
|
790
|
+
setRepo(repositoryStatus(active.workspace));
|
|
791
|
+
setBusy("idle");
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
setBusy("idle");
|
|
795
|
+
append("error", `Kairo: ${error.message}`);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (line === "/auto") {
|
|
800
|
+
if (!autoModelRouting && (!jevEnabled || !jevKey))
|
|
801
|
+
return append("error", "Enable Jev and add its API key with /jev before enabling auto routing.");
|
|
802
|
+
const next = !autoModelRouting;
|
|
803
|
+
await setAutoModelRoutingEnabled(next);
|
|
804
|
+
setAutoModelRouting(next);
|
|
805
|
+
setRoutedSelection(undefined);
|
|
806
|
+
return append("system", next
|
|
807
|
+
? "Automatic Jev model routing enabled. Your selected model remains the fallback."
|
|
808
|
+
: `Manual model selection enabled: ${selection.provider}/${selection.model}.`);
|
|
809
|
+
}
|
|
810
|
+
if (line === "/models") {
|
|
811
|
+
const selected = models.findIndex((item) => item.provider === selection.provider && item.model === selection.model);
|
|
812
|
+
setModelIndex(Math.max(0, selected));
|
|
813
|
+
setModelPicker(true);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (line === "/jev") {
|
|
817
|
+
setJevCredentialInput("");
|
|
818
|
+
setJevCredentialError(undefined);
|
|
819
|
+
setJevCredentialMode(false);
|
|
820
|
+
setJevIndex(0);
|
|
821
|
+
setJevPanel(true);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (line === "/model" || line.startsWith("/model ")) {
|
|
825
|
+
const [provider, ...modelParts] = line.slice(6).trim().split(/\s+/);
|
|
826
|
+
if (!provider || !modelParts.length || !isProviderId(provider))
|
|
827
|
+
return append("system", `Current model: ${selection.provider}/${selection.model}\nUse: /model <gemini|groq|mistral> <model-id>`);
|
|
828
|
+
const next = { provider, model: modelParts.join(" ") };
|
|
829
|
+
await chooseModel(next);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (line === "/logout") {
|
|
833
|
+
await props.credentials.clear(selection.provider);
|
|
834
|
+
setApiKey(undefined);
|
|
835
|
+
return append("system", `${selection.provider} credential removed. Use /model with another logged-in provider.`);
|
|
836
|
+
}
|
|
837
|
+
await runTask(line, mode);
|
|
838
|
+
}, [
|
|
839
|
+
active,
|
|
840
|
+
agent,
|
|
841
|
+
append,
|
|
842
|
+
busy,
|
|
843
|
+
exit,
|
|
844
|
+
mode,
|
|
845
|
+
autoModelRouting,
|
|
846
|
+
jevEnabled,
|
|
847
|
+
jevKey,
|
|
848
|
+
pendingApproval,
|
|
849
|
+
props.credentials,
|
|
850
|
+
props.store,
|
|
851
|
+
runTask,
|
|
852
|
+
selection,
|
|
853
|
+
models,
|
|
854
|
+
chooseModel,
|
|
855
|
+
commandIndex,
|
|
856
|
+
commandMatches,
|
|
857
|
+
]);
|
|
858
|
+
useInput((inputKey, key) => {
|
|
859
|
+
if (changesTask || pendingApproval || modelPicker || jevPanel || !commandMatches.length)
|
|
860
|
+
return;
|
|
861
|
+
if (key.upArrow)
|
|
862
|
+
return setCommandIndex((current) => Math.max(0, current - 1));
|
|
863
|
+
if (key.downArrow)
|
|
864
|
+
return setCommandIndex((current) => Math.min(commandMatches.length - 1, current + 1));
|
|
865
|
+
if (key.tab) {
|
|
866
|
+
const selected = commandMatches[commandIndex];
|
|
867
|
+
if (!selected)
|
|
868
|
+
return;
|
|
869
|
+
if ("acceptsArgument" in selected && selected.acceptsArgument) {
|
|
870
|
+
setInput(commandInput(selected));
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
void submit(selected.name);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
const visibleEntries = entries.slice(-8);
|
|
878
|
+
const hiddenEntries = entries.length - visibleEntries.length;
|
|
879
|
+
const terminalHeight = process.stdout.rows ? Math.max(20, process.stdout.rows - 1) : undefined;
|
|
880
|
+
const activeModel = routedSelection ?? selection;
|
|
881
|
+
return (_jsxs(Box, { flexDirection: "column", minHeight: terminalHeight, children: [entries.length ? (_jsx(WorkspaceBar, { workspace: active.workspace, repo: repo, model: activeModel })) : null, _jsx(Box, { flexDirection: "column", flexGrow: 1, children: entries.length ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [hiddenEntries > 0 ? (_jsxs(Text, { color: colors.muted, children: [" \u2026 ", hiddenEntries, " earlier messages hidden"] })) : null, visibleEntries.map((entry) => (_jsx(TranscriptRow, { entry: entry }, entry.id)))] })) : (_jsx(EmptyState, { mode: mode, workspace: active.workspace, repo: repo })) }), changesTask ? (_jsx(ChangesPanel, { workspace: active.workspace, files: changesTask.changedFiles, selected: changesIndex })) : pendingApproval ? (_jsx(ApprovalCard, { pending: pendingApproval })) : credentialModel ? (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: [providerById(credentialModel.provider).name, " API key"] }), _jsx(Text, { color: "gray", children: "Saved securely in macOS Keychain." }), _jsx(TextInput, { value: credentialInput, onChange: (value) => {
|
|
882
|
+
setCredentialInput(value);
|
|
883
|
+
setCredentialError(undefined);
|
|
884
|
+
}, onSubmit: saveCredential, mask: "\u2022", placeholder: savingCredential ? "Validating…" : "Paste API key", focus: !savingCredential }), credentialError ? _jsx(Text, { color: "red", children: credentialError }) : null, _jsx(Text, { color: "gray", children: "Enter save \u00B7 Esc cancel" })] })) : jevPanel ? (_jsxs(Box, { borderStyle: "single", borderColor: "magenta", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Jev safety advisor \u00B7 ", jevEnabled ? "enabled" : "disabled"] }), _jsx(Text, { color: "gray", children: jevKey ? "TypeSafe key saved in macOS Keychain." : "No TypeSafe API key saved." }), jevCredentialMode ? (_jsxs(_Fragment, { children: [_jsx(TextInput, { value: jevCredentialInput, onChange: (value) => {
|
|
885
|
+
setJevCredentialInput(value);
|
|
886
|
+
setJevCredentialError(undefined);
|
|
887
|
+
}, onSubmit: saveJevCredential, mask: "\u2022", placeholder: savingJevCredential ? "Validating…" : "Paste TypeSafe API key", focus: !savingJevCredential }), jevCredentialError ? _jsx(Text, { color: "red", children: jevCredentialError }) : null, _jsx(Text, { color: "gray", children: "Enter save \u00B7 Esc cancel" })] })) : (_jsxs(_Fragment, { children: [[
|
|
888
|
+
`${jevEnabled ? "Disable" : "Enable"} advisor`,
|
|
889
|
+
`Task routing: ${jevFeatures.routing ? "on" : "off"}`,
|
|
890
|
+
`Safety context: ${jevFeatures.safety ? "on" : "off"}`,
|
|
891
|
+
`Recovery advice: ${jevFeatures.recovery ? "on" : "off"}`,
|
|
892
|
+
`Safe autonomy: ${jevFeatures.autonomy ? "on" : "off"}`,
|
|
893
|
+
"Add or replace API key",
|
|
894
|
+
"Remove API key",
|
|
895
|
+
].map((label, index) => (_jsxs(Text, { color: index === jevIndex ? "magenta" : undefined, children: [index === jevIndex ? "›" : " ", " ", index + 1, ". ", label] }, label))), _jsx(Text, { color: "gray", children: "\u2191/\u2193 choose \u00B7 Enter select \u00B7 Esc close" })] }))] })) : modelPicker ? (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Select a model" }), models.map((item, index) => (_jsxs(Text, { color: index === modelIndex ? "cyan" : undefined, children: [index === modelIndex ? "›" : " ", " ", index + 1, ". ", item.label, " \u2014 ", item.model] }, `${item.provider}/${item.model}`))), _jsx(Text, { color: "gray", children: "\u2191/\u2193 choose \u00B7 Enter select \u00B7 Esc cancel" })] })) : (_jsxs(Box, { flexDirection: "column", children: [commandMatches.length ? (_jsxs(Box, { borderStyle: "round", borderColor: colors.accent, flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsx(Text, { bold: true, color: colors.accent, children: "COMMANDS" }), commandMatches.map((command, index) => (_jsxs(Text, { color: index === commandIndex ? colors.accent : colors.textSoft, bold: index === commandIndex, children: [index === commandIndex ? "›" : " ", " ", command.name.padEnd(10), " ", command.description] }, command.name))), _jsx(Text, { color: colors.muted, children: "\u2191/\u2193 navigate \u00B7 Tab complete \u00B7 Enter run" })] })) : null, _jsxs(Box, { borderStyle: "round", borderColor: mode === "plan" ? colors.warning : colors.accentSoft, marginTop: 1, paddingX: 1, minHeight: 3, alignItems: "center", children: [_jsx(Text, { color: colors.accent, bold: true, children: ">" }), _jsx(Text, { children: " " }), _jsx(TextInput, { value: input, onChange: (value) => {
|
|
896
|
+
setInput(value);
|
|
897
|
+
setCommandIndex(0);
|
|
898
|
+
}, onSubmit: submit, placeholder: busy === "idle"
|
|
899
|
+
? mode === "plan"
|
|
900
|
+
? "Ask Kairo to inspect and plan…"
|
|
901
|
+
: "Ask Kairo to implement…"
|
|
902
|
+
: "Task in progress…", focus: busy === "idle" && !changesTask })] }), _jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsx(Box, { children: _jsx(Text, { color: colors.muted, children: "enter send \u00B7 / commands" }) }), _jsxs(Text, { color: colors.textSoft, children: [activeModel.provider, "/", activeModel.model] })] }), _jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(ModeBadge, { mode: mode }), _jsxs(Text, { color: colors.muted, children: [" ", "\u00B7 ", autoModelRouting ? "Auto routing" : "Manual model"] })] }), autoModelRouting || jevEnabled ? (_jsxs(Box, { children: [autoModelRouting ? _jsx(Text, { color: colors.accent, children: "AUTO" }) : null, autoModelRouting && jevEnabled ? _jsx(Text, { color: colors.muted, children: " \u00B7 " }) : null, jevEnabled ? _jsx(Text, { color: colors.jev, children: "JEV" }) : null] })) : null] })] }))] }));
|
|
903
|
+
}
|
|
904
|
+
/** Starts the Ink renderer and keeps the existing CLI composition boundary intact. */
|
|
905
|
+
export async function runTui(props) {
|
|
906
|
+
const key = await props.credentials.get(props.initialSelection.provider);
|
|
907
|
+
const jevKey = await props.credentials.get("jev");
|
|
908
|
+
const app = render(_jsx(KairoTui, { ...props, initialApiKey: key, initialJevKey: jevKey }));
|
|
909
|
+
await app.waitUntilExit();
|
|
910
|
+
}
|