minecodex 1.2.1 → 2.0.0

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.
Files changed (99) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -14
  4. package/features/images/src/http-server.mjs +15 -5
  5. package/features/images/src/image-library.mjs +37 -9
  6. package/features/images/src/library-prompt-page.mjs +239 -0
  7. package/features/images/src/prompt-index.mjs +27 -24
  8. package/features/images/src/prompt-source.mjs +100 -0
  9. package/features/images/web/app.js +180 -67
  10. package/features/images/web/i18n.mjs +5 -0
  11. package/features/images/web/index.html +29 -24
  12. package/features/images/web/styles.css +169 -139
  13. package/features/model-slider/README.md +13 -1
  14. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  15. package/features/notes/DESIGN.md +66 -0
  16. package/features/notes/README.md +18 -99
  17. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  18. package/features/notes/assets/icons/book-open.svg +16 -0
  19. package/features/notes/assets/native-emojis.json +1 -0
  20. package/features/notes/assets/native-prompt-icons.json +28 -0
  21. package/features/notes/codex-feature.json +12 -70
  22. package/features/notes/src/main.mjs +2 -2
  23. package/features/notes/src/product.mjs +287 -613
  24. package/features/notes/src/prompt-schema.mjs +117 -0
  25. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  26. package/features/notes/src/quick-prompts-server.mjs +167 -0
  27. package/features/session-status/DESIGN.md +10 -2
  28. package/features/session-status/README.md +13 -3
  29. package/features/session-status/src/session-status.mjs +329 -24
  30. package/features/sidebar-focus/DESIGN.md +16 -0
  31. package/features/sidebar-focus/codex-feature.json +20 -0
  32. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  33. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  34. package/features/unread-inbox/DESIGN.md +28 -0
  35. package/features/unread-inbox/codex-feature.json +20 -0
  36. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  37. package/features/usage-meter/DESIGN.md +49 -0
  38. package/features/usage-meter/README.md +18 -0
  39. package/features/usage-meter/codex-feature.json +13 -0
  40. package/features/usage-meter/src/usage-meter.mjs +200 -0
  41. package/package.json +21 -6
  42. package/packages/cli/src/config.mjs +4 -1
  43. package/packages/cli/src/control-server.mjs +2 -2
  44. package/packages/runtime-host/src/cloud-images.mjs +123 -0
  45. package/packages/runtime-host/src/codex-cdp.mjs +42 -8
  46. package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
  47. package/packages/runtime-host/src/codex-injection.mjs +364 -116
  48. package/packages/runtime-host/src/codex-runtime.mjs +105 -28
  49. package/packages/runtime-host/src/feature-registry.mjs +3 -0
  50. package/packages/runtime-host/src/session-status-reader.mjs +65 -24
  51. package/features/notes/assets/icons/circle.svg +0 -15
  52. package/features/notes/assets/icons/ellipsis.svg +0 -17
  53. package/features/notes/assets/icons/external-link.svg +0 -17
  54. package/features/notes/assets/icons/list-todo.svg +0 -19
  55. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  56. package/features/notes/assets/icons/paperclip.svg +0 -15
  57. package/features/notes/assets/icons/star-off.svg +0 -17
  58. package/features/notes/assets/icons/star.svg +0 -15
  59. package/features/notes/assets/icons/sticky-note.svg +0 -16
  60. package/features/notes/assets/icons/trash-2.svg +0 -19
  61. package/features/notes/src/http-server.mjs +0 -580
  62. package/features/notes/web/app.js +0 -1202
  63. package/features/notes/web/codex-artifact-document.svg +0 -1
  64. package/features/notes/web/codex-attachment.svg +0 -1
  65. package/features/notes/web/codex-build.svg +0 -1
  66. package/features/notes/web/codex-code.svg +0 -1
  67. package/features/notes/web/codex-cplusplus.svg +0 -1
  68. package/features/notes/web/codex-css.svg +0 -1
  69. package/features/notes/web/codex-document.svg +0 -1
  70. package/features/notes/web/codex-file.svg +0 -1
  71. package/features/notes/web/codex-folder.svg +0 -1
  72. package/features/notes/web/codex-hashes.svg +0 -1
  73. package/features/notes/web/codex-html.svg +0 -1
  74. package/features/notes/web/codex-image.svg +0 -1
  75. package/features/notes/web/codex-java.svg +0 -1
  76. package/features/notes/web/codex-javascript.svg +0 -1
  77. package/features/notes/web/codex-json.svg +0 -1
  78. package/features/notes/web/codex-notebook.svg +0 -1
  79. package/features/notes/web/codex-pdf.svg +0 -1
  80. package/features/notes/web/codex-pencil.svg +0 -3
  81. package/features/notes/web/codex-php.svg +0 -1
  82. package/features/notes/web/codex-plus.svg +0 -3
  83. package/features/notes/web/codex-presentation.svg +0 -1
  84. package/features/notes/web/codex-python.svg +0 -1
  85. package/features/notes/web/codex-react.svg +0 -1
  86. package/features/notes/web/codex-rust.svg +0 -1
  87. package/features/notes/web/codex-shell.svg +0 -1
  88. package/features/notes/web/codex-skill.svg +0 -1
  89. package/features/notes/web/codex-spreadsheet.svg +0 -1
  90. package/features/notes/web/codex-task.svg +0 -3
  91. package/features/notes/web/codex-terminal.svg +0 -1
  92. package/features/notes/web/codex-toml.svg +0 -1
  93. package/features/notes/web/codex-typescript.svg +0 -1
  94. package/features/notes/web/codex-view-all.svg +0 -3
  95. package/features/notes/web/file-icons.mjs +0 -68
  96. package/features/notes/web/i18n.mjs +0 -237
  97. package/features/notes/web/index.html +0 -15
  98. package/features/notes/web/styles.css +0 -1155
  99. package/features/notes/web/todo-drag.mjs +0 -18
@@ -1,3 +1,4 @@
1
+ import { cloudImagesAction } from "./cloud-images.mjs";
1
2
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
2
3
  // 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
3
4
 
@@ -46,15 +47,11 @@ export {
46
47
  } from "./codex-injection.mjs";
47
48
 
48
49
  export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
49
- const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
50
- ?? document.querySelector('[contenteditable="true"][role="textbox"]');
51
- const card = composer?.closest('[data-codex-composer-root]');
52
- const portal = card?.querySelector('[data-above-composer-portal]');
53
- if (portal) return portal.getAttribute('data-above-composer-conversation-id');
54
- if (card?.getAttribute('data-composer-placement') === 'home') return null;
55
- const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
56
- return selected?.getAttribute('data-app-action-sidebar-thread-id')
57
- ?? location.pathname + location.search + location.hash;
50
+ return [...new Set([...document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')].map(composer => {
51
+ const card = composer.closest('[data-codex-composer-root]');
52
+ if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
53
+ return card.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
54
+ }).filter(Boolean))];
58
55
  })()`;
59
56
 
60
57
  export class CodexRuntime {
@@ -103,6 +100,8 @@ export class CodexRuntime {
103
100
  this.scriptIds = new Map();
104
101
  this.bindingTokens = new Map();
105
102
  this.hostActionChains = new Map();
103
+ this.sessionStatusUpdates = new Map();
104
+ this.sessionStatusRevision = 0;
106
105
  this.refreshPromise = null;
107
106
  this.stopping = false;
108
107
  this.monitorPromise = null;
@@ -290,7 +289,9 @@ export class CodexRuntime {
290
289
  }
291
290
 
292
291
  async startTargetDiscovery() {
293
- const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`);
292
+ const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
293
+ signal: AbortSignal.timeout(3_000),
294
+ });
294
295
  if (!response?.ok) {
295
296
  throw new Error(`Codex CDP version returned HTTP ${response?.status ?? "error"}`);
296
297
  }
@@ -355,7 +356,9 @@ export class CodexRuntime {
355
356
 
356
357
  async isAvailable() {
357
358
  try {
358
- const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`);
359
+ const response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/version`, {
360
+ signal: AbortSignal.timeout(3_000),
361
+ });
359
362
  if (!response?.ok) return false;
360
363
  requireCdpWebSocketUrl(await response.json(), this.cdpPort);
361
364
  return true;
@@ -518,6 +521,12 @@ export class CodexRuntime {
518
521
  }
519
522
  if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
520
523
  const feature = this.features.find((candidate) => candidate.id === request.featureId);
524
+ if (feature?.id === "session-status" && request.action === "refresh-session-status") {
525
+ if (this.sessionStatusReader && this.clients.get(targetId) === client && !this.stopping) {
526
+ await this.publishSessionStatus(targetId);
527
+ }
528
+ return;
529
+ }
521
530
  if (feature && request.action === SURFACE_LOAD_ACTION) {
522
531
  try {
523
532
  const result = await this.loadSurfaceIntoFrame(client, feature, request.payload);
@@ -537,10 +546,45 @@ export class CodexRuntime {
537
546
  });
538
547
  return;
539
548
  }
549
+ if (feature.id === "notes" && request.action === "quick-prompts") {
550
+ try {
551
+ const { route, method = "GET", body } = request.payload ?? {};
552
+ const allowed = (route === "/api/state" && method === "GET")
553
+ || (["/api/prompts", "/api/categories", "/api/reorder"].includes(route) && method === "POST")
554
+ || (/^\/api\/categories\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
555
+ || (/^\/api\/prompts\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
556
+ || (/^\/api\/prompts\/[a-zA-Z0-9-]+\/use$/.test(route) && method === "POST");
557
+ const origin = pageScriptServiceOrigin(feature);
558
+ if (!allowed || !origin) throw new Error("Prompt request is not allowed");
559
+ const response = await this.fetchImpl(new URL(route, origin), {
560
+ method, headers: { "Content-Type": "application/json", Origin: CODEX_APP_ORIGIN },
561
+ body: body === undefined ? undefined : JSON.stringify(body),
562
+ signal: AbortSignal.timeout(8_000),
563
+ });
564
+ const result = response.status === 204 ? null : await response.json();
565
+ if (!response.ok) throw new Error(result?.error?.message ?? "Prompt request failed");
566
+ await this.resolveHostAction(client, request.requestId, { ok: true, result });
567
+ } catch (error) {
568
+ await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "PROMPT_REQUEST_FAILED", message: error.message } });
569
+ }
570
+ return;
571
+ }
540
572
  if (request.action === "import-generated-image") {
541
573
  await this.importGeneratedImage(feature, request.payload);
542
574
  return;
543
575
  }
576
+ if (["list-cloud-images", "import-cloud-image"].includes(request.action)) {
577
+ try {
578
+ let result = await cloudImagesAction(client, request.action === "list-cloud-images" ? "list" : "import", request.payload);
579
+ if (request.action === "import-cloud-image" && !result.skipped) {
580
+ result = await this.importGeneratedImage(feature, result);
581
+ }
582
+ await this.resolveHostAction(client, request.requestId, { ok: true, result });
583
+ } catch (error) {
584
+ await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "CLOUD_IMAGES_FAILED", message: error.message } });
585
+ }
586
+ return;
587
+ }
544
588
  try {
545
589
  const result = request.action === "create-image-edit-thread"
546
590
  ? await this.createImageEditThread(client, feature, request.payload, request.requestId)
@@ -568,12 +612,15 @@ export class CodexRuntime {
568
612
  if (!response?.ok) {
569
613
  throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
570
614
  }
615
+ return response.json();
571
616
  }
572
617
 
573
618
  async refreshTargets() {
574
619
  let response;
575
620
  try {
576
- response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list`);
621
+ response = await this.fetchImpl(`http://127.0.0.1:${this.cdpPort}/json/list`, {
622
+ signal: AbortSignal.timeout(3_000),
623
+ });
577
624
  } catch (error) {
578
625
  throw this.markCodexUnavailable(error);
579
626
  }
@@ -604,7 +651,8 @@ export class CodexRuntime {
604
651
  ...this.targetFailures.keys(),
605
652
  ]);
606
653
  for (const id of trackedTargetIds) {
607
- if (liveRendererIds.has(id)) continue;
654
+ const socketState = this.clients.get(id)?.socket.readyState;
655
+ if (liveRendererIds.has(id) && (socketState == null || socketState === 1)) continue;
608
656
  removedTarget = true;
609
657
  this.clients.get(id)?.socket.close();
610
658
  this.clients.delete(id);
@@ -640,7 +688,13 @@ export class CodexRuntime {
640
688
  const bindingToken = randomBytes(24).toString("hex");
641
689
  this.bindingTokens.set(target.id, bindingToken);
642
690
  client.on?.("Runtime.bindingCalled", (params) => {
643
- this.enqueueHostAction(target.id, () => this.handleBindingCalled(target.id, client, params))
691
+ let action;
692
+ try { action = JSON.parse(params.payload)?.action; } catch { return; }
693
+ // 统计读取独立合并,避免排在图片等用户操作后面。
694
+ const work = action === "refresh-session-status"
695
+ ? this.handleBindingCalled(target.id, client, params)
696
+ : this.enqueueHostAction(target.id, () => this.handleBindingCalled(target.id, client, params));
697
+ work
644
698
  .catch((error) => this.logger.warn?.("Codex Host action failed", error.message));
645
699
  });
646
700
  const source = createInjectionSource(this.features, {
@@ -693,22 +747,45 @@ export class CodexRuntime {
693
747
  this.notifyStatusChange();
694
748
  }
695
749
 
696
- async publishSessionStatus() {
750
+ async publishSessionStatus(onlyTargetId = null) {
697
751
  for (const [targetId, client] of this.clients) {
752
+ if (onlyTargetId && targetId !== onlyTargetId) continue;
698
753
  if (!this.injectedTargets.has(targetId)) continue;
699
- try {
700
- const response = await client.send("Runtime.evaluate", {
701
- expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
702
- returnByValue: true,
703
- });
704
- const threadId = threadIdFromLocation(response.result?.value);
705
- const snapshot = threadId ? await this.sessionStatusReader.read(threadId) : null;
706
- await client.send("Runtime.evaluate", {
707
- expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)})`,
708
- returnByValue: true,
709
- });
710
- } catch (error) {
711
- this.logger.warn?.("Session Status update failed", error.message);
754
+ const pending = this.sessionStatusUpdates.get(targetId);
755
+ if (pending) { await pending; continue; }
756
+ const revision = ++this.sessionStatusRevision;
757
+ const update = (async () => {
758
+ try {
759
+ const response = await client.send("Runtime.evaluate", {
760
+ expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
761
+ returnByValue: true,
762
+ }, { timeoutMs: 3_000 });
763
+ for (const value of response.result?.value ?? []) {
764
+ const threadId = threadIdFromLocation(value);
765
+ const data = threadId ? await this.sessionStatusReader.read(threadId) : null;
766
+ const snapshot = data ?? (threadId ? {
767
+ threadId,
768
+ retryAfterMs: this.sessionStatusReader.retryAfterMs?.(threadId) ?? 500,
769
+ } : null);
770
+ if (this.stopping || this.clients.get(targetId) !== client) return;
771
+ await client.send("Runtime.evaluate", {
772
+ expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)}, ${revision})`,
773
+ returnByValue: true,
774
+ }, { timeoutMs: 3_000 });
775
+ }
776
+ } catch (error) {
777
+ this.logger.warn?.("Session Status update failed", error.message);
778
+ if (this.clients.get(targetId) === client) {
779
+ client.socket.close();
780
+ this.injectedTargets.delete(targetId);
781
+ this.notifyStatusChange();
782
+ }
783
+ }
784
+ })();
785
+ this.sessionStatusUpdates.set(targetId, update);
786
+ try { await update; }
787
+ finally {
788
+ if (this.sessionStatusUpdates.get(targetId) === update) this.sessionStatusUpdates.delete(targetId);
712
789
  }
713
790
  }
714
791
  }
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
 
7
7
  const MANIFEST_NAME = "codex-feature.json";
8
8
  const HOST_ACTIONS = new Set([
9
+ "quick-prompts",
9
10
  "insert-text",
10
11
  "attach-file",
11
12
  "open-detail-tab",
@@ -13,6 +14,8 @@ const HOST_ACTIONS = new Set([
13
14
  "open-sidebar",
14
15
  "resolve-file-paths",
15
16
  "import-generated-image",
17
+ "list-cloud-images",
18
+ "import-cloud-image",
16
19
  "create-image-edit-thread",
17
20
  ]);
18
21
 
@@ -62,6 +62,7 @@ function emptyState(filePath) {
62
62
  offset: 0,
63
63
  pending: "",
64
64
  turns: new Set(),
65
+ runningTurns: new Set(),
65
66
  responses: new Set(),
66
67
  hasResponseUsage: false,
67
68
  total: null,
@@ -69,9 +70,7 @@ function emptyState(filePath) {
69
70
  contextWindow: null,
70
71
  firstOutputAt: null,
71
72
  pendingToolIntervals: [],
72
- decodeTokens: 0,
73
- decodeMs: 0,
74
- speedSamples: 0,
73
+ speedMeasurements: [],
75
74
  ttftTotalMs: 0,
76
75
  ttftSamples: 0,
77
76
  toolMs: 0,
@@ -109,6 +108,9 @@ export function applySessionStatusEvent(state, event) {
109
108
  const payload = event?.payload ?? {};
110
109
  if (event?.type === "compacted") {
111
110
  state.compactionRecords += 1;
111
+ state.last = null;
112
+ state.firstOutputAt = null;
113
+ state.pendingToolIntervals = [];
112
114
  if (Number.isSafeInteger(payload.window_number) && payload.window_number >= 0) {
113
115
  state.compactionWindow = Math.max(state.compactionWindow, payload.window_number);
114
116
  }
@@ -133,8 +135,24 @@ export function applySessionStatusEvent(state, event) {
133
135
  state.baseInstructionTokens = estimatedTextTokens(payload.base_instructions?.text);
134
136
  }
135
137
  if (event?.type === "event_msg") {
136
- if (payload.type === "context_compacted") state.compactionEvents += 1;
137
- if (payload.type === "task_started" && payload.turn_id) state.turns.add(payload.turn_id);
138
+ if (payload.type === "context_compacted") {
139
+ state.compactionEvents += 1;
140
+ state.last = null;
141
+ state.firstOutputAt = null;
142
+ state.pendingToolIntervals = [];
143
+ }
144
+ if (payload.type === "task_started" && payload.turn_id) {
145
+ state.firstOutputAt = null;
146
+ state.pendingToolIntervals = [];
147
+ state.turns.add(payload.turn_id);
148
+ state.runningTurns.add(payload.turn_id);
149
+ }
150
+ if (["task_complete", "task_interrupted", "turn_aborted"].includes(payload.type)) {
151
+ state.firstOutputAt = null;
152
+ state.pendingToolIntervals = [];
153
+ if (payload.turn_id) state.runningTurns.delete(payload.turn_id);
154
+ else state.runningTurns.clear();
155
+ }
138
156
  if (payload.type === "task_complete" && Number.isFinite(payload.time_to_first_token_ms)) {
139
157
  state.ttftTotalMs += payload.time_to_first_token_ms;
140
158
  state.ttftSamples += 1;
@@ -150,13 +168,17 @@ export function applySessionStatusEvent(state, event) {
150
168
  if (TOOL_ITEMS.has(itemType) && Number.isFinite(start) && Number.isFinite(end)) {
151
169
  state.toolMs += Math.max(0, end - start);
152
170
  state.pendingToolIntervals.push([start, end]);
171
+ // 工具完成事件可能晚于用量记录落盘,补扣已记录样本中的重叠区间。
172
+ for (const sample of state.speedMeasurements) {
173
+ if (start < sample.end && end > sample.start) sample.tools.push([start, end]);
174
+ }
153
175
  }
154
176
  }
155
177
  if (payload.type === "token_count") {
156
178
  if (!state.hasResponseUsage && validUsage(payload.info?.total_token_usage)) {
157
179
  state.total = payload.info.total_token_usage;
158
180
  }
159
- if (!state.hasResponseUsage && validUsage(payload.info?.last_token_usage)) {
181
+ if (validUsage(payload.info?.last_token_usage)) {
160
182
  state.last = payload.info.last_token_usage;
161
183
  }
162
184
  if (Number.isFinite(payload.info?.model_context_window)) {
@@ -166,18 +188,18 @@ export function applySessionStatusEvent(state, event) {
166
188
  }
167
189
  if (event?.type === "token_usage_record") {
168
190
  const responseId = payload.response_id;
169
- if (responseId && !state.responses.has(responseId)) {
191
+ if (responseId && state.responses.has(responseId)) return;
192
+ if (responseId) {
170
193
  state.responses.add(responseId);
171
194
  const responseEnd = Date.parse(event.timestamp);
172
- const elapsed = state.firstOutputAt == null ? null : responseEnd - state.firstOutputAt
173
- - overlappingToolMs(state.pendingToolIntervals, state.firstOutputAt, responseEnd);
174
195
  const output = payload.usage?.output_tokens;
175
- // 会话事件只给出首个可见条目的时间,故 TPS 是观测估算。
196
+ // 起止点来自会话事件,并非模型首末 token 时间;只能提供观测约值。
176
197
  if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null
177
- && Number.isFinite(elapsed) && elapsed > 0 && elapsed < 600_000) {
178
- state.decodeTokens += output;
179
- state.decodeMs += elapsed;
180
- state.speedSamples += 1;
198
+ && Number.isFinite(responseEnd) && responseEnd > state.firstOutputAt) {
199
+ state.speedMeasurements.push({
200
+ start: state.firstOutputAt, end: responseEnd, output,
201
+ tools: state.pendingToolIntervals.slice(),
202
+ });
181
203
  }
182
204
  }
183
205
  state.firstOutputAt = null;
@@ -192,6 +214,16 @@ export function applySessionStatusEvent(state, event) {
192
214
  }
193
215
 
194
216
  export function sessionStatusSnapshot(state, threadId) {
217
+ let decodeTokens = 0;
218
+ let decodeMs = 0;
219
+ let speedSamples = 0;
220
+ for (const sample of state.speedMeasurements) {
221
+ const elapsed = sample.end - sample.start - overlappingToolMs(sample.tools, sample.start, sample.end);
222
+ if (elapsed <= 0 || elapsed >= 600_000) continue;
223
+ decodeTokens += sample.output;
224
+ decodeMs += elapsed;
225
+ speedSamples += 1;
226
+ }
195
227
  const total = state.total;
196
228
  const input = total?.input_tokens ?? 0;
197
229
  const cached = total?.cached_input_tokens ?? 0;
@@ -200,6 +232,7 @@ export function sessionStatusSnapshot(state, threadId) {
200
232
  const contextWindow = state.contextWindow > 0 ? state.contextWindow : null;
201
233
  return {
202
234
  threadId,
235
+ isRunning: state.runningTurns.size > 0,
203
236
  turns: state.turns.size,
204
237
  steps: state.responses.size,
205
238
  totalTokens: total?.total_tokens ?? null,
@@ -209,9 +242,9 @@ export function sessionStatusSnapshot(state, threadId) {
209
242
  uncachedInputTokens: total ? Math.max(0, input - cached - cacheWrite) : null,
210
243
  outputTokens: total?.output_tokens ?? null,
211
244
  cacheHitPercent: input > 0 ? Math.round(cached / input * 100) : null,
212
- observedTps: state.decodeMs > 0 ? Math.round(state.decodeTokens / (state.decodeMs / 1000)) : null,
213
- speedSamples: state.speedSamples,
214
- observedModelMs: state.decodeMs,
245
+ observedTps: decodeMs > 0 ? Math.round(decodeTokens / (decodeMs / 1000)) : null,
246
+ speedSamples,
247
+ observedModelMs: decodeMs,
215
248
  toolMs: state.toolMs,
216
249
  averageFirstTokenMs: state.ttftSamples > 0 ? Math.round(state.ttftTotalMs / state.ttftSamples) : null,
217
250
  contextUsedTokens: contextUsed,
@@ -248,24 +281,32 @@ async function findLog(directory, threadId) {
248
281
  }
249
282
 
250
283
  export class SessionStatusReader {
251
- constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions") } = {}) {
284
+ constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions"), now = Date.now } = {}) {
252
285
  this.sessionsDir = sessionsDir;
286
+ this.now = now;
253
287
  this.states = new Map();
254
- this.missingCheckedAt = new Map();
288
+ this.missingChecks = new Map();
289
+ }
290
+
291
+ retryAfterMs(threadId) {
292
+ return Math.max(0, (this.missingChecks.get(threadId)?.nextCheckAt ?? 0) - this.now());
255
293
  }
256
294
 
257
295
  async read(threadId) {
258
296
  if (!THREAD_ID.test(threadId ?? "")) return null;
259
297
  let state = this.states.get(threadId);
260
298
  if (!state) {
261
- const lastCheck = this.missingCheckedAt.get(threadId) ?? 0;
262
- if (Date.now() - lastCheck < 10_000) return null;
263
- this.missingCheckedAt.set(threadId, Date.now());
299
+ if (this.retryAfterMs(threadId) > 0) return null;
264
300
  const filePath = await findLog(this.sessionsDir, threadId);
265
- if (!filePath) return null;
301
+ if (!filePath) {
302
+ const attempts = this.missingChecks.get(threadId)?.attempts ?? 0;
303
+ const delay = [500, 1000, 2000][attempts] ?? 10_000;
304
+ this.missingChecks.set(threadId, { attempts: attempts + 1, nextCheckAt: this.now() + delay });
305
+ return null;
306
+ }
266
307
  state = emptyState(filePath);
267
308
  this.states.set(threadId, state);
268
- this.missingCheckedAt.delete(threadId);
309
+ this.missingChecks.delete(threadId);
269
310
  }
270
311
  let info;
271
312
  try { info = await stat(state.filePath); }
@@ -1,15 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-circle"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <circle cx="12" cy="12" r="10" />
15
- </svg>
@@ -1,17 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-ellipsis"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <circle cx="12" cy="12" r="1" />
15
- <circle cx="19" cy="12" r="1" />
16
- <circle cx="5" cy="12" r="1" />
17
- </svg>
@@ -1,17 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-external-link"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M15 3h6v6" />
15
- <path d="M10 14 21 3" />
16
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
17
- </svg>
@@ -1,19 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-list-todo"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M13 5h8" />
15
- <path d="M13 12h8" />
16
- <path d="M13 19h8" />
17
- <path d="m3 17 2 2 4-4" />
18
- <rect x="3" y="4" width="6" height="6" rx="1" />
19
- </svg>
@@ -1,17 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-message-square-quote"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M14 14a2 2 0 0 0 2-2V8h-2" />
15
- <path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
16
- <path d="M8 14a2 2 0 0 0 2-2V8H8" />
17
- </svg>
@@ -1,15 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-paperclip"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" />
15
- </svg>
@@ -1,17 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-star-off"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="m10.344 4.688 1.181-2.393a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.237 3.152" />
15
- <path d="m17.945 17.945.43 2.505a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a8 8 0 0 0 .4-.099" />
16
- <path d="m2 2 20 20" />
17
- </svg>
@@ -1,15 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-star"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
15
- </svg>
@@ -1,16 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-sticky-note"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z" />
15
- <path d="M15 3v5a1 1 0 0 0 1 1h5" />
16
- </svg>
@@ -1,19 +0,0 @@
1
- <!-- @license lucide-static v1.31.0 - ISC -->
2
- <svg
3
- class="lucide lucide-trash-2"
4
- xmlns="http://www.w3.org/2000/svg"
5
- width="24"
6
- height="24"
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke="currentColor"
10
- stroke-width="2"
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- >
14
- <path d="M10 11v6" />
15
- <path d="M14 11v6" />
16
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
17
- <path d="M3 6h18" />
18
- <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
19
- </svg>