kritya 0.8.2-beta
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 +434 -0
- package/dist/agent/budget.js +25 -0
- package/dist/agent/compactor.js +78 -0
- package/dist/agent/contextWarning.js +6 -0
- package/dist/agent/killSwitch.js +111 -0
- package/dist/agent/loop.js +543 -0
- package/dist/agent/memory.js +145 -0
- package/dist/agent/plugins.js +408 -0
- package/dist/agent/skills.js +210 -0
- package/dist/agent/skillsCli.js +51 -0
- package/dist/agent/systemPrompt.js +122 -0
- package/dist/agent/tokens.js +55 -0
- package/dist/agent/toolExecutor.js +331 -0
- package/dist/agent/workflow.js +496 -0
- package/dist/agent/worktree.js +116 -0
- package/dist/atomicWrite.js +109 -0
- package/dist/audit/audit.js +230 -0
- package/dist/audit/cli.js +135 -0
- package/dist/commands/custom.js +69 -0
- package/dist/commands/mcpCommand.js +433 -0
- package/dist/commands/pluginsCommand.js +204 -0
- package/dist/commands/registry.js +739 -0
- package/dist/config/config.js +214 -0
- package/dist/config/debug.js +19 -0
- package/dist/config/models.js +82 -0
- package/dist/config/retention.js +24 -0
- package/dist/config/winAcl.js +38 -0
- package/dist/crash.js +102 -0
- package/dist/engine.js +113 -0
- package/dist/git/git.js +46 -0
- package/dist/headless.js +274 -0
- package/dist/hooks/hooks.js +122 -0
- package/dist/index.js +670 -0
- package/dist/lsp/client.js +413 -0
- package/dist/lsp/manager.js +0 -0
- package/dist/lsp/registry.js +62 -0
- package/dist/mcp/callback.js +141 -0
- package/dist/mcp/client.js +945 -0
- package/dist/mcp/login.js +117 -0
- package/dist/mcp/oauth.js +345 -0
- package/dist/mcp/servers.js +114 -0
- package/dist/mcp/spawnWin.js +86 -0
- package/dist/mcp/tokens.js +97 -0
- package/dist/mcp/transport.js +295 -0
- package/dist/net/urlSafety.js +158 -0
- package/dist/permissions/danger.js +96 -0
- package/dist/permissions/permissions.js +62 -0
- package/dist/permissions/rules.js +69 -0
- package/dist/plugins/discover.js +108 -0
- package/dist/plugins/mcp.js +80 -0
- package/dist/provider/client.js +460 -0
- package/dist/provider/switchyardClient.js +46 -0
- package/dist/provider/switchyardSidecar.js +245 -0
- package/dist/provider/textToolCalls.js +140 -0
- package/dist/repomap/repoMap.js +168 -0
- package/dist/repomap/symbols.js +190 -0
- package/dist/session/store.js +328 -0
- package/dist/shell/background.js +131 -0
- package/dist/shell/sandbox.js +396 -0
- package/dist/telemetry/metrics.js +172 -0
- package/dist/telemetry/otlp.js +128 -0
- package/dist/telemetry/tracer.js +240 -0
- package/dist/tools/askUser.js +64 -0
- package/dist/tools/bg.js +59 -0
- package/dist/tools/common.js +158 -0
- package/dist/tools/deepResearch.js +126 -0
- package/dist/tools/diff.js +70 -0
- package/dist/tools/document/docx.js +44 -0
- package/dist/tools/document/pdf.js +190 -0
- package/dist/tools/document/pptx.js +148 -0
- package/dist/tools/document/types.js +26 -0
- package/dist/tools/document/xlsx.js +87 -0
- package/dist/tools/document.js +389 -0
- package/dist/tools/edit.js +56 -0
- package/dist/tools/fetchUrl.js +218 -0
- package/dist/tools/fuzzyMatch.js +74 -0
- package/dist/tools/glob.js +34 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/ignore.js +28 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/ls.js +27 -0
- package/dist/tools/lsp.js +239 -0
- package/dist/tools/notebook.js +215 -0
- package/dist/tools/read.js +32 -0
- package/dist/tools/repoMap.js +26 -0
- package/dist/tools/secretScan.js +143 -0
- package/dist/tools/shell.js +134 -0
- package/dist/tools/skills.js +69 -0
- package/dist/tools/subagent.js +57 -0
- package/dist/tools/tasks.js +43 -0
- package/dist/tools/webSearch.js +97 -0
- package/dist/tools/write.js +47 -0
- package/dist/tools/writeAgent.js +72 -0
- package/dist/trust/aiDisclosure.js +45 -0
- package/dist/trust/mcpTrust.js +111 -0
- package/dist/trust/trust.js +231 -0
- package/dist/types.js +1 -0
- package/dist/ui/AiDisclosurePrompt.js +23 -0
- package/dist/ui/App.js +449 -0
- package/dist/ui/Banner.js +71 -0
- package/dist/ui/ElicitationPrompt.js +46 -0
- package/dist/ui/Markdown.js +148 -0
- package/dist/ui/McpTrustPrompt.js +42 -0
- package/dist/ui/ModelPicker.js +19 -0
- package/dist/ui/PermissionPrompt.js +27 -0
- package/dist/ui/SelectList.js +24 -0
- package/dist/ui/Spinner.js +12 -0
- package/dist/ui/StatusLine.js +11 -0
- package/dist/ui/TranscriptItem.js +13 -0
- package/dist/ui/TrustPrompt.js +12 -0
- package/dist/ui/highlight.js +137 -0
- package/dist/ui/inline.js +241 -0
- package/dist/ui/mermaid.js +98 -0
- package/dist/ui/table.js +148 -0
- package/dist/ui/toolOutputPreview.js +34 -0
- package/dist/ui/useAgent.js +534 -0
- package/dist/ui/useKillSwitch.js +65 -0
- package/dist/ui/useSessionResume.js +39 -0
- package/dist/ui/useUsageBudget.js +149 -0
- package/dist/ui/viewport.js +71 -0
- package/dist/undo/undo.js +293 -0
- package/dist/version.js +4 -0
- package/package.json +122 -0
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import fg from "fast-glob";
|
|
7
|
+
import stringWidth from "string-width";
|
|
8
|
+
import { gitDiffStat } from "../git/git.js";
|
|
9
|
+
import { modelDisplaySlug } from "../config/models.js";
|
|
10
|
+
import { SessionStore } from "../session/store.js";
|
|
11
|
+
import { resolveSafe } from "../tools/common.js";
|
|
12
|
+
import { loadIgnorePatterns } from "../tools/ignore.js";
|
|
13
|
+
import { Markdown } from "./Markdown.js";
|
|
14
|
+
import { ModelPicker } from "./ModelPicker.js";
|
|
15
|
+
import { ElicitationPrompt } from "./ElicitationPrompt.js";
|
|
16
|
+
import { PermissionPrompt } from "./PermissionPrompt.js";
|
|
17
|
+
import { SelectList } from "./SelectList.js";
|
|
18
|
+
import { Spinner } from "./Spinner.js";
|
|
19
|
+
import { StatusLine } from "./StatusLine.js";
|
|
20
|
+
import { TranscriptItem } from "./TranscriptItem.js";
|
|
21
|
+
import { tailForViewport, terminalColumns, terminalRows } from "./viewport.js";
|
|
22
|
+
import { BUILTIN_COMMANDS, runCommand } from "../commands/registry.js";
|
|
23
|
+
import { mcpPrompts, mcpResources } from "../mcp/client.js";
|
|
24
|
+
import { useAgent } from "./useAgent.js";
|
|
25
|
+
const MENTION_RE = /(^|\s)@([^\s@]*)$/;
|
|
26
|
+
const MENTION_ALL_RE = /(?:^|\s)@([^\s@]+)/g;
|
|
27
|
+
const MAX_MENTION_CHARS = 8000;
|
|
28
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
29
|
+
// Erase screen + scrollback, then home the cursor (same sequence Ink itself
|
|
30
|
+
// uses for its own "content taller than the terminal" clear path).
|
|
31
|
+
const CLEAR_TERMINAL = "\x1b[2J\x1b[3J\x1b[H";
|
|
32
|
+
export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, }) {
|
|
33
|
+
const { exit } = useApp();
|
|
34
|
+
const { stdout } = useStdout();
|
|
35
|
+
const [input, setInput] = useState("");
|
|
36
|
+
const [inputKey, setInputKey] = useState(0);
|
|
37
|
+
const [steerInput, setSteerInput] = useState("");
|
|
38
|
+
const [resumeFilter, setResumeFilter] = useState("");
|
|
39
|
+
const [cmdIndex, setCmdIndex] = useState(0);
|
|
40
|
+
const [fileIndex, setFileIndex] = useState(0);
|
|
41
|
+
const [fileList, setFileList] = useState([]);
|
|
42
|
+
const [verbose, setVerbose] = useState(false);
|
|
43
|
+
const [elapsed, setElapsed] = useState(0);
|
|
44
|
+
const [staticKey, setStaticKey] = useState(0);
|
|
45
|
+
const inputHistory = useRef([]);
|
|
46
|
+
const histIndex = useRef(-1); // -1 means "current, not browsing history"
|
|
47
|
+
/**
|
|
48
|
+
* Ctrl-chord bookkeeping for the text inputs.
|
|
49
|
+
*
|
|
50
|
+
* Ink delivers a keypress to every `useInput` hook, and ink-text-input
|
|
51
|
+
* ignores only Ctrl+C — so Ctrl+K and Ctrl+O run their shortcut *and* get
|
|
52
|
+
* typed into the box as a bare "k"/"o". Hitting the kill switch left junk on
|
|
53
|
+
* the prompt line, which the next Enter would have sent as a message.
|
|
54
|
+
*
|
|
55
|
+
* The two hooks can fire in either order, so both directions are covered:
|
|
56
|
+
* `ctrlChord` makes onChange drop the echoed letter when the shortcut ran
|
|
57
|
+
* first, and re-setting the value from `inputRef` undoes it when the input
|
|
58
|
+
* ran first. `inputRef` tracks committed state, so it always holds the value
|
|
59
|
+
* from before this keypress.
|
|
60
|
+
*/
|
|
61
|
+
const ctrlChord = useRef(null);
|
|
62
|
+
const inputRef = useRef("");
|
|
63
|
+
const steerInputRef = useRef("");
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
inputRef.current = input;
|
|
66
|
+
}, [input]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
steerInputRef.current = steerInput;
|
|
69
|
+
}, [steerInput]);
|
|
70
|
+
const noteCtrlChord = (letter) => {
|
|
71
|
+
ctrlChord.current = letter;
|
|
72
|
+
setInput(inputRef.current);
|
|
73
|
+
setSteerInput(steerInputRef.current);
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
ctrlChord.current = null;
|
|
76
|
+
}, 0);
|
|
77
|
+
};
|
|
78
|
+
/** The change a Ctrl chord echoed into the box, rather than a real keystroke. */
|
|
79
|
+
const isCtrlEcho = (prev, next) => ctrlChord.current !== null && next === prev + ctrlChord.current;
|
|
80
|
+
// Terminals that reflow their buffer on resize (e.g. VS Code's xterm.js)
|
|
81
|
+
// rewrap already-printed full-width lines (banner art, box borders) to the
|
|
82
|
+
// new column count on their own, before Ink's next redraw arrives. Ink then
|
|
83
|
+
// erases based on a stale line count from before that reflow, so it clears
|
|
84
|
+
// the wrong number of rows and leaves stale copies of boxes/banners behind
|
|
85
|
+
// — compounding on every resize. There's no way to reflow in place safely,
|
|
86
|
+
// so once the column count actually settles on a new value (ignoring
|
|
87
|
+
// height-only resizes, e.g. VS Code adding a split terminal), wipe the
|
|
88
|
+
// screen ourselves and remount <Static> so Ink reprints everything fresh
|
|
89
|
+
// onto a blank canvas. The manual clear is safe here specifically because
|
|
90
|
+
// the forced remount immediately triggers Ink's own static-flush path,
|
|
91
|
+
// which resyncs its internal line-count bookkeeping — it's only unsafe to
|
|
92
|
+
// write raw ANSI without following it with something that resets that
|
|
93
|
+
// bookkeeping.
|
|
94
|
+
const lastColumns = useRef(stdout?.columns);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!stdout)
|
|
97
|
+
return;
|
|
98
|
+
let timer;
|
|
99
|
+
const onResize = () => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
timer = setTimeout(() => {
|
|
102
|
+
if (stdout.columns === lastColumns.current)
|
|
103
|
+
return;
|
|
104
|
+
lastColumns.current = stdout.columns;
|
|
105
|
+
stdout.write(CLEAR_TERMINAL);
|
|
106
|
+
setStaticKey((k) => k + 1);
|
|
107
|
+
}, 150);
|
|
108
|
+
};
|
|
109
|
+
stdout.on("resize", onResize);
|
|
110
|
+
return () => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
stdout.off("resize", onResize);
|
|
113
|
+
};
|
|
114
|
+
}, [stdout]);
|
|
115
|
+
const refreshFileList = useCallback(() => {
|
|
116
|
+
fg("**/*", {
|
|
117
|
+
cwd: workspace,
|
|
118
|
+
dot: false,
|
|
119
|
+
onlyFiles: true,
|
|
120
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", ...loadIgnorePatterns(workspace)],
|
|
121
|
+
suppressErrors: true,
|
|
122
|
+
})
|
|
123
|
+
.then((files) => setFileList(files.sort().slice(0, 2000)))
|
|
124
|
+
.catch(() => { });
|
|
125
|
+
}, [workspace]);
|
|
126
|
+
useEffect(refreshFileList, [refreshFileList]);
|
|
127
|
+
const { items, addItem, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, workflow, refreshWorkflow, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, autoApprovedCount, cycleMode, onAcceptEditsConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, } = useAgent({
|
|
128
|
+
agent,
|
|
129
|
+
workspace,
|
|
130
|
+
modelRef,
|
|
131
|
+
providerRef,
|
|
132
|
+
config,
|
|
133
|
+
uiBridge,
|
|
134
|
+
resumedCount,
|
|
135
|
+
initialTasks,
|
|
136
|
+
resumeSessions,
|
|
137
|
+
refreshFileList,
|
|
138
|
+
onSwitchClient,
|
|
139
|
+
});
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
onRequestPermissionReady?.(requestPermission);
|
|
142
|
+
}, [onRequestPermissionReady, requestPermission]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
onRequestElicitationReady?.(requestElicitation);
|
|
145
|
+
}, [onRequestElicitationReady, requestElicitation]);
|
|
146
|
+
// Tick an elapsed-seconds counter while the agent is working.
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (phase !== "working") {
|
|
149
|
+
setElapsed(0);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
const t = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1000)), 1000);
|
|
154
|
+
return () => clearInterval(t);
|
|
155
|
+
}, [phase]);
|
|
156
|
+
const allCommands = [
|
|
157
|
+
...BUILTIN_COMMANDS,
|
|
158
|
+
...customCommands.map((c) => ({ name: c.name, description: c.description })),
|
|
159
|
+
// Servers can contribute slash commands too (MCP prompts).
|
|
160
|
+
...mcpPrompts().map((p) => ({ name: p.command, description: p.description })),
|
|
161
|
+
];
|
|
162
|
+
// Command suggestions while typing a slash command (before any arguments).
|
|
163
|
+
const suggestions = phase === "input" && input.startsWith("/") && !input.includes(" ")
|
|
164
|
+
? allCommands.filter((c) => c.name.startsWith(input.trim()))
|
|
165
|
+
: [];
|
|
166
|
+
const selectedCmd = suggestions.length ? Math.min(cmdIndex, suggestions.length - 1) : 0;
|
|
167
|
+
// File suggestions while typing an @mention.
|
|
168
|
+
const mentionMatch = phase === "input" && !input.startsWith("/") ? MENTION_RE.exec(input) : null;
|
|
169
|
+
const mentionFragment = mentionMatch ? mentionMatch[2].toLowerCase() : null;
|
|
170
|
+
const fileSuggestions = mentionFragment !== null
|
|
171
|
+
? [...fileList, ...mcpResources().map((r) => r.mention)]
|
|
172
|
+
.filter((f) => f.toLowerCase().includes(mentionFragment))
|
|
173
|
+
.slice(0, 8)
|
|
174
|
+
: [];
|
|
175
|
+
const selectedFile = fileSuggestions.length ? Math.min(fileIndex, fileSuggestions.length - 1) : 0;
|
|
176
|
+
const completeMention = (file) => {
|
|
177
|
+
if (!mentionMatch)
|
|
178
|
+
return;
|
|
179
|
+
const head = input.slice(0, mentionMatch.index + mentionMatch[1].length);
|
|
180
|
+
setInput(`${head}@${file} `);
|
|
181
|
+
setInputKey((k) => k + 1);
|
|
182
|
+
setFileIndex(0);
|
|
183
|
+
};
|
|
184
|
+
useInput((_input, key) => {
|
|
185
|
+
// Ctrl+K is the kill switch, and it comes before every other binding and
|
|
186
|
+
// phase check on purpose: a panic button that only works from the idle
|
|
187
|
+
// input line is not a panic button. It fires mid-stream, mid-tool, and
|
|
188
|
+
// while a permission prompt is on screen.
|
|
189
|
+
if (key.ctrl && _input === "k") {
|
|
190
|
+
noteCtrlChord(_input);
|
|
191
|
+
if (killed)
|
|
192
|
+
return; // already stopped; /kill off is the way back
|
|
193
|
+
engageKill("Ctrl+K");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (key.escape && phase === "working") {
|
|
197
|
+
abortRef.current?.abort();
|
|
198
|
+
}
|
|
199
|
+
// Ctrl+O toggles showing full tool output.
|
|
200
|
+
if (key.ctrl && _input === "o") {
|
|
201
|
+
noteCtrlChord(_input);
|
|
202
|
+
setVerbose((v) => !v);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Shift+Tab cycles normal → accept-edits → plan → normal. Only from the
|
|
206
|
+
// plain input state — not mid-permission-prompt, mid-resume-picker, etc.
|
|
207
|
+
if (key.tab && key.shift && phase === "input") {
|
|
208
|
+
cycleMode();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// First-time accept-edits confirmation: Yes proceeds, anything else cancels.
|
|
212
|
+
if (phase === "confirmMode") {
|
|
213
|
+
const c = _input.toLowerCase();
|
|
214
|
+
if (c === "y" || key.return)
|
|
215
|
+
onAcceptEditsConfirm(true);
|
|
216
|
+
else if (c === "n" || key.escape)
|
|
217
|
+
onAcceptEditsConfirm(false);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// History recall with ↑/↓ when no autocomplete popup is open.
|
|
221
|
+
if (phase === "input" && !suggestions.length && !fileSuggestions.length) {
|
|
222
|
+
const hist = inputHistory.current;
|
|
223
|
+
if (key.upArrow && hist.length) {
|
|
224
|
+
histIndex.current =
|
|
225
|
+
histIndex.current === -1 ? hist.length - 1 : Math.max(0, histIndex.current - 1);
|
|
226
|
+
setInput(hist[histIndex.current]);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (key.downArrow && histIndex.current !== -1) {
|
|
230
|
+
histIndex.current += 1;
|
|
231
|
+
if (histIndex.current >= hist.length) {
|
|
232
|
+
histIndex.current = -1;
|
|
233
|
+
setInput("");
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setInput(hist[histIndex.current]);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (phase === "resume") {
|
|
242
|
+
if (key.backspace || key.delete)
|
|
243
|
+
setResumeFilter((f) => f.slice(0, -1));
|
|
244
|
+
else if (_input && !key.upArrow && !key.downArrow && !key.return && !key.escape && !key.tab) {
|
|
245
|
+
setResumeFilter((f) => f + _input);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (suggestions.length) {
|
|
249
|
+
if (key.upArrow)
|
|
250
|
+
setCmdIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
|
|
251
|
+
else if (key.downArrow)
|
|
252
|
+
setCmdIndex((i) => (i + 1) % suggestions.length);
|
|
253
|
+
else if (key.tab) {
|
|
254
|
+
setInput(suggestions[selectedCmd].name + " ");
|
|
255
|
+
setInputKey((k) => k + 1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (fileSuggestions.length) {
|
|
259
|
+
if (key.upArrow)
|
|
260
|
+
setFileIndex((i) => (i - 1 + fileSuggestions.length) % fileSuggestions.length);
|
|
261
|
+
else if (key.downArrow)
|
|
262
|
+
setFileIndex((i) => (i + 1) % fileSuggestions.length);
|
|
263
|
+
else if (key.tab)
|
|
264
|
+
completeMention(fileSuggestions[selectedFile]);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const handleSlash = (raw) => {
|
|
268
|
+
const [cmd, ...rest] = raw.trim().split(/\s+/);
|
|
269
|
+
const arg = rest.join(" ");
|
|
270
|
+
const ctx = {
|
|
271
|
+
arg,
|
|
272
|
+
raw,
|
|
273
|
+
agent,
|
|
274
|
+
workspace,
|
|
275
|
+
config,
|
|
276
|
+
undoStack,
|
|
277
|
+
customCommands,
|
|
278
|
+
mcpToolCount,
|
|
279
|
+
planMode,
|
|
280
|
+
acceptEdits,
|
|
281
|
+
setAcceptEdits,
|
|
282
|
+
tokenBudget,
|
|
283
|
+
budgetPct,
|
|
284
|
+
budgetUsed,
|
|
285
|
+
budgetStopped,
|
|
286
|
+
resetBudget,
|
|
287
|
+
setBudgetLimit,
|
|
288
|
+
addItem,
|
|
289
|
+
setPhase,
|
|
290
|
+
setActivity,
|
|
291
|
+
setRunningPhase,
|
|
292
|
+
refreshWorkflow,
|
|
293
|
+
setCtxPct,
|
|
294
|
+
setTasks,
|
|
295
|
+
setPlanMode,
|
|
296
|
+
killed,
|
|
297
|
+
killReason,
|
|
298
|
+
engageKill,
|
|
299
|
+
releaseKill,
|
|
300
|
+
setModelEverywhere,
|
|
301
|
+
provider,
|
|
302
|
+
model,
|
|
303
|
+
setProviderEverywhere,
|
|
304
|
+
refreshFileList,
|
|
305
|
+
runAgent,
|
|
306
|
+
runWebSearch,
|
|
307
|
+
expandMentions,
|
|
308
|
+
costReport,
|
|
309
|
+
gitDiffStat,
|
|
310
|
+
exit,
|
|
311
|
+
};
|
|
312
|
+
void runCommand(cmd, ctx);
|
|
313
|
+
};
|
|
314
|
+
const expandMentions = async (text) => {
|
|
315
|
+
const mentions = [...new Set([...text.matchAll(MENTION_ALL_RE)].map((m) => m[1]))];
|
|
316
|
+
let extra = "";
|
|
317
|
+
for (const p of mentions) {
|
|
318
|
+
if (IMAGE_RE.test(p))
|
|
319
|
+
continue; // images are attached separately, not inlined as text
|
|
320
|
+
// An @mcp:… mention names a document a server offers rather than a file
|
|
321
|
+
// on disk, so it's fetched instead of read — and marked as external,
|
|
322
|
+
// since a server wrote it.
|
|
323
|
+
const resource = mcpResources().find((r) => r.mention === p);
|
|
324
|
+
if (resource) {
|
|
325
|
+
try {
|
|
326
|
+
const body = await resource.read();
|
|
327
|
+
const capped = body.length > MAX_MENTION_CHARS
|
|
328
|
+
? body.slice(0, MAX_MENTION_CHARS) + "\n… (truncated)"
|
|
329
|
+
: body;
|
|
330
|
+
extra +=
|
|
331
|
+
`\n\n[Attached MCP resource: ${resource.uri} from server "${resource.server}" — ` +
|
|
332
|
+
`external content]\n\`\`\`\n${capped}\n\`\`\``;
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
extra += `\n\n[MCP resource ${p} could not be read: ${err instanceof Error ? err.message : String(err)}]`;
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const abs = resolveSafe(workspace, p);
|
|
341
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
342
|
+
const capped = content.length > MAX_MENTION_CHARS
|
|
343
|
+
? content.slice(0, MAX_MENTION_CHARS) + "\n… (truncated)"
|
|
344
|
+
: content;
|
|
345
|
+
extra += `\n\n[Attached file: ${p}]\n\`\`\`\n${capped}\n\`\`\``;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Not a real file — leave the @word as-is.
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return text + extra;
|
|
352
|
+
};
|
|
353
|
+
// Collect @-mentioned image files as data URIs for vision-capable models.
|
|
354
|
+
const collectImages = (text) => {
|
|
355
|
+
const images = [];
|
|
356
|
+
for (const p of new Set([...text.matchAll(MENTION_ALL_RE)].map((m) => m[1]))) {
|
|
357
|
+
if (!IMAGE_RE.test(p))
|
|
358
|
+
continue;
|
|
359
|
+
try {
|
|
360
|
+
const abs = resolveSafe(workspace, p);
|
|
361
|
+
const b64 = fs.readFileSync(abs).toString("base64");
|
|
362
|
+
const ext = p.split(".").pop().toLowerCase();
|
|
363
|
+
const mime = ext === "jpg" ? "jpeg" : ext;
|
|
364
|
+
images.push(`data:image/${mime};base64,${b64}`);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Not a readable image — skip.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return images;
|
|
371
|
+
};
|
|
372
|
+
const handleSubmit = async (value) => {
|
|
373
|
+
// Enter while command suggestions are open selects the highlighted command instead of sending.
|
|
374
|
+
if (suggestions.length && value.trim() !== suggestions[selectedCmd].name) {
|
|
375
|
+
setInput(suggestions[selectedCmd].name + " ");
|
|
376
|
+
setInputKey((k) => k + 1);
|
|
377
|
+
setCmdIndex(0);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Enter while file suggestions are open completes the mention instead of sending.
|
|
381
|
+
if (fileSuggestions.length && mentionFragment !== fileSuggestions[selectedFile].toLowerCase()) {
|
|
382
|
+
completeMention(fileSuggestions[selectedFile]);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const text = value.trim();
|
|
386
|
+
setInput("");
|
|
387
|
+
setCmdIndex(0);
|
|
388
|
+
setFileIndex(0);
|
|
389
|
+
histIndex.current = -1;
|
|
390
|
+
if (!text)
|
|
391
|
+
return;
|
|
392
|
+
if (inputHistory.current[inputHistory.current.length - 1] !== text) {
|
|
393
|
+
inputHistory.current.push(text);
|
|
394
|
+
}
|
|
395
|
+
if (text.startsWith("/")) {
|
|
396
|
+
handleSlash(text);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
addItem({ kind: "user", text });
|
|
400
|
+
const images = collectImages(text);
|
|
401
|
+
if (images.length)
|
|
402
|
+
addItem({ kind: "info", text: `Attached ${images.length} image(s).` });
|
|
403
|
+
await runAgent(await expandMentions(text), images);
|
|
404
|
+
};
|
|
405
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item) => _jsx(TranscriptItem, { item: item, verbose: verbose, stdout: stdout }, item.id) }, staticKey), stream ? (_jsx(Box, { marginBottom: 1, children: _jsx(Markdown, { text: tailForViewport(stream, terminalColumns(stdout), terminalRows(stdout)), streaming: true }) })) : null, tasks.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, width: Math.min(terminalColumns(stdout) - 2, Math.max(...tasks.map((t) => stringWidth(t.text))) + 6), children: tasks.map((t, i) => (_jsxs(Text, { color: t.status === "done" ? "green" : t.status === "in_progress" ? "yellow" : undefined, dimColor: t.status === "pending", children: [t.status === "done" ? "☑" : t.status === "in_progress" ? "◐" : "☐", " ", t.text] }, i))) })), phase === "working" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Spinner, { label:
|
|
406
|
+
// The workflow prefix rides on every branch, not just "thinking":
|
|
407
|
+
// during a phase you want to know which phase a tool call belongs
|
|
408
|
+
// to just as much as which phase is being reasoned about.
|
|
409
|
+
(runningPhase ? `${workflow?.name ?? ""} · ${runningPhase} phase · ` : "") +
|
|
410
|
+
(inFlight.length > 1
|
|
411
|
+
? `${inFlight.length} tools running (Esc to cancel)`
|
|
412
|
+
: inFlight.length === 1
|
|
413
|
+
? `${inFlight[0].status ? `${inFlight[0].summary} — ${inFlight[0].status}` : inFlight[0].summary} (Esc to cancel)`
|
|
414
|
+
: activity
|
|
415
|
+
? activity
|
|
416
|
+
: thinking
|
|
417
|
+
? "thinking… (Esc to cancel)"
|
|
418
|
+
: "working… (Esc to cancel)") }), inFlight.length > 1 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: inFlight.map((t) => (_jsxs(Text, { dimColor: true, children: ["\u00B7 ", t.status ? `${t.summary} — ${t.status}` : t.summary] }, t.id))) })), _jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", children: "\u21B3 " }), _jsx(TextInput, { value: steerInput, onChange: (v) => setSteerInput((prev) => (isCtrlEcho(prev, v) ? prev : v)), onSubmit: (value) => {
|
|
419
|
+
const text = value.trim();
|
|
420
|
+
setSteerInput("");
|
|
421
|
+
if (!text)
|
|
422
|
+
return;
|
|
423
|
+
if (text.startsWith("/")) {
|
|
424
|
+
addItem({
|
|
425
|
+
kind: "info",
|
|
426
|
+
text: "Commands are unavailable while the agent is working — press Esc to interrupt first.",
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
addItem({ kind: "user", text: `${text} (queued)` });
|
|
431
|
+
void expandMentions(text).then((t) => agent.queueSteer(t));
|
|
432
|
+
}, placeholder: "steer the agent\u2026 (Enter to queue)" })] })] })), phase === "permission" && permission && (_jsx(PermissionPrompt, { toolName: permission.toolName, summary: permission.summary, diff: permission.diff, warning: permission.warning, onDecision: onPermissionDecision })), phase === "elicitation" && elicitation && (_jsx(ElicitationPrompt, { message: elicitation.message, fields: elicitation.fields, onDecision: onElicitationDecision })), phase === "confirmMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Switch to accept-edits mode?" }), _jsx(Text, { children: "File writes and edits will auto-approve without asking." }), _jsx(Text, { dimColor: true, children: "Destructive shell commands (rm -rf, force-push, etc.) always still ask, in every mode." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again moves to dry-run mode; once more back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
433
|
+
setPhase("input");
|
|
434
|
+
setModelEverywhere(id);
|
|
435
|
+
}, onCancel: () => setPhase("input") })), phase === "resume" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: ["Resume a session ", _jsx(Text, { dimColor: true, children: "(type to search \u00B7 Esc for a fresh one)" })] }), resumeFilter ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "filter: " }), resumeFilter] })) : null, _jsx(SelectList, { items: (resumeSessions ?? [])
|
|
436
|
+
.filter((s) => s.title.toLowerCase().includes(resumeFilter.toLowerCase()) ||
|
|
437
|
+
SessionStore.matchesContent(s.file, resumeFilter))
|
|
438
|
+
.map((s) => ({
|
|
439
|
+
label: s.title,
|
|
440
|
+
value: s.file,
|
|
441
|
+
hint: `${s.date} · ${s.count} msgs`,
|
|
442
|
+
})), onSelect: onResumeSelect, onCancel: () => onResumeSelect("") })] })), phase === "input" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { color: "green", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: (v) => {
|
|
443
|
+
setInput((prev) => (isCtrlEcho(prev, v) ? prev : v.replace(/\t/g, "")));
|
|
444
|
+
setCmdIndex(0);
|
|
445
|
+
setFileIndex(0);
|
|
446
|
+
}, onSubmit: handleSubmit }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [suggestions.map((c, i) => (_jsxs(Text, { color: i === selectedCmd ? "cyan" : undefined, children: [i === selectedCmd ? "❯ " : " ", _jsx(Text, { bold: i === selectedCmd, children: c.name }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", c.description] })] }, c.name))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter select \u00B7 Enter again to run" })] })), fileSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [fileSuggestions.map((f, i) => (_jsxs(Text, { color: i === selectedFile ? "cyan" : undefined, children: [i === selectedFile ? "❯ " : " ", f] }, f))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter attach file" })] }))] })), _jsx(Box, { children: _jsx(StatusLine, { killed: killed, killReason: killReason, dryRunMode: dryRunMode, planMode: planMode, acceptEdits: acceptEdits, autoApprovedCount: autoApprovedCount, model: servedModel && servedModel !== model
|
|
447
|
+
? `${model}-${modelDisplaySlug(servedModel)}`
|
|
448
|
+
: model, workflow: workflow, branch: branch, tasks: tasks, ctxPct: ctxPct, budgetPct: budgetPct, budgetStopped: budgetStopped, phase: phase, elapsed: elapsed, totalUsage: totalUsage, totalCost: totalCost, verbose: verbose, workspace: workspace }) })] }));
|
|
449
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { terminalColumns } from "./viewport.js";
|
|
4
|
+
// Dot-matrix glyphs (7 rows). "#" is an on pixel, "." is off.
|
|
5
|
+
const GLYPHS = {
|
|
6
|
+
K: ["#....#", "#...#.", "#..#..", "###...", "#..#..", "#...#.", "#....#"],
|
|
7
|
+
R: ["#####.", "#....#", "#....#", "#####.", "#..#..", "#...#.", "#....#"],
|
|
8
|
+
T: ["######", "..##..", "..##..", "..##..", "..##..", "..##..", "..##.."],
|
|
9
|
+
Y: ["#....#", "#....#", ".#..#.", "..##..", "..##..", "..##..", "..##.."],
|
|
10
|
+
A: [".####.", "#....#", "#....#", "######", "#....#", "#....#", "#....#"],
|
|
11
|
+
I: ["####", ".##.", ".##.", ".##.", ".##.", ".##.", "####"],
|
|
12
|
+
"-": ["....", "....", "....", "####", "....", "....", "...."],
|
|
13
|
+
};
|
|
14
|
+
const ROWS = 7;
|
|
15
|
+
export function bannerLines(text, pixel) {
|
|
16
|
+
const off = " ".repeat(pixel.length);
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (let row = 0; row < ROWS; row++) {
|
|
19
|
+
lines.push([...text]
|
|
20
|
+
.map((ch) => (GLYPHS[ch] ?? GLYPHS["-"])[row]
|
|
21
|
+
.split("")
|
|
22
|
+
.map((p) => (p === "#" ? pixel : off))
|
|
23
|
+
.join(""))
|
|
24
|
+
.join(off));
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
/** Column (within a bannerLines() row) where each character of `text` starts. */
|
|
29
|
+
function letterOffsets(text, pixelLen) {
|
|
30
|
+
const offsets = [];
|
|
31
|
+
let col = 0;
|
|
32
|
+
[...text].forEach((ch, i) => {
|
|
33
|
+
if (i > 0)
|
|
34
|
+
col += pixelLen;
|
|
35
|
+
offsets.push(col);
|
|
36
|
+
col += (GLYPHS[ch] ?? GLYPHS["-"])[0].length * pixelLen;
|
|
37
|
+
});
|
|
38
|
+
return offsets;
|
|
39
|
+
}
|
|
40
|
+
/** Width (in bannerLines() columns) of one glyph. */
|
|
41
|
+
function letterWidth(ch, pixelLen) {
|
|
42
|
+
return (GLYPHS[ch] ?? GLYPHS["-"])[0].length * pixelLen;
|
|
43
|
+
}
|
|
44
|
+
// Vertical gradient: cyan at the top fading into NVIDIA green.
|
|
45
|
+
const TOP_RGB = [0x00, 0xd9, 0xff];
|
|
46
|
+
const BOTTOM_RGB = [0x76, 0xb9, 0x00];
|
|
47
|
+
function rowColor(row) {
|
|
48
|
+
const t = row / (ROWS - 1);
|
|
49
|
+
const [r, g, b] = TOP_RGB.map((c, i) => Math.round(c + (BOTTOM_RGB[i] - c) * t));
|
|
50
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
51
|
+
}
|
|
52
|
+
const BETA_LABEL = "beta";
|
|
53
|
+
export function Banner({ subtitle }) {
|
|
54
|
+
const columns = terminalColumns(process.stdout);
|
|
55
|
+
const wide = bannerLines("KRITYA", "░░");
|
|
56
|
+
const narrow = bannerLines("KRITYA", "░");
|
|
57
|
+
const pixelLen = wide[0].length <= columns ? 2 : narrow[0].length <= columns ? 1 : 0;
|
|
58
|
+
const lines = pixelLen === 2 ? wide : pixelLen === 1 ? narrow : null;
|
|
59
|
+
const center = (text) => " ".repeat(Math.max(0, Math.floor((columns - text.length) / 2))) + text;
|
|
60
|
+
// Right-align "beta" under the final "A" of KRITYA, one line below the glyph.
|
|
61
|
+
const betaLine = (() => {
|
|
62
|
+
if (!lines)
|
|
63
|
+
return null;
|
|
64
|
+
const leftPad = Math.max(0, Math.floor((columns - lines[0].length) / 2));
|
|
65
|
+
const aStart = letterOffsets("KRITYA", pixelLen)[5];
|
|
66
|
+
const aWidth = letterWidth("A", pixelLen);
|
|
67
|
+
const indent = leftPad + aStart + Math.max(0, aWidth - BETA_LABEL.length);
|
|
68
|
+
return " ".repeat(indent) + BETA_LABEL;
|
|
69
|
+
})();
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 2, marginBottom: 1, children: [lines ? (lines.map((line, i) => (_jsx(Text, { color: rowColor(i), children: center(line) }, i)))) : (_jsx(Text, { bold: true, color: "cyan", children: center("KRITYA") })), betaLine ? _jsx(Text, { dimColor: true, children: betaLine }) : null, _jsx(Box, { marginTop: 2, children: subtitle ? _jsx(Text, { dimColor: true, children: center(subtitle) }) : null }), _jsx(Text, { dimColor: true, children: center("AI can make mistakes. Verify important output.") })] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { SelectList } from "./SelectList.js";
|
|
6
|
+
/**
|
|
7
|
+
* One field at a time, same reasoning as McpTrustPrompt's one-server-at-a-time
|
|
8
|
+
* flow: a multi-field form rendered all at once in a terminal is hard to
|
|
9
|
+
* navigate and easy to fat-finger past a field the user meant to change.
|
|
10
|
+
*/
|
|
11
|
+
export function ElicitationPrompt({ message, fields, onDecision, }) {
|
|
12
|
+
const [index, setIndex] = useState(0);
|
|
13
|
+
const [content, setContent] = useState({});
|
|
14
|
+
const [textValue, setTextValue] = useState("");
|
|
15
|
+
const finish = (finalContent) => onDecision({ action: "accept", content: finalContent });
|
|
16
|
+
const answer = (value) => {
|
|
17
|
+
const field = fields[index];
|
|
18
|
+
const next = { ...content, [field.name]: value };
|
|
19
|
+
if (index + 1 < fields.length) {
|
|
20
|
+
setContent(next);
|
|
21
|
+
setTextValue("");
|
|
22
|
+
setIndex(index + 1);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
finish(next);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const field = fields[index];
|
|
29
|
+
// SelectList handles its own Escape-to-cancel; the free-text field below has
|
|
30
|
+
// no such built-in, so it needs its own listener.
|
|
31
|
+
useInput((_input, key) => {
|
|
32
|
+
if (field?.kind === "string" && key.escape)
|
|
33
|
+
onDecision({ action: "cancel" });
|
|
34
|
+
});
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Server request", fields.length > 1 ? ` (${index + 1} of ${fields.length})` : ""] }), _jsx(Text, { children: message }), field ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: field.label }), field.kind === "boolean" ? (_jsx(SelectList, { items: [
|
|
36
|
+
{ label: "Yes", value: "yes" },
|
|
37
|
+
{ label: "No", value: "no" },
|
|
38
|
+
], onSelect: (v) => answer(v === "yes"), onCancel: () => onDecision({ action: "cancel" }) }, field.name)) : field.kind === "enum" ? (_jsx(SelectList, { items: field.options.map((o) => ({ label: o, value: o })), onSelect: (v) => answer(v), onCancel: () => onDecision({ action: "cancel" }) }, field.name)) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), _jsx(TextInput, { value: textValue, onChange: setTextValue, onSubmit: (v) => {
|
|
39
|
+
const trimmed = v.trim();
|
|
40
|
+
if (trimmed)
|
|
41
|
+
answer(trimmed);
|
|
42
|
+
} })] }))] })) : (_jsx(Box, { marginTop: 1, children: _jsx(SelectList, { items: [
|
|
43
|
+
{ label: "Accept (no fields to fill)", value: "accept" },
|
|
44
|
+
{ label: "Decline", value: "decline" },
|
|
45
|
+
], onSelect: (v) => (v === "accept" ? finish({}) : onDecision({ action: "decline" })), onCancel: () => onDecision({ action: "cancel" }) }) }))] }));
|
|
46
|
+
}
|