kritya 0.8.31-beta → 0.8.33-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
@@ -124,7 +124,8 @@ kritya # enter
124
124
  kritya [directory] [options]
125
125
 
126
126
  -c, --continue resume the most recent session for this directory
127
- -r, --resume pick a past session from a list
127
+ -r, --resume [name] list past sessions, or resume one by its short name
128
+ (e.g. `kritya -r a3f9k2`; plain `-r` opens the list)
128
129
  -m, --model <id> use any model ID your provider offers
129
130
  -p, --provider <name> nvidia (default), openai, openrouter, groq, deepseek,
130
131
  mistral, together, ollama, switchyard (multi-model
@@ -443,6 +443,7 @@ const handlers = {
443
443
  "/clear": (ctx) => {
444
444
  ctx.agent.reset();
445
445
  ctx.setTasks([]);
446
+ ctx.clearItems();
446
447
  ctx.addItem({ kind: "info", text: "Conversation cleared." });
447
448
  },
448
449
  "/cost": (ctx) => {
package/dist/crash.js CHANGED
@@ -95,6 +95,15 @@ export function installCrashHandlers(opts) {
95
95
  process.on("uncaughtException", (err) => onCrash(err, "uncaught exception"));
96
96
  process.on("unhandledRejection", (reason) => onCrash(reason, "unhandled rejection"));
97
97
  }
98
+ /**
99
+ * True while the crash path is running. The crash report already tells the user
100
+ * where the transcript is and how to resume, so anything that would print its
101
+ * own sign-off on the way out (the interactive exit notice) checks this first
102
+ * and stays quiet rather than saying it twice, in two different formats.
103
+ */
104
+ export function isHandlingCrash() {
105
+ return handling;
106
+ }
98
107
  /** Test seam: forget that handlers were installed. */
99
108
  export function resetCrashHandlersForTest() {
100
109
  installed = false;
package/dist/index.js CHANGED
@@ -11,7 +11,8 @@ import { loadRules } from "./permissions/rules.js";
11
11
  import { ProviderClient } from "./provider/client.js";
12
12
  import { createSwitchyardClient } from "./provider/switchyardClient.js";
13
13
  import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel, staleSwitchyardModelWarning, } from "./provider/switchyardSidecar.js";
14
- import { SessionStore } from "./session/store.js";
14
+ import { SessionStore, shortSessionId } from "./session/store.js";
15
+ import { exitNotice } from "./session/exitNotice.js";
15
16
  import { AuditLog } from "./audit/audit.js";
16
17
  import { runAuditCli } from "./audit/cli.js";
17
18
  import { runSkillsCli } from "./agent/skillsCli.js";
@@ -39,7 +40,7 @@ import { isAiDisclosureShown, markAiDisclosureShown } from "./trust/aiDisclosure
39
40
  import { isBannerSeen, markBannerSeen } from "./trust/bannerSeen.js";
40
41
  import { partitionByTrust, serverFingerprint, trustServer } from "./trust/mcpTrust.js";
41
42
  import { runHeadless } from "./headless.js";
42
- import { installCrashHandlers } from "./crash.js";
43
+ import { installCrashHandlers, isHandlingCrash } from "./crash.js";
43
44
  import { createSubagentRunner } from "./agent/subagents.js";
44
45
  import { VERSION } from "./version.js";
45
46
  import { updateNoticeForUser } from "./update/check.js";
@@ -49,7 +50,8 @@ Usage: kritya [directory] [options]
49
50
 
50
51
  Options:
51
52
  -c, --continue resume the most recent session for this directory
52
- -r, --resume pick a past session for this directory from a list
53
+ -r, --resume [name] list past sessions, or resume one by its short name
54
+ (e.g. kritya -r a3f9k2; plain -r opens the picker)
53
55
  -m, --model <id> model ID to use (any model your provider offers)
54
56
  -p, --provider <n> provider: nvidia (default), openai, openrouter, groq,
55
57
  deepseek, mistral, together, ollama, or a custom one
@@ -88,11 +90,34 @@ Setup:
88
90
  3. cd your-project && kritya .
89
91
 
90
92
  Config file: ~/.kritya/config.json { "apiKey", "model", "customModels": [{"id"}] }`;
93
+ /** True if `p` names an existing directory — used to keep `-r <dir>` unambiguous. */
94
+ function isDirectory(p) {
95
+ try {
96
+ return fs.statSync(p).isDirectory();
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * Write the exit notice to stdout. Best-effort: this runs from an "exit"
104
+ * handler, where the stream may already be gone, and there is nowhere left to
105
+ * report that if it is.
106
+ */
107
+ function writeExitNotice(text) {
108
+ try {
109
+ process.stdout.write(text + "\n");
110
+ }
111
+ catch {
112
+ // Nothing left to write to.
113
+ }
114
+ }
91
115
  function parseArgs(argv) {
92
116
  const args = {
93
117
  dir: ".",
94
118
  continue: false,
95
119
  resume: false,
120
+ resumeName: "",
96
121
  model: "",
97
122
  provider: "",
98
123
  help: false,
@@ -108,8 +133,23 @@ function parseArgs(argv) {
108
133
  const a = argv[i];
109
134
  if (a === "-c" || a === "--continue")
110
135
  args.continue = true;
111
- else if (a === "-r" || a === "--resume")
136
+ else if (a === "-r" || a === "--resume") {
137
+ args.resume = true;
138
+ // `-r <name>` resumes one session directly; a bare `--resume` opens the
139
+ // picker. The value is only consumed when it cannot be the directory
140
+ // positional — `kritya -r some-dir` is the documented way to open a
141
+ // project, so an existing directory always wins. When the name really is
142
+ // an existing directory, `--resume=<name>` still forces it through.
143
+ const next = argv[i + 1];
144
+ if (next && !next.startsWith("-") && !isDirectory(next)) {
145
+ args.resumeName = next;
146
+ i++;
147
+ }
148
+ }
149
+ else if (a.startsWith("--resume=")) {
112
150
  args.resume = true;
151
+ args.resumeName = a.slice("--resume=".length);
152
+ }
113
153
  else if (a === "-m" || a === "--model")
114
154
  args.model = argv[++i] ?? "";
115
155
  else if (a === "-p" || a === "--provider")
@@ -390,10 +430,60 @@ async function main() {
390
430
  const sessionMeter = privacyMode
391
431
  ? createMeter(session.id, "off")
392
432
  : createMeter(session.id, config.otel);
393
- const initialHistory = !privacyMode && args.continue ? (SessionStore.loadLatest(workspace) ?? []) : [];
394
- const initialTasks = !privacyMode && args.continue ? SessionStore.loadLatestTasks(workspace) : [];
433
+ // `-r <name>` is resolved before the session file is opened, so a typo fails
434
+ // fast with a clear message instead of silently starting an empty session.
435
+ // Precedence is named resume > -c > a fresh session.
436
+ let resumedFile;
437
+ if (!privacyMode && args.resumeName) {
438
+ const found = SessionStore.resolveSession(workspace, args.resumeName);
439
+ if ("error" in found) {
440
+ console.error(found.error);
441
+ process.exit(1);
442
+ }
443
+ resumedFile = found.file;
444
+ }
445
+ let initialHistory = [];
446
+ let initialTasks = [];
447
+ if (!privacyMode) {
448
+ if (resumedFile) {
449
+ initialHistory = SessionStore.loadFile(resumedFile);
450
+ initialTasks = SessionStore.loadTasksForSession(resumedFile);
451
+ }
452
+ else if (args.continue) {
453
+ initialHistory = SessionStore.loadLatest(workspace) ?? [];
454
+ initialTasks = SessionStore.loadLatestTasks(workspace);
455
+ }
456
+ }
395
457
  session.start(initialHistory);
396
- const resumeSessions = !privacyMode && args.resume ? SessionStore.listSessions(workspace) : [];
458
+ // The picker only opens when no session was named — `-r a3f9k2` has already
459
+ // decided which conversation to open.
460
+ const resumeSessions = !privacyMode && args.resume && !resumedFile ? SessionStore.listSessions(workspace) : [];
461
+ // Where the conversation went, said once on the way out. This is the last
462
+ // thing on screen and the only place the session's short name is discoverable
463
+ // without re-running --resume, so it is worth printing rather than leaving
464
+ // the user to guess. Registered on "exit" rather than drawn as a transcript
465
+ // item so it covers every way out — Ctrl+C and a real signal included, not
466
+ // just /exit. The crash handler prints its own version of this, so it is
467
+ // skipped there rather than said twice in two formats.
468
+ let farewellPrinted = false;
469
+ const printFarewell = () => {
470
+ if (farewellPrinted || isHandlingCrash())
471
+ return;
472
+ farewellPrinted = true;
473
+ const file = session.path;
474
+ const text = exitNotice({
475
+ privacyMode,
476
+ messageCount: session.messageCount,
477
+ // A count with no file on disk means persistence failed somewhere along
478
+ // the way — don't promise a resume that cannot work.
479
+ file: file && fs.existsSync(file) ? file : undefined,
480
+ workspace,
481
+ cwd: process.cwd(),
482
+ isTTY: Boolean(process.stdout.isTTY),
483
+ });
484
+ if (text)
485
+ writeExitNotice(text);
486
+ };
397
487
  // Filled in once the app is mounted, below; the crash handler needs a way to
398
488
  // tear the UI down and is installed before there is a UI to tear down.
399
489
  const ui = {};
@@ -409,6 +499,10 @@ async function main() {
409
499
  // resort fallback path stays on the synchronous, fire-and-forget flush()
410
500
  // inside cleanup() — it's best-effort only, not guaranteed to land.
411
501
  process.on("exit", cleanup);
502
+ // Registered after cleanup so the sign-off really is the last thing printed:
503
+ // cleanup kills background servers and MCP children, and anything they write
504
+ // to stderr on the way down would otherwise land after it.
505
+ process.on("exit", printFarewell);
412
506
  // Same reasoning as the signal handlers below, for the other way the process
413
507
  // can die without firing "exit": an error nothing caught. Also hands the
414
508
  // terminal back — Ink leaves it in raw mode with the cursor hidden.
@@ -418,9 +512,14 @@ async function main() {
418
512
  unmountUi: () => ui.instance?.unmount(),
419
513
  details: () => {
420
514
  const file = session.path;
421
- return file
422
- ? ["", `The conversation was saved to ${file}`, `Resume it with: kritya -c ${workspace}`]
423
- : [];
515
+ if (!file)
516
+ return [];
517
+ const where = workspace === process.cwd() ? "" : ` ${workspace}`;
518
+ return [
519
+ "",
520
+ `The conversation was saved to ${file}`,
521
+ `Resume it with: kritya -r ${shortSessionId(file)}${where}`,
522
+ ];
424
523
  },
425
524
  });
426
525
  // Default signal handling terminates WITHOUT firing "exit", which would
@@ -608,7 +707,7 @@ async function main() {
608
707
  // `renderTime` excludes Ink's own throttling, so it is the raw cost of the
609
708
  // frame rather than the interval between frames.
610
709
  const renderHistogram = sessionMeter.histogram("kritya.render.duration_ms", RENDER_BOUNDS_MS);
611
- ui.instance = render(_jsx(App, { agent: agent, workspace: workspace, modelRef: modelRef, providerRef: providerRef, config: config, resumedCount: initialHistory.length, initialTasks: initialTasks, undoStack: undoStack, uiBridge: uiBridge, resumeSessions: resumeSessions.length ? resumeSessions : undefined, customCommands: loadCustomCommands(workspace, trustWorkspace, plugins), mcpToolCount: mcpTools.length, onSwitchClient: (newClient) => {
710
+ ui.instance = render(_jsx(App, { agent: agent, workspace: workspace, modelRef: modelRef, providerRef: providerRef, config: config, resumedCount: initialHistory.length, resumedName: resumedFile ? shortSessionId(resumedFile) : undefined, initialTasks: initialTasks, undoStack: undoStack, uiBridge: uiBridge, resumeSessions: resumeSessions.length ? resumeSessions : undefined, customCommands: loadCustomCommands(workspace, trustWorkspace, plugins), mcpToolCount: mcpTools.length, onSwitchClient: (newClient) => {
612
711
  client = newClient;
613
712
  }, onRequestPermissionReady: (fn) => {
614
713
  permissionRef.current = fn;
@@ -0,0 +1,30 @@
1
+ import { shortSessionId } from "./store.js";
2
+ /**
3
+ * The sign-off printed when an interactive session ends, or null when there is
4
+ * nothing worth saying.
5
+ *
6
+ * This is the only place the session's short name is discoverable without
7
+ * re-running `--resume`, so it is the whole point of the feature: quit, and the
8
+ * terminal tells you the exact command that brings this conversation back.
9
+ *
10
+ * Pulled out of the CLI entry point as a pure function on purpose. It is the
11
+ * one piece of the exit path with real logic — privacy mode, an empty session,
12
+ * singular/plural, and whether the resume command even works from here — and
13
+ * `index.tsx` is excluded from coverage, so anything left in there is logic
14
+ * nothing measures.
15
+ */
16
+ export function exitNotice(input) {
17
+ if (input.privacyMode)
18
+ return "Session not saved (--privacy).";
19
+ if (!input.file || input.messageCount <= 0)
20
+ return null;
21
+ const dim = input.isTTY ? "\x1b[2m" : "";
22
+ const plain = input.isTTY ? "\x1b[22m" : "";
23
+ const n = input.messageCount;
24
+ // `kritya -r <code>` only resolves from the directory the session belongs to,
25
+ // so say where that was when it is not where the user is standing.
26
+ const where = input.workspace === input.cwd ? "" : ` ${input.workspace}`;
27
+ return (`${dim}Session saved · ${n} message${n === 1 ? "" : "s"}${plain}\n` +
28
+ `${dim} resume ${plain}kritya -r ${shortSessionId(input.file)}${where}\n` +
29
+ `${dim} list ${plain}kritya -r`);
30
+ }
@@ -58,6 +58,29 @@ function sessionDir(workspace) {
58
58
  const hash = crypto.createHash("sha1").update(workspace).digest("hex").slice(0, 12);
59
59
  return path.join(CONFIG_DIR, "sessions", hash);
60
60
  }
61
+ /**
62
+ * Short, typeable handle for a session — a 5-character base36 code derived
63
+ * deterministically from the transcript's filename.
64
+ *
65
+ * The full id (the ISO timestamp basename) stays the on-disk name and the key
66
+ * that correlates the audit log and telemetry spans, and nothing here changes
67
+ * that. This is only what the user reads and retypes: `2026-10-06T19-08-39-123Z`
68
+ * is not a thing anyone types twice, which is why the exit notice and the
69
+ * `--resume` picker both show this instead.
70
+ *
71
+ * Derived rather than stored on purpose — there is no index file to drift out
72
+ * of sync with the directory, and the code is stable for the life of the
73
+ * transcript. Uniqueness is therefore *checked* at lookup time rather than
74
+ * assumed: see resolveSession(), which reports an ambiguous prefix instead of
75
+ * silently opening the wrong conversation.
76
+ */
77
+ export function shortSessionId(file) {
78
+ const name = path.basename(file, ".jsonl");
79
+ const digest = crypto.createHash("sha1").update(name).digest();
80
+ // Low-order base36 digits, so the code is uniformly distributed over the
81
+ // alphabet rather than clustered by the timestamp's shared prefix.
82
+ return digest.readUInt32BE(0).toString(36).padStart(5, "0").slice(-5);
83
+ }
61
84
  /**
62
85
  * Transcripts can contain secrets that passed through tool output, so they are
63
86
  * always written 0o600 rather than inheriting whatever mode the file had.
@@ -76,12 +99,23 @@ export class SessionStore {
76
99
  ephemeral;
77
100
  dir;
78
101
  file;
102
+ /**
103
+ * Messages persisted to the current file so far. Tracked in memory rather
104
+ * than re-read from disk because the exit notice needs it after the process
105
+ * has already begun tearing down, where reading back a possibly 50 MB
106
+ * transcript would be both slow and pointless.
107
+ */
108
+ count = 0;
79
109
  /** When ephemeral, nothing is persisted to disk (used by subagents). */
80
110
  constructor(workspace, ephemeral = false) {
81
111
  this.ephemeral = ephemeral;
82
112
  this.dir = sessionDir(workspace);
83
113
  this.file = this.newFilePath();
84
114
  }
115
+ /** How many messages this session has written. 0 means nothing is on disk yet. */
116
+ get messageCount() {
117
+ return this.count;
118
+ }
85
119
  newFilePath() {
86
120
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
87
121
  return path.join(this.dir, `${stamp}.jsonl`);
@@ -157,6 +191,7 @@ export class SessionStore {
157
191
  if (seed.length) {
158
192
  writeSessionFile(this.file, seed.map((m) => JSON.stringify(capMessageContent(m)) + "\n").join(""));
159
193
  }
194
+ this.count = seed.length;
160
195
  }
161
196
  /**
162
197
  * Appends one JSON-encoded message per call — a crash mid-write can only
@@ -175,6 +210,7 @@ export class SessionStore {
175
210
  fs.appendFileSync(this.file, JSON.stringify(capMessageContent(message)) + "\n", {
176
211
  mode: 0o600,
177
212
  });
213
+ this.count++;
178
214
  }
179
215
  catch (err) {
180
216
  // Persistence is best-effort; never crash the session over it.
@@ -184,6 +220,7 @@ export class SessionStore {
184
220
  /** Start over with a fresh session file (used by /clear). */
185
221
  rotate() {
186
222
  this.file = this.newFilePath();
223
+ this.count = 0;
187
224
  }
188
225
  /**
189
226
  * Rewrite the session file to hold exactly `messages`, atomically. The log
@@ -198,6 +235,7 @@ export class SessionStore {
198
235
  fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
199
236
  hardenWindowsDir(CONFIG_DIR);
200
237
  writeSessionFile(this.file, messages.map((m) => JSON.stringify(capMessageContent(m)) + "\n").join(""));
238
+ this.count = messages.length;
201
239
  }
202
240
  catch (err) {
203
241
  // Persistence is best-effort; never crash the session over it.
@@ -252,6 +290,63 @@ export class SessionStore {
252
290
  return false;
253
291
  }
254
292
  }
293
+ /**
294
+ * Turn a user-typed session handle into a transcript file.
295
+ *
296
+ * Accepts the short code shown by the exit notice and the `--resume` picker,
297
+ * or the full timestamp basename, or any unambiguous prefix of either — the
298
+ * whole point is that the user can retype something short. Deliberately
299
+ * searches the entire directory rather than listSessions()' display window of
300
+ * 20, so a code copied from an earlier exit notice still resolves.
301
+ *
302
+ * Returns an error string instead of throwing so the CLI can print it and
303
+ * exit(1) without the caller having to distinguish "no such session" from
304
+ * "ambiguous" itself.
305
+ */
306
+ static resolveSession(workspace, name) {
307
+ const query = name.trim().toLowerCase();
308
+ if (!query)
309
+ return { error: "No session name given." };
310
+ const dir = sessionDir(workspace);
311
+ let entries;
312
+ try {
313
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
314
+ }
315
+ catch {
316
+ return { error: "No saved sessions for this directory yet." };
317
+ }
318
+ if (!entries.length)
319
+ return { error: "No saved sessions for this directory yet." };
320
+ // Keyed by file, not appended to a list: a query can prefix both a
321
+ // transcript's short code *and* its timestamp basename (every basename
322
+ // starts with the year), and counting that file twice would report a
323
+ // perfectly unambiguous name as ambiguous.
324
+ const matches = new Map();
325
+ for (const entry of entries) {
326
+ const file = path.join(dir, entry);
327
+ // Same containment check the IPC path uses — a symlink dropped into the
328
+ // session directory must not turn `--resume <name>` into an arbitrary
329
+ // file read.
330
+ if (!SessionStore.isSessionFile(workspace, file))
331
+ continue;
332
+ const base = path.basename(entry, ".jsonl").toLowerCase();
333
+ const short = shortSessionId(file);
334
+ if (short.startsWith(query) || base.startsWith(query))
335
+ matches.set(file, short);
336
+ }
337
+ if (!matches.size) {
338
+ return {
339
+ error: `No session matching "${name}" in this directory. Run \`kritya -r\` to list them.`,
340
+ };
341
+ }
342
+ if (matches.size > 1) {
343
+ const codes = [...matches.values()].join(", ");
344
+ return {
345
+ error: `"${name}" matches ${matches.size} sessions (${codes}) — use more characters.`,
346
+ };
347
+ }
348
+ return { file: [...matches.keys()][0] };
349
+ }
255
350
  static loadFile(filePath) {
256
351
  const messages = [];
257
352
  let raw;
@@ -404,7 +499,14 @@ export class SessionStore {
404
499
  catch {
405
500
  // leave empty
406
501
  }
407
- sessions.push({ file, date, preview, title, count: lines.length });
502
+ sessions.push({
503
+ file,
504
+ date,
505
+ preview,
506
+ title,
507
+ shortId: shortSessionId(file),
508
+ count: lines.length,
509
+ });
408
510
  }
409
511
  return sessions;
410
512
  }
package/dist/ui/App.js CHANGED
@@ -30,7 +30,7 @@ const MENTION_RE = /(^|\s)@([^\s@]*)$/;
30
30
  const MENTION_ALL_RE = /(?:^|\s)@([^\s@]+)/g;
31
31
  const MAX_MENTION_CHARS = 8000;
32
32
  const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i;
33
- export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, privacyMode, firstLaunch, }) {
33
+ export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, resumedName, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, privacyMode, firstLaunch, }) {
34
34
  const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
35
35
  /**
36
36
  * Leave, but not until the screen has caught up.
@@ -111,7 +111,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
111
111
  .catch(() => { });
112
112
  }, [workspace]);
113
113
  useEffect(refreshFileList, [refreshFileList]);
114
- 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({
114
+ const { items, addItem, clearItems, 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({
115
115
  agent,
116
116
  workspace,
117
117
  modelRef,
@@ -119,6 +119,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
119
119
  config,
120
120
  uiBridge,
121
121
  resumedCount,
122
+ resumedName,
122
123
  initialTasks,
123
124
  resumeSessions,
124
125
  refreshFileList,
@@ -357,6 +358,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
357
358
  resetBudget,
358
359
  setBudgetLimit,
359
360
  addItem,
361
+ clearItems,
360
362
  setPhase,
361
363
  setActivity,
362
364
  setRunningPhase,
@@ -504,13 +506,13 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
504
506
  }, 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) => {
505
507
  setPhase("input");
506
508
  setModelEverywhere(id);
507
- }, 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 ?? [])
509
+ }, 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 \u00B7 -r <code> to skip this)" })] }), resumeFilter ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "filter: " }), resumeFilter] })) : null, _jsx(SelectList, { items: (resumeSessions ?? [])
508
510
  .filter((s) => s.title.toLowerCase().includes(resumeFilter.toLowerCase()) ||
509
511
  SessionStore.matchesContent(s.file, resumeFilter))
510
512
  .map((s) => ({
511
513
  label: s.title,
512
514
  value: s.file,
513
- hint: `${s.date} · ${s.count} msgs`,
515
+ hint: `${s.shortId} · ${s.date} · ${s.count} msgs`,
514
516
  })), 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) => {
515
517
  setInput(v.replace(/\t/g, ""));
516
518
  setCmdIndex(0);
@@ -45,7 +45,7 @@ function initialToolStatus(name) {
45
45
  * permission state, usage/cost tracking, and session resume. Input handling
46
46
  * (text box, slash-command suggestions, @mention autocomplete) stays in App.
47
47
  */
48
- export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBridge, resumedCount, initialTasks, resumeSessions, refreshFileList, onSwitchClient, }) {
48
+ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBridge, resumedCount, resumedName, initialTasks, resumeSessions, refreshFileList, onSwitchClient, }) {
49
49
  const nextId = useRef(0);
50
50
  const [items, setItems] = useState(() => {
51
51
  const initial = [
@@ -64,7 +64,9 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
64
64
  initial.push({
65
65
  id: nextId.current++,
66
66
  kind: "info",
67
- text: `Resumed previous session (${resumedCount} messages)${taskNote}.`,
67
+ text: resumedName
68
+ ? `Resumed session ${resumedName} (${resumedCount} messages)${taskNote}.`
69
+ : `Resumed previous session (${resumedCount} messages)${taskNote}.`,
68
70
  });
69
71
  }
70
72
  return initial;
@@ -176,6 +178,11 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
176
178
  return [...prev, { ...next, id: nextId.current++ }];
177
179
  });
178
180
  }, []);
181
+ /** Empty the visible transcript (e.g. /clear). The agent's own history is
182
+ * separate — see Agent.reset() — so callers clear both when they mean it. */
183
+ const clearItems = useCallback(() => {
184
+ setItems([]);
185
+ }, []);
179
186
  const { usageByModel, totalUsage, totalCost, costReport, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, recordUsage, totalTokensRef, } = useUsageBudget({ agent, config, modelRef, model, addItem, abortRef });
180
187
  const { killed, killReason, engageKill, releaseKill } = useKillSwitch({
181
188
  agent,
@@ -589,6 +596,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
589
596
  return {
590
597
  items,
591
598
  addItem,
599
+ clearItems,
592
600
  phase,
593
601
  setPhase,
594
602
  stream,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.31-beta",
3
+ "version": "0.8.33-beta",
4
4
  "description": "Kritya — Superintelligence at your command line",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -59,36 +59,36 @@
59
59
  ],
60
60
  "license": "MIT",
61
61
  "dependencies": {
62
- "docx": "^9.7.1",
62
+ "docx": "^9.8.1",
63
63
  "exceljs": "^4.4.0",
64
64
  "ink": "^8.0.0",
65
- "jszip": "^3.10.1",
66
- "mammoth": "^1.12.2",
65
+ "jszip": "^3.10.2",
66
+ "mammoth": "^1.13.0",
67
67
  "openai": "^7.28.0",
68
68
  "pdf-lib": "^1.17.1",
69
- "pdfjs-dist": "^6.3.289",
69
+ "pdfjs-dist": "^6.4.299",
70
70
  "pptxgenjs": "^4.0.1",
71
- "react": "^19.2.8",
72
- "string-width": "^8.2.2",
71
+ "react": "^19.3.0",
72
+ "string-width": "^8.3.0",
73
73
  "tinyglobby": "^0.2.17",
74
74
  "undici": "^8.11.2",
75
75
  "wrap-ansi": "^10.0.2"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@eslint/js": "^10.0.1",
79
- "@types/node": "^26.4.1",
80
- "@types/react": "^19.2.18",
79
+ "@types/node": "^26.6.4",
80
+ "@types/react": "^19.3.0",
81
81
  "@typescript/native": "npm:typescript@^7.0.2",
82
82
  "c8": "^12.0.0",
83
- "electron": "^44.2.0",
83
+ "electron": "^44.5.1",
84
84
  "electron-builder": "^26.15.3",
85
- "eslint": "^10.9.1",
86
- "globals": "^17.12.0",
85
+ "eslint": "^10.12.0",
86
+ "globals": "^17.13.0",
87
87
  "husky": "^9.1.7",
88
88
  "ink-testing-library": "^4.0.0",
89
- "lint-staged": "^17.4.1",
90
- "prettier": "^3.9.0",
91
- "tsx": "^4.23.13",
89
+ "lint-staged": "^17.6.0",
90
+ "prettier": "^3.9.9",
91
+ "tsx": "^4.23.15",
92
92
  "typescript": "npm:@typescript/typescript6@^6.0.2",
93
93
  "typescript-eslint": "^8.71.1"
94
94
  },