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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. 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
+ }