kritya 0.8.24-beta → 0.8.26-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/README.md +10 -5
- package/dist/agent/loop.js +9 -0
- package/dist/agent/toolExecutor.js +10 -1
- package/dist/commands/registry.js +103 -30
- package/dist/config/config.js +15 -0
- package/dist/config/models.js +10 -0
- package/dist/headless.js +4 -2
- package/dist/index.js +32 -3
- package/dist/provider/client.js +19 -0
- package/dist/trust/bannerSeen.js +36 -0
- package/dist/ui/AiDisclosurePrompt.js +1 -1
- package/dist/ui/ApiKeySetupPrompt.js +22 -0
- package/dist/ui/App.js +71 -9
- package/dist/ui/Banner.js +4 -1
- package/dist/ui/StatusLine.js +18 -13
- package/dist/ui/TranscriptItem.js +1 -1
- package/dist/ui/useAgent.js +87 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,6 +65,9 @@ Requires Node.js >=22.
|
|
|
65
65
|
- Linux/macOS: `export NVIDIA_API_KEY=nvapi-...`
|
|
66
66
|
- Windows: `setx NVIDIA_API_KEY nvapi-...` (then open a new terminal)
|
|
67
67
|
|
|
68
|
+
Skip this and kritya will prompt for the key on first launch (interactive
|
|
69
|
+
mode only) and save it to `~/.kritya/.env` for you.
|
|
70
|
+
|
|
68
71
|
Optional: add `TAVILY_API_KEY` the same way to enable the agent's
|
|
69
72
|
web-search tool (`/web-search`) — get one at [tavily.com](https://tavily.com).
|
|
70
73
|
|
|
@@ -159,6 +162,7 @@ In-session commands (type `/` to see them with autocomplete; letters filter the
|
|
|
159
162
|
| `/cost` | token usage and estimated $ (see Pricing below) |
|
|
160
163
|
| `/audit` | show this session's permission decisions and verify the audit log's chain ([more](docs/CONFIGURATION.md#audit-log--telemetry)) |
|
|
161
164
|
| `/budget` | show session token budget; `/budget reset` or `/budget <number>` |
|
|
165
|
+
| `/status` | show context/budget/token/task detail left off the always-visible status line |
|
|
162
166
|
| `/kill` | emergency stop: `/kill [reason]` halts everything; `/kill off` releases |
|
|
163
167
|
| `/help` | command list |
|
|
164
168
|
| `/exit` | quit |
|
|
@@ -282,8 +286,8 @@ full tool output. `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
|
|
|
282
286
|
message is queued and absorbed before its next step (no need to interrupt).
|
|
283
287
|
- **Auto-compaction → self-improving project memory** — when the conversation
|
|
284
288
|
nears the model's context window (80% of `contextWindow`, default 120k
|
|
285
|
-
tokens), older turns are summarized automatically;
|
|
286
|
-
|
|
289
|
+
tokens), older turns are summarized automatically; `/status` shows current
|
|
290
|
+
usage as `context used: N%`. Compaction (auto or manual `/compact`) also
|
|
287
291
|
distills durable, objective facts out of what's being summarized away —
|
|
288
292
|
build/test commands, package manager, conventions actually observed — and
|
|
289
293
|
merges any new ones into a `## Learned by kritya` section in `KRITYA.md`,
|
|
@@ -294,7 +298,8 @@ full tool output. `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
|
|
|
294
298
|
- **Token budget** — a session-wide cap on combined prompt + completion
|
|
295
299
|
tokens across every turn and model (default 1,000,000; set `tokenBudget` in
|
|
296
300
|
config, or `/budget <number>` mid-session). The statusline shows `budget N%`
|
|
297
|
-
once usage
|
|
301
|
+
once usage crosses 80% (yellow, with a one-time warning) or the budget is
|
|
302
|
+
hit (red); `/status` shows it from the start. It then
|
|
298
303
|
stops further turns entirely at 100% until you run `/budget reset` (clears
|
|
299
304
|
the count) or `/budget <number>` (raises the cap). `/cost` also reports it.
|
|
300
305
|
- **Background processes** — the agent can start dev servers/watchers with
|
|
@@ -311,7 +316,7 @@ full tool output. `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
|
|
|
311
316
|
every request.
|
|
312
317
|
- **Sub-task checklist → resumable plans** — for multi-step requests the agent
|
|
313
318
|
plans first and shows a live ☐/◐/☑ checklist as it works, with a compact
|
|
314
|
-
`tasks N/M` summary
|
|
319
|
+
`tasks N/M` summary available via `/status`. The checklist is saved alongside the
|
|
315
320
|
session, so `kritya -c` (and picking a session via `-r`) restores not just
|
|
316
321
|
the conversation but exactly which steps were done, in progress, or still
|
|
317
322
|
pending.
|
|
@@ -346,7 +351,7 @@ full tool output. `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
|
|
|
346
351
|
- **Prompt-caching awareness** — the system prompt is ordered stable-first
|
|
347
352
|
(identity and rules → project memory → volatile git status/listing last) so
|
|
348
353
|
providers can reuse their cached prompt prefix across turns instead of
|
|
349
|
-
re-reading everything. `/cost` and
|
|
354
|
+
re-reading everything. `/cost` and `/status` show how many prompt
|
|
350
355
|
tokens were served from the provider's cache; add an optional
|
|
351
356
|
`"cachedInput"` rate to your `pricing` config to see the dollar savings.
|
|
352
357
|
- **LSP integration** — the agent gets go-to-definition, find-references,
|
package/dist/agent/loop.js
CHANGED
|
@@ -45,6 +45,15 @@ export class Agent {
|
|
|
45
45
|
dryRunMode = false;
|
|
46
46
|
/** When true, file-edit tools auto-approve without prompting (see ACCEPT_EDITS_TOOL_NAMES). */
|
|
47
47
|
acceptEdits = false;
|
|
48
|
+
/**
|
|
49
|
+
* When true, EVERY tool call auto-approves without prompting — including
|
|
50
|
+
* `shell` and classifyDanger-flagged destructive commands, unlike
|
|
51
|
+
* `acceptEdits`. Only reachable from the UI when the sandbox is active
|
|
52
|
+
* (see sandboxAvailable in shell/sandbox.ts), since that's the safety net
|
|
53
|
+
* this mode leans on instead of a human in the loop. Deny rules and the
|
|
54
|
+
* kill switch still apply regardless of this flag.
|
|
55
|
+
*/
|
|
56
|
+
bypassMode = false;
|
|
48
57
|
/**
|
|
49
58
|
* Whether a human can see and respond to a permission prompt right now.
|
|
50
59
|
* True by default (the CLI and its subagents render one); headless runs
|
|
@@ -236,8 +236,17 @@ export class ToolExecutor {
|
|
|
236
236
|
danger === null &&
|
|
237
237
|
tool.requiresPermission &&
|
|
238
238
|
ACCEPT_EDITS_TOOL_NAMES.has(tool.name);
|
|
239
|
+
// Unlike autoApproveEdit, this covers every tool — including `shell` and
|
|
240
|
+
// classifyDanger-flagged destructive commands — since bypassMode leans on
|
|
241
|
+
// the sandbox (not a human) to contain the blast radius. planMode/dryRunMode
|
|
242
|
+
// already returned above, so this can't apply while either is on.
|
|
243
|
+
const autoApproveBypass = host.bypassMode;
|
|
239
244
|
let source;
|
|
240
|
-
if (
|
|
245
|
+
if (autoApproveBypass) {
|
|
246
|
+
host.onAutoApprove?.();
|
|
247
|
+
source = "bypass-mode";
|
|
248
|
+
}
|
|
249
|
+
else if (autoApproveEdit) {
|
|
241
250
|
host.onAutoApprove?.();
|
|
242
251
|
source = "accept-edits";
|
|
243
252
|
}
|
|
@@ -9,73 +9,138 @@ import { scanPluginMcpServers } from "../plugins/mcp.js";
|
|
|
9
9
|
import { listProviders } from "../config/config.js";
|
|
10
10
|
import { expandCommand } from "./custom.js";
|
|
11
11
|
import { artifactPath, clearProjectState, loadProjectState, parseIdea, parsePhase, phaseBlocker, phasePrompt, renameProject, staleArtifacts, PHASE_COMMAND, PHASE_ORDER, PHASE_SUMMARY, saveProjectState, slugify, } from "../agent/workflow.js";
|
|
12
|
+
const WORKFLOW = "Workflow";
|
|
13
|
+
const SESSION = "Session";
|
|
14
|
+
const SAFETY = "Safety & limits";
|
|
15
|
+
const INFO = "Info";
|
|
12
16
|
export const BUILTIN_COMMANDS = [
|
|
13
|
-
{ name: "/help", description: "show available commands" },
|
|
14
|
-
{ name: "/model", description: "pick a model, or /model <id> for any provider model ID" },
|
|
15
|
-
{
|
|
16
|
-
name: "/provider",
|
|
17
|
-
description: "list providers, or /provider <name> to switch mid-session (keeps history); add --default to persist",
|
|
18
|
-
},
|
|
19
17
|
{
|
|
20
18
|
name: "/flow-brainstorm",
|
|
21
19
|
description: "start a new-project workflow: /flow-brainstorm <idea> (brainstorm→spec→plan→build→review→fix)",
|
|
20
|
+
category: WORKFLOW,
|
|
22
21
|
},
|
|
23
22
|
{
|
|
24
23
|
name: "/flow-spec",
|
|
25
24
|
description: "project workflow: write the spec from the approved brainstorm",
|
|
25
|
+
category: WORKFLOW,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "/flow-plan",
|
|
29
|
+
description: "project workflow: plan from the spec (read-only)",
|
|
30
|
+
category: WORKFLOW,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "/flow-build",
|
|
34
|
+
description: "project workflow: implement the plan, with tests",
|
|
35
|
+
category: WORKFLOW,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "/flow-review",
|
|
39
|
+
description: "project workflow: spec-compliance and security review",
|
|
40
|
+
category: WORKFLOW,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "/flow-fix",
|
|
44
|
+
description: "project workflow: fix the review's findings, re-verified",
|
|
45
|
+
category: WORKFLOW,
|
|
26
46
|
},
|
|
27
|
-
{ name: "/plan", description: "toggle plan mode: /plan, /plan on, /plan off" },
|
|
28
|
-
{ name: "/flow-plan", description: "project workflow: plan from the spec (read-only)" },
|
|
29
|
-
{ name: "/flow-build", description: "project workflow: implement the plan, with tests" },
|
|
30
|
-
{ name: "/flow-review", description: "project workflow: spec-compliance and security review" },
|
|
31
|
-
{ name: "/flow-fix", description: "project workflow: fix the review's findings, re-verified" },
|
|
32
47
|
{
|
|
33
48
|
name: "/project",
|
|
34
49
|
description: "workflow status · /project goto <phase> · rename <name> · clear",
|
|
50
|
+
category: WORKFLOW,
|
|
35
51
|
},
|
|
36
|
-
{ name: "/diff", description: "show the cumulative git diff of this session's changes" },
|
|
37
|
-
{ name: "/redo", description: "reapply the change most recently undone" },
|
|
38
|
-
{ name: "/init", description: "scan the repo and generate a KRITYA.md project-memory file" },
|
|
39
|
-
{ name: "/web-search", description: "search the web: /web-search <query>" },
|
|
40
52
|
{
|
|
41
|
-
name: "/
|
|
42
|
-
description: "
|
|
53
|
+
name: "/model",
|
|
54
|
+
description: "pick a model, or /model <id> for any provider model ID",
|
|
55
|
+
category: SESSION,
|
|
43
56
|
},
|
|
44
57
|
{
|
|
45
|
-
name: "/
|
|
46
|
-
description: "list
|
|
58
|
+
name: "/provider",
|
|
59
|
+
description: "list providers, or /provider <name> to switch mid-session (keeps history); add --default to persist",
|
|
60
|
+
category: SESSION,
|
|
47
61
|
},
|
|
48
62
|
{
|
|
49
|
-
name: "/
|
|
50
|
-
description: "
|
|
63
|
+
name: "/diff",
|
|
64
|
+
description: "show the cumulative git diff of this session's changes",
|
|
65
|
+
category: SESSION,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "/init",
|
|
69
|
+
description: "scan the repo and generate a KRITYA.md project-memory file",
|
|
70
|
+
category: SESSION,
|
|
71
|
+
},
|
|
72
|
+
{ name: "/web-search", description: "search the web: /web-search <query>", category: SESSION },
|
|
73
|
+
{
|
|
74
|
+
name: "/commit",
|
|
75
|
+
description: "have the agent stage and commit the current changes",
|
|
76
|
+
category: SESSION,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "/compact",
|
|
80
|
+
description: "summarize older conversation to free context space",
|
|
81
|
+
category: SESSION,
|
|
82
|
+
},
|
|
83
|
+
{ name: "/clear", description: "start a fresh conversation", category: SESSION },
|
|
84
|
+
{
|
|
85
|
+
name: "/undo",
|
|
86
|
+
description: "revert the file changes from the agent's last turn",
|
|
87
|
+
category: SESSION,
|
|
51
88
|
},
|
|
52
|
-
{ name: "/
|
|
89
|
+
{ name: "/redo", description: "reapply the change most recently undone", category: SESSION },
|
|
53
90
|
{
|
|
54
91
|
name: "/checkpoint",
|
|
55
92
|
description: "save a named point: /checkpoint <name> (no name lists saved ones)",
|
|
93
|
+
category: SESSION,
|
|
56
94
|
},
|
|
57
95
|
{
|
|
58
96
|
name: "/rewind",
|
|
59
97
|
description: "rewind the conversation and files to a checkpoint: /rewind <name>",
|
|
98
|
+
category: SESSION,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "/plan",
|
|
102
|
+
description: "toggle plan mode (read-only): /plan, /plan on, /plan off",
|
|
103
|
+
category: SAFETY,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "/kill",
|
|
107
|
+
description: "emergency stop: /kill [reason] halts everything · /kill off releases (Ctrl+K)",
|
|
108
|
+
category: SAFETY,
|
|
60
109
|
},
|
|
61
|
-
{ name: "/commit", description: "have the agent stage and commit the current changes" },
|
|
62
|
-
{ name: "/compact", description: "summarize older conversation to free context space" },
|
|
63
|
-
{ name: "/clear", description: "start a fresh conversation" },
|
|
64
|
-
{ name: "/cost", description: "show token usage and estimated cost" },
|
|
65
110
|
{
|
|
66
111
|
name: "/audit",
|
|
67
112
|
description: "show this session's permission decisions and verify the audit log's chain",
|
|
113
|
+
category: SAFETY,
|
|
68
114
|
},
|
|
69
115
|
{
|
|
70
116
|
name: "/budget",
|
|
71
117
|
description: "show session token budget, /budget reset, or /budget <number> to set it",
|
|
118
|
+
category: SAFETY,
|
|
72
119
|
},
|
|
120
|
+
{ name: "/cost", description: "show token usage and estimated cost", category: SAFETY },
|
|
73
121
|
{
|
|
74
|
-
name: "/
|
|
75
|
-
description: "
|
|
122
|
+
name: "/status",
|
|
123
|
+
description: "show context/budget/token/task detail left off the status line",
|
|
124
|
+
category: SAFETY,
|
|
76
125
|
},
|
|
77
|
-
{ name: "/
|
|
78
|
-
{
|
|
126
|
+
{ name: "/help", description: "show available commands", category: INFO },
|
|
127
|
+
{
|
|
128
|
+
name: "/skills",
|
|
129
|
+
description: "list discovered skills (project + user-global) and why any were skipped",
|
|
130
|
+
category: INFO,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "/plugins",
|
|
134
|
+
description: "list discovered Agent Plugins, what each contributes, and why any were skipped",
|
|
135
|
+
category: INFO,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "/mcp",
|
|
139
|
+
description: "MCP servers: status, /mcp add|remove <name>, /mcp login|logout <name>",
|
|
140
|
+
category: INFO,
|
|
141
|
+
},
|
|
142
|
+
{ name: "/exit", description: "leave", category: INFO },
|
|
143
|
+
{ name: "/quit", description: "leave", category: INFO },
|
|
79
144
|
];
|
|
80
145
|
export const HELP_TEXT = `Commands:
|
|
81
146
|
${BUILTIN_COMMANDS.map((c) => ` ${c.name.padEnd(14)} ${c.description}`).join("\n")}
|
|
@@ -167,6 +232,10 @@ async function runPhase(ctx, phase) {
|
|
|
167
232
|
ctx.agent.acceptEdits = false;
|
|
168
233
|
ctx.setAcceptEdits(false);
|
|
169
234
|
}
|
|
235
|
+
if (wantPlanMode && ctx.bypassMode) {
|
|
236
|
+
ctx.agent.bypassMode = false;
|
|
237
|
+
ctx.setBypassMode(false);
|
|
238
|
+
}
|
|
170
239
|
saveProjectState(ctx.workspace, project.name, phase);
|
|
171
240
|
const artifact = artifactPath(project.name, phase);
|
|
172
241
|
ctx.addItem({
|
|
@@ -379,6 +448,9 @@ const handlers = {
|
|
|
379
448
|
"/cost": (ctx) => {
|
|
380
449
|
ctx.addItem({ kind: "info", text: ctx.costReport() });
|
|
381
450
|
},
|
|
451
|
+
"/status": (ctx) => {
|
|
452
|
+
ctx.addItem({ kind: "info", text: ctx.statusReport() });
|
|
453
|
+
},
|
|
382
454
|
"/audit": (ctx) => {
|
|
383
455
|
const audit = ctx.agent.audit;
|
|
384
456
|
if (!audit) {
|
|
@@ -674,6 +746,7 @@ const ALLOWED_WHILE_KILLED = new Set([
|
|
|
674
746
|
"/quit",
|
|
675
747
|
"/audit",
|
|
676
748
|
"/cost",
|
|
749
|
+
"/status",
|
|
677
750
|
"/diff",
|
|
678
751
|
"/budget",
|
|
679
752
|
"/mcp",
|
package/dist/config/config.js
CHANGED
|
@@ -165,6 +165,7 @@ export function resolveProvider(config, override) {
|
|
|
165
165
|
name,
|
|
166
166
|
baseUrl,
|
|
167
167
|
apiKey,
|
|
168
|
+
apiKeyEnv: merged.apiKeyEnv,
|
|
168
169
|
temperature: merged.temperature,
|
|
169
170
|
topP: merged.topP,
|
|
170
171
|
maxTokens: merged.maxTokens,
|
|
@@ -322,3 +323,17 @@ export function loadDotEnv(paths) {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Append one KEY=VALUE line to ~/.kritya/.env, creating the directory (and
|
|
328
|
+
* hardening its permissions) the same way saveConfig does for config.json.
|
|
329
|
+
* Used by the first-run API key setup prompt so a pasted key survives across
|
|
330
|
+
* restarts without the user having to edit a dotfile by hand.
|
|
331
|
+
*/
|
|
332
|
+
export function appendGlobalEnvVar(name, value) {
|
|
333
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
334
|
+
hardenWindowsDir(CONFIG_DIR);
|
|
335
|
+
const envFile = path.join(CONFIG_DIR, ".env");
|
|
336
|
+
const sanitized = value.replace(/[\r\n]/g, "");
|
|
337
|
+
fs.appendFileSync(envFile, `${name}=${sanitized}\n`, { mode: 0o600 });
|
|
338
|
+
fs.chmodSync(envFile, 0o600);
|
|
339
|
+
}
|
package/dist/config/models.js
CHANGED
|
@@ -76,3 +76,13 @@ export function modelDisplaySlug(modelId) {
|
|
|
76
76
|
.replace(/^-+|-+$/g, "");
|
|
77
77
|
return `${prefix}${slug}`;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* "provider/model" for display, without the doubled prefix that comes from
|
|
81
|
+
* naively joining them: several providers' model IDs already carry their own
|
|
82
|
+
* namespace (NVIDIA's catalog is "nvidia/nemotron-...", so the "nvidia"
|
|
83
|
+
* provider plus that model id would otherwise print "nvidia/nvidia/...").
|
|
84
|
+
* Only skip the provider prefix when the model id already starts with it.
|
|
85
|
+
*/
|
|
86
|
+
export function displayModelId(providerName, modelId) {
|
|
87
|
+
return modelId.startsWith(`${providerName}/`) ? modelId : `${providerName}/${modelId}`;
|
|
88
|
+
}
|
package/dist/headless.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CONFIG_DIR, legacyGlobalModel, listProviders, loadConfig, loadDotEnv, p
|
|
|
5
5
|
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
6
6
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
7
7
|
import { loadRules } from "./permissions/rules.js";
|
|
8
|
-
import { ProviderClient, RetryExhaustedError } from "./provider/client.js";
|
|
8
|
+
import { ProviderClient, RetryExhaustedError, friendlyProviderErrorHint, } from "./provider/client.js";
|
|
9
9
|
import { createSwitchyardClient } from "./provider/switchyardClient.js";
|
|
10
10
|
import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel, staleSwitchyardModelWarning, stopSwitchyardSidecar, } from "./provider/switchyardSidecar.js";
|
|
11
11
|
import { SessionStore } from "./session/store.js";
|
|
@@ -257,7 +257,9 @@ export async function runHeadless(args) {
|
|
|
257
257
|
errorMessage = `Stopped (${err.killReason ?? "kill switch engaged"})`;
|
|
258
258
|
}
|
|
259
259
|
else {
|
|
260
|
-
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
const friendlyHint = friendlyProviderErrorHint(err, provider.name);
|
|
262
|
+
errorMessage = friendlyHint ? `${friendlyHint} (${message})` : message;
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
265
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { Agent } from "./agent/loop.js";
|
|
7
|
-
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
7
|
+
import { CONFIG_DIR, appendGlobalEnvVar, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
8
8
|
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
9
9
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
10
10
|
import { loadRules } from "./permissions/rules.js";
|
|
@@ -26,6 +26,7 @@ import { UndoStack } from "./undo/undo.js";
|
|
|
26
26
|
import { App } from "./ui/App.js";
|
|
27
27
|
import { TrustPrompt } from "./ui/TrustPrompt.js";
|
|
28
28
|
import { AiDisclosurePrompt } from "./ui/AiDisclosurePrompt.js";
|
|
29
|
+
import { ApiKeySetupPrompt } from "./ui/ApiKeySetupPrompt.js";
|
|
29
30
|
import { McpTrustPrompt } from "./ui/McpTrustPrompt.js";
|
|
30
31
|
import { loadHooks, HookRunner } from "./hooks/hooks.js";
|
|
31
32
|
import { loadMcpTools, shutdownMcp, } from "./mcp/client.js";
|
|
@@ -35,6 +36,7 @@ import { pluginsDir, scanPlugins, userPluginsDir } from "./plugins/discover.js";
|
|
|
35
36
|
import { loadPluginMcpServers } from "./plugins/mcp.js";
|
|
36
37
|
import { describeGatedContent, gatedContentHash, isTrusted, saveTrust } from "./trust/trust.js";
|
|
37
38
|
import { isAiDisclosureShown, markAiDisclosureShown } from "./trust/aiDisclosure.js";
|
|
39
|
+
import { isBannerSeen, markBannerSeen } from "./trust/bannerSeen.js";
|
|
38
40
|
import { partitionByTrust, serverFingerprint, trustServer } from "./trust/mcpTrust.js";
|
|
39
41
|
import { runHeadless } from "./headless.js";
|
|
40
42
|
import { installCrashHandlers } from "./crash.js";
|
|
@@ -264,9 +266,27 @@ async function showAiDisclosureNotice() {
|
|
|
264
266
|
} }));
|
|
265
267
|
});
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Interactive fallback when no API key resolved for the active provider.
|
|
271
|
+
* Lets the user paste one straight in, saving it to ~/.kritya/.env, rather
|
|
272
|
+
* than forcing them out to read env-var instructions and relaunch. Only
|
|
273
|
+
* called from the interactive TTY path (see main() below) — headless mode
|
|
274
|
+
* (src/headless.ts) never imports this and keeps failing with a plain error.
|
|
275
|
+
*/
|
|
276
|
+
async function promptApiKeySetup(providerName, envVarName) {
|
|
277
|
+
if (!envVarName)
|
|
278
|
+
return null;
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
const instance = render(_jsx(ApiKeySetupPrompt, { providerName: providerName, envVarName: envVarName, onDecision: (result) => {
|
|
281
|
+
instance.unmount();
|
|
282
|
+
resolve(result.action === "save" ? result.key : null);
|
|
283
|
+
} }));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
267
286
|
async function main() {
|
|
268
287
|
const config = loadConfig();
|
|
269
288
|
const privacyMode = args.privacy || privacyModeFor(config);
|
|
289
|
+
const firstLaunch = !isBannerSeen(workspace);
|
|
270
290
|
if (!isAiDisclosureShown(workspace)) {
|
|
271
291
|
await showAiDisclosureNotice();
|
|
272
292
|
markAiDisclosureShown(workspace);
|
|
@@ -279,7 +299,15 @@ async function main() {
|
|
|
279
299
|
loadDotEnv([path.join(workspace, ".env")]);
|
|
280
300
|
}
|
|
281
301
|
const provider = resolveProvider(config, args.provider || undefined);
|
|
282
|
-
|
|
302
|
+
let apiKey = provider.apiKey;
|
|
303
|
+
if (!apiKey) {
|
|
304
|
+
const entered = await promptApiKeySetup(provider.name, provider.apiKeyEnv);
|
|
305
|
+
if (entered) {
|
|
306
|
+
appendGlobalEnvVar(provider.apiKeyEnv, entered);
|
|
307
|
+
process.env[provider.apiKeyEnv] = entered;
|
|
308
|
+
apiKey = entered;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
283
311
|
if (!apiKey) {
|
|
284
312
|
const hint = provider.name === "nvidia"
|
|
285
313
|
? `Get one at https://build.nvidia.com, then one of:\n` +
|
|
@@ -687,5 +715,6 @@ async function main() {
|
|
|
687
715
|
permissionRef.current = fn;
|
|
688
716
|
}, onRequestElicitationReady: (fn) => {
|
|
689
717
|
elicitationRef.current = fn;
|
|
690
|
-
}, privacyMode: privacyMode }));
|
|
718
|
+
}, privacyMode: privacyMode, firstLaunch: firstLaunch }));
|
|
719
|
+
markBannerSeen(workspace);
|
|
691
720
|
}
|
package/dist/provider/client.js
CHANGED
|
@@ -99,6 +99,25 @@ export function isRetryable(err) {
|
|
|
99
99
|
}
|
|
100
100
|
return false;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A short, actionable translation for the non-retryable failures a
|
|
104
|
+
* first-time user is most likely to hit — a rejected key or an unknown
|
|
105
|
+
* model id — instead of the raw `{"status":403,"title":"Forbidden",...}`
|
|
106
|
+
* body the provider sent. Returns undefined for anything else, so callers
|
|
107
|
+
* fall back to the raw message unchanged.
|
|
108
|
+
*/
|
|
109
|
+
export function friendlyProviderErrorHint(err, providerName) {
|
|
110
|
+
const status = err?.status;
|
|
111
|
+
if (status === 401 || status === 403) {
|
|
112
|
+
return (`Your API key for provider "${providerName}" was rejected. Check it's correct, ` +
|
|
113
|
+
`still active, and hasn't expired, then try again.`);
|
|
114
|
+
}
|
|
115
|
+
if (status === 404 && !isEmptyBodyNotFound(err)) {
|
|
116
|
+
return (`Provider "${providerName}" doesn't recognize this model. Check the model ID with ` +
|
|
117
|
+
`/model, or that your account has access to it.`);
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
102
121
|
/** Read one header case-insensitively from whatever shape the SDK attached. */
|
|
103
122
|
function headerValue(err, name) {
|
|
104
123
|
const headers = err?.headers;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_DIR } from "../config/config.js";
|
|
4
|
+
import { hardenWindowsDir } from "../config/winAcl.js";
|
|
5
|
+
import { debugLog } from "../config/debug.js";
|
|
6
|
+
/**
|
|
7
|
+
* Tracks which workspaces have already shown the full ASCII banner once, so
|
|
8
|
+
* repeat launches (which cost real vertical space in a terminal) can fall
|
|
9
|
+
* back to a compact one-line header instead. Same shape and persistence
|
|
10
|
+
* pattern as aiDisclosure.ts's ai-disclosure.json.
|
|
11
|
+
*/
|
|
12
|
+
const BANNER_FILE = path.join(CONFIG_DIR, "banner-seen.json");
|
|
13
|
+
function loadStore(storeFile) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(fs.readFileSync(storeFile, "utf8"));
|
|
16
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
// A missing file means "not shown anywhere yet" (normal); a malformed one
|
|
20
|
+
// means every workspace re-shows the full banner, worth being able to see.
|
|
21
|
+
debugLog(`loadStore(${storeFile})`, err);
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Whether the full ASCII banner has already been shown for this workspace. */
|
|
26
|
+
export function isBannerSeen(workspace, storeFile = BANNER_FILE) {
|
|
27
|
+
return typeof loadStore(storeFile)[path.resolve(workspace)] === "string";
|
|
28
|
+
}
|
|
29
|
+
/** Record that the full ASCII banner has been shown for this workspace, now. */
|
|
30
|
+
export function markBannerSeen(workspace, storeFile = BANNER_FILE) {
|
|
31
|
+
const store = loadStore(storeFile);
|
|
32
|
+
store[path.resolve(workspace)] = new Date().toISOString();
|
|
33
|
+
fs.mkdirSync(path.dirname(storeFile), { recursive: true, mode: 0o700 });
|
|
34
|
+
hardenWindowsDir(path.dirname(storeFile));
|
|
35
|
+
fs.writeFileSync(storeFile, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
|
|
36
|
+
}
|
|
@@ -19,5 +19,5 @@ export function AiDisclosurePrompt({ onDismiss }) {
|
|
|
19
19
|
const id = setInterval(() => setBlinkOn((v) => !v), BLINK_INTERVAL_MS);
|
|
20
20
|
return () => clearInterval(id);
|
|
21
21
|
}, []);
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "kritya is an AI agent" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { children: "kritya is an AI agent" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "It reads files, edits your project, and runs commands \u2014 driven by an AI model, not a human. You approve changes before they happen." }) }), _jsx(Text, { dimColor: true, children: "Details: /help, SECURITY.md (EU AI Act Art. 50)." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Shown once. ", _jsx(Text, { color: "yellow", children: blinkOn ? "AI Disclosure" : " " })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Press Enter to continue\u2026" }) })] }));
|
|
23
23
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Shown once, only when the active provider has no API key resolved, on the
|
|
7
|
+
* interactive TTY path (never from headless — see runHeadless in
|
|
8
|
+
* src/headless.ts, which never imports this component). Lets a first-time
|
|
9
|
+
* user paste a key instead of hunting through env-var/dotfile instructions.
|
|
10
|
+
*/
|
|
11
|
+
export function ApiKeySetupPrompt({ providerName, envVarName, onDecision, }) {
|
|
12
|
+
const [value, setValue] = useState("");
|
|
13
|
+
useInput((_input, key) => {
|
|
14
|
+
if (key.escape)
|
|
15
|
+
onDecision({ action: "skip" });
|
|
16
|
+
});
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["No API key found for provider \"", providerName, "\""] }), _jsxs(Text, { children: ["Paste your ", envVarName, " value below to save it to ~/.kritya/.env and continue \u2014 or press Esc to skip and set it up yourself."] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "> " }), _jsx(TextInput, { value: value, onChange: setValue, mask: "*", onSubmit: (v) => {
|
|
18
|
+
const trimmed = v.trim();
|
|
19
|
+
if (trimmed)
|
|
20
|
+
onDecision({ action: "save", key: trimmed });
|
|
21
|
+
} })] })] }));
|
|
22
|
+
}
|
package/dist/ui/App.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -30,7 +30,7 @@ const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
|
30
30
|
// Erase screen + scrollback, then home the cursor (same sequence Ink itself
|
|
31
31
|
// uses for its own "content taller than the terminal" clear path).
|
|
32
32
|
const CLEAR_TERMINAL = "\x1b[2J\x1b[3J\x1b[H";
|
|
33
|
-
export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, privacyMode, }) {
|
|
33
|
+
export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, privacyMode, firstLaunch, }) {
|
|
34
34
|
const { exit } = useApp();
|
|
35
35
|
const { stdout } = useStdout();
|
|
36
36
|
const [input, setInput] = useState("");
|
|
@@ -125,7 +125,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
125
125
|
.catch(() => { });
|
|
126
126
|
}, [workspace]);
|
|
127
127
|
useEffect(refreshFileList, [refreshFileList]);
|
|
128
|
-
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, persistenceWarningCount, } = useAgent({
|
|
128
|
+
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, bypassMode, setBypassMode, autoApprovedCount, cycleMode, onAcceptEditsConfirm, onBypassModeConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, persistenceWarningCount, } = useAgent({
|
|
129
129
|
agent,
|
|
130
130
|
workspace,
|
|
131
131
|
modelRef,
|
|
@@ -137,6 +137,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
137
137
|
resumeSessions,
|
|
138
138
|
refreshFileList,
|
|
139
139
|
onSwitchClient,
|
|
140
|
+
firstLaunch,
|
|
140
141
|
});
|
|
141
142
|
useEffect(() => {
|
|
142
143
|
onRequestPermissionReady?.(requestPermission);
|
|
@@ -154,11 +155,49 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
154
155
|
const t = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1000)), 1000);
|
|
155
156
|
return () => clearInterval(t);
|
|
156
157
|
}, [phase]);
|
|
158
|
+
// Detail moved out of the always-visible status line (see StatusLine.tsx) —
|
|
159
|
+
// surfaced instead via the /status command, on demand.
|
|
160
|
+
const statusReport = useCallback(() => {
|
|
161
|
+
const sandboxActive = (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable();
|
|
162
|
+
const cachedPct = (totalUsage.cachedPromptTokens ?? 0) > 0
|
|
163
|
+
? ` (${Math.round(((totalUsage.cachedPromptTokens ?? 0) / totalUsage.promptTokens) * 100)}% cached)`
|
|
164
|
+
: "";
|
|
165
|
+
const lines = [
|
|
166
|
+
`workspace: ${workspace}`,
|
|
167
|
+
`sandbox: ${sandboxActive ? "active" : "inactive"}`,
|
|
168
|
+
`context used: ${ctxPct}%`,
|
|
169
|
+
`token budget: ${budgetPct}% of ${tokenBudget.toLocaleString()} (${budgetUsed.toLocaleString()} used)${budgetStopped ? " — stopped" : ""}`,
|
|
170
|
+
`tokens: ${totalUsage.estimated ? "~" : ""}${totalUsage.promptTokens.toLocaleString()} in${cachedPct} / ${totalUsage.completionTokens.toLocaleString()} out`,
|
|
171
|
+
tasks.length > 0
|
|
172
|
+
? `tasks: ${tasks.filter((t) => t.status === "done").length}/${tasks.length} done`
|
|
173
|
+
: null,
|
|
174
|
+
phase === "working" && elapsed > 0 ? `elapsed: ${elapsed}s` : null,
|
|
175
|
+
verbose ? "verbose: on" : null,
|
|
176
|
+
].filter((l) => l !== null);
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}, [
|
|
179
|
+
config,
|
|
180
|
+
workspace,
|
|
181
|
+
ctxPct,
|
|
182
|
+
budgetPct,
|
|
183
|
+
tokenBudget,
|
|
184
|
+
budgetUsed,
|
|
185
|
+
budgetStopped,
|
|
186
|
+
totalUsage,
|
|
187
|
+
tasks,
|
|
188
|
+
phase,
|
|
189
|
+
elapsed,
|
|
190
|
+
verbose,
|
|
191
|
+
]);
|
|
157
192
|
const allCommands = [
|
|
158
193
|
...BUILTIN_COMMANDS,
|
|
159
|
-
...customCommands.map((c) => ({
|
|
194
|
+
...customCommands.map((c) => ({
|
|
195
|
+
name: c.name,
|
|
196
|
+
description: c.description,
|
|
197
|
+
category: "Custom",
|
|
198
|
+
})),
|
|
160
199
|
// Servers can contribute slash commands too (MCP prompts).
|
|
161
|
-
...mcpPrompts().map((p) => ({ name: p.command, description: p.description })),
|
|
200
|
+
...mcpPrompts().map((p) => ({ name: p.command, description: p.description, category: "MCP" })),
|
|
162
201
|
];
|
|
163
202
|
// Command suggestions while typing a slash command (before any arguments).
|
|
164
203
|
const suggestions = phase === "input" && input.startsWith("/") && !input.includes(" ")
|
|
@@ -219,6 +258,15 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
219
258
|
onAcceptEditsConfirm(false);
|
|
220
259
|
return;
|
|
221
260
|
}
|
|
261
|
+
// First-time auto/bypass-mode confirmation: Yes proceeds, anything else cancels.
|
|
262
|
+
if (phase === "confirmBypassMode") {
|
|
263
|
+
const c = _input.toLowerCase();
|
|
264
|
+
if (c === "y" || key.return)
|
|
265
|
+
onBypassModeConfirm(true);
|
|
266
|
+
else if (c === "n" || key.escape)
|
|
267
|
+
onBypassModeConfirm(false);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
222
270
|
// History recall with ↑/↓ when no autocomplete popup is open.
|
|
223
271
|
if (phase === "input" && !suggestions.length && !fileSuggestions.length) {
|
|
224
272
|
const hist = inputHistory.current;
|
|
@@ -281,6 +329,8 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
281
329
|
planMode,
|
|
282
330
|
acceptEdits,
|
|
283
331
|
setAcceptEdits,
|
|
332
|
+
bypassMode,
|
|
333
|
+
setBypassMode,
|
|
284
334
|
tokenBudget,
|
|
285
335
|
budgetPct,
|
|
286
336
|
budgetUsed,
|
|
@@ -308,6 +358,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
308
358
|
runWebSearch,
|
|
309
359
|
expandMentions,
|
|
310
360
|
costReport,
|
|
361
|
+
statusReport,
|
|
311
362
|
gitDiffStat,
|
|
312
363
|
exit,
|
|
313
364
|
};
|
|
@@ -404,7 +455,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
404
455
|
addItem({ kind: "info", text: `Attached ${images.length} image(s).` });
|
|
405
456
|
await runAgent(await expandMentions(text), images);
|
|
406
457
|
};
|
|
407
|
-
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:
|
|
458
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item) => _jsx(TranscriptItem, { item: item, verbose: verbose, stdout: stdout }, item.id) }, staticKey), firstLaunch && items.length <= 1 && phase === "input" && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 2, children: [_jsx(Text, { dimColor: true, children: "Try asking:" }), _jsx(Text, { dimColor: true, children: " \u00B7 Explain this codebase" }), _jsx(Text, { dimColor: true, children: " \u00B7 Find and fix failing tests" }), _jsx(Text, { dimColor: true, children: " \u00B7 Review my uncommitted changes" }), _jsx(Text, { dimColor: true, children: " \u00B7 Add a new feature" }), _jsx(Text, { dimColor: true, children: " \u00B7 /help for commands" })] })), 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:
|
|
408
459
|
// The workflow prefix rides on every branch, not just "thinking":
|
|
409
460
|
// during a phase you want to know which phase a tool call belongs
|
|
410
461
|
// to just as much as which phase is being reasoned about.
|
|
@@ -431,7 +482,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
431
482
|
}
|
|
432
483
|
addItem({ kind: "user", text: `${text} (queued)` });
|
|
433
484
|
void expandMentions(text).then((t) => agent.queueSteer(t));
|
|
434
|
-
}, 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) => {
|
|
485
|
+
}, 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 === "confirmBypassMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Switch to auto mode? (sandbox-gated)" }), _jsx(Text, { children: "EVERY tool call will auto-approve, including destructive shell commands (rm -rf, force-push, etc.) \u2014 nothing will ask first." }), _jsx(Text, { dimColor: true, children: "The sandbox \u2014 not a human \u2014 is what contains any damage this mode causes." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again goes back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
435
486
|
setPhase("input");
|
|
436
487
|
setModelEverywhere(id);
|
|
437
488
|
}, 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 ?? [])
|
|
@@ -445,7 +496,18 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
445
496
|
setInput((prev) => (isCtrlEcho(prev, v) ? prev : v.replace(/\t/g, "")));
|
|
446
497
|
setCmdIndex(0);
|
|
447
498
|
setFileIndex(0);
|
|
448
|
-
}, onSubmit: handleSubmit }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [
|
|
499
|
+
}, onSubmit: handleSubmit }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [(() => {
|
|
500
|
+
// Group headers only earn their keep once more than one
|
|
501
|
+
// category is actually on screen — once typing has filtered
|
|
502
|
+
// down to a single family, a lone header is just noise.
|
|
503
|
+
const showHeaders = new Set(suggestions.map((c) => c.category)).size > 1;
|
|
504
|
+
let lastCategory;
|
|
505
|
+
return suggestions.map((c, i) => {
|
|
506
|
+
const showHeader = showHeaders && c.category !== lastCategory;
|
|
507
|
+
lastCategory = c.category;
|
|
508
|
+
return (_jsxs(Fragment, { children: [showHeader && _jsx(Text, { dimColor: true, children: c.category ?? "Other" }), _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));
|
|
509
|
+
});
|
|
510
|
+
})(), _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, bypassMode: bypassMode, autoApprovedCount: autoApprovedCount, provider: provider, model: servedModel && servedModel !== model
|
|
449
511
|
? `${model}-${modelDisplaySlug(servedModel)}`
|
|
450
|
-
: model, workflow: workflow, branch: branch,
|
|
512
|
+
: model, workflow: workflow, branch: branch, budgetPct: budgetPct, budgetStopped: budgetStopped, totalCost: totalCost, sandboxActive: (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable(), persistenceWarningCount: persistenceWarningCount, privacyMode: privacyMode }) })] }));
|
|
451
513
|
}
|
package/dist/ui/Banner.js
CHANGED
|
@@ -50,7 +50,10 @@ function rowColor(row) {
|
|
|
50
50
|
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
|
51
51
|
}
|
|
52
52
|
const BETA_LABEL = "beta";
|
|
53
|
-
export function Banner({ subtitle }) {
|
|
53
|
+
export function Banner({ subtitle, compact }) {
|
|
54
|
+
if (compact) {
|
|
55
|
+
return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: subtitle }) }));
|
|
56
|
+
}
|
|
54
57
|
const columns = terminalColumns(process.stdout);
|
|
55
58
|
const wide = bannerLines("KRITYA", "░░");
|
|
56
59
|
const narrow = bannerLines("KRITYA", "░");
|
package/dist/ui/StatusLine.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from "ink";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
import { displayModelId } from "../config/models.js";
|
|
4
|
+
/**
|
|
5
|
+
* The single dim status bar pinned to the bottom of the screen. Keeps only
|
|
6
|
+
* the fields worth glancing at on every render; everything else (context %,
|
|
7
|
+
* task checklist, elapsed time, token counts, workspace path, verbose flag)
|
|
8
|
+
* moved to the on-demand `/status` command — see CommandContext.statusReport
|
|
9
|
+
* in src/commands/registry.ts.
|
|
10
|
+
*/
|
|
11
|
+
export function StatusLine({ killed, killReason, dryRunMode, planMode, acceptEdits, bypassMode, autoApprovedCount, provider, model, workflow, branch, budgetPct, budgetStopped, totalCost, sandboxActive, persistenceWarningCount, privacyMode, }) {
|
|
12
|
+
return (_jsxs(Text, { dimColor: true, children: [killed ? (_jsxs(Text, { bold: true, color: "red", children: ["\u26D4 KILLED", killReason ? ` (${killReason})` : "", " \u00B7", " "] })) : (""), _jsxs(Text, { color: bypassMode ? "red" : dryRunMode || planMode ? "cyan" : acceptEdits ? "green" : undefined, children: ["mode:", " ", bypassMode
|
|
13
|
+
? `auto (${autoApprovedCount} auto)`
|
|
14
|
+
: dryRunMode
|
|
15
|
+
? "dry-run"
|
|
16
|
+
: planMode
|
|
17
|
+
? "plan"
|
|
18
|
+
: acceptEdits
|
|
19
|
+
? `accept-edits (${autoApprovedCount} auto)`
|
|
20
|
+
: "default", " · "] }), displayModelId(provider, model), _jsxs(Text, { color: sandboxActive ? "green" : "red", children: [" ", "\u00B7 ", sandboxActive ? "🔒 sandbox:active" : "🔓 sandbox:inactive"] }), workflow ? (_jsxs(Text, { color: "magenta", children: [" ", "\u00B7 \u2691 ", workflow.name, ":", workflow.phase] })) : (""), branch ? ` · ⎇ ${branch}` : "", budgetStopped || budgetPct >= 80 ? (_jsxs(Text, { color: budgetStopped ? "red" : "yellow", children: [" \u00B7 budget ", budgetPct, "%"] })) : (""), totalCost > 0 ? ` · $${totalCost.toFixed(4)}` : "", persistenceWarningCount > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 \u26A0 persistence warnings: ", persistenceWarningCount] })) : (""), privacyMode ? _jsx(Text, { color: "cyan", children: " \u00B7 privacy:on" }) : "", " · /diff review · /undo revert · Ctrl+K stop · /status for details"] }));
|
|
16
21
|
}
|
|
@@ -9,5 +9,5 @@ export function TranscriptItem({ item, verbose, stdout }) {
|
|
|
9
9
|
return (_jsxs(Box, { marginBottom: item.kind === "tool" ? 0 : 1, flexDirection: "column", children: [item.kind === "user" && (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: "green", children: ["\u276F", " "] }), item.text] })), item.kind === "assistant" && _jsx(Markdown, { text: item.text }), item.kind === "tool" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [item.error ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "green", children: "\u2713" }), " ", item.summary, item.resultSummary && !verbose ? ` — ${item.resultSummary}` : ""] }), item.output && item.output.trim() && (item.resultSummary === undefined || verbose) && (_jsx(Text, { dimColor: true, children: toolOutputPreview(item.output, verbose, Math.max(20, terminalColumns(stdout) - 6), item.error)
|
|
10
10
|
.split("\n")
|
|
11
11
|
.map((l) => ` ${l}`)
|
|
12
|
-
.join("\n") }))] })), item.kind === "info" && _jsx(Text, { dimColor: true, children: item.text }), item.kind === "banner" && _jsx(Banner, { subtitle: item.subtitle })] }));
|
|
12
|
+
.join("\n") }))] })), item.kind === "info" && _jsx(Text, { dimColor: true, children: item.text }), item.kind === "banner" && _jsx(Banner, { subtitle: item.subtitle, compact: item.compact }), item.kind === "summary" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "\u2713 Completed" }), item.files.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Changed:" }), item.files.map((f) => (_jsxs(Text, { children: [" ", f] }, f)))] })) : (_jsx(Text, { dimColor: true, children: "No files changed." })), _jsxs(Text, { dimColor: true, children: ["Next: ", item.nextStep] })] }))] }));
|
|
13
13
|
}
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -2,17 +2,32 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { gitBranch } from "../git/git.js";
|
|
4
4
|
import { listProviders, resolveProvider, saveConfig, saveProviderModel, } from "../config/config.js";
|
|
5
|
-
import { DEFAULT_MODEL, contextWindowFor } from "../config/models.js";
|
|
6
|
-
import { ProviderClient, RetryExhaustedError } from "../provider/client.js";
|
|
5
|
+
import { DEFAULT_MODEL, contextWindowFor, displayModelId } from "../config/models.js";
|
|
6
|
+
import { ProviderClient, RetryExhaustedError, friendlyProviderErrorHint, } from "../provider/client.js";
|
|
7
7
|
import { createSwitchyardClient } from "../provider/switchyardClient.js";
|
|
8
8
|
import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel } from "../provider/switchyardSidecar.js";
|
|
9
9
|
import { KillSwitchError } from "../agent/killSwitch.js";
|
|
10
|
+
import { defaultSandboxMode, sandboxAvailable } from "../shell/sandbox.js";
|
|
10
11
|
import { loadProjectState, nextPhase, PHASE_COMMAND, } from "../agent/workflow.js";
|
|
11
12
|
import { tavilySearch } from "../tools/webSearch.js";
|
|
12
13
|
import { killActiveNotice, useKillSwitch } from "./useKillSwitch.js";
|
|
13
14
|
import { useUsageBudget } from "./useUsageBudget.js";
|
|
14
15
|
import { useSessionResume } from "./useSessionResume.js";
|
|
15
16
|
import { activePersistenceWarnings, onPersistenceWarning } from "../config/debug.js";
|
|
17
|
+
/** Tool names whose successful calls mutate a file the user would want to review. */
|
|
18
|
+
const FILE_MUTATING_TOOLS = new Set([
|
|
19
|
+
"edit_file",
|
|
20
|
+
"write_file",
|
|
21
|
+
"edit_notebook",
|
|
22
|
+
"edit_spreadsheet",
|
|
23
|
+
"edit_pdf",
|
|
24
|
+
"write_document",
|
|
25
|
+
]);
|
|
26
|
+
/** Pulls the path out of a tool summary like "Edit src/foo.ts" or "Write src/foo.ts (12 bytes)". */
|
|
27
|
+
function pathFromToolSummary(summary) {
|
|
28
|
+
const match = summary.match(/^\S+\s+(\S+)/);
|
|
29
|
+
return match?.[1];
|
|
30
|
+
}
|
|
16
31
|
function initialToolStatus(name) {
|
|
17
32
|
if (name.startsWith("mcp_"))
|
|
18
33
|
return "contacting MCP server…";
|
|
@@ -29,7 +44,7 @@ function initialToolStatus(name) {
|
|
|
29
44
|
* permission state, usage/cost tracking, and session resume. Input handling
|
|
30
45
|
* (text box, slash-command suggestions, @mention autocomplete) stays in App.
|
|
31
46
|
*/
|
|
32
|
-
export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBridge, resumedCount, initialTasks, resumeSessions, refreshFileList, onSwitchClient, }) {
|
|
47
|
+
export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBridge, resumedCount, initialTasks, resumeSessions, refreshFileList, onSwitchClient, firstLaunch, }) {
|
|
33
48
|
const nextId = useRef(0);
|
|
34
49
|
const [items, setItems] = useState(() => {
|
|
35
50
|
const initial = [
|
|
@@ -37,6 +52,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
37
52
|
id: nextId.current++,
|
|
38
53
|
kind: "banner",
|
|
39
54
|
subtitle: `${path.basename(workspace)} · ${modelRef.current} · type a request, or /help for commands`,
|
|
55
|
+
compact: !firstLaunch,
|
|
40
56
|
},
|
|
41
57
|
];
|
|
42
58
|
if (resumedCount > 0) {
|
|
@@ -101,8 +117,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
101
117
|
* workflow's own `planMode`. */
|
|
102
118
|
const [dryRunMode, setDryRunMode] = useState(false);
|
|
103
119
|
const [acceptEdits, setAcceptEdits] = useState(false);
|
|
120
|
+
const [bypassMode, setBypassMode] = useState(false);
|
|
104
121
|
const [autoApprovedCount, setAutoApprovedCount] = useState(0);
|
|
105
122
|
const hasConfirmedAcceptEdits = useRef(false);
|
|
123
|
+
const hasConfirmedBypassMode = useRef(false);
|
|
124
|
+
/** Whether auto/bypass mode is even reachable — requires a real sandbox
|
|
125
|
+
* backend on this platform (Windows never has one; see sandboxAvailable). */
|
|
126
|
+
const sandboxActive = (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable();
|
|
106
127
|
const abortRef = useRef(null);
|
|
107
128
|
/** Tool calls currently running, keyed by call id — more than one when a
|
|
108
129
|
* turn's read-only calls are dispatched in parallel. Rendered as live rows. */
|
|
@@ -191,12 +212,44 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
191
212
|
"more for normal.",
|
|
192
213
|
});
|
|
193
214
|
};
|
|
194
|
-
|
|
215
|
+
const enterBypassMode = () => {
|
|
216
|
+
hasConfirmedBypassMode.current = true;
|
|
217
|
+
agent.bypassMode = true;
|
|
218
|
+
setBypassMode(true);
|
|
219
|
+
setAutoApprovedCount(0);
|
|
220
|
+
addItem({
|
|
221
|
+
kind: "info",
|
|
222
|
+
text: "Auto mode ON — EVERY tool call auto-approves, including destructive shell commands " +
|
|
223
|
+
"(rm -rf, force-push, etc.). Relies on the sandbox, not a human, to contain damage. " +
|
|
224
|
+
"Shift+Tab again for normal.",
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Cycles normal → accept-edits → dry-run → auto (only when the sandbox is
|
|
229
|
+
* active) → normal. First entry into accept-edits or auto mode this session
|
|
230
|
+
* pauses on a confirmation instead of switching immediately. When the
|
|
231
|
+
* sandbox isn't active, auto mode is skipped and dry-run goes straight back
|
|
232
|
+
* to normal, same as before this mode existed.
|
|
233
|
+
*/
|
|
195
234
|
const cycleMode = () => {
|
|
235
|
+
if (bypassMode) {
|
|
236
|
+
agent.bypassMode = false;
|
|
237
|
+
setBypassMode(false);
|
|
238
|
+
addItem({ kind: "info", text: "Auto mode OFF — back to normal, prompts resume." });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
196
241
|
if (dryRunMode) {
|
|
197
242
|
agent.dryRunMode = false;
|
|
198
243
|
setDryRunMode(false);
|
|
199
|
-
|
|
244
|
+
if (!sandboxActive) {
|
|
245
|
+
addItem({ kind: "info", text: "Dry-run mode OFF — the agent can make changes again." });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!hasConfirmedBypassMode.current) {
|
|
249
|
+
setPhase("confirmBypassMode");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
enterBypassMode();
|
|
200
253
|
return;
|
|
201
254
|
}
|
|
202
255
|
if (acceptEdits) {
|
|
@@ -221,6 +274,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
221
274
|
if (confirmed)
|
|
222
275
|
enterAcceptEdits();
|
|
223
276
|
};
|
|
277
|
+
const onBypassModeConfirm = (confirmed) => {
|
|
278
|
+
setPhase("input");
|
|
279
|
+
if (confirmed)
|
|
280
|
+
enterBypassMode();
|
|
281
|
+
else
|
|
282
|
+
addItem({ kind: "info", text: "Auto mode declined — back to normal." });
|
|
283
|
+
};
|
|
224
284
|
const setModelEverywhere = (id) => {
|
|
225
285
|
modelRef.current = id;
|
|
226
286
|
setModel(id);
|
|
@@ -382,10 +442,12 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
382
442
|
return;
|
|
383
443
|
}
|
|
384
444
|
setPhase("working");
|
|
385
|
-
setActivity(`Calling ${provider
|
|
445
|
+
setActivity(`Calling ${displayModelId(provider, model)}…`);
|
|
386
446
|
const ac = new AbortController();
|
|
387
447
|
abortRef.current = ac;
|
|
388
448
|
setInFlight([]);
|
|
449
|
+
const changedFiles = new Set();
|
|
450
|
+
let turnSucceeded = false;
|
|
389
451
|
try {
|
|
390
452
|
await agent.runTurn(text, {
|
|
391
453
|
onTextDelta: (delta) => {
|
|
@@ -407,6 +469,11 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
407
469
|
},
|
|
408
470
|
onToolEnd: (id, name, summary, preview, isError, resultSummary) => {
|
|
409
471
|
setInFlight((prev) => prev.filter((t) => t.id !== id));
|
|
472
|
+
if (!isError && FILE_MUTATING_TOOLS.has(name)) {
|
|
473
|
+
const p = pathFromToolSummary(summary);
|
|
474
|
+
if (p)
|
|
475
|
+
changedFiles.add(p);
|
|
476
|
+
}
|
|
410
477
|
if (name !== "update_tasks")
|
|
411
478
|
addItem({
|
|
412
479
|
kind: "tool",
|
|
@@ -438,6 +505,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
438
505
|
recordUsage(usage);
|
|
439
506
|
},
|
|
440
507
|
}, ac.signal, images);
|
|
508
|
+
turnSucceeded = true;
|
|
441
509
|
}
|
|
442
510
|
catch (err) {
|
|
443
511
|
const isAbort = (err instanceof Error && err.name === "AbortError") ||
|
|
@@ -468,12 +536,20 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
468
536
|
text = `Error: ${message}${hint}`;
|
|
469
537
|
}
|
|
470
538
|
else {
|
|
471
|
-
|
|
539
|
+
const friendlyHint = friendlyProviderErrorHint(err, provider);
|
|
540
|
+
text = friendlyHint ? `${friendlyHint}\n\n(${message})` : `Error: ${message}`;
|
|
472
541
|
}
|
|
473
542
|
}
|
|
474
543
|
addItem({ kind: "info", text });
|
|
475
544
|
}
|
|
476
545
|
finally {
|
|
546
|
+
if (turnSucceeded && changedFiles.size > 0) {
|
|
547
|
+
addItem({
|
|
548
|
+
kind: "summary",
|
|
549
|
+
files: [...changedFiles],
|
|
550
|
+
nextStep: "review with /diff · revert with /undo",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
477
553
|
abortRef.current = null;
|
|
478
554
|
setInFlight([]);
|
|
479
555
|
setStream("");
|
|
@@ -552,9 +628,13 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
552
628
|
releaseKill,
|
|
553
629
|
acceptEdits,
|
|
554
630
|
setAcceptEdits,
|
|
631
|
+
bypassMode,
|
|
632
|
+
setBypassMode,
|
|
633
|
+
sandboxActive,
|
|
555
634
|
autoApprovedCount,
|
|
556
635
|
cycleMode,
|
|
557
636
|
onAcceptEditsConfirm,
|
|
637
|
+
onBypassModeConfirm,
|
|
558
638
|
abortRef,
|
|
559
639
|
setModelEverywhere,
|
|
560
640
|
setProviderEverywhere,
|
package/package.json
CHANGED