linkshell-cli 0.5.25 → 0.5.27

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.
@@ -1215,6 +1215,98 @@ function runningCodexSessionIds(nowMs) {
1215
1215
  walk(root);
1216
1216
  return running;
1217
1217
  }
1218
+ /** Read a Codex session's last-known settings (reasoning effort, plan mode,
1219
+ * permission/sandbox) from its on-disk rollout, so opening a session LinkShell
1220
+ * never drove shows the config it actually ran with. Codex records the live
1221
+ * turn config in `turn_context` records; the last one is the current config.
1222
+ * Matched by the filename session id (cheap — no per-file reads to locate it),
1223
+ * newest first. Only called on open (one file), never per-list. */
1224
+ function codexSessionConfig(sessionId) {
1225
+ const root = join(homedir(), ".codex", "sessions");
1226
+ if (!existsSync(root))
1227
+ return {};
1228
+ const ID_RE = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/;
1229
+ const matches = [];
1230
+ const walk = (dir) => {
1231
+ let entries;
1232
+ try {
1233
+ entries = readdirSync(dir);
1234
+ }
1235
+ catch {
1236
+ return;
1237
+ }
1238
+ for (const name of entries) {
1239
+ const path = join(dir, name);
1240
+ let stat;
1241
+ try {
1242
+ stat = statSync(path);
1243
+ }
1244
+ catch {
1245
+ continue;
1246
+ }
1247
+ if (stat.isDirectory()) {
1248
+ walk(path);
1249
+ continue;
1250
+ }
1251
+ const match = ID_RE.exec(name);
1252
+ if (match && match[1] === sessionId)
1253
+ matches.push({ path, mt: stat.mtimeMs });
1254
+ }
1255
+ };
1256
+ walk(root);
1257
+ if (matches.length === 0)
1258
+ return {};
1259
+ matches.sort((a, b) => b.mt - a.mt);
1260
+ const newest = matches[0];
1261
+ if (!newest)
1262
+ return {};
1263
+ let lastTurnContext;
1264
+ try {
1265
+ const text = readFileSync(newest.path, "utf8");
1266
+ for (const line of text.split(/\r?\n/)) {
1267
+ if (!line.trim())
1268
+ continue;
1269
+ let record;
1270
+ try {
1271
+ record = asRecord(JSON.parse(line));
1272
+ }
1273
+ catch {
1274
+ continue;
1275
+ }
1276
+ if (record?.type === "turn_context") {
1277
+ lastTurnContext = asRecord(record.payload) ?? record;
1278
+ }
1279
+ }
1280
+ }
1281
+ catch {
1282
+ return {};
1283
+ }
1284
+ if (!lastTurnContext)
1285
+ return {};
1286
+ const cfg = {};
1287
+ const collab = asRecord(lastTurnContext.collaboration_mode);
1288
+ const effort = firstString(asRecord(collab?.settings), ["reasoning_effort"]);
1289
+ if (effort)
1290
+ cfg.reasoningEffort = effort;
1291
+ const mode = firstString(collab, ["mode"]);
1292
+ if (mode === "plan")
1293
+ cfg.collaborationMode = "plan";
1294
+ else if (mode)
1295
+ cfg.collaborationMode = "default";
1296
+ // Map Codex's sandbox policy (kebab-case, e.g. "danger-full-access") to ours.
1297
+ const sandbox = firstString(asRecord(lastTurnContext.sandbox_policy), ["type"]);
1298
+ if (sandbox) {
1299
+ const norm = sandbox.replace(/-/g, "").toLowerCase();
1300
+ cfg.permissionMode = norm.includes("readonly")
1301
+ ? "read_only"
1302
+ : norm.includes("workspace")
1303
+ ? "workspace_write"
1304
+ : norm.includes("danger") || norm.includes("full")
1305
+ ? "full_access"
1306
+ : undefined;
1307
+ }
1308
+ return cfg;
1309
+ }
1218
1310
  function parseRemoteSessions(value) {
1219
1311
  const raw = asRecord(value);
1220
1312
  const sessionsValue = Array.isArray(value) ? value :
@@ -2087,6 +2179,36 @@ export class AgentWorkspaceProxy {
2087
2179
  const model = firstString(thread, ["model", "modelId", "currentModel"]);
2088
2180
  if (model && !conversation.model)
2089
2181
  conversation.model = model;
2182
+ // Fill the control settings (reasoning effort / permission / plan mode) from
2183
+ // the on-disk transcript when we don't already have them, so opening a
2184
+ // session LinkShell never drove shows what it actually ran with instead of
2185
+ // the UI's first-option default. Existing (LinkShell-set) values win — this
2186
+ // only backfills. One file read, on open only; never per-list.
2187
+ if (conversation.reasoningEffort === undefined &&
2188
+ conversation.permissionMode === undefined &&
2189
+ conversation.collaborationMode === undefined) {
2190
+ try {
2191
+ let cfg = {};
2192
+ if (conversation.provider === "codex") {
2193
+ cfg = codexSessionConfig(conversation.agentSessionId);
2194
+ }
2195
+ else {
2196
+ const readConfig = client.readSessionConfig;
2197
+ if (typeof readConfig === "function") {
2198
+ cfg = (asRecord(await readConfig.call(client, { sessionId: conversation.agentSessionId })) ?? {});
2199
+ }
2200
+ }
2201
+ if (cfg.reasoningEffort && !conversation.reasoningEffort)
2202
+ conversation.reasoningEffort = cfg.reasoningEffort;
2203
+ if (cfg.permissionMode && !conversation.permissionMode)
2204
+ conversation.permissionMode = cfg.permissionMode;
2205
+ if (cfg.collaborationMode && !conversation.collaborationMode)
2206
+ conversation.collaborationMode = cfg.collaborationMode;
2207
+ }
2208
+ catch {
2209
+ // Best-effort — a missing/unreadable transcript just leaves the pills at 默认.
2210
+ }
2211
+ }
2090
2212
  // Seed a scroll-back cursor for Claude, which hydrates via readSession (full
2091
2213
  // transcript) yet pages via listTurns. The timeline only keeps the last
2092
2214
  // MAX_TIMELINE_ITEMS, so anything older is dropped here but reachable via