@walkhi/code-relax 0.1.0-beta.22 → 0.1.0-beta.23

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.
@@ -18,13 +18,16 @@ import { runtimePaths } from './platform/windows/runtime-paths.mjs';
18
18
  import { openControl } from './service-control.mjs';
19
19
  import { writeRecord, readRecord } from './service-lifecycle.mjs';
20
20
  import { ExecutionHealth } from './execution-health.mjs';
21
- import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
22
- import { createRelayManager } from './self-relay/lifecycle.mjs';
21
+ import { authorizeLanDevice } from './self-relay/lan-authorizations.mjs';
22
+ import { createRelayManager } from './self-relay/lifecycle.mjs';
23
23
  import { generateAndSetThreadTitle } from './thread-title-generation.mjs';
24
24
  import { desktopHost } from './platform/windows/desktop-host.mjs';
25
25
  import { SharedCatalog } from './shared-catalog.mjs';
26
26
  import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
27
27
  import { requestServiceRestart } from './service-restart.mjs';
28
+ import { TurnModelHistory } from './turn-model-history.mjs';
29
+
30
+ const turnModelHistory = new TurnModelHistory();
28
31
 
29
32
  const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
30
33
  const MAX_CACHED_ATTACHMENTS = 32;
@@ -85,8 +88,8 @@ const sessionSettingsCache = new Map();
85
88
  const liveActivityCache = new Map();
86
89
  let sharedServer = null;
87
90
  const managedAppServer = new ManagedAppServer({ stateRoot, host: desktopHost });
88
- const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
89
- const relayManager = createRelayManager(stateRoot);
91
+ const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost, url: managedAppServerUrl });
92
+ const relayManager = createRelayManager(stateRoot);
90
93
  let sharedCatalog = null, executionMode = 'ordinary';
91
94
  function selectSharedExecution(enabled) {
92
95
  const next = enabled ? managedAppServer.client : null;
@@ -195,9 +198,10 @@ async function handleRequest(request, response) {
195
198
  if (request.method === 'GET' && url.pathname === '/api/health') {
196
199
  // Status readers consume the workstation's cached sample. A phone query
197
200
  // must not trigger PowerShell, process-identity, or app-server checks.
201
+ const executionReady = executionHealth.state.ready && sharedServer?.ready === true;
198
202
  sendJson(response, 200, {
199
203
  status: 'ok', resident: true, version: runtimeVersion,
200
- execution: { ...executionHealth.state, ready: executionHealth.state.ready && sharedServer?.ready === true,
204
+ execution: { ...executionHealth.state, ready: executionReady, canWrite: executionReady,
201
205
  mode: executionMode },
202
206
  managedAppServer: managedAppServer.snapshot(),
203
207
  ...runtimeDiscovery.state,
@@ -283,7 +287,7 @@ async function handleRequest(request, response) {
283
287
  return;
284
288
  }
285
289
 
286
- if (request.method === 'GET' && url.pathname === '/api/usage-events') {
290
+ if (request.method === 'GET' && url.pathname === '/api/usage-events') {
287
291
  const execution = sharedServer;
288
292
  response.startEvents();
289
293
  const emit = value => { if (!response.destroyed) response.event('usage', value); };
@@ -292,45 +296,45 @@ async function handleRequest(request, response) {
292
296
  execution.on('usage', emit); execution.on('usage-error', failed); execution.on('disconnected', disconnected);
293
297
  response.once('close', () => { execution.off('usage', emit); execution.off('usage-error', failed); execution.off('disconnected', disconnected); });
294
298
  try { emit(await execution.usage()); } catch (error) { failed(error); }
295
- return;
296
- }
297
-
298
- if (request.method === 'GET' && url.pathname === '/api/turn-events') {
299
- response.startEvents();
300
- const listener = ({ method, params }) => {
301
- if (!['turn/started', 'turn/completed'].includes(method)) return;
302
- const threadId = params?.threadId, turnId = params?.turn?.id;
303
- if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
304
- response.event('turn', { method, threadId, turnId });
305
- }
306
- };
307
- const disconnected = () => response.end();
308
- sharedServer.on('notification', listener);
309
- sharedServer.on('disconnected', disconnected);
310
- response.once('close', () => {
311
- sharedServer.off('notification', listener);
312
- sharedServer.off('disconnected', disconnected);
313
- });
314
- return;
315
- }
316
-
317
- const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
318
- if (request.method === 'GET' && activeTurnMatch) {
319
- const threadId = decodeURIComponent(activeTurnMatch[1]);
320
- await sharedServer.connect();
321
- const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
322
- const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
323
- let turnId = null;
324
- if (status === 'active') {
325
- const page = await sharedServer.request('thread/turns/list', {
326
- threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
327
- });
328
- const latest = page.data?.[0];
329
- if (latest?.status === 'inProgress') turnId = latest.id;
330
- }
331
- sendJson(response, 200, { turnId });
332
- return;
333
- }
299
+ return;
300
+ }
301
+
302
+ if (request.method === 'GET' && url.pathname === '/api/turn-events') {
303
+ response.startEvents();
304
+ const listener = ({ method, params }) => {
305
+ if (!['turn/started', 'turn/completed'].includes(method)) return;
306
+ const threadId = params?.threadId, turnId = params?.turn?.id;
307
+ if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
308
+ response.event('turn', { method, threadId, turnId });
309
+ }
310
+ };
311
+ const disconnected = () => response.end();
312
+ sharedServer.on('notification', listener);
313
+ sharedServer.on('disconnected', disconnected);
314
+ response.once('close', () => {
315
+ sharedServer.off('notification', listener);
316
+ sharedServer.off('disconnected', disconnected);
317
+ });
318
+ return;
319
+ }
320
+
321
+ const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
322
+ if (request.method === 'GET' && activeTurnMatch) {
323
+ const threadId = decodeURIComponent(activeTurnMatch[1]);
324
+ await sharedServer.connect();
325
+ const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
326
+ const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
327
+ let turnId = null;
328
+ if (status === 'active') {
329
+ const page = await sharedServer.request('thread/turns/list', {
330
+ threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
331
+ });
332
+ const latest = page.data?.[0];
333
+ if (latest?.status === 'inProgress') turnId = latest.id;
334
+ }
335
+ sendJson(response, 200, { turnId });
336
+ return;
337
+ }
334
338
 
335
339
  const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
336
340
  if (request.method === 'POST' && createThreadMatch) {
@@ -374,22 +378,11 @@ async function handleRequest(request, response) {
374
378
  if (request.method === 'GET' && timelineMatch) {
375
379
  const threadId = decodeURIComponent(timelineMatch[1]);
376
380
  const timeline = normalizeSharedTimeline(await sharedServer.timeline(threadId, sharedHistoryPageOptions()));
377
- const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
378
- const knownTurnId = url.searchParams.get('knownTurnId') || '';
379
- const knownTurnStatus = url.searchParams.get('knownTurnStatus') || '';
380
- const knownHistoryView = url.searchParams.get('knownHistoryView') || '';
381
- const latest = timeline.turns.at(-1);
382
- const knownIndex = knownTurnId ? timeline.turns.findIndex(turn => turn.id === knownTurnId) : -1;
383
- const sameHistoryView = knownHistoryView === timeline.historyView;
384
- const unchanged = sameHistoryView
385
- && knownIndex === timeline.turns.length - 1
386
- && timeline.thread.status === 'idle' && latest?.status !== 'inProgress'
387
- && knownTurnStatus && knownTurnStatus !== 'inProgress'
388
- && (!Number.isFinite(knownUpdatedAt) || knownUpdatedAt <= 0 || timeline.thread.updatedAt === knownUpdatedAt);
389
- const partial = !unchanged && sameHistoryView && knownIndex >= 0
390
- ? { ...timeline, turns: timeline.turns.slice(knownIndex), partialFromTurnId: knownTurnId }
391
- : timeline;
392
- sendJson(response, 200, unchanged ? { ...timeline, turns: [], notModified: true } : partial);
381
+ if (request.socketRequest) {
382
+ response.on('progress', () => sharedServer.keepSnapshotAlive(threadId, timeline.sync));
383
+ response.once('close', () => sharedServer.keepSnapshotAlive(threadId, timeline.sync));
384
+ }
385
+ sendJson(response, 200, timeline);
393
386
  return;
394
387
  }
395
388
 
@@ -407,17 +400,18 @@ async function handleRequest(request, response) {
407
400
  const after = Number(url.searchParams.get('afterSeq'));
408
401
  const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
409
402
  ? { id: url.searchParams.get('syncId'), epoch: url.searchParams.get('syncEpoch'), after } : undefined;
410
- const knownUpdatedAt = Number(url.searchParams.get('knownUpdatedAt'));
411
- const known = url.searchParams.get('knownTurnId') ? {
412
- updatedAt: Number.isFinite(knownUpdatedAt) ? knownUpdatedAt : 0,
413
- turnId: url.searchParams.get('knownTurnId'),
414
- turnStatus: url.searchParams.get('knownTurnStatus') || '',
415
- } : undefined;
416
403
  await streamSharedThread(response, threadId, sharedServer,
417
- normalizeSharedTimeline, requestedSync, known);
404
+ normalizeSharedTimeline, requestedSync);
418
405
  return;
419
406
  }
420
407
 
408
+ const modelChangesMatch = /^\/api\/threads\/([^/]+)\/model-changes$/.exec(url.pathname);
409
+ if (request.method === 'GET' && modelChangesMatch) {
410
+ const threadId = decodeURIComponent(modelChangesMatch[1]);
411
+ if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
412
+ sendJson(response, 200, await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
413
+ return;
414
+ }
421
415
  const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
422
416
  if (request.method === 'GET' && contextMatch) {
423
417
  const threadId = decodeURIComponent(contextMatch[1]);
@@ -449,17 +443,6 @@ async function handleRequest(request, response) {
449
443
  }
450
444
  }
451
445
 
452
- const sharedOperationGroup = /^\/api\/threads\/([^/]+)\/turns\/([^/]+)\/groups\/([^/]+)\/items$/.exec(url.pathname);
453
- if (request.method === 'GET' && sharedOperationGroup) {
454
- const threadId = decodeURIComponent(sharedOperationGroup[1]);
455
- const turnId = decodeURIComponent(sharedOperationGroup[2]);
456
- const groupId = decodeURIComponent(sharedOperationGroup[3]);
457
- const page = await sharedServer.operationGroupItems(threadId, turnId, groupId, url.searchParams.get('offset'));
458
- sendJson(response, 200, { turnId: page.turnId, groupId: page.groupId,
459
- entries: page.items.map(normalizeItem).filter(Boolean), nextOffset: page.nextOffset, total: page.total });
460
- return;
461
- }
462
-
463
446
  const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
464
447
  if (request.method === 'POST' && archiveMatch) {
465
448
  const threadId = decodeURIComponent(archiveMatch[1]);
@@ -477,8 +460,9 @@ async function handleRequest(request, response) {
477
460
  if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
478
461
  if (request.method === 'POST') {
479
462
  await requireExecutionWritable(threadId);
480
- const args = appServerTurnArguments(threadId, await readJsonBody(request));
481
- sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID())); return;
463
+ const body = await readJsonBody(request);
464
+ const args = appServerTurnArguments(threadId, body);
465
+ sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID(), body.containsVoiceInput)); return;
482
466
  }
483
467
  }
484
468
 
@@ -510,8 +494,8 @@ async function handleRequest(request, response) {
510
494
  await requireExecutionWritable(threadId);
511
495
  const body = await readJsonBody(request);
512
496
  if (body.hostId && body.hostId !== 'local') throw new RequestError(410, '此对话的连接方式已停用。', { code: 'unsupported_control' });
513
- const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
514
- sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
497
+ const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
498
+ sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
515
499
  return;
516
500
  }
517
501
 
@@ -543,7 +527,7 @@ void managedAppServer.start();
543
527
  executionHealth.start();
544
528
  runtimeDiscovery.start();
545
529
  server.on('error', error => {
546
- console.error('桌面服务监听失败:', error.message);
530
+ console.error('桌面服务监听失败:', error.message);
547
531
  process.exit(1);
548
532
  });
549
533
  server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
@@ -551,12 +535,12 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
551
535
  if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
552
536
  control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
553
537
  secret: process.env.CODEX_REMOTE_CONTROL_SECRET, shutdown: shutDown, actions: {
554
- 'refresh-execution': refreshExecution,
555
- 'relay-start': () => relayManager.run('relay-start'),
556
- 'relay-status': () => relayManager.run('relay-status'),
557
- 'relay-stop': () => relayManager.run('relay-stop'),
558
- 'relay-pair': () => relayManager.run('relay-pair'),
559
- 'relay-revoke': () => relayManager.run('relay-revoke'),
538
+ 'refresh-execution': refreshExecution,
539
+ 'relay-start': () => relayManager.run('relay-start'),
540
+ 'relay-status': () => relayManager.run('relay-status'),
541
+ 'relay-stop': () => relayManager.run('relay-stop'),
542
+ 'relay-pair': () => relayManager.run('relay-pair'),
543
+ 'relay-revoke': () => relayManager.run('relay-revoke'),
560
544
  'restart-managed-app-server': () => {
561
545
  const state = managedAppServer.snapshot();
562
546
  return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
@@ -566,10 +550,10 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
566
550
  pid: process.pid, controlPort: control.port });
567
551
  }
568
552
  } catch (error) {
569
- console.error('桌面服务管理通道启动失败:', error.message);
553
+ console.error('桌面服务管理通道启动失败:', error.message);
570
554
  process.exit(1);
571
555
  }
572
- console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
556
+ console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
573
557
  if (options.lan) {
574
558
  console.log('LAN 模式使用鉴权 WebSocket,仅适合受信任局域网测试,不要暴露到公网。');
575
559
  }
@@ -582,8 +566,8 @@ async function shutDown() {
582
566
  lanSocket.close();
583
567
  server.close();
584
568
  executionHealth?.stop();
585
- runtimeDiscovery.stop();
586
- await relayManager.close();
569
+ runtimeDiscovery.stop();
570
+ await relayManager.close();
587
571
  managedAppServer.close();
588
572
  await control?.close();
589
573
  process.exit(0);
@@ -749,12 +733,12 @@ function messagePayload(body) {
749
733
  return payload;
750
734
  }
751
735
 
752
- function appServerTurnArguments(threadId, body) {
736
+ function appServerTurnArguments(threadId, body) {
753
737
  const payload = messagePayload(body);
754
738
  const input = [];
755
739
  if (payload.prompt) input.push({ type: 'text', text: payload.prompt });
756
740
  for (const image of payload.images) input.push(image.startsWith('data:') ? { type: 'image', url: image } : { type: 'localImage', path: image });
757
- const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId) };
741
+ const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId, body.containsVoiceInput) };
758
742
  if (typeof body.clientUserMessageId === 'string' && /^[a-f0-9]{32}$/.test(body.clientUserMessageId)) value.clientUserMessageId = body.clientUserMessageId;
759
743
  if (payload.model) value.model = payload.model;
760
744
  if (payload.thinking) value.effort = payload.thinking;
@@ -775,7 +759,7 @@ function normalizeTimeline(data) {
775
759
  function normalizeSharedTimeline({ thread, nextCursor, sync }) {
776
760
  const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
777
761
  return { ...timeline,
778
- historyView: 'grouped-lazy-skeleton-v13',
762
+ historyView: 'item-pages-v1',
779
763
  ...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
780
764
  }
781
765
 
@@ -799,7 +783,7 @@ function normalizeThread(thread) {
799
783
  function normalizeTurn(turn) {
800
784
  const source = turn && typeof turn === 'object' ? turn : {};
801
785
  const turnId = source.id || '';
802
- const entries = collapseSubAgentEntries(Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : []);
786
+ const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
803
787
  for (const entry of entries) {
804
788
  const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
805
789
  for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
@@ -811,6 +795,7 @@ function normalizeTurn(turn) {
811
795
  completedAt: source.completedAt || null,
812
796
  durationMs: source.durationMs ?? null,
813
797
  error: source.error ?? null,
798
+ ...(source.itemPage ? { itemPage: source.itemPage } : {}),
814
799
  entries,
815
800
  };
816
801
  }
@@ -818,17 +803,6 @@ function normalizeTurn(turn) {
818
803
  function normalizeItem(item) {
819
804
  if (!item || typeof item !== 'object') return null;
820
805
 
821
- if (item.type === 'lazyOperationGroup') {
822
- return {
823
- type: 'activity',
824
- id: item.id || '',
825
- kind: 'lazyOperationGroup',
826
- turnId: item.turnId || '',
827
- groupId: item.groupId || '',
828
- operationTypes: Array.isArray(item.operationTypes) ? item.operationTypes : [],
829
- };
830
- }
831
-
832
806
  if (item.type === 'contextCompaction') {
833
807
  return {
834
808
  type: 'activity',
@@ -965,28 +939,28 @@ function normalizeItem(item) {
965
939
  };
966
940
  }
967
941
 
968
- if (item.type === 'imageView') {
969
- const image = typeof item.path === 'string' && item.path
970
- ? normalizeAttachment({ type: 'localImage', path: item.path })
971
- : null;
972
- if (image?.available) {
973
- return {
974
- type: 'message',
975
- id: item.id || '',
976
- role: 'assistant',
977
- phase: 'commentary',
978
- text: '',
979
- attachments: [image],
980
- };
981
- }
982
- return {
942
+ if (item.type === 'imageView') {
943
+ const image = typeof item.path === 'string' && item.path
944
+ ? normalizeAttachment({ type: 'localImage', path: item.path })
945
+ : null;
946
+ if (image?.available) {
947
+ return {
948
+ type: 'message',
949
+ id: item.id || '',
950
+ role: 'assistant',
951
+ phase: 'commentary',
952
+ text: '',
953
+ attachments: [image],
954
+ };
955
+ }
956
+ return {
983
957
  type: 'activity',
984
958
  id: item.id || '',
985
959
  kind: 'imageView',
986
960
  title: '查看图片',
987
961
  titleOnly: true,
988
962
  path: item.path || '',
989
- image,
963
+ image,
990
964
  };
991
965
  }
992
966
 
@@ -1048,83 +1022,6 @@ function normalizeItem(item) {
1048
1022
  };
1049
1023
  }
1050
1024
 
1051
- function collapseSubAgentEntries(entries) {
1052
- const agents = new Map();
1053
- let firstIndex = -1;
1054
- const retained = [];
1055
- const ensureAgent = threadId => {
1056
- if (!threadId) return null;
1057
- let agent = agents.get(threadId);
1058
- if (!agent) {
1059
- agent = { threadId, name: '', status: 'pendingInit', objective: '', model: '', reasoningEffort: '' };
1060
- agents.set(threadId, agent);
1061
- }
1062
- return agent;
1063
- };
1064
- const setProtocolStatus = (agent, status) => {
1065
- if (!agent || !status || status === 'unknown') return;
1066
- const terminal = ['interrupted', 'completed', 'errored', 'shutdown', 'notFound'];
1067
- if (terminal.includes(agent.status) && ['pendingInit', 'running'].includes(status)) return;
1068
- if (status === 'shutdown' && agent.status === 'completed') return;
1069
- agent.status = status;
1070
- };
1071
- for (const entry of entries) {
1072
- if (entry.kind === 'subAgent') {
1073
- if (firstIndex < 0) firstIndex = retained.length;
1074
- const agent = ensureAgent(entry.agentThreadId);
1075
- if (!agent) continue;
1076
- agent.name = subAgentDisplayName(entry.agentPath) || agent.name;
1077
- const activityStatus = {
1078
- started: 'running',
1079
- interacted: 'running',
1080
- interrupted: 'interrupted',
1081
- completed: 'completed',
1082
- }[entry.status];
1083
- agent.activity = entry.status;
1084
- setProtocolStatus(agent, activityStatus);
1085
- continue;
1086
- }
1087
- if (entry.kind === 'collaboration') {
1088
- const receiverIds = entry.receiverThreadIds || [];
1089
- const createsAgent = entry.tool === 'spawnAgent';
1090
- const updatesKnownAgent = receiverIds.some(threadId => agents.has(threadId));
1091
- if (!createsAgent && !updatesKnownAgent) {
1092
- retained.push(entry);
1093
- continue;
1094
- }
1095
- if (firstIndex < 0) firstIndex = retained.length;
1096
- for (const threadId of receiverIds) {
1097
- const agent = ensureAgent(threadId);
1098
- if (!agent) continue;
1099
- if (entry.prompt) agent.objective = entry.prompt;
1100
- if (entry.model) agent.model = entry.model;
1101
- if (entry.reasoningEffort) agent.reasoningEffort = entry.reasoningEffort;
1102
- setProtocolStatus(agent, statusText(entry.agentsStates?.[threadId]?.status));
1103
- if (entry.status === 'failed' && !entry.agentsStates?.[threadId]?.status) agent.status = 'errored';
1104
- if (entry.status === 'interrupted' && !entry.agentsStates?.[threadId]?.status) agent.status = 'interrupted';
1105
- }
1106
- continue;
1107
- }
1108
- retained.push(entry);
1109
- }
1110
- if (!agents.size) return retained;
1111
- retained.splice(firstIndex < 0 ? retained.length : firstIndex, 0, {
1112
- type: 'activity',
1113
- id: `subagents:${[...agents.keys()].join(':')}`,
1114
- kind: 'subAgentGroup',
1115
- title: '子 Agent',
1116
- agents: [...agents.values()],
1117
- });
1118
- return retained;
1119
- }
1120
-
1121
- function subAgentDisplayName(agentPath) {
1122
- const segment = String(agentPath || '').split('/').map(value => value.trim()).filter(value => value && value !== 'root').at(-1);
1123
- if (!segment) return '';
1124
- const text = segment.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
1125
- return text ? text[0].toUpperCase() + text.slice(1) : '';
1126
- }
1127
-
1128
1025
  function sharedHistoryPageOptions() {
1129
1026
  return {
1130
1027
  maxTurns: SHARED_HISTORY_PAGE_TURNS,
@@ -1367,7 +1264,7 @@ function imageMimeType(filePath) {
1367
1264
  function extractUserText(content) {
1368
1265
  if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
1369
1266
  let text = content
1370
- .filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
1267
+ .filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string' && !appServerEvents.isMobileInputNotice(item))
1371
1268
  .map(item => item.text)
1372
1269
  // Codex injects project instructions as user-role context, not chat input.
1373
1270
  .filter(value => !/^\s*# AGENTS\.md instructions(?: for [^\r\n]+)?\r?\n\s*<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>\s*$/.test(value))