@foxden-app/foxclaw 0.3.12 → 0.3.13

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.
@@ -11,6 +11,14 @@ export interface CodexLocalUsageStats {
11
11
  turns: number;
12
12
  usageEvents: number;
13
13
  totals: CodexLocalUsageTotals;
14
+ outputSpeed: CodexLocalOutputSpeedStats;
14
15
  latestSessionMtimeMs: number | null;
15
16
  }
17
+ export interface CodexLocalOutputSpeedStats {
18
+ samples: number;
19
+ outputTokens: number;
20
+ seconds: number;
21
+ latestTokensPerSecond: number | null;
22
+ latestSampleAtMs: number | null;
23
+ }
16
24
  export declare function readCodexLocalUsageStats(codexHome?: string): Promise<CodexLocalUsageStats>;
@@ -13,6 +13,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
13
13
  let sessionsWithUsage = 0;
14
14
  let usageEvents = 0;
15
15
  let latestSessionMtimeMs = null;
16
+ const outputSpeed = emptyOutputSpeedStats();
16
17
  for (const filePath of sessionFiles) {
17
18
  const stat = await fs.stat(filePath).catch(() => null);
18
19
  if (stat) {
@@ -20,6 +21,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
20
21
  }
21
22
  const fileUsage = await readSessionUsage(filePath, turnIds);
22
23
  usageEvents += fileUsage.usageEvents;
24
+ addOutputSpeed(outputSpeed, fileUsage.outputSpeed);
23
25
  if (fileUsage.totalUsage) {
24
26
  sessionsWithUsage += 1;
25
27
  addUsage(totals, fileUsage.totalUsage);
@@ -31,6 +33,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
31
33
  turns: turnIds.size,
32
34
  usageEvents,
33
35
  totals,
36
+ outputSpeed,
34
37
  latestSessionMtimeMs,
35
38
  };
36
39
  }
@@ -66,6 +69,8 @@ async function readSessionUsage(filePath, turnIds) {
66
69
  const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
67
70
  let totalUsage = null;
68
71
  let usageEvents = 0;
72
+ let generationStartMs = null;
73
+ const outputSpeed = emptyOutputSpeedStats();
69
74
  try {
70
75
  for await (const line of reader) {
71
76
  if (!line.trim())
@@ -77,13 +82,22 @@ async function readSessionUsage(filePath, turnIds) {
77
82
  catch {
78
83
  continue;
79
84
  }
85
+ const timestampMs = parseTimestampMs(event?.timestamp);
80
86
  const turnId = typeof event?.payload?.turn_id === 'string' ? event.payload.turn_id : null;
81
87
  if (turnId) {
82
88
  turnIds.add(turnId);
83
89
  }
90
+ if (timestampMs !== null && isGenerationBoundary(event)) {
91
+ generationStartMs = timestampMs;
92
+ }
84
93
  const info = event?.payload?.info;
85
- if (info?.last_token_usage || info?.lastTokenUsage) {
94
+ const lastTokenUsage = info?.last_token_usage ?? info?.lastTokenUsage;
95
+ if (lastTokenUsage) {
86
96
  usageEvents += 1;
97
+ addOutputSpeedSample(outputSpeed, lastTokenUsage, generationStartMs, timestampMs);
98
+ if (timestampMs !== null) {
99
+ generationStartMs = timestampMs;
100
+ }
87
101
  }
88
102
  const totalTokenUsage = info?.total_token_usage ?? info?.totalTokenUsage;
89
103
  if (totalTokenUsage) {
@@ -94,7 +108,7 @@ async function readSessionUsage(filePath, turnIds) {
94
108
  finally {
95
109
  reader.close();
96
110
  }
97
- return { usageEvents, totalUsage };
111
+ return { usageEvents, totalUsage, outputSpeed };
98
112
  }
99
113
  function emptyTotals() {
100
114
  return {
@@ -105,6 +119,15 @@ function emptyTotals() {
105
119
  totalTokens: 0,
106
120
  };
107
121
  }
122
+ function emptyOutputSpeedStats() {
123
+ return {
124
+ samples: 0,
125
+ outputTokens: 0,
126
+ seconds: 0,
127
+ latestTokensPerSecond: null,
128
+ latestSampleAtMs: null,
129
+ };
130
+ }
108
131
  function addUsage(totals, usage) {
109
132
  const inputTokens = numberField(usage, 'input_tokens', 'inputTokens');
110
133
  const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
@@ -115,9 +138,57 @@ function addUsage(totals, usage) {
115
138
  totals.reasoningOutputTokens += numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens');
116
139
  totals.totalTokens += totalTokens || inputTokens + outputTokens;
117
140
  }
141
+ function addOutputSpeed(target, source) {
142
+ target.samples += source.samples;
143
+ target.outputTokens += source.outputTokens;
144
+ target.seconds += source.seconds;
145
+ if (source.latestTokensPerSecond !== null
146
+ && source.latestSampleAtMs !== null
147
+ && (target.latestSampleAtMs === null || source.latestSampleAtMs > target.latestSampleAtMs)) {
148
+ target.latestTokensPerSecond = source.latestTokensPerSecond;
149
+ target.latestSampleAtMs = source.latestSampleAtMs;
150
+ }
151
+ }
152
+ function addOutputSpeedSample(stats, usage, generationStartMs, timestampMs) {
153
+ if (generationStartMs === null || timestampMs === null || timestampMs <= generationStartMs) {
154
+ return;
155
+ }
156
+ const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
157
+ if (outputTokens <= 0) {
158
+ return;
159
+ }
160
+ const seconds = (timestampMs - generationStartMs) / 1000;
161
+ if (!Number.isFinite(seconds) || seconds <= 0) {
162
+ return;
163
+ }
164
+ stats.samples += 1;
165
+ stats.outputTokens += outputTokens;
166
+ stats.seconds += seconds;
167
+ stats.latestTokensPerSecond = outputTokens / seconds;
168
+ stats.latestSampleAtMs = timestampMs;
169
+ }
118
170
  function numberField(source, snakeKey, camelKey) {
119
171
  const snakeValue = source[snakeKey];
120
172
  const value = snakeValue === undefined || snakeValue === null ? source[camelKey] : snakeValue;
121
173
  const numeric = Number(value);
122
174
  return Number.isFinite(numeric) ? numeric : 0;
123
175
  }
176
+ function parseTimestampMs(value) {
177
+ if (typeof value !== 'string' || value.trim().length === 0) {
178
+ return null;
179
+ }
180
+ const timestamp = Date.parse(value);
181
+ return Number.isFinite(timestamp) ? timestamp : null;
182
+ }
183
+ function isGenerationBoundary(event) {
184
+ const payload = event?.payload;
185
+ if (event?.type === 'response_item' && payload?.type === 'function_call_output') {
186
+ return true;
187
+ }
188
+ if (event?.type !== 'event_msg') {
189
+ return false;
190
+ }
191
+ return payload?.type === 'task_started'
192
+ || payload?.type === 'exec_command_end'
193
+ || payload?.type === 'user_message';
194
+ }
@@ -255,8 +255,10 @@ export declare class BridgeSessionCore {
255
255
  private buildNativeCollaborationMode;
256
256
  private buildCodexUsageStatusLines;
257
257
  private buildCodexLocalUsageStatusLines;
258
+ private formatCodexLocalOutputSpeedStatusLines;
258
259
  private resolveFastStatusLabel;
259
260
  private readCachedCodexLocalUsageStats;
261
+ private sendThreadContextSummary;
260
262
  private handleModelCommand;
261
263
  private handleEffortCommand;
262
264
  private handleThreadOpenCallback;
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { normalizeLocale, t } from '../i18n.js';
6
6
  import { parseCommand } from './commands.js';
7
- import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
7
+ import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
8
8
  import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
9
9
  import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeImageAttachment, planAttachmentStoragePath, summarizeTelegramInput, } from '../telegram/media.js';
10
10
  import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage, } from '../telegram/text.js';
@@ -505,6 +505,7 @@ export class BridgeSessionCore {
505
505
  lines.push(revealError ? t(locale, 'codex_sync_failed', { error: revealError }) : t(locale, 'opened_in_codex'));
506
506
  }
507
507
  await this.sendMessage(scopeId, lines.join('\n'));
508
+ await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
508
509
  return;
509
510
  }
510
511
  case 'watch': {
@@ -831,13 +832,16 @@ export class BridgeSessionCore {
831
832
  const mode = watch.mode;
832
833
  if (mode === 'already') {
833
834
  await this.sendMessage(scopeId, t(locale, 'watch_already_enabled', { threadId: watchedThreadId }));
835
+ await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
834
836
  return;
835
837
  }
836
838
  if (mode === 'active') {
837
839
  await this.sendMessage(scopeId, t(locale, 'watch_started_active', { threadId: watchedThreadId }));
840
+ await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
838
841
  return;
839
842
  }
840
843
  await this.sendMessage(scopeId, t(locale, 'watch_started_idle', { threadId: watchedThreadId }));
844
+ await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
841
845
  }
842
846
  async handleThreadArchiveIndexCommand(scopeId, locale, args) {
843
847
  const index = Number.parseInt(args[0] || '', 10);
@@ -4439,6 +4443,7 @@ export class BridgeSessionCore {
4439
4443
  cached: formatTokenCount(stats.totals.cachedInputTokens),
4440
4444
  reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
4441
4445
  }),
4446
+ ...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
4442
4447
  ];
4443
4448
  }
4444
4449
  catch (error) {
@@ -4446,6 +4451,18 @@ export class BridgeSessionCore {
4446
4451
  return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
4447
4452
  }
4448
4453
  }
4454
+ formatCodexLocalOutputSpeedStatusLines(locale, stats) {
4455
+ const speed = stats.outputSpeed;
4456
+ if (speed.samples === 0 || speed.outputTokens <= 0 || speed.seconds <= 0) {
4457
+ return [];
4458
+ }
4459
+ const avg = speed.outputTokens / speed.seconds;
4460
+ return [t(locale, 'status_codex_local_speed', {
4461
+ avg: formatCompactNumber(avg),
4462
+ latest: speed.latestTokensPerSecond === null ? t(locale, 'unknown') : formatCompactNumber(speed.latestTokensPerSecond),
4463
+ samples: formatTokenCount(speed.samples),
4464
+ })];
4465
+ }
4449
4466
  async resolveFastStatusLabel(locale, settings) {
4450
4467
  try {
4451
4468
  const models = await this.app.listModels();
@@ -4466,6 +4483,22 @@ export class BridgeSessionCore {
4466
4483
  this.localUsageCache = { stats, expiresAt: now + CODEX_LOCAL_USAGE_CACHE_MS };
4467
4484
  return stats;
4468
4485
  }
4486
+ async sendThreadContextSummary(scopeId, locale, threadId) {
4487
+ try {
4488
+ const turns = await this.app.listThreadTurns(threadId, 5);
4489
+ const text = formatThreadContextSummary(locale, turns);
4490
+ if (text) {
4491
+ await this.sendMessage(scopeId, text);
4492
+ }
4493
+ }
4494
+ catch (error) {
4495
+ this.logger.warn('codex.thread_context_summary_failed', {
4496
+ scopeId,
4497
+ threadId,
4498
+ error: formatUserError(error),
4499
+ });
4500
+ }
4501
+ }
4469
4502
  async handleModelCommand(event, locale, args) {
4470
4503
  const scopeId = event.scopeId;
4471
4504
  if (args.length === 0) {
@@ -4627,6 +4660,7 @@ export class BridgeSessionCore {
4627
4660
  callbackText = revealError ? t(locale, 'opened_sync_failed_short') : t(locale, 'opened_in_codex_short');
4628
4661
  }
4629
4662
  await this.messaging.answerCallback(event.callbackQueryId, callbackText);
4663
+ await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
4630
4664
  }
4631
4665
  async handleThreadActionCallback(event, action, threadId, locale) {
4632
4666
  const scopeId = event.scopeId;
@@ -4673,6 +4707,7 @@ export class BridgeSessionCore {
4673
4707
  const watch = await this.watchThread(scopeId, target.chatId, target.chatType, target.topicId, binding);
4674
4708
  await this.showThreadsPanelFromStoredState(scopeId, event.messageId, locale, false);
4675
4709
  await this.messaging.answerCallback(event.callbackQueryId, formatWatchCallbackText(locale, watch.mode, watch.threadId));
4710
+ await this.sendThreadContextSummary(scopeId, locale, watch.threadId);
4676
4711
  return;
4677
4712
  }
4678
4713
  if (action === 'archive') {
@@ -1,4 +1,4 @@
1
- import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, AppThread, ApprovalPolicyValue, ChatSessionSettings, CollaborationModeValue, ModelInfo, ReasoningEffortValue, SandboxModeValue } from '../types.js';
1
+ import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, AppThread, AppTurnSnapshot, ApprovalPolicyValue, ChatSessionSettings, CollaborationModeValue, ModelInfo, ReasoningEffortValue, SandboxModeValue } from '../types.js';
2
2
  import type { ResolvedAccessMode } from './access.js';
3
3
  type InlineButton = {
4
4
  text: string;
@@ -60,6 +60,7 @@ pageOffset?: number): string;
60
60
  export declare function formatWeixinModelCopyPaste(locale: AppLocale, models: ModelInfo[], settings: ChatSessionSettings | null): string;
61
61
  export declare function formatWeixinAccessCopyPaste(locale: AppLocale): string;
62
62
  export declare function formatWeixinWhereNavCopyPaste(locale: AppLocale, hasBinding: boolean, defaultCwd?: string): string;
63
+ export declare function formatThreadContextSummary(locale: AppLocale, turns: AppTurnSnapshot[]): string | null;
63
64
  export declare function formatSetupPanelMessage(locale: AppLocale, ctx: SetupPanelContext): string;
64
65
  export declare function buildSetupPanelKeyboard(locale: AppLocale, ctx: SetupPanelContext): InlineButton[][];
65
66
  export declare function resolveSetupSummaryLine(ctx: SetupPanelContext, locale?: AppLocale): string;
@@ -225,6 +225,20 @@ export function formatWeixinWhereNavCopyPaste(locale, hasBinding, defaultCwd) {
225
225
  }
226
226
  return lines.join('\n');
227
227
  }
228
+ export function formatThreadContextSummary(locale, turns) {
229
+ const context = selectRecentThreadContext(turns);
230
+ if (!context) {
231
+ return null;
232
+ }
233
+ return [
234
+ t(locale, 'thread_context_title'),
235
+ t(locale, 'thread_context_user'),
236
+ context.userText || t(locale, 'empty'),
237
+ '',
238
+ t(locale, 'thread_context_codex'),
239
+ context.codexText || t(locale, 'empty'),
240
+ ].join('\n');
241
+ }
228
242
  export function formatSetupPanelMessage(locale, ctx) {
229
243
  const currentModel = resolveCurrentModel(ctx.models, ctx.settings?.model ?? null);
230
244
  const fastTier = resolveFastTierForModel(currentModel);
@@ -393,6 +407,52 @@ function formatFastSetupLabel(locale, supported, enabled, tierName) {
393
407
  function resolveCollaborationMode(mode) {
394
408
  return mode === 'plan' ? 'plan' : 'default';
395
409
  }
410
+ function selectRecentThreadContext(turns) {
411
+ for (const turn of turns) {
412
+ const userText = lastItemText(turn.items, isUserTurnItem);
413
+ const codexText = lastItemText(turn.items, isCodexTurnItem);
414
+ if (userText || codexText) {
415
+ return { userText, codexText };
416
+ }
417
+ }
418
+ return null;
419
+ }
420
+ function lastItemText(items, predicate) {
421
+ for (let index = items.length - 1; index >= 0; index -= 1) {
422
+ const item = items[index];
423
+ if (!predicate(item)) {
424
+ continue;
425
+ }
426
+ const text = normalizeThreadContextText(item.text ?? item.aggregatedOutput ?? item.command ?? '');
427
+ if (text) {
428
+ return text;
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+ function isUserTurnItem(item) {
434
+ const type = normalizeTurnItemType(item.type);
435
+ return type === 'user'
436
+ || type === 'usermessage'
437
+ || type === 'humanmessage'
438
+ || type === 'userinput'
439
+ || type === 'userrequest';
440
+ }
441
+ function isCodexTurnItem(item) {
442
+ const type = normalizeTurnItemType(item.type);
443
+ return type === 'agent'
444
+ || type === 'assistant'
445
+ || type === 'agentmessage'
446
+ || type === 'assistantmessage'
447
+ || type === 'plan';
448
+ }
449
+ function normalizeTurnItemType(value) {
450
+ return value.replace(/[^a-z]/gi, '').toLowerCase();
451
+ }
452
+ function normalizeThreadContextText(value) {
453
+ const trimmed = value.replace(/\r\n/g, '\n').trim().replace(/\n{3,}/g, '\n\n');
454
+ return truncate(trimmed, 1200);
455
+ }
396
456
  function selectedButtonText(selected, label) {
397
457
  return `${selected ? '• ' : ''}${label}`;
398
458
  }
package/dist/i18n.d.ts CHANGED
@@ -111,6 +111,7 @@ declare const MESSAGES: {
111
111
  readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
112
112
  readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
113
113
  readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
114
+ readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
114
115
  readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
115
116
  readonly status_codex_credits: "Codex credits: {value}";
116
117
  readonly status_codex_limit_reached: "Codex limit: {value}";
@@ -315,6 +316,9 @@ declare const MESSAGES: {
315
316
  readonly threads_filter: "Filter: <code>{searchTerm}</code>";
316
317
  readonly threads_range: "Showing {start}-{end}";
317
318
  readonly threads_filter_cleared_short: "Filter cleared";
319
+ readonly thread_context_title: "Recent context:";
320
+ readonly thread_context_user: "User:";
321
+ readonly thread_context_codex: "Codex:";
318
322
  readonly button_prev_page: "⬅️ Prev";
319
323
  readonly button_next_page: "➡️ Next";
320
324
  readonly button_clear_filter: "🧹 Clear";
@@ -661,6 +665,7 @@ declare const MESSAGES: {
661
665
  readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
662
666
  readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
663
667
  readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
668
+ readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
664
669
  readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
665
670
  readonly status_codex_credits: "Codex 额度:{value}";
666
671
  readonly status_codex_limit_reached: "Codex 限制:{value}";
@@ -865,6 +870,9 @@ declare const MESSAGES: {
865
870
  readonly threads_filter: "筛选:<code>{searchTerm}</code>";
866
871
  readonly threads_range: "显示第 {start}-{end} 条";
867
872
  readonly threads_filter_cleared_short: "已清除筛选";
873
+ readonly thread_context_title: "最近上下文:";
874
+ readonly thread_context_user: "用户指令:";
875
+ readonly thread_context_codex: "Codex 最后输出:";
868
876
  readonly button_prev_page: "⬅️ 上一页";
869
877
  readonly button_next_page: "➡️ 下一页";
870
878
  readonly button_clear_filter: "🧹 清除";
package/dist/i18n.js CHANGED
@@ -109,6 +109,7 @@ const MESSAGES = {
109
109
  status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
110
110
  status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
111
111
  status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
112
+ status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
112
113
  status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
113
114
  status_codex_credits: 'Codex credits: {value}',
114
115
  status_codex_limit_reached: 'Codex limit: {value}',
@@ -313,6 +314,9 @@ const MESSAGES = {
313
314
  threads_filter: 'Filter: <code>{searchTerm}</code>',
314
315
  threads_range: 'Showing {start}-{end}',
315
316
  threads_filter_cleared_short: 'Filter cleared',
317
+ thread_context_title: 'Recent context:',
318
+ thread_context_user: 'User:',
319
+ thread_context_codex: 'Codex:',
316
320
  button_prev_page: '⬅️ Prev',
317
321
  button_next_page: '➡️ Next',
318
322
  button_clear_filter: '🧹 Clear',
@@ -659,6 +663,7 @@ const MESSAGES = {
659
663
  status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
660
664
  status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
661
665
  status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
666
+ status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
662
667
  status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
663
668
  status_codex_credits: 'Codex 额度:{value}',
664
669
  status_codex_limit_reached: 'Codex 限制:{value}',
@@ -863,6 +868,9 @@ const MESSAGES = {
863
868
  threads_filter: '筛选:<code>{searchTerm}</code>',
864
869
  threads_range: '显示第 {start}-{end} 条',
865
870
  threads_filter_cleared_short: '已清除筛选',
871
+ thread_context_title: '最近上下文:',
872
+ thread_context_user: '用户指令:',
873
+ thread_context_codex: 'Codex 最后输出:',
866
874
  button_prev_page: '⬅️ 上一页',
867
875
  button_next_page: '➡️ 下一页',
868
876
  button_clear_filter: '🧹 清除',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",