kritya 0.8.39-beta → 0.8.41-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 CHANGED
@@ -195,7 +195,8 @@ Custom `/commands` you define (see [docs/CONFIGURATION.md](docs/CONFIGURATION.md
195
195
 
196
196
  `Esc` cancels a running request. `↑/↓` recalls input history. `Ctrl+O` toggles
197
197
  full tool output. `Ctrl+E` opens the file the agent touched most recently in
198
- `$EDITOR`. `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
198
+ `$EDITOR`. `Ctrl+P` opens the command palette (fuzzy-find commands and
199
+ checkpoints). `Ctrl+K` is the kill switch (see below). `Ctrl+C` exits.
199
200
 
200
201
  ### More features
201
202
 
package/dist/index.js CHANGED
@@ -221,17 +221,21 @@ if (args.version) {
221
221
  // pipe never pays for it and never sees it), cached for a day, bounded, and
222
222
  // written to stderr.
223
223
  console.log(VERSION);
224
+ // Awaited, not fired off behind a deferred process.exit(). This is
225
+ // top-level module code with no `return`, so a detached exit does not stop
226
+ // the statements below it: on a TTY the whole interactive path ran anyway
227
+ // (the TTY check, then main() and its AI-disclosure banner) and was then
228
+ // killed mid-frame when the notice resolved, which trips a libuv assertion
229
+ // on Windows — "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
230
+ // file src\win\async.c". Awaiting keeps the notice and makes the exit
231
+ // synchronous. checkForUpdate is capped by a 2s AbortSignal timeout, cached
232
+ // for a day, and never throws, so this cannot hang or fail.
224
233
  if (process.stdout.isTTY) {
225
- void updateNoticeForUser()
226
- .then((notice) => {
227
- if (notice)
228
- console.error(notice);
229
- })
230
- .finally(() => process.exit(0));
231
- }
232
- else {
233
- process.exit(0);
234
+ const notice = await updateNoticeForUser();
235
+ if (notice)
236
+ console.error(notice);
234
237
  }
238
+ process.exit(0);
235
239
  }
236
240
  const workspace = path.resolve(args.dir);
237
241
  if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
@@ -1,9 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { classifyDanger } from "../permissions/danger.js";
6
7
  let cachedTool;
8
+ let cachedMxcExec;
7
9
  function commandExists(bin) {
8
10
  const finder = os.platform() === "win32" ? "where" : "which";
9
11
  try {
@@ -13,7 +15,66 @@ function commandExists(bin) {
13
15
  return false;
14
16
  }
15
17
  }
16
- /** Which sandbox binary (if any) is usable on this platform, cached after the first check. */
18
+ /**
19
+ * MXC ships `wxc-exec.exe` per architecture (`bin/x64/`, `bin/arm64/`), and its
20
+ * own resolver picks the same way — see `sdk/node/src/v1/platform.ts::
21
+ * findWxcExecutable`, which this mirrors.
22
+ */
23
+ function mxcArch() {
24
+ return os.arch() === "arm64" ? "arm64" : "x64";
25
+ }
26
+ /** The `@microsoft/mxc-sdk` install directory, or null when it isn't installed. */
27
+ function mxcPackageRoot() {
28
+ try {
29
+ // `./package.json` is in the SDK's exports map, so this resolves whether or
30
+ // not the package's main entry is importable.
31
+ const require = createRequire(import.meta.url);
32
+ return path.dirname(require.resolve("@microsoft/mxc-sdk/package.json"));
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Locate `wxc-exec.exe`, in the order a user would expect to override it: an
40
+ * explicit path, then MXC's own `MXC_BIN_DIR`, then `PATH`, then an optional
41
+ * `@microsoft/mxc-sdk` install. Null means MXC isn't present, which leaves
42
+ * Windows behaving exactly as it did before MXC support existed.
43
+ */
44
+ function locateMxcExecutable() {
45
+ const explicit = process.env.KRITYA_MXC_EXEC;
46
+ if (explicit && fs.existsSync(explicit))
47
+ return explicit;
48
+ // MXC's own override: <MXC_BIN_DIR>/<arch>/wxc-exec.exe.
49
+ const binDir = process.env.MXC_BIN_DIR;
50
+ if (binDir) {
51
+ const candidate = path.join(binDir, mxcArch(), "wxc-exec.exe");
52
+ if (fs.existsSync(candidate))
53
+ return candidate;
54
+ }
55
+ // On PATH — kept as a bare name so the OS resolves it at spawn time.
56
+ if (commandExists("wxc-exec.exe"))
57
+ return "wxc-exec.exe";
58
+ const root = mxcPackageRoot();
59
+ if (root) {
60
+ const candidate = path.join(root, "bin", mxcArch(), "wxc-exec.exe");
61
+ if (fs.existsSync(candidate))
62
+ return candidate;
63
+ }
64
+ return null;
65
+ }
66
+ /** Cached after the first check — locating it can shell out to `where`. */
67
+ function mxcExecutable() {
68
+ if (cachedMxcExec === undefined)
69
+ cachedMxcExec = locateMxcExecutable();
70
+ return cachedMxcExec;
71
+ }
72
+ /** Test-only: drops the memoized backend and MXC path between cases. */
73
+ export function resetSandboxToolCache() {
74
+ cachedTool = undefined;
75
+ cachedMxcExec = undefined;
76
+ }
77
+ /** Which sandbox backend (if any) is usable on this platform, cached after the first check. */
17
78
  function sandboxTool() {
18
79
  if (cachedTool !== undefined)
19
80
  return cachedTool;
@@ -24,6 +85,12 @@ function sandboxTool() {
24
85
  else if (platform === "darwin") {
25
86
  cachedTool = commandExists("sandbox-exec") ? "sandbox-exec" : null;
26
87
  }
88
+ else if (platform === "win32") {
89
+ // MXC (Microsoft eXecution Containers) is Windows' only containment
90
+ // backend — AppContainer + DACLs, enforced by the kernel. It is an
91
+ // optional install, so "not there" is the common case and stays null.
92
+ cachedTool = mxcExecutable() ? "mxc" : null;
93
+ }
27
94
  else {
28
95
  cachedTool = null;
29
96
  }
@@ -35,10 +102,11 @@ export function sandboxAvailable() {
35
102
  /**
36
103
  * Default `sandboxMode` when the config leaves `sandboxExec` unset. "auto" on
37
104
  * Linux/macOS, where bwrap/sandbox-exec can actually confine writes to the
38
- * workspace. On Windows there's no sandbox backend at all (see
39
- * `sandboxUnavailableReason`), so "auto"/"always" would let a flagged command
40
- * run unprotected outside the workspace — "strict" refuses those instead of
41
- * silently falling back.
105
+ * workspace. On Windows "strict" is kept even though MXC can now back it,
106
+ * because "strict" is the mode that fails closed: with MXC installed every
107
+ * command runs *inside* the container instead of being refused, and on a host
108
+ * where MXC isn't present (or can't be served) commands are refused rather
109
+ * than silently falling back to an unconfined run.
42
110
  */
43
111
  export function defaultSandboxMode() {
44
112
  return os.platform() === "win32" ? "strict" : "auto";
@@ -46,8 +114,10 @@ export function defaultSandboxMode() {
46
114
  /** One-line reason sandboxing can't run here, for a fallback warning. */
47
115
  export function sandboxUnavailableReason() {
48
116
  const platform = os.platform();
49
- if (platform === "win32")
50
- return "sandboxed execution isn't supported on Windows yet";
117
+ if (platform === "win32") {
118
+ return ("wxc-exec.exe not found (install @microsoft/mxc-sdk, set MXC_BIN_DIR, " +
119
+ "or point KRITYA_MXC_EXEC at the binary)");
120
+ }
51
121
  if (platform === "linux")
52
122
  return "bwrap (bubblewrap) not found on PATH";
53
123
  if (platform === "darwin")
@@ -58,18 +128,18 @@ export function sandboxUnavailableReason() {
58
128
  export function shouldSandbox(mode, command) {
59
129
  if (!mode || mode === "off")
60
130
  return false;
61
- // "always"/"strict" mean always, on every platform — including Windows,
62
- // where there's no sandbox binary to back it, so every command falls back
63
- // to the "[sandbox unavailable]" note ("always") or is refused outright
64
- // ("strict"). That's deliberate: these are the modes for someone who wants
65
- // maximum enforcement/visibility even without a real sandbox backing it.
131
+ // "always"/"strict" mean always, on every platform. Where no backend can be
132
+ // found that falls back to the "[sandbox unavailable]" note ("always") or a
133
+ // refusal ("strict") — deliberate, since these are the modes for someone who
134
+ // wants maximum enforcement/visibility even without a sandbox behind them.
66
135
  if (mode === "always" || mode === "strict")
67
136
  return true;
68
- // "auto": Windows has no sandbox binary at all — falling back to "only
69
- // flagged commands" (today's behavior) avoids a spurious fallback note on
70
- // every single shell call, which "sandbox everything" would otherwise cause.
137
+ // "auto" on Windows without a backend: falling back to "only flagged
138
+ // commands" avoids a spurious fallback note on every single shell call,
139
+ // which "sandbox everything" would otherwise cause. With MXC installed
140
+ // there *is* a backend, so "auto" means here what it means elsewhere.
71
141
  if (os.platform() === "win32")
72
- return classifyDanger(command) !== null;
142
+ return sandboxAvailable() || classifyDanger(command) !== null;
73
143
  return true;
74
144
  }
75
145
  /**
@@ -365,6 +435,169 @@ ${writeExtra}
365
435
  ${readExtra}
366
436
  `;
367
437
  }
438
+ /**
439
+ * Windows' system-drive root, e.g. `C:\` — the read-everything analogue of
440
+ * bwrap's `--ro-bind / /`. It is granted through MXC's *readwrite* list rather
441
+ * than its readonly one; see `mxcPolicyJson` for why that is not a loosening.
442
+ */
443
+ function systemDriveRoot() {
444
+ const drive = process.env.SystemDrive || "C:";
445
+ return drive.endsWith("\\") ? drive : `${drive}\\`;
446
+ }
447
+ /**
448
+ * `command` as a `cmd.exe` command line, the way `buildSandboxedCommand`'s
449
+ * other two branches get theirs from `sh -c`.
450
+ *
451
+ * `wxc-exec` takes one command-line string and hands it to CreateProcess, which
452
+ * reads it as an image plus arguments — no shell, so `a && b` arrives at the
453
+ * first program as literal arguments and `|`, `>`, `cd …` and the like do
454
+ * nothing at all. This mirrors what Node's own `child_process.exec` passes on
455
+ * Windows (`cmd.exe /d /s /c "…"`), so a sandboxed command and an unsandboxed
456
+ * one behave the same: `/d` skips the registry AutoRun hooks, and `/s` makes
457
+ * cmd strip only the outermost quotes so a command carrying its own quoted
458
+ * arguments survives. Verified through wxc-exec: `&&`, `|`, `>` and nested
459
+ * quotes (`node -e "…"`) all work, and `dir`/`echo` resolve as cmd builtins.
460
+ */
461
+ function windowsShellCommand(command) {
462
+ return `cmd.exe /d /s /c "${command}"`;
463
+ }
464
+ /**
465
+ * `PATH` directories the container must be able to read for `cmd.exe` to
466
+ * resolve bare tool names such as `git`, `npm` and `node`.
467
+ *
468
+ * The container's `PATH` is the real, registry-derived one, so it lists
469
+ * directories like `C:\Program Files\nodejs` — but those are `EPERM` to the
470
+ * container until granted, so cmd.exe's lookup fails inside them and reports
471
+ * `'node' is not recognized as an internal or external command`. Granting them
472
+ * read-only fixes it. This is the same job MXC's own
473
+ * `policy.filesystem.getAvailableToolsPolicy` does, and the filters below
474
+ * follow it.
475
+ *
476
+ * Filtered rather than passed through wholesale:
477
+ * - only paths that exist and are directories,
478
+ * - nothing under `%SystemRoot%` — the container already has a baseline over
479
+ * the system directories, and MXC's helper excludes them for that reason,
480
+ * - never a drive root, and never a path that contains the home directory or
481
+ * the workspace: granting an ancestor of either aborts container creation
482
+ * outright (see `mxcPolicyJson`), and a `PATH` entry that contains the
483
+ * workspace would also contradict the read-write grant it already has.
484
+ */
485
+ function windowsToolPaths(workspace) {
486
+ const home = os.homedir();
487
+ const systemRoot = (process.env.SystemRoot || process.env.WINDIR || "C:\\Windows").replace(/\\+$/, "");
488
+ // True when `parent` is `child` or contains it.
489
+ const contains = (parent, child) => {
490
+ const rel = path.relative(parent, child);
491
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
492
+ };
493
+ const seen = new Set();
494
+ const out = [];
495
+ for (const raw of (process.env.PATH ?? "").split(path.delimiter)) {
496
+ const entry = raw.trim();
497
+ if (!entry || !path.isAbsolute(entry))
498
+ continue;
499
+ const abs = path.resolve(entry);
500
+ const key = abs.toLowerCase();
501
+ if (seen.has(key))
502
+ continue;
503
+ seen.add(key);
504
+ // `C:\` (and a bare `C:`) is the read-everything entry, handled separately.
505
+ if (abs === path.parse(abs).root)
506
+ continue;
507
+ if (abs.toLowerCase() === systemRoot.toLowerCase())
508
+ continue;
509
+ if (abs.toLowerCase().startsWith(systemRoot.toLowerCase() + "\\"))
510
+ continue;
511
+ if (contains(abs, home) || contains(abs, workspace))
512
+ continue;
513
+ try {
514
+ if (!fs.statSync(abs).isDirectory())
515
+ continue;
516
+ }
517
+ catch {
518
+ continue;
519
+ }
520
+ out.push(abs);
521
+ }
522
+ return out;
523
+ }
524
+ /**
525
+ * The MXC request for one command, as JSON. Mirrors the posture of the bwrap
526
+ * and sandbox-exec branches below: reads stay open, writes are confined to the
527
+ * workspace plus the same short allowlist of tool caches.
528
+ *
529
+ * MXC's schema is versioned and the version marker is part of the contract, so
530
+ * this targets 1.0.0 (`schemas/stable/mxc-config.schema.1.0.0.json`).
531
+ * `wxc-exec` validates natively and refuses a request it cannot serve, so a
532
+ * policy that is wrong here fails the command rather than running it unconfined.
533
+ *
534
+ * Deliberate choices, all to keep Windows consistent with the other platforms
535
+ * rather than stricter by accident:
536
+ * - The network is left open. Neither bwrap nor sandbox-exec restricts it, and
537
+ * a contained command that cannot reach the network cannot `npm install`,
538
+ * `git push`, or run a dev server. Tightening it is a cross-platform call.
539
+ * - The temp dir is not isolated. Windows has no tmpfs equivalent, so scratch
540
+ * state already persists across calls without a shared-dir bind. MXC hands
541
+ * the container its own writable temp dir regardless (it rewrites `TEMP` to
542
+ * `%LOCALAPPDATA%\Packages\sandbox.{…}\AC\Temp`), so nothing needs granting
543
+ * here either.
544
+ * - UI access stays enabled. MXC's default blocks the Win32k subsystem, and a
545
+ * contained process that cannot reach it dies before running a line of its
546
+ * own code, with STATUS_DLL_INIT_FAILED (0xC0000142) — wxc-exec names this
547
+ * exact setting when it reports that. bwrap and sandbox-exec do not restrict
548
+ * the UI either, so this is the consistent posture rather than a loosening.
549
+ * - The system drive root is granted as read-write, and `readonlyPaths` is
550
+ * omitted entirely. Both spellings are meant to say "reads stay open over
551
+ * the whole drive", and MXC's own `policy.filesystem.getPowerShellPolicy`
552
+ * emits the readonly one — but on Windows 11 24H2 that spelling aborts
553
+ * container creation. wxc-exec logs `process created (PID …)` and the child
554
+ * then exits 1 with no output, so the command silently never runs; it is
555
+ * reproducible with a bare `readonlyPaths: ["C:\\"]` and with the drive root
556
+ * added to a policy that already works. Every ancestor of the profile chain
557
+ * behaves the same way (`C:\Users`, `%USERPROFILE%`, `…\AppData`,
558
+ * `…\AppData\Local`), which is consistent with the drive root needing to be
559
+ * DACL-brokered for the contained token. Listing it under `readwritePaths`
560
+ * instead is harmless and grants what was actually intended: the command can
561
+ * stat `C:\` — which `node.exe`, `cmd.exe` and `pwsh.exe` all do at startup,
562
+ * and without which they die with `EPERM: lstat 'C:\'` — while writes to the
563
+ * root, to the profile and to `%USERPROFILE%` are still denied. Verified
564
+ * against `wxc-exec.exe` 1.0.0 on Windows 11 24H2 with a policy of exactly
565
+ * this shape: reads of `C:\`, `System32` and `Program Files` succeed, the
566
+ * workspace and the container's own temp dir are writable, and the profile
567
+ * root, the drive root and `C:\Windows\Temp` all stay unwritable.
568
+ * `readonlyPaths` carries the `PATH` tool directories from
569
+ * `windowsToolPaths`. MXC already gives the container a baseline over the
570
+ * system directories and over anything whose ACL carries an AppContainer
571
+ * capability SID, so most of `PATH` needs nothing said about it — but a
572
+ * directory outside that baseline (per-user installs such as
573
+ * `%APPDATA%\npm`) is `EPERM` until granted, and `cmd.exe` cannot resolve a
574
+ * bare `npm` or `node` that lives in one. The key is omitted rather than
575
+ * sent as `[]` when there is nothing to add, so a policy built without it
576
+ * is byte-identical to what this function produced before.
577
+ */
578
+ export function mxcPolicyJson(command, workspace, writable, readable = []) {
579
+ return JSON.stringify({
580
+ version: "1.0.0",
581
+ containment: "processcontainer",
582
+ process: { commandLine: command, cwd: workspace },
583
+ filesystem: {
584
+ // Existing paths only: the bwrap branch skips missing binds for the same
585
+ // reason, and naming a path that is not there is a needless way to have
586
+ // the whole request rejected. The drive root closes the list because it
587
+ // is the read-everything entry — see the note above for why it belongs
588
+ // here rather than in `readonlyPaths`.
589
+ readwritePaths: [...new Set([...writable, workspace, systemDriveRoot()])],
590
+ // Never the drive root or the profile chain — see the note above; both
591
+ // are fatal under `readonlyPaths`.
592
+ ...(readable.length > 0 ? { readonlyPaths: [...new Set(readable)] } : {}),
593
+ },
594
+ network: { egress: { default: "allow" }, ingress: { default: "allow" } },
595
+ ui: { disable: false },
596
+ // MXC telemetry is off unless the run opts in, the user consents, and
597
+ // administrative policy permits it. Kritya opts out on the user's behalf.
598
+ telemetry: { enabled: false },
599
+ });
600
+ }
368
601
  /**
369
602
  * Wraps `command` (run via `sh -c`) so it's confined to `workspace`: writes
370
603
  * are blocked everywhere else, network and (outside the real temp-dir roots)
@@ -382,6 +615,33 @@ export function buildSandboxedCommand(command, workspace) {
382
615
  return null;
383
616
  // Linked worktrees / submodules keep their real git dir outside the workspace.
384
617
  const gitDir = externalGitCommonDir(workspace);
618
+ if (tool === "mxc") {
619
+ // Windows: hand the whole request to wxc-exec, which creates the
620
+ // AppContainer and runs the command inside it. The policy travels as
621
+ // base64 rather than a file — the same way MXC's own SDK passes it — so
622
+ // there is no profile file to create, guard, or clean up. Compare the
623
+ // symlink/TOCTOU defences the macOS branch needs for its profile: never
624
+ // writing a policy to disk removes that whole class of problem.
625
+ //
626
+ // Deliberately ahead of the shared-temp-dir setup below, which creates a
627
+ // directory as a side effect: Windows has no tmpfs to carve it out of, so
628
+ // there is nothing for it to do here.
629
+ const exe = mxcExecutable();
630
+ if (!exe)
631
+ return null;
632
+ const writable = [...extraWritablePaths(), ...(gitDir ? [gitDir] : [])].filter((p) => fs.existsSync(p));
633
+ // Unlike the branches below there is no `sh -c` here to give the command a
634
+ // shell, so `windowsShellCommand` supplies the `cmd.exe` one — see its
635
+ // note. `windowsToolPaths` supplies the read-only grants cmd.exe needs to
636
+ // resolve bare tool names inside the container.
637
+ return {
638
+ cmd: exe,
639
+ args: [
640
+ "--config-base64",
641
+ Buffer.from(mxcPolicyJson(windowsShellCommand(command), workspace, writable, windowsToolPaths(workspace)), "utf8").toString("base64"),
642
+ ],
643
+ };
644
+ }
385
645
  // null if the path exists but isn't a real, self-owned directory (e.g.
386
646
  // another local user squatted it as a symlink) — see safeSandboxSharedTmpDir.
387
647
  const shared = safeSandboxSharedTmpDir();
package/dist/ui/App.js CHANGED
@@ -17,6 +17,7 @@ import { ModelPicker } from "./ModelPicker.js";
17
17
  import { ElicitationPrompt } from "./ElicitationPrompt.js";
18
18
  import { PermissionPrompt } from "./PermissionPrompt.js";
19
19
  import { SelectList } from "./SelectList.js";
20
+ import { buildPaletteItems } from "./palette.js";
20
21
  import { Spinner } from "./Spinner.js";
21
22
  import { StatusLine } from "./StatusLine.js";
22
23
  import { StreamViewport } from "./StreamViewport.js";
@@ -86,6 +87,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
86
87
  const [inputKey, setInputKey] = useState(0);
87
88
  const [steerInput, setSteerInput] = useState("");
88
89
  const [resumeFilter, setResumeFilter] = useState("");
90
+ const [paletteFilter, setPaletteFilter] = useState("");
89
91
  const [cmdIndex, setCmdIndex] = useState(0);
90
92
  const [fileIndex, setFileIndex] = useState(0);
91
93
  const [fileList, setFileList] = useState([]);
@@ -243,6 +245,19 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
243
245
  engageKill("Ctrl+K");
244
246
  return;
245
247
  }
248
+ // Ctrl+P opens the command palette — fuzzy-find commands and checkpoints.
249
+ // Idle prompt only, like Ctrl+E: opening it mid-turn would race the agent,
250
+ // and the kill switch above must keep its chord unshared.
251
+ if (key.ctrl && _input === "p") {
252
+ if (phase === "input") {
253
+ setPaletteFilter("");
254
+ setPhase("palette");
255
+ }
256
+ else if (phase === "palette") {
257
+ setPhase("input");
258
+ }
259
+ return;
260
+ }
246
261
  if (key.escape && phase === "working") {
247
262
  setActivity("cancelling…");
248
263
  abortRef.current?.abort();
@@ -315,6 +330,13 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
315
330
  setResumeFilter((f) => f + _input);
316
331
  }
317
332
  }
333
+ if (phase === "palette") {
334
+ if (key.backspace || key.delete)
335
+ setPaletteFilter((f) => f.slice(0, -1));
336
+ else if (_input && !key.upArrow && !key.downArrow && !key.return && !key.escape && !key.tab) {
337
+ setPaletteFilter((f) => f + _input);
338
+ }
339
+ }
318
340
  if (suggestions.length) {
319
341
  if (key.upArrow)
320
342
  setCmdIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
@@ -385,6 +407,24 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
385
407
  };
386
408
  void runCommand(cmd, ctx);
387
409
  };
410
+ // Command palette selection. Commands are inserted into the input line —
411
+ // never executed blind: a fuzzy finder that runs on Enter is how
412
+ // conversations get cleared by accident. Checkpoints rewind immediately;
413
+ // the name is complete and unambiguous, exactly like /rewind <name>.
414
+ const onPaletteSelect = (value) => {
415
+ if (value.startsWith("checkpoint:")) {
416
+ const name = value.slice("checkpoint:".length);
417
+ setPhase("input");
418
+ handleSlash(`/rewind ${name}`);
419
+ return;
420
+ }
421
+ if (value.startsWith("cmd:")) {
422
+ setInput(`${value.slice("cmd:".length)} `);
423
+ setInputKey((k) => k + 1);
424
+ setPhase("input");
425
+ }
426
+ };
427
+ const paletteItems = buildPaletteItems(allCommands, agent.listCheckpoints(), paletteFilter);
388
428
  const expandMentions = async (text) => {
389
429
  const mentions = [...new Set([...text.matchAll(MENTION_ALL_RE)].map((m) => m[1]))];
390
430
  let extra = "";
@@ -517,7 +557,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
517
557
  label: s.name || s.title,
518
558
  value: s.file,
519
559
  hint: `${s.shortId} · ${s.date} · ${s.count} msgs`,
520
- })), 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) => {
560
+ })), onSelect: onResumeSelect, onCancel: () => onResumeSelect("") })] })), phase === "palette" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Command palette", " ", _jsx(Text, { dimColor: true, children: "(type to filter \u00B7 \u2191\u2193 navigate \u00B7 Enter selects \u00B7 Esc closes \u00B7 Ctrl+P toggles)" })] }), paletteFilter ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "filter: " }), paletteFilter] })) : null, _jsx(SelectList, { items: paletteItems, onSelect: onPaletteSelect, onCancel: () => setPhase("input") })] })), 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) => {
521
561
  setInput(v.replace(/\t/g, ""));
522
562
  setCmdIndex(0);
523
563
  setFileIndex(0);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Subsequence match, case-insensitive: "rwd" matches "/rewind", "exmd"
3
+ * matches "/export". Order matters ("mre" does not match "/rename") but
4
+ * contiguity doesn't — this is what makes a palette feel like a palette
5
+ * rather than the `/` prefix completion.
6
+ */
7
+ export function fuzzyMatch(haystack, needle) {
8
+ const h = haystack.toLowerCase();
9
+ const n = needle.trim().toLowerCase();
10
+ if (!n)
11
+ return true;
12
+ let i = 0;
13
+ for (const ch of h) {
14
+ if (ch === n[i])
15
+ i++;
16
+ if (i === n.length)
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ /**
22
+ * The palette's rows: every command first, then every checkpoint. Values are
23
+ * prefixed so the selection handler knows what was picked without a second
24
+ * lookup — `cmd:/rename` inserts the command into the input line (never runs
25
+ * it blind: a fuzzy finder that executes on Enter is how conversations get
26
+ * cleared by accident), `checkpoint:<name>` rewinds to that checkpoint.
27
+ */
28
+ export function buildPaletteItems(commands, checkpoints, filter) {
29
+ const items = [];
30
+ for (const c of commands) {
31
+ if (!fuzzyMatch(`${c.name} ${c.description}`, filter))
32
+ continue;
33
+ items.push({ label: c.name, value: `cmd:${c.name}`, hint: c.description });
34
+ }
35
+ for (const cp of checkpoints) {
36
+ if (!fuzzyMatch(`checkpoint ${cp.name}`, filter))
37
+ continue;
38
+ items.push({
39
+ label: `checkpoint: ${cp.name}`,
40
+ value: `checkpoint:${cp.name}`,
41
+ hint: `rewind · saved ${new Date(cp.createdAt).toLocaleTimeString()}`,
42
+ });
43
+ }
44
+ return items;
45
+ }
@@ -125,7 +125,8 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
125
125
  const hasConfirmedAcceptEdits = useRef(false);
126
126
  const hasConfirmedBypassMode = useRef(false);
127
127
  /** Whether auto/bypass mode is even reachable — requires a real sandbox
128
- * backend on this platform (Windows never has one; see sandboxAvailable). */
128
+ * backend on this platform (on Windows that means MXC is installed; see
129
+ * sandboxAvailable). */
129
130
  const sandboxActive = (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable();
130
131
  const abortRef = useRef(null);
131
132
  /** Tool calls currently running, keyed by call id — more than one when a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.39-beta",
3
+ "version": "0.8.41-beta",
4
4
  "description": "Kritya — Superintelligence at your command line",
5
5
  "type": "module",
6
6
  "publishConfig": {