@cjhyy/code-shell-core 0.6.0-rc.15 → 0.6.0-rc.16

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.
@@ -5,7 +5,7 @@
5
5
  * Tier 2: LLM summary (async) — generate summary of older messages via model call
6
6
  * Tier 3: window compact (sync, emergency) — aggressive truncation fallback
7
7
  */
8
- import type { Message, ContextUsageAnchor } from "../types.js";
8
+ import type { Message, ContextUsageAnchor, PromptTokenConfidence, PromptTokenSource } from "../types.js";
9
9
  export interface ContextManagerConfig {
10
10
  maxTokens: number;
11
11
  compactAtRatio: number;
@@ -81,6 +81,7 @@ export declare class ContextManager {
81
81
  * Best-effort token estimate: uses actual API usage as base if available,
82
82
  * plus estimation for messages added since the last API call.
83
83
  */
84
+ private estimateTokensHybridInfo;
84
85
  private estimateTokensHybrid;
85
86
  /**
86
87
  * Set the summarize function (injected by Engine).
@@ -150,5 +151,7 @@ export declare class ContextManager {
150
151
  ratio: number;
151
152
  needsCompact: boolean;
152
153
  needsEmergency: boolean;
154
+ promptTokensSource: PromptTokenSource;
155
+ promptTokensConfidence: PromptTokenConfidence;
153
156
  };
154
157
  }
@@ -130,20 +130,31 @@ export class ContextManager {
130
130
  * Best-effort token estimate: uses actual API usage as base if available,
131
131
  * plus estimation for messages added since the last API call.
132
132
  */
133
- estimateTokensHybrid(messages) {
133
+ estimateTokensHybridInfo(messages) {
134
134
  const currentEstimate = estimateTokens(messages);
135
135
  if (this.lastActualTokens !== undefined &&
136
136
  this.lastActualAtMessageCount !== undefined) {
137
137
  if (this.lastActualAtMessageCount < messages.length) {
138
138
  const newMessages = messages.slice(this.lastActualAtMessageCount);
139
139
  const newTokens = estimateTokens(newMessages);
140
- return this.lastActualTokens + newTokens;
140
+ return {
141
+ tokens: this.lastActualTokens + newTokens,
142
+ source: "anchor_delta",
143
+ confidence: "medium",
144
+ };
141
145
  }
142
146
  if (this.lastActualAnchorEstimate !== undefined && this.lastActualAnchorEstimate > 0) {
143
- return Math.round(this.lastActualTokens * (currentEstimate / this.lastActualAnchorEstimate));
147
+ return {
148
+ tokens: Math.round(this.lastActualTokens * (currentEstimate / this.lastActualAnchorEstimate)),
149
+ source: "anchor_rescale",
150
+ confidence: "medium",
151
+ };
144
152
  }
145
153
  }
146
- return currentEstimate;
154
+ return { tokens: currentEstimate, source: "heuristic_estimate", confidence: "low" };
155
+ }
156
+ estimateTokensHybrid(messages) {
157
+ return this.estimateTokensHybridInfo(messages).tokens;
147
158
  }
148
159
  /**
149
160
  * Set the summarize function (injected by Engine).
@@ -577,13 +588,16 @@ export class ContextManager {
577
588
  * Check if context is approaching limits.
578
589
  */
579
590
  checkLimits(messages) {
580
- const tokens = this.estimateTokensHybrid(messages);
591
+ const estimate = this.estimateTokensHybridInfo(messages);
592
+ const tokens = estimate.tokens;
581
593
  const ratio = tokens / this.config.maxTokens;
582
594
  return {
583
595
  tokens,
584
596
  ratio,
585
597
  needsCompact: ratio >= this.config.compactAtRatio,
586
598
  needsEmergency: ratio >= this.config.summarizeAtRatio,
599
+ promptTokensSource: estimate.source,
600
+ promptTokensConfidence: estimate.confidence,
587
601
  };
588
602
  }
589
603
  }
@@ -1332,27 +1332,34 @@ export class Engine {
1332
1332
  persistedContextAnchor.model === this.config.llm.model) &&
1333
1333
  (persistedContextAnchor.messageCount <= messages.length ||
1334
1334
  persistedContextAnchor.estimateAtAnchor !== undefined);
1335
- const seededContextAnchor = contextAnchorCompatible
1336
- ? contextManager.seedActualUsage(persistedContextAnchor)
1337
- : undefined;
1338
- // Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
1339
- // before the first real usage_update arrives. The authoritative count
1335
+ if (contextAnchorCompatible) {
1336
+ contextManager.seedActualUsage(persistedContextAnchor);
1337
+ }
1338
+ // Best-effort token estimate of the full prompt so the UI's ctx bar isn't
1339
+ // 0% before the first real usage_update arrives. The authoritative count
1340
1340
  // comes from `usage.promptTokens` after the first LLM response — this is
1341
- // just a display-friendly approximation for the first frame.
1341
+ // just a display-friendly approximation for the first frame, annotated
1342
+ // with source/confidence so consumers don't treat heuristics as truth.
1342
1343
  //
1343
1344
  // Only seed once per (process, sid). On subsequent turns the UI already
1344
- // shows the previous turn's accurate ctx; overwriting it with this rough
1345
- // char/4 estimate would make the bar visibly drop on every submit.
1345
+ // shows the previous turn's accurate ctx; overwriting it with a fresh
1346
+ // best-effort estimate would make the bar visibly drop on every submit.
1346
1347
  const sid = session.state.sessionId;
1347
1348
  const needsCtxSeed = !this.ctxSeedSent.has(sid);
1348
- const roughPromptTokens = needsCtxSeed
1349
- ? seededContextAnchor
1350
- ? contextManager.checkLimits(messages).tokens
1351
- : messages.reduce((sum, m) => {
1352
- const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1353
- return sum + Math.ceil(text.length / 4);
1354
- }, 0)
1355
- : 0;
1349
+ const ctxSeed = needsCtxSeed
1350
+ ? (() => {
1351
+ const checked = contextManager.checkLimits(messages);
1352
+ return {
1353
+ tokens: checked.tokens,
1354
+ source: checked.promptTokensSource,
1355
+ confidence: checked.promptTokensConfidence,
1356
+ };
1357
+ })()
1358
+ : {
1359
+ tokens: 0,
1360
+ source: "heuristic_estimate",
1361
+ confidence: "low",
1362
+ };
1356
1363
  if (needsCtxSeed)
1357
1364
  this.ctxSeedSent.add(sid);
1358
1365
  // Tell the client the sid *now* instead of waiting for run() to resolve.
@@ -1361,7 +1368,9 @@ export class Engine {
1361
1368
  options?.onStream?.({
1362
1369
  type: "session_started",
1363
1370
  sessionId: sid,
1364
- promptTokens: roughPromptTokens,
1371
+ promptTokens: ctxSeed.tokens,
1372
+ promptTokensSource: ctxSeed.source,
1373
+ promptTokensConfidence: ctxSeed.confidence,
1365
1374
  });
1366
1375
  // Replay the last TodoWrite snapshot on resume so the UI's pinned
1367
1376
  // task panel re-hydrates without the LLM needing to call TodoWrite
@@ -1847,6 +1856,8 @@ export class Engine {
1847
1856
  options?.onStream?.({
1848
1857
  type: "usage_update",
1849
1858
  promptTokens: cumulative.cumulativePromptTokens,
1859
+ promptTokensSource: "session_cumulative",
1860
+ promptTokensConfidence: "high",
1850
1861
  cumulativePromptTokens: cumulative.cumulativePromptTokens,
1851
1862
  cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
1852
1863
  cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
@@ -281,7 +281,12 @@ export class TurnLoop {
281
281
  if (ctx === this.lastCtxEmit)
282
282
  return;
283
283
  this.lastCtxEmit = ctx;
284
- this.config.onStream({ type: "usage_update", promptTokens: ctx });
284
+ this.config.onStream({
285
+ type: "usage_update",
286
+ promptTokens: ctx,
287
+ promptTokensSource: overhead > 0 ? "calibrated_estimate" : "heuristic_estimate",
288
+ promptTokensConfidence: overhead > 0 ? "medium" : "low",
289
+ });
285
290
  }
286
291
  recordResponseUsage(usage) {
287
292
  this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
@@ -320,6 +325,8 @@ export class TurnLoop {
320
325
  this.config.onStream({
321
326
  type: "usage_update",
322
327
  promptTokens,
328
+ promptTokensSource: "provider_usage",
329
+ promptTokensConfidence: "high",
323
330
  ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
324
331
  ...(usage.cacheCreationTokens !== undefined
325
332
  ? { cacheCreationTokens: usage.cacheCreationTokens }
@@ -734,7 +734,8 @@ export class AgentServer {
734
734
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
735
735
  return;
736
736
  }
737
- const items = listBackgroundWorkForUI(sessionId);
737
+ const scope = params.scope === "all" ? "all" : "session";
738
+ const items = listBackgroundWorkForUI(sessionId, { scope });
738
739
  this.transport.send(createResponse(req.id, { items }));
739
740
  }
740
741
  // ─── CloseSession ───────────────────────────────────────────────
@@ -103,6 +103,7 @@ export declare class BackgroundShellManager {
103
103
  /** Raw retained output (no cleaning) — test/UI helper. */
104
104
  readOutputRaw(shellId: string): string | undefined;
105
105
  listForSession(sessionId: string): BgShell[];
106
+ list(): BgShell[];
106
107
  /**
107
108
  * Read a shell's output. `mode=incremental` (default) returns text since the
108
109
  * agent's last read; `mode=all` returns the full retained buffer. Output is
@@ -24,10 +24,10 @@
24
24
  */
25
25
  import { spawn } from "node:child_process";
26
26
  import { join } from "node:path";
27
- import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync, } from "node:fs";
27
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync } from "node:fs";
28
28
  import { StringDecoder } from "node:string_decoder";
29
29
  import { utf8SafeCutLength } from "./utf8-cut.js";
30
- import { resolveSpawnTarget, buildSandboxEnv, mergeShellEnv, killProcessGroup, groupAlive, defaultShellBinary } from "./spawn-common.js";
30
+ import { resolveSpawnTarget, buildSandboxEnv, mergeShellEnv, killProcessGroup, groupAlive, defaultShellBinary, } from "./spawn-common.js";
31
31
  import { RingFile } from "./ring-file.js";
32
32
  import { cleanOutput } from "./output-clean.js";
33
33
  import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
@@ -46,7 +46,7 @@ function nextShellId() {
46
46
  // Short, collision-resistant enough within a process: counter + base36 of
47
47
  // a monotonic-ish value. Avoids Date.now()/Math.random() (banned in some
48
48
  // contexts and unnecessary here).
49
- return `bg_${shellCounter.toString(36)}${(shellCounter * 2654435761 % 0xffffff).toString(36)}`;
49
+ return `bg_${shellCounter.toString(36)}${((shellCounter * 2654435761) % 0xffffff).toString(36)}`;
50
50
  }
51
51
  function bgShellsRoot() {
52
52
  return join(codeShellHome(), "bg-shells");
@@ -77,7 +77,11 @@ export class BackgroundShellManager {
77
77
  // the bg-shells root — refuse rather than trust the caller. Mirrors
78
78
  // SessionManager.assertSafeSessionId without importing across the layer.
79
79
  const sid = opts.sessionId;
80
- if (typeof sid !== "string" || sid.length === 0 || sid.includes("..") || sid.includes("/") || sid.includes("\\")) {
80
+ if (typeof sid !== "string" ||
81
+ sid.length === 0 ||
82
+ sid.includes("..") ||
83
+ sid.includes("/") ||
84
+ sid.includes("\\")) {
81
85
  return { ok: false, error: `invalid sessionId for background shell` };
82
86
  }
83
87
  // Per-session soft cap (fork-bomb guard, §7).
@@ -219,9 +223,7 @@ export class BackgroundShellManager {
219
223
  }
220
224
  enqueueExitNotification(sh) {
221
225
  const ok = sh.exitCode === 0;
222
- const exitDesc = sh.signal
223
- ? `signal ${sh.signal}`
224
- : `exit ${sh.exitCode ?? "?"}`;
226
+ const exitDesc = sh.signal ? `signal ${sh.signal}` : `exit ${sh.exitCode ?? "?"}`;
225
227
  notificationQueue.enqueue({
226
228
  agentId: sh.shellId,
227
229
  // English description is the agent-facing wakeup text. The UI ignores
@@ -233,7 +235,9 @@ export class BackgroundShellManager {
233
235
  status: ok ? "completed" : "failed",
234
236
  workKind: "shell",
235
237
  command: sh.command,
236
- error: ok ? undefined : `Background shell ${sh.shellId} exited with ${exitDesc}. Use BashOutput("${sh.shellId}") to inspect.`,
238
+ error: ok
239
+ ? undefined
240
+ : `Background shell ${sh.shellId} exited with ${exitDesc}. Use BashOutput("${sh.shellId}") to inspect.`,
237
241
  enqueuedAt: Date.now(),
238
242
  }, sh.sessionId);
239
243
  }
@@ -266,6 +270,9 @@ export class BackgroundShellManager {
266
270
  .filter((s) => s.sessionId === sessionId)
267
271
  .map((s) => this.toPublic(s));
268
272
  }
273
+ list() {
274
+ return [...this.shells.values()].map((s) => this.toPublic(s));
275
+ }
269
276
  /**
270
277
  * Read a shell's output. `mode=incremental` (default) returns text since the
271
278
  * agent's last read; `mode=all` returns the full retained buffer. Output is
@@ -419,7 +426,9 @@ export class BackgroundShellManager {
419
426
  try {
420
427
  rmSync(path, { force: true });
421
428
  }
422
- catch { /* ignore */ }
429
+ catch {
430
+ /* ignore */
431
+ }
423
432
  continue;
424
433
  }
425
434
  // Skip ones we already track in-process (this worker's own).
@@ -433,7 +442,9 @@ export class BackgroundShellManager {
433
442
  try {
434
443
  rmSync(path, { force: true });
435
444
  }
436
- catch { /* ignore */ }
445
+ catch {
446
+ /* ignore */
447
+ }
437
448
  }
438
449
  }
439
450
  }
@@ -12,7 +12,11 @@
12
12
  * auto-extracted entries (extract-memories output). LLM may
13
13
  * only modify these through permission-gated tool calls.
14
14
  * - dream/ The dream pipeline's workspace. The LLM is free to add /
15
- * merge / delete entries here; user/ is read-only to dream.
15
+ * merge / delete entries here. The dream loop may ALSO maintain
16
+ * user/ entries whose origin is `auto` or `dream` (dedup/merge/
17
+ * improve), but origin:`manual` user entries — things the user
18
+ * explicitly asked to remember — are never touched by dream.
19
+ * See dream-consolidation.ts (the origin guard at ~:240).
16
20
  *
17
21
  * MEMORY.md is the index file (one per scope).
18
22
  *
@@ -12,7 +12,11 @@
12
12
  * auto-extracted entries (extract-memories output). LLM may
13
13
  * only modify these through permission-gated tool calls.
14
14
  * - dream/ The dream pipeline's workspace. The LLM is free to add /
15
- * merge / delete entries here; user/ is read-only to dream.
15
+ * merge / delete entries here. The dream loop may ALSO maintain
16
+ * user/ entries whose origin is `auto` or `dream` (dedup/merge/
17
+ * improve), but origin:`manual` user entries — things the user
18
+ * explicitly asked to remember — are never touched by dream.
19
+ * See dream-consolidation.ts (the origin guard at ~:240).
16
20
  *
17
21
  * MEMORY.md is the index file (one per scope).
18
22
  *
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Skill scanner — discovers <base>/<name>/SKILL.md files from project + user
3
- * directories AND from installed plugins. Mirrors Claude Code's
2
+ * Skill scanner — discovers <base>/<name>/SKILL.md files from project
3
+ * `.code-shell/skills`, project `.agents/skills`, user `.code-shell/skills`,
4
+ * and installed plugins. Mirrors Claude Code's
4
5
  * `loadSkillsFromSkillsDir` (skills/loadSkillsDir.ts:407) plus plugin
5
6
  * integration (utils/plugins/pluginLoader.ts).
6
7
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Skill scanner — discovers <base>/<name>/SKILL.md files from project + user
3
- * directories AND from installed plugins. Mirrors Claude Code's
2
+ * Skill scanner — discovers <base>/<name>/SKILL.md files from project
3
+ * `.code-shell/skills`, project `.agents/skills`, user `.code-shell/skills`,
4
+ * and installed plugins. Mirrors Claude Code's
4
5
  * `loadSkillsFromSkillsDir` (skills/loadSkillsDir.ts:407) plus plugin
5
6
  * integration (utils/plugins/pluginLoader.ts).
6
7
  */
@@ -20,6 +21,7 @@ function userHome() {
20
21
  function bases(cwd) {
21
22
  return [
22
23
  { dir: join(cwd, ".code-shell", "skills"), source: "project" },
24
+ { dir: join(cwd, ".agents", "skills"), source: "project" },
23
25
  { dir: join(userHome(), ".code-shell", "skills"), source: "user" },
24
26
  ];
25
27
  }
@@ -158,13 +160,14 @@ function installedPluginsMtime() {
158
160
  }
159
161
  }
160
162
  /**
161
- * mtime of each local skills base dir (project + user). A directory's mtime
162
- * changes when a child entry is added/removed, so installing a new skill into
163
- * `<cwd>/.code-shell/skills` or `~/.code-shell/skills` busts the cache on the
164
- * next scan — the just-installed skill becomes visible to the running session
165
- * without a restart. (Editing an existing skill's *contents* does not bump the
166
- * dir mtime; install paths additionally call invalidateSkillCache() to cover
167
- * that, so this is the passive half of a two-part guard.)
163
+ * mtime of each local skills base dir (project .code-shell/.agents + user). A
164
+ * directory's mtime changes when a child entry is added/removed, so installing
165
+ * a new skill into `<cwd>/.code-shell/skills`, `<cwd>/.agents/skills`, or
166
+ * `~/.code-shell/skills` busts the cache on the next scan — the just-installed
167
+ * skill becomes visible to the running session without a restart. (Editing an
168
+ * existing skill's *contents* does not bump the dir mtime; install paths
169
+ * additionally call invalidateSkillCache() to cover that, so this is the
170
+ * passive half of a two-part guard.)
168
171
  */
169
172
  function skillsDirsMtime(cwd) {
170
173
  return bases(cwd)
@@ -69,6 +69,8 @@ declare class BackgroundJobRegistry {
69
69
  listRunningByCwd(cwd: string): BackgroundJobEntry[];
70
70
  /** All jobs (running + retained terminal) for `sessionId`. Feeds the panel. */
71
71
  listForSession(sessionId: string): BackgroundJobEntry[];
72
+ /** All jobs (running + retained terminal) across sessions. Feeds all-scope UI. */
73
+ list(): BackgroundJobEntry[];
72
74
  /** Drop every job of a session — called when the session is deleted/closed. */
73
75
  dropForSession(sessionId: string): void;
74
76
  subscribe: (cb: Listener) => (() => void);
@@ -82,6 +82,10 @@ class BackgroundJobRegistry {
82
82
  listForSession(sessionId) {
83
83
  return [...this.jobs.values()].filter((e) => e.sessionId === sessionId);
84
84
  }
85
+ /** All jobs (running + retained terminal) across sessions. Feeds all-scope UI. */
86
+ list() {
87
+ return [...this.jobs.values()];
88
+ }
85
89
  /** Drop every job of a session — called when the session is deleted/closed. */
86
90
  dropForSession(sessionId) {
87
91
  let removed = false;
@@ -1,16 +1,3 @@
1
- /**
2
- * Unified view over the THREE kinds of async background work — background
3
- * sub-agents (asyncAgentRegistry), background jobs / video polls
4
- * (backgroundJobRegistry), and background shells (backgroundShellManager).
5
- *
6
- * Used by the goal-stop-hook to show the judge LLM what's still running, so it
7
- * can tell "the goal is done except for a finite task that will wake me" (a
8
- * download / video render → allow stop, wait for the wakeup) from "a long-lived
9
- * service that the goal doesn't depend on" (a dev server → judge the goal
10
- * normally). Replacing the old mechanical `hasRunningForSession` short-circuit:
11
- * a boolean can't distinguish a finite download from a never-ending dev server,
12
- * but the judge — given each task's kind + command — can.
13
- */
14
1
  import { type AsyncAgentStatus } from "./agent-registry.js";
15
2
  import { type BackgroundJobStatus } from "./background-jobs.js";
16
3
  import { type BgShell } from "../../runtime/background-shell.js";
@@ -30,12 +17,21 @@ export interface BackgroundWorkItem {
30
17
  /** List every still-running background work item spawned by `sessionId`. */
31
18
  export declare function listRunningBackgroundWork(sessionId: string): BackgroundWorkItem[];
32
19
  /** One background-work row for the desktop panel, discriminated by `kind`. */
33
- export type BackgroundWorkEntry = {
20
+ export interface BackgroundWorkSourceSession {
21
+ sessionId: string;
22
+ shortId: string;
23
+ title?: string;
24
+ current: boolean;
25
+ }
26
+ type WithSource<T> = T & {
27
+ sourceSession: BackgroundWorkSourceSession;
28
+ };
29
+ export type BackgroundWorkEntry = WithSource<{
34
30
  kind: "shell";
35
31
  /** Full shell snapshot — the panel already renders this shape (output/kill
36
32
  * still go through the dedicated agent/backgroundShells RPC by shellId). */
37
33
  shell: BgShell;
38
- } | {
34
+ }> | WithSource<{
39
35
  kind: "subagent";
40
36
  agentId: string;
41
37
  name?: string;
@@ -44,7 +40,7 @@ export type BackgroundWorkEntry = {
44
40
  status: AsyncAgentStatus;
45
41
  startedAt: number;
46
42
  finishedAt?: number;
47
- } | {
43
+ }> | WithSource<{
48
44
  kind: "job";
49
45
  jobId: string;
50
46
  description: string;
@@ -55,7 +51,7 @@ export type BackgroundWorkEntry = {
55
51
  finalText?: string;
56
52
  /** Files an external agent (DriveAgent) changed, parsed from its transcript. */
57
53
  changedFiles?: string[];
58
- };
54
+ }>;
59
55
  /**
60
56
  * Every background-work item spawned by `sessionId`, with per-kind detail, for
61
57
  * the desktop panel. Includes finished sub-agents that are still within their
@@ -64,4 +60,7 @@ export type BackgroundWorkEntry = {
64
60
  * keep them. Jobs are only ever present while running (the registry drops them
65
61
  * on finish).
66
62
  */
67
- export declare function listBackgroundWorkForUI(sessionId: string): BackgroundWorkEntry[];
63
+ export declare function listBackgroundWorkForUI(sessionId: string, opts?: {
64
+ scope?: "session" | "all";
65
+ }): BackgroundWorkEntry[];
66
+ export {};
@@ -11,9 +11,12 @@
11
11
  * a boolean can't distinguish a finite download from a never-ending dev server,
12
12
  * but the judge — given each task's kind + command — can.
13
13
  */
14
+ import { readFileSync, statSync } from "node:fs";
15
+ import { join } from "node:path";
14
16
  import { asyncAgentRegistry } from "./agent-registry.js";
15
17
  import { backgroundJobRegistry } from "./background-jobs.js";
16
18
  import { backgroundShellManager } from "../../runtime/background-shell.js";
19
+ import { codeShellHome } from "../../session/session-manager.js";
17
20
  /** List every still-running background work item spawned by `sessionId`. */
18
21
  export function listRunningBackgroundWork(sessionId) {
19
22
  const items = [];
@@ -39,6 +42,38 @@ export function listRunningBackgroundWork(sessionId) {
39
42
  }
40
43
  return items;
41
44
  }
45
+ const sessionTitleCache = new Map();
46
+ function shortSessionId(sessionId) {
47
+ return sessionId.length <= 10 ? sessionId : sessionId.slice(0, 10);
48
+ }
49
+ function readSessionTitle(sessionId) {
50
+ try {
51
+ const statePath = join(codeShellHome(), "sessions", sessionId, "state.json");
52
+ const st = statSync(statePath);
53
+ const cached = sessionTitleCache.get(sessionId);
54
+ if (cached && cached.mtimeMs === st.mtimeMs)
55
+ return cached.title;
56
+ const raw = JSON.parse(readFileSync(statePath, "utf8"));
57
+ const title = typeof raw.title === "string" && raw.title.trim()
58
+ ? raw.title.trim()
59
+ : typeof raw.summary === "string" && raw.summary.trim()
60
+ ? raw.summary.trim()
61
+ : undefined;
62
+ sessionTitleCache.set(sessionId, { mtimeMs: st.mtimeMs, title });
63
+ return title;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ function sourceSession(currentSessionId, ownerSessionId) {
70
+ return {
71
+ sessionId: ownerSessionId,
72
+ shortId: shortSessionId(ownerSessionId),
73
+ title: readSessionTitle(ownerSessionId),
74
+ current: ownerSessionId === currentSessionId,
75
+ };
76
+ }
42
77
  /**
43
78
  * Every background-work item spawned by `sessionId`, with per-kind detail, for
44
79
  * the desktop panel. Includes finished sub-agents that are still within their
@@ -47,18 +82,24 @@ export function listRunningBackgroundWork(sessionId) {
47
82
  * keep them. Jobs are only ever present while running (the registry drops them
48
83
  * on finish).
49
84
  */
50
- export function listBackgroundWorkForUI(sessionId) {
85
+ export function listBackgroundWorkForUI(sessionId, opts = {}) {
51
86
  const entries = [];
52
- for (const s of backgroundShellManager.listForSession(sessionId)) {
53
- entries.push({ kind: "shell", shell: s });
87
+ const scope = opts.scope ?? "session";
88
+ const shells = scope === "all"
89
+ ? backgroundShellManager.list()
90
+ : backgroundShellManager.listForSession(sessionId);
91
+ for (const s of shells) {
92
+ entries.push({ kind: "shell", shell: s, sourceSession: sourceSession(sessionId, s.sessionId) });
54
93
  }
55
94
  const now = Date.now();
56
- for (const a of asyncAgentRegistry.listForSession(sessionId)) {
95
+ const agents = scope === "all" ? asyncAgentRegistry.list() : asyncAgentRegistry.listForSession(sessionId);
96
+ for (const a of agents) {
57
97
  // Keep running agents, plus finished ones still inside their fade window so
58
98
  // a completion is briefly visible. (finishedFadeAt = finishedAt + 30s.)
59
99
  const fresh = a.status === "running" || (a.finishedFadeAt != null && a.finishedFadeAt > now);
60
100
  if (!fresh)
61
101
  continue;
102
+ const ownerSessionId = a.sessionId ?? sessionId;
62
103
  entries.push({
63
104
  kind: "subagent",
64
105
  agentId: a.agentId,
@@ -68,9 +109,13 @@ export function listBackgroundWorkForUI(sessionId) {
68
109
  status: a.status,
69
110
  startedAt: a.startedAt,
70
111
  finishedAt: a.finishedAt,
112
+ sourceSession: sourceSession(sessionId, ownerSessionId),
71
113
  });
72
114
  }
73
- for (const j of backgroundJobRegistry.listForSession(sessionId)) {
115
+ const jobs = scope === "all"
116
+ ? backgroundJobRegistry.list()
117
+ : backgroundJobRegistry.listForSession(sessionId);
118
+ for (const j of jobs) {
74
119
  entries.push({
75
120
  kind: "job",
76
121
  jobId: j.jobId,
@@ -80,6 +125,7 @@ export function listBackgroundWorkForUI(sessionId) {
80
125
  ...(j.finishedAt != null ? { finishedAt: j.finishedAt } : {}),
81
126
  ...(j.finalText != null ? { finalText: j.finalText } : {}),
82
127
  ...(j.changedFiles && j.changedFiles.length ? { changedFiles: j.changedFiles } : {}),
128
+ sourceSession: sourceSession(sessionId, j.sessionId),
83
129
  });
84
130
  }
85
131
  return entries;
package/dist/types.d.ts CHANGED
@@ -319,10 +319,14 @@ export interface TaskInfo {
319
319
  activeForm?: string;
320
320
  status: "pending" | "in_progress" | "completed" | "stopped";
321
321
  }
322
+ export type PromptTokenSource = "provider_usage" | "anchor_delta" | "anchor_rescale" | "calibrated_estimate" | "heuristic_estimate" | "session_cumulative";
323
+ export type PromptTokenConfidence = "high" | "medium" | "low";
322
324
  export type StreamEvent = {
323
325
  type: "session_started";
324
326
  sessionId: string;
325
327
  promptTokens: number;
328
+ promptTokensSource?: PromptTokenSource;
329
+ promptTokensConfidence?: PromptTokenConfidence;
326
330
  } | {
327
331
  type: "session_title";
328
332
  sessionId: string;
@@ -436,6 +440,8 @@ export type StreamEvent = {
436
440
  } | {
437
441
  type: "usage_update";
438
442
  promptTokens: number;
443
+ promptTokensSource?: PromptTokenSource;
444
+ promptTokensConfidence?: PromptTokenConfidence;
439
445
  /**
440
446
  * Provider-reported prompt-cache counts, forwarded so the UI can show a
441
447
  * cache hit rate in the context-ring tooltip. Present only when the LLM
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.6.0-rc.15",
3
+ "version": "0.6.0-rc.16",
4
4
  "description": "Core engine for code-shell — agent orchestration, tool execution, hooks, protocol. UI-agnostic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",