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

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,46 @@ 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
-
299
+ return;
300
+ }
301
+
298
302
  if (request.method === 'GET' && url.pathname === '/api/turn-events') {
303
+ const execution = sharedServer;
299
304
  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);
305
+ const listener = ({ method, params }) => {
306
+ if (!['turn/started', 'turn/completed'].includes(method)) return;
307
+ const threadId = params?.threadId, turnId = params?.turn?.id;
308
+ if (typeof threadId === 'string' && typeof turnId === 'string' && !response.destroyed) {
309
+ response.event('turn', { method, threadId, turnId });
310
+ }
311
+ };
312
+ const disconnected = () => response.end();
313
+ execution.on('notification', listener);
314
+ execution.on('disconnected', disconnected);
310
315
  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
- }
316
+ execution.off('notification', listener);
317
+ execution.off('disconnected', disconnected);
318
+ });
319
+ return;
320
+ }
321
+
322
+ const activeTurnMatch = /^\/api\/threads\/([^/]+)\/active-turn$/.exec(url.pathname);
323
+ if (request.method === 'GET' && activeTurnMatch) {
324
+ const threadId = decodeURIComponent(activeTurnMatch[1]);
325
+ await sharedServer.connect();
326
+ const { thread } = await sharedServer.request('thread/read', { threadId, includeTurns: false });
327
+ const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
328
+ let turnId = null;
329
+ if (status === 'active') {
330
+ const page = await sharedServer.request('thread/turns/list', {
331
+ threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
332
+ });
333
+ const latest = page.data?.[0];
334
+ if (latest?.status === 'inProgress') turnId = latest.id;
335
+ }
336
+ sendJson(response, 200, { turnId });
337
+ return;
338
+ }
334
339
 
335
340
  const createThreadMatch = /^\/api\/projects\/([^/]+)\/threads$/.exec(url.pathname);
336
341
  if (request.method === 'POST' && createThreadMatch) {
@@ -371,25 +376,15 @@ async function handleRequest(request, response) {
371
376
  }
372
377
 
373
378
  const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
374
- if (request.method === 'GET' && timelineMatch) {
375
- const threadId = decodeURIComponent(timelineMatch[1]);
376
- 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);
379
+ if (request.method === 'GET' && timelineMatch) {
380
+ const threadId = decodeURIComponent(timelineMatch[1]);
381
+ const execution = sharedServer;
382
+ const timeline = normalizeSharedTimeline(await execution.timeline(threadId, sharedHistoryPageOptions()));
383
+ if (request.socketRequest) {
384
+ response.on('progress', () => execution.keepSnapshotAlive(threadId, timeline.sync));
385
+ response.once('close', () => execution.keepSnapshotAlive(threadId, timeline.sync));
386
+ }
387
+ sendJson(response, 200, timeline);
393
388
  return;
394
389
  }
395
390
 
@@ -407,17 +402,18 @@ async function handleRequest(request, response) {
407
402
  const after = Number(url.searchParams.get('afterSeq'));
408
403
  const requestedSync = url.searchParams.get('syncId') && url.searchParams.get('syncEpoch') && Number.isSafeInteger(after) && after >= 0
409
404
  ? { 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
405
  await streamSharedThread(response, threadId, sharedServer,
417
- normalizeSharedTimeline, requestedSync, known);
406
+ normalizeSharedTimeline, requestedSync);
418
407
  return;
419
408
  }
420
409
 
410
+ const modelChangesMatch = /^\/api\/threads\/([^/]+)\/model-changes$/.exec(url.pathname);
411
+ if (request.method === 'GET' && modelChangesMatch) {
412
+ const threadId = decodeURIComponent(modelChangesMatch[1]);
413
+ if (!/^[0-9a-f]{8}-[0-9a-f-]{20,55}$/i.test(threadId)) throw new RequestError(400, '对话 ID 无效。');
414
+ sendJson(response, 200, await turnModelHistory.read(findSessionPath(threadId), (url.searchParams.get('turnIds') || '').split(',').filter(Boolean)));
415
+ return;
416
+ }
421
417
  const contextMatch = /^\/api\/threads\/([^/]+)\/context$/.exec(url.pathname);
422
418
  if (request.method === 'GET' && contextMatch) {
423
419
  const threadId = decodeURIComponent(contextMatch[1]);
@@ -449,17 +445,6 @@ async function handleRequest(request, response) {
449
445
  }
450
446
  }
451
447
 
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
448
  const archiveMatch = /^\/api\/threads\/([^/]+)\/(archive|unarchive)$/.exec(url.pathname);
464
449
  if (request.method === 'POST' && archiveMatch) {
465
450
  const threadId = decodeURIComponent(archiveMatch[1]);
@@ -477,8 +462,9 @@ async function handleRequest(request, response) {
477
462
  if (request.method === 'GET') { sendJson(response, 200, await appServerEvents.readQueue(rpc, threadId)); return; }
478
463
  if (request.method === 'POST') {
479
464
  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;
465
+ const body = await readJsonBody(request);
466
+ const args = appServerTurnArguments(threadId, body);
467
+ sendJson(response, 202, await appServerEvents.addQueue(rpc, threadId, args.input, crypto.randomUUID(), body.containsVoiceInput)); return;
482
468
  }
483
469
  }
484
470
 
@@ -510,8 +496,8 @@ async function handleRequest(request, response) {
510
496
  await requireExecutionWritable(threadId);
511
497
  const body = await readJsonBody(request);
512
498
  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 });
499
+ const submitted = await sharedServer.send(appServerTurnArguments(threadId, body));
500
+ sendJson(response, 200, { success: true, data: submitted.result, usage: submitted.usage });
515
501
  return;
516
502
  }
517
503
 
@@ -543,7 +529,7 @@ void managedAppServer.start();
543
529
  executionHealth.start();
544
530
  runtimeDiscovery.start();
545
531
  server.on('error', error => {
546
- console.error('桌面服务监听失败:', error.message);
532
+ console.error('桌面服务监听失败:', error.message);
547
533
  process.exit(1);
548
534
  });
549
535
  server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
@@ -551,12 +537,12 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
551
537
  if (process.env.CODEX_REMOTE_INSTANCE && process.env.CODEX_REMOTE_CONTROL_SECRET && process.env.CODEX_REMOTE_READY_PATH) {
552
538
  control = await openControl({ instanceId: process.env.CODEX_REMOTE_INSTANCE,
553
539
  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'),
540
+ 'refresh-execution': refreshExecution,
541
+ 'relay-start': () => relayManager.run('relay-start'),
542
+ 'relay-status': () => relayManager.run('relay-status'),
543
+ 'relay-stop': () => relayManager.run('relay-stop'),
544
+ 'relay-pair': () => relayManager.run('relay-pair'),
545
+ 'relay-revoke': () => relayManager.run('relay-revoke'),
560
546
  'restart-managed-app-server': () => {
561
547
  const state = managedAppServer.snapshot();
562
548
  return state.launcherRestartRequired ? managedAppServer.restart(crypto.randomUUID()) : state;
@@ -566,10 +552,10 @@ server.listen(options.port, options.lan ? '0.0.0.0' : '127.0.0.1', async () => {
566
552
  pid: process.pid, controlPort: control.port });
567
553
  }
568
554
  } catch (error) {
569
- console.error('桌面服务管理通道启动失败:', error.message);
555
+ console.error('桌面服务管理通道启动失败:', error.message);
570
556
  process.exit(1);
571
557
  }
572
- console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
558
+ console.log(`Code Relax 桌面服务已启动:http://127.0.0.1:${options.port}/`);
573
559
  if (options.lan) {
574
560
  console.log('LAN 模式使用鉴权 WebSocket,仅适合受信任局域网测试,不要暴露到公网。');
575
561
  }
@@ -582,8 +568,8 @@ async function shutDown() {
582
568
  lanSocket.close();
583
569
  server.close();
584
570
  executionHealth?.stop();
585
- runtimeDiscovery.stop();
586
- await relayManager.close();
571
+ runtimeDiscovery.stop();
572
+ await relayManager.close();
587
573
  managedAppServer.close();
588
574
  await control?.close();
589
575
  process.exit(0);
@@ -749,12 +735,12 @@ function messagePayload(body) {
749
735
  return payload;
750
736
  }
751
737
 
752
- function appServerTurnArguments(threadId, body) {
738
+ function appServerTurnArguments(threadId, body) {
753
739
  const payload = messagePayload(body);
754
740
  const input = [];
755
741
  if (payload.prompt) input.push({ type: 'text', text: payload.prompt });
756
742
  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) };
743
+ const value = { threadId, input, ...appServerEvents.mobileTurnSource(body.clientUserMessageId, body.containsVoiceInput) };
758
744
  if (typeof body.clientUserMessageId === 'string' && /^[a-f0-9]{32}$/.test(body.clientUserMessageId)) value.clientUserMessageId = body.clientUserMessageId;
759
745
  if (payload.model) value.model = payload.model;
760
746
  if (payload.thinking) value.effort = payload.thinking;
@@ -775,7 +761,7 @@ function normalizeTimeline(data) {
775
761
  function normalizeSharedTimeline({ thread, nextCursor, sync }) {
776
762
  const timeline = normalizeTimeline({ thread: { ...thread, title: thread.name || thread.title || '', hostId: 'local', transport: 'shared-app-server' } });
777
763
  return { ...timeline,
778
- historyView: 'grouped-lazy-skeleton-v13',
764
+ historyView: 'item-pages-v1',
779
765
  ...(nextCursor !== undefined ? { nextCursor } : {}), ...(sync ? { sync } : {}) };
780
766
  }
781
767
 
@@ -799,7 +785,7 @@ function normalizeThread(thread) {
799
785
  function normalizeTurn(turn) {
800
786
  const source = turn && typeof turn === 'object' ? turn : {};
801
787
  const turnId = source.id || '';
802
- const entries = collapseSubAgentEntries(Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : []);
788
+ const entries = Array.isArray(source.items) ? source.items.map(normalizeItem).filter(Boolean) : [];
803
789
  for (const entry of entries) {
804
790
  const images = [...(entry.attachments || []), entry.image, ...Object.values(entry.imageAssets || {})].filter(Boolean);
805
791
  for (const image of images) if (image.remotePath && !image.recoveryTurnId) image.recoveryTurnId = turnId;
@@ -811,6 +797,7 @@ function normalizeTurn(turn) {
811
797
  completedAt: source.completedAt || null,
812
798
  durationMs: source.durationMs ?? null,
813
799
  error: source.error ?? null,
800
+ ...(source.itemPage ? { itemPage: source.itemPage } : {}),
814
801
  entries,
815
802
  };
816
803
  }
@@ -818,17 +805,6 @@ function normalizeTurn(turn) {
818
805
  function normalizeItem(item) {
819
806
  if (!item || typeof item !== 'object') return null;
820
807
 
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
808
  if (item.type === 'contextCompaction') {
833
809
  return {
834
810
  type: 'activity',
@@ -965,28 +941,28 @@ function normalizeItem(item) {
965
941
  };
966
942
  }
967
943
 
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 {
944
+ if (item.type === 'imageView') {
945
+ const image = typeof item.path === 'string' && item.path
946
+ ? normalizeAttachment({ type: 'localImage', path: item.path })
947
+ : null;
948
+ if (image?.available) {
949
+ return {
950
+ type: 'message',
951
+ id: item.id || '',
952
+ role: 'assistant',
953
+ phase: 'commentary',
954
+ text: '',
955
+ attachments: [image],
956
+ };
957
+ }
958
+ return {
983
959
  type: 'activity',
984
960
  id: item.id || '',
985
961
  kind: 'imageView',
986
962
  title: '查看图片',
987
963
  titleOnly: true,
988
964
  path: item.path || '',
989
- image,
965
+ image,
990
966
  };
991
967
  }
992
968
 
@@ -1048,83 +1024,6 @@ function normalizeItem(item) {
1048
1024
  };
1049
1025
  }
1050
1026
 
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
1027
  function sharedHistoryPageOptions() {
1129
1028
  return {
1130
1029
  maxTurns: SHARED_HISTORY_PAGE_TURNS,
@@ -1367,7 +1266,7 @@ function imageMimeType(filePath) {
1367
1266
  function extractUserText(content) {
1368
1267
  if (!Array.isArray(content)) return { text: '(包含图片或界面批注)', files: [] };
1369
1268
  let text = content
1370
- .filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')
1269
+ .filter(item => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string' && !appServerEvents.isMobileInputNotice(item))
1371
1270
  .map(item => item.text)
1372
1271
  // Codex injects project instructions as user-role context, not chat input.
1373
1272
  .filter(value => !/^\s*# AGENTS\.md instructions(?: for [^\r\n]+)?\r?\n\s*<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>\s*$/.test(value))