@cjhyy/code-shell-core 0.8.8 → 0.8.10
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/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +81 -27
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-finalize.js +8 -6
- package/dist/engine/run-session-open.js +1 -1
- package/dist/engine/run-setup.d.ts +4 -0
- package/dist/engine/run-setup.js +5 -1
- package/dist/engine/run-tooling.js +7 -1
- package/dist/engine/run-types.d.ts +33 -0
- package/dist/engine/run-types.js +21 -0
- package/dist/engine/run-workspace.js +11 -5
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +3 -1
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/prompt/composer.d.ts +8 -0
- package/dist/prompt/composer.js +8 -0
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +22 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +112 -29
- package/dist/protocol/types.d.ts +20 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +4 -1
- package/dist/session/session-manager.js +94 -47
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +48 -1
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/path-policy.js +25 -33
- package/dist/tool-system/registry.js +5 -0
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -108,7 +108,7 @@ export declare class SessionManager {
|
|
|
108
108
|
* Create a session. `qchat-` sessions stay process-local; ordinary sessions
|
|
109
109
|
* materialize state.json + transcript.jsonl before return.
|
|
110
110
|
*/
|
|
111
|
-
create(cwd: string, model: string, provider: string, explicitSessionId?: string, parentSessionId?: string | null, origin?: import("../types.js").SessionOrigin, kind?: SessionKind): SessionBundle;
|
|
111
|
+
create(cwd: string, model: string, provider: string, explicitSessionId?: string, parentSessionId?: string | null, origin?: import("../types.js").SessionOrigin, kind?: SessionKind, ephemeral?: boolean): SessionBundle;
|
|
112
112
|
/** Whether a persisted or process-local session exists. */
|
|
113
113
|
exists(sessionId: string): boolean;
|
|
114
114
|
/**
|
|
@@ -236,6 +236,8 @@ export declare class SessionManager {
|
|
|
236
236
|
* workspace/profile metadata.
|
|
237
237
|
*/
|
|
238
238
|
readSessionState(sessionId: string): SessionState | undefined;
|
|
239
|
+
/** Whether a live or persisted Session is explicitly process-local. */
|
|
240
|
+
isEphemeralSession(sessionId: string): boolean;
|
|
239
241
|
/**
|
|
240
242
|
* Merge a field-level state update into the latest persisted snapshot.
|
|
241
243
|
*
|
|
@@ -288,6 +290,7 @@ export declare class SessionManager {
|
|
|
288
290
|
*/
|
|
289
291
|
saveStateOrUpdateFields(state: SessionState, partial: SessionStateFieldPatch): boolean;
|
|
290
292
|
private saveStateAttempt;
|
|
293
|
+
private writeStateAtomically;
|
|
291
294
|
private acquireStateLock;
|
|
292
295
|
/** Recover only the old implementation's regular-file orphan lock. */
|
|
293
296
|
private prepareLegacyStateLock;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session lifecycle manager.
|
|
3
3
|
*/
|
|
4
|
-
import { closeSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { closeSync, chmodSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -148,26 +148,34 @@ function adoptCompatibilityGoalMutation(state) {
|
|
|
148
148
|
// disk. The storage root remains part of the key to preserve identity/data-root
|
|
149
149
|
// isolation.
|
|
150
150
|
const processLocalSessionBundles = new Map();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Every known transcript event must make an explicit fork decision. The
|
|
153
|
+
* `satisfies` constraint turns new TranscriptEventType additions into a
|
|
154
|
+
* compile-time error instead of a quick-chat failure discovered at runtime.
|
|
155
|
+
* The runtime fallback below still rejects unknown events from newer or
|
|
156
|
+
* malformed persisted transcripts.
|
|
157
|
+
*/
|
|
158
|
+
const FORK_EVENT_POLICY = {
|
|
159
|
+
message: "copy",
|
|
160
|
+
tool_use: "copy",
|
|
161
|
+
tool_result: "copy",
|
|
162
|
+
summary: "copy",
|
|
163
|
+
context_transfer: "copy",
|
|
164
|
+
range_archive: "copy",
|
|
165
|
+
content_replace: "copy",
|
|
166
|
+
file_history: "skip",
|
|
167
|
+
plan_operation: "skip",
|
|
168
|
+
session_meta: "skip",
|
|
169
|
+
subagent: "copy",
|
|
170
|
+
external_file_changes: "copy",
|
|
171
|
+
turn_boundary: "copy",
|
|
172
|
+
// Idempotency receipts belong to the source session and never contribute to
|
|
173
|
+
// model context. Copying them could replay a source response in the child.
|
|
174
|
+
run_result: "skip",
|
|
175
|
+
goal_progress: "copy",
|
|
176
|
+
turn_stopped: "copy",
|
|
177
|
+
error: "copy",
|
|
178
|
+
};
|
|
171
179
|
const FORK_STAGING_NAME = /^\.pending-fork-[A-Za-z0-9_.-]+-[A-Za-z0-9_-]{8}$/;
|
|
172
180
|
const FORK_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
173
181
|
const FORK_STAGING_CLEANUP_LIMIT = 32;
|
|
@@ -296,7 +304,9 @@ export class SessionManager {
|
|
|
296
304
|
resolveCapabilities()
|
|
297
305
|
.map((capability) => capability.sessionWorkspace)
|
|
298
306
|
.find((candidate) => candidate !== undefined);
|
|
299
|
-
mkdirSync(this.sessionsDir, { recursive: true });
|
|
307
|
+
mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
|
|
308
|
+
if (process.platform !== "win32")
|
|
309
|
+
chmodSync(this.sessionsDir, 0o700);
|
|
300
310
|
this.cleanupStaleForkStaging();
|
|
301
311
|
}
|
|
302
312
|
processLocalKey(sessionId) {
|
|
@@ -366,7 +376,7 @@ export class SessionManager {
|
|
|
366
376
|
* Create a session. `qchat-` sessions stay process-local; ordinary sessions
|
|
367
377
|
* materialize state.json + transcript.jsonl before return.
|
|
368
378
|
*/
|
|
369
|
-
create(cwd, model, provider, explicitSessionId, parentSessionId, origin, kind = "work") {
|
|
379
|
+
create(cwd, model, provider, explicitSessionId, parentSessionId, origin, kind = "work", ephemeral = false) {
|
|
370
380
|
// External callers may pass any string; nanoid output is trusted. Either
|
|
371
381
|
// way the ID gets joined into a filesystem path, so the public entry
|
|
372
382
|
// point validates before that join.
|
|
@@ -394,7 +404,7 @@ export class SessionManager {
|
|
|
394
404
|
// new top-level session (key present, null) apart from a legacy session
|
|
395
405
|
// (key absent) and from a sub-agent (key present, non-empty string).
|
|
396
406
|
parentSessionId: parentSessionId ?? null,
|
|
397
|
-
...(sessionId.startsWith("qchat-") ? { ephemeral: true } : {}),
|
|
407
|
+
...(ephemeral || sessionId.startsWith("qchat-") ? { ephemeral: true } : {}),
|
|
398
408
|
...(origin ? { origin } : {}),
|
|
399
409
|
};
|
|
400
410
|
if (isEphemeralSessionState(state)) {
|
|
@@ -410,13 +420,16 @@ export class SessionManager {
|
|
|
410
420
|
startedAt: state.startedAt,
|
|
411
421
|
kind,
|
|
412
422
|
});
|
|
423
|
+
if (transcript.flushFailed()) {
|
|
424
|
+
throw new SessionError(`Failed to persist initial transcript for ${sessionId}`);
|
|
425
|
+
}
|
|
413
426
|
const bundle = { state, transcript };
|
|
414
427
|
this.storeProcessLocalBundle(bundle);
|
|
415
428
|
return bundle;
|
|
416
429
|
}
|
|
417
430
|
const sessionDir = join(this.sessionsDir, sessionId);
|
|
418
431
|
try {
|
|
419
|
-
mkdirSync(sessionDir);
|
|
432
|
+
mkdirSync(sessionDir, { mode: 0o700 });
|
|
420
433
|
}
|
|
421
434
|
catch (err) {
|
|
422
435
|
if (err.code === "EEXIST") {
|
|
@@ -426,20 +439,26 @@ export class SessionManager {
|
|
|
426
439
|
}
|
|
427
440
|
// Atomic write (tmp+rename) like saveState, so a crash during this one-time
|
|
428
441
|
// create can't leave a torn state.json that resume() then fails to parse.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
442
|
+
try {
|
|
443
|
+
const stateTarget = join(sessionDir, "state.json");
|
|
444
|
+
this.writeStateAtomically(stateTarget, state, "create");
|
|
445
|
+
const transcript = new Transcript(join(sessionDir, "transcript.jsonl"));
|
|
446
|
+
transcript.append("session_meta", {
|
|
447
|
+
sessionId,
|
|
448
|
+
cwd,
|
|
449
|
+
model,
|
|
450
|
+
provider,
|
|
451
|
+
startedAt: state.startedAt,
|
|
452
|
+
kind,
|
|
453
|
+
});
|
|
454
|
+
return { state, transcript };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// A failed first materialization must not reserve the session id forever
|
|
458
|
+
// with an empty/partial directory.
|
|
459
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
443
462
|
}
|
|
444
463
|
/** Whether a persisted or process-local session exists. */
|
|
445
464
|
exists(sessionId) {
|
|
@@ -1017,6 +1036,10 @@ export class SessionManager {
|
|
|
1017
1036
|
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
1037
|
}
|
|
1019
1038
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
1039
|
+
if (process.platform !== "win32") {
|
|
1040
|
+
chmodSync(sessionDir, 0o700);
|
|
1041
|
+
chmodSync(stateFile, 0o600);
|
|
1042
|
+
}
|
|
1020
1043
|
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
1021
1044
|
state.kind = normalizedSessionKind(state.kind);
|
|
1022
1045
|
state.status = "active";
|
|
@@ -1049,6 +1072,10 @@ export class SessionManager {
|
|
|
1049
1072
|
return undefined;
|
|
1050
1073
|
}
|
|
1051
1074
|
}
|
|
1075
|
+
/** Whether a live or persisted Session is explicitly process-local. */
|
|
1076
|
+
isEphemeralSession(sessionId) {
|
|
1077
|
+
return this.readSessionState(sessionId)?.ephemeral === true;
|
|
1078
|
+
}
|
|
1052
1079
|
/**
|
|
1053
1080
|
* Merge a field-level state update into the latest persisted snapshot.
|
|
1054
1081
|
*
|
|
@@ -1357,9 +1384,7 @@ export class SessionManager {
|
|
|
1357
1384
|
state.title = persisted.title;
|
|
1358
1385
|
}
|
|
1359
1386
|
state.stateRevision = (persistedRevision ?? incomingRevision ?? 0) + 1;
|
|
1360
|
-
|
|
1361
|
-
writeFileSync(tmp, JSON.stringify(stateForPersistence(state), null, 2), "utf-8");
|
|
1362
|
-
renameSync(tmp, target);
|
|
1387
|
+
this.writeStateAtomically(target, stateForPersistence(state));
|
|
1363
1388
|
return { ok: true };
|
|
1364
1389
|
}
|
|
1365
1390
|
finally {
|
|
@@ -1371,6 +1396,24 @@ export class SessionManager {
|
|
|
1371
1396
|
}
|
|
1372
1397
|
}
|
|
1373
1398
|
}
|
|
1399
|
+
writeStateAtomically(target, state, label = "state") {
|
|
1400
|
+
const tmp = `${target}.${process.pid}.${nanoid(8)}.${label}.tmp`;
|
|
1401
|
+
try {
|
|
1402
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), {
|
|
1403
|
+
encoding: "utf-8",
|
|
1404
|
+
mode: 0o600,
|
|
1405
|
+
});
|
|
1406
|
+
renameSync(tmp, target);
|
|
1407
|
+
}
|
|
1408
|
+
finally {
|
|
1409
|
+
try {
|
|
1410
|
+
rmSync(tmp, { force: true });
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// Preserve the original write/rename error; cleanup is best effort.
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1374
1417
|
acquireStateLock(target) {
|
|
1375
1418
|
const lockPath = `${target}.lock`;
|
|
1376
1419
|
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
@@ -1569,7 +1612,10 @@ export class SessionManager {
|
|
|
1569
1612
|
return this.freezeForkSnapshot(sourceSessionId, sourceState, parsed.events, throughEventId, snapshotMode);
|
|
1570
1613
|
}
|
|
1571
1614
|
freezeForkSnapshot(sourceSessionId, sourceState, events, throughEventId, snapshotMode) {
|
|
1572
|
-
|
|
1615
|
+
// Both process-local and disk readers provide a snapshot array. Fork
|
|
1616
|
+
// selection is synchronous, so keep event references here and clone once
|
|
1617
|
+
// when the independently-owned target transcript is constructed below.
|
|
1618
|
+
const sourceEvents = [...events];
|
|
1573
1619
|
let frozen = sourceEvents;
|
|
1574
1620
|
const effectiveCursor = snapshotMode === "completed" ? sourceState.completedThroughEventId : throughEventId;
|
|
1575
1621
|
if (snapshotMode === "completed" && effectiveCursor === undefined) {
|
|
@@ -1600,12 +1646,13 @@ export class SessionManager {
|
|
|
1600
1646
|
}
|
|
1601
1647
|
const copiedEvents = [];
|
|
1602
1648
|
for (const event of frozen) {
|
|
1603
|
-
|
|
1649
|
+
const policy = FORK_EVENT_POLICY[event.type];
|
|
1650
|
+
if (policy === "skip")
|
|
1604
1651
|
continue;
|
|
1605
|
-
if (
|
|
1652
|
+
if (policy !== "copy") {
|
|
1606
1653
|
throw new SessionError(`Unsupported transcript event in fork: ${String(event.type)}`);
|
|
1607
1654
|
}
|
|
1608
|
-
copiedEvents.push(
|
|
1655
|
+
copiedEvents.push(event);
|
|
1609
1656
|
}
|
|
1610
1657
|
validateForkToolPairs(copiedEvents);
|
|
1611
1658
|
return { sourceState: structuredClone(sourceState), copiedEvents };
|
|
@@ -1628,7 +1675,7 @@ export class SessionManager {
|
|
|
1628
1675
|
const stagingDir = join(this.sessionsDir, `.pending-fork-${targetSessionId}-${nanoid(8)}`);
|
|
1629
1676
|
let published = false;
|
|
1630
1677
|
try {
|
|
1631
|
-
mkdirSync(stagingDir);
|
|
1678
|
+
mkdirSync(stagingDir, { mode: 0o700 });
|
|
1632
1679
|
writeFileSync(join(stagingDir, "state.json"), JSON.stringify(state, null, 2), {
|
|
1633
1680
|
encoding: "utf-8",
|
|
1634
1681
|
mode: 0o600,
|
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
* Transcript — JSONL event log (NOT chat history).
|
|
3
3
|
* toMessages() derives Message[] from events for sending to LLM.
|
|
4
4
|
*/
|
|
5
|
-
import { appendFileSync,
|
|
5
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fchmodSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, writeFileSync, } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { logger } from "../logging/logger.js";
|
|
9
|
+
function appendTranscriptLine(filePath, data) {
|
|
10
|
+
// Use one append-mode descriptor so concurrent OS writers cannot overwrite
|
|
11
|
+
// one another. Also repair the record boundary after a crash-torn final line;
|
|
12
|
+
// otherwise the next valid event is concatenated to the fragment and both
|
|
13
|
+
// are discarded on replay.
|
|
14
|
+
const fd = openSync(filePath, "a+", 0o600);
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform !== "win32")
|
|
17
|
+
fchmodSync(fd, 0o600);
|
|
18
|
+
const size = fstatSync(fd).size;
|
|
19
|
+
let prefix = "";
|
|
20
|
+
if (size > 0) {
|
|
21
|
+
const lastByte = Buffer.allocUnsafe(1);
|
|
22
|
+
readSync(fd, lastByte, 0, 1, size - 1);
|
|
23
|
+
if (lastByte[0] !== 0x0a)
|
|
24
|
+
prefix = "\n";
|
|
25
|
+
}
|
|
26
|
+
appendFileSync(fd, prefix + data, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const defaultTranscriptWriter = (filePath, data) => {
|
|
33
|
+
appendTranscriptLine(filePath, data);
|
|
34
|
+
};
|
|
9
35
|
const CONTEXT_EVENT_TYPES = new Set([
|
|
10
36
|
"message",
|
|
11
37
|
"tool_use",
|
|
@@ -68,7 +94,7 @@ export class Transcript {
|
|
|
68
94
|
getFilePath() {
|
|
69
95
|
return this.filePath;
|
|
70
96
|
}
|
|
71
|
-
constructor(filePath, writer =
|
|
97
|
+
constructor(filePath, writer = defaultTranscriptWriter, options = {}) {
|
|
72
98
|
this.filePath = filePath;
|
|
73
99
|
this.writer = writer;
|
|
74
100
|
this.persistent = options.persistent !== false;
|
|
@@ -76,7 +102,11 @@ export class Transcript {
|
|
|
76
102
|
return;
|
|
77
103
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
78
104
|
if (!existsSync(filePath)) {
|
|
79
|
-
writeFileSync(filePath, "", "utf-8");
|
|
105
|
+
writeFileSync(filePath, "", { encoding: "utf-8", mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
else if (process.platform !== "win32") {
|
|
108
|
+
// Tighten transcripts created by older releases on first use.
|
|
109
|
+
chmodSync(filePath, 0o600);
|
|
80
110
|
}
|
|
81
111
|
}
|
|
82
112
|
/** A process-local transcript that never creates or appends a file. */
|
|
@@ -55,12 +55,21 @@ export declare function latestTurnUndoTargets(snapshots: FileSnapshot[]): FileSn
|
|
|
55
55
|
* `redoLatestTurn` should re-apply — but ONLY when that turn is still the latest
|
|
56
56
|
* undone state (i.e. nothing newer happened since the undo). Otherwise [].
|
|
57
57
|
*
|
|
58
|
-
* Rule: let R = the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* Rule: let R = the LOWEST turnSeq present in `redoRecords` (a RedoRecord exists
|
|
59
|
+
* only for a turn that was undone and not yet redone). Undo walks BACKWARD down
|
|
60
|
+
* the turn stack, so after `undo(2); undo(1)` both records are pending and the
|
|
61
|
+
* one to re-apply first is turn 1 — the most recently undone, i.e. the lowest.
|
|
62
|
+
* Selecting the greatest instead replayed turns out of order: `redo` jumped
|
|
63
|
+
* straight to turn 2, skipping turn 1's state entirely and stranding turn 1's
|
|
64
|
+
* record forever (its snapshots stay `undone`, so it never becomes selectable).
|
|
65
|
+
* With two files that also produced a state that never existed — turn 1 still
|
|
66
|
+
* reverted while turn 2 was applied.
|
|
67
|
+
*
|
|
68
|
+
* R is redoable iff NO snapshot belongs to a strictly newer LIVE (non-undone)
|
|
69
|
+
* turn — a fresh edit after the undo supersedes the redo and invalidates it
|
|
70
|
+
* (spec: "新轮使 redo 失效"). Created-only turns have no pre-turn snapshot, so
|
|
71
|
+
* the RedoRecord itself is the sole evidence and is honoured as long as nothing
|
|
72
|
+
* newer superseded it.
|
|
64
73
|
*
|
|
65
74
|
* Empty `redoRecords` → []. fs-free so /redo's target decision is unit-testable.
|
|
66
75
|
*/
|
|
@@ -114,25 +114,42 @@ export function latestTurnUndoTargets(snapshots) {
|
|
|
114
114
|
* `redoLatestTurn` should re-apply — but ONLY when that turn is still the latest
|
|
115
115
|
* undone state (i.e. nothing newer happened since the undo). Otherwise [].
|
|
116
116
|
*
|
|
117
|
-
* Rule: let R = the
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
117
|
+
* Rule: let R = the LOWEST turnSeq present in `redoRecords` (a RedoRecord exists
|
|
118
|
+
* only for a turn that was undone and not yet redone). Undo walks BACKWARD down
|
|
119
|
+
* the turn stack, so after `undo(2); undo(1)` both records are pending and the
|
|
120
|
+
* one to re-apply first is turn 1 — the most recently undone, i.e. the lowest.
|
|
121
|
+
* Selecting the greatest instead replayed turns out of order: `redo` jumped
|
|
122
|
+
* straight to turn 2, skipping turn 1's state entirely and stranding turn 1's
|
|
123
|
+
* record forever (its snapshots stay `undone`, so it never becomes selectable).
|
|
124
|
+
* With two files that also produced a state that never existed — turn 1 still
|
|
125
|
+
* reverted while turn 2 was applied.
|
|
126
|
+
*
|
|
127
|
+
* R is redoable iff NO snapshot belongs to a strictly newer LIVE (non-undone)
|
|
128
|
+
* turn — a fresh edit after the undo supersedes the redo and invalidates it
|
|
129
|
+
* (spec: "新轮使 redo 失效"). Created-only turns have no pre-turn snapshot, so
|
|
130
|
+
* the RedoRecord itself is the sole evidence and is honoured as long as nothing
|
|
131
|
+
* newer superseded it.
|
|
123
132
|
*
|
|
124
133
|
* Empty `redoRecords` → []. fs-free so /redo's target decision is unit-testable.
|
|
125
134
|
*/
|
|
126
135
|
export function latestRedoTargets(redoRecords, snapshots) {
|
|
127
136
|
if (redoRecords.length === 0)
|
|
128
137
|
return [];
|
|
138
|
+
// The most recently undone turn is the LOWEST pending one (undo pops the
|
|
139
|
+
// stack from the top down), and it is the next one to re-apply.
|
|
140
|
+
let nextRedoTurn = Infinity;
|
|
141
|
+
for (const r of redoRecords) {
|
|
142
|
+
if (r.turnSeq < nextRedoTurn)
|
|
143
|
+
nextRedoTurn = r.turnSeq;
|
|
144
|
+
}
|
|
145
|
+
// Supersession is still judged against the NEWEST pending redo turn: a live
|
|
146
|
+
// snapshot newer than that is a fresh edit made after the undo, which
|
|
147
|
+
// invalidates the whole pending redo chain rather than just one turn.
|
|
129
148
|
let maxRedoTurn = -Infinity;
|
|
130
149
|
for (const r of redoRecords) {
|
|
131
150
|
if (r.turnSeq > maxRedoTurn)
|
|
132
151
|
maxRedoTurn = r.turnSeq;
|
|
133
152
|
}
|
|
134
|
-
// If any LIVE snapshot belongs to a strictly newer turn, the redo turn is no
|
|
135
|
-
// longer the latest undone state → not redoable.
|
|
136
153
|
for (const s of snapshots) {
|
|
137
154
|
if (s.undone)
|
|
138
155
|
continue;
|
|
@@ -140,5 +157,5 @@ export function latestRedoTargets(redoRecords, snapshots) {
|
|
|
140
157
|
if (k > maxRedoTurn)
|
|
141
158
|
return [];
|
|
142
159
|
}
|
|
143
|
-
return redoRecords.filter((r) => r.turnSeq ===
|
|
160
|
+
return redoRecords.filter((r) => r.turnSeq === nextRedoTurn);
|
|
144
161
|
}
|
|
@@ -145,24 +145,43 @@ export declare class SettingsManager {
|
|
|
145
145
|
* cache invalidation mirror saveUserSetting.
|
|
146
146
|
*/
|
|
147
147
|
saveProjectSetting(key: string, value: unknown, cwd: string): void;
|
|
148
|
+
/**
|
|
149
|
+
* Persist a machine-private setting for one project. The local layer has
|
|
150
|
+
* higher precedence than the shared project layer and lives at
|
|
151
|
+
* `${cwd}/.code-shell/settings.local.json`, matching the file already read
|
|
152
|
+
* by {@link load}. It is useful for MCP endpoints or policy that should not
|
|
153
|
+
* be shared with collaborators.
|
|
154
|
+
*/
|
|
155
|
+
saveLocalSetting(key: string, value: unknown, cwd: string): void;
|
|
148
156
|
/**
|
|
149
157
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
150
158
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
|
151
159
|
* override key. No-ops if the file or any intermediate segment is absent.
|
|
152
160
|
*/
|
|
153
161
|
deleteProjectSetting(key: string, cwd: string): void;
|
|
162
|
+
/** Delete one dotted key from the machine-private project settings layer. */
|
|
163
|
+
deleteLocalSetting(key: string, cwd: string): void;
|
|
164
|
+
/** Delete one dotted key from the user settings layer. */
|
|
165
|
+
deleteUserSetting(key: string): void;
|
|
166
|
+
private deleteSettingFromFile;
|
|
154
167
|
/**
|
|
155
168
|
* Read ONE scope's raw settings file, validated but UNMERGED. Capability
|
|
156
169
|
* overlay math needs the project overlay and the user/global baseline
|
|
157
170
|
* separately — the merged get() collapses provenance and can't express
|
|
158
171
|
* tri-state inheritance. user → ~/.code-shell/settings.json, project →
|
|
159
|
-
* ${cwd}/.code-shell/settings.json
|
|
160
|
-
*
|
|
172
|
+
* ${cwd}/.code-shell/settings.json, local →
|
|
173
|
+
* ${cwd}/.code-shell/settings.local.json. Only keys actually present in the
|
|
174
|
+
* file are returned (defaults are not synthesized), so an absent file → {}.
|
|
161
175
|
*/
|
|
162
|
-
getForScope(scope: "user" | "project", cwd?: string): Partial<ValidatedSettings>;
|
|
176
|
+
getForScope(scope: "user" | "project" | "local", cwd?: string): Partial<ValidatedSettings>;
|
|
163
177
|
private projectSettingsPath;
|
|
178
|
+
private localSettingsPath;
|
|
179
|
+
private validateProjectCwd;
|
|
180
|
+
/** Resolve the project root once and refuse a linked/non-directory state root. */
|
|
181
|
+
private tryProjectSettingsPath;
|
|
164
182
|
private readJsonObject;
|
|
165
183
|
private atomicWriteJson;
|
|
184
|
+
private writeBackup;
|
|
166
185
|
/**
|
|
167
186
|
* Read-modify-write a settings file under a cross-process lock.
|
|
168
187
|
*
|