kritya 0.8.32-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 +2 -1
- package/dist/crash.js +9 -0
- package/dist/index.js +110 -11
- package/dist/session/exitNotice.js +30 -0
- package/dist/session/store.js +103 -1
- package/dist/ui/App.js +4 -3
- package/dist/ui/useAgent.js +4 -2
- package/package.json +1 -1
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
|
|
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
|
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
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
+
}
|
package/dist/session/store.js
CHANGED
|
@@ -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({
|
|
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.
|
|
@@ -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,
|
|
@@ -505,13 +506,13 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
505
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) => {
|
|
506
507
|
setPhase("input");
|
|
507
508
|
setModelEverywhere(id);
|
|
508
|
-
}, 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 ?? [])
|
|
509
510
|
.filter((s) => s.title.toLowerCase().includes(resumeFilter.toLowerCase()) ||
|
|
510
511
|
SessionStore.matchesContent(s.file, resumeFilter))
|
|
511
512
|
.map((s) => ({
|
|
512
513
|
label: s.title,
|
|
513
514
|
value: s.file,
|
|
514
|
-
hint: `${s.date} · ${s.count} msgs`,
|
|
515
|
+
hint: `${s.shortId} · ${s.date} · ${s.count} msgs`,
|
|
515
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) => {
|
|
516
517
|
setInput(v.replace(/\t/g, ""));
|
|
517
518
|
setCmdIndex(0);
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -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:
|
|
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;
|