@walkhi/code-relax 0.1.0-beta.7 → 0.1.0-beta.9

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 (45) hide show
  1. package/README.md +12 -13
  2. package/dist/bin/codex-remote.mjs +43 -18
  3. package/dist/bin/self-relay-demo.mjs +1 -1
  4. package/dist/shared/app-server-events.cjs +5 -3
  5. package/dist/src/desktop-launcher.mjs +7 -10
  6. package/dist/src/managed-app-server.mjs +3 -0
  7. package/dist/src/message-images.mjs +0 -18
  8. package/dist/src/onboarding.mjs +3 -4
  9. package/dist/src/platform/README.md +1 -1
  10. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
  11. package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
  12. package/dist/src/platform/windows/resident-startup.ps1 +58 -18
  13. package/dist/src/platform/windows/service-host.mjs +4 -7
  14. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  15. package/dist/src/self-relay/admin-page.mjs +54 -0
  16. package/dist/src/self-relay/admin-state.mjs +25 -12
  17. package/dist/src/self-relay/demo.mjs +2 -3
  18. package/dist/src/self-relay/lifecycle.mjs +6 -6
  19. package/dist/src/self-relay/server.mjs +7 -11
  20. package/dist/src/server.mjs +163 -1034
  21. package/dist/src/service-doctor.mjs +10 -5
  22. package/dist/src/service-lifecycle.mjs +3 -8
  23. package/dist/src/shared-app-server.mjs +13 -11
  24. package/dist/src/shared-recovery.mjs +5 -5
  25. package/dist/src/shared-thread-stream.mjs +1 -1
  26. package/dist/web/capabilities.js +1 -8
  27. package/dist/web/chat-transport.js +6 -12
  28. package/dist/web/chat.css +13 -14
  29. package/dist/web/chat.js +23 -41
  30. package/dist/web/community.css +15 -10
  31. package/dist/web/community.html +4 -5
  32. package/dist/web/composer-controller.js +0 -4
  33. package/dist/web/index.html +7 -39
  34. package/dist/web/resources.json +1 -1
  35. package/dist/web/task-list-view.js +1 -8
  36. package/dist/web/timeline-reducer.js +0 -4
  37. package/package.json +16 -18
  38. package/tools/postinstall.mjs +4 -1
  39. package/dist/src/app-server-client.mjs +0 -468
  40. package/dist/src/app-server-tasks.mjs +0 -358
  41. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  42. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  43. package/dist/src/thread-catalog.mjs +0 -47
  44. package/dist/tools/find-desktop-pipe.mjs +0 -10
  45. package/dist/web/community-view.js +0 -20
@@ -7,6 +7,7 @@ import { managedAppServerUrl } from './managed-app-server.mjs';
7
7
 
8
8
  export async function doctor({ paths, host, service, skillTarget }) {
9
9
  const checks = [];
10
+ let installedVersion;
10
11
  const add = (id, status, message) => checks.push({ id, status, message });
11
12
  const check = async (id, task) => {
12
13
  try { await task(); } catch (error) { add(id, 'error', error.message); }
@@ -15,16 +16,17 @@ export async function doctor({ paths, host, service, skillTarget }) {
15
16
  const record = readRecord(path.join(paths.serviceRoot, 'installation.json'));
16
17
  if (!record || !fs.existsSync(path.join(paths.serviceRoot, 'dist/bin/codex-remote.mjs'))) throw Error('尚未完整部署服务,请重新安装 Code Relax。');
17
18
  if (!record.nodeExecutable || !fs.existsSync(record.nodeExecutable)) throw Error('记录的 Node 已失效,请用可用的 Node 重新安装 Code Relax。');
18
- add('installation', 'ok', `服务 ${record.version};Node:${record.nodeExecutable}`);
19
+ installedVersion = record.version;
20
+ add('installation', 'ok', `已安装版本:${record.version};Node:${record.nodeExecutable}`);
19
21
  });
20
22
  let sharedUrl;
21
23
  await check('shared-service', async () => {
22
24
  sharedUrl = await host.sharedServiceUrl();
23
- add('shared-service', 'ok', sharedUrl ? '完整控制连接可用。' : '当前使用基础控制。');
25
+ add('shared-service', sharedUrl ? 'ok' : 'warning', sharedUrl ? 'Codex 内核连接可用。' : 'Codex 内核未就绪。');
24
26
  });
25
27
  await check('desktop', async () => {
26
- await host.desktopEnvironment(readRecord(path.join(paths.stateRoot, 'bridge-startup-context.json')), sharedUrl);
27
- add('desktop', 'ok', 'Desktop 来源上下文和工具管道已定位;实际任务操作尚未验证。');
28
+ await host.bridgeEnvironment();
29
+ add('desktop', 'ok', 'Bridge 启动环境已就绪;实际任务操作尚未验证。');
28
30
  });
29
31
  await check('network', async () => {
30
32
  const address = await host.lanAddress();
@@ -34,7 +36,10 @@ export async function doctor({ paths, host, service, skillTarget }) {
34
36
  await check('service', async () => {
35
37
  const state = await service.status();
36
38
  add('service', state.running ? 'ok' : 'warning', state.running
37
- ? `本机服务正常:${state.mode},端口 ${state.port}` : '服务未就绪;安装后执行 relax start。');
39
+ ? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
40
+ if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'warning', state.version
41
+ ? `已安装版本 ${installedVersion},但当前运行版本为 ${state.version};请重新执行安装或重启工作站服务。`
42
+ : `已安装版本 ${installedVersion},但当前运行 Bridge 未报告版本,可能仍是旧进程;请重新执行安装或重启工作站服务。`);
38
43
  if (state.running) add('managed-app-server', state.managedAppServer?.state === 'ready' ? 'ok' : 'error',
39
44
  state.managedAppServer?.state === 'ready' ? '受管 app-server 已就绪。'
40
45
  : state.managedAppServer?.error || '受管 app-server 尚未就绪。');
@@ -23,7 +23,6 @@ export function lifecycle({ stateRoot, host }) {
23
23
  fs.mkdirSync(stateRoot, { recursive: true });
24
24
  const sessionPath = path.join(stateRoot, 'bridge-session.json');
25
25
  const configPath = path.join(stateRoot, 'bridge-config.json');
26
- const contextPath = path.join(stateRoot, 'bridge-startup-context.json');
27
26
  const scriptPath = fileURLToPath(new URL('./server.mjs', import.meta.url));
28
27
 
29
28
  async function alive(session) {
@@ -56,7 +55,7 @@ export function lifecycle({ stateRoot, host }) {
56
55
  const running = await alive(session);
57
56
  const result = running ? await health(session) : null;
58
57
  return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
59
- port: session?.port, mode: result?.transportMode, execution: result?.execution,
58
+ port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
60
59
  managedAppServer: result?.managedAppServer, desktop: result?.desktop,
61
60
  desktopRestartRecommended: result?.desktopRestartRecommended === true,
62
61
  resident: result?.resident === true };
@@ -87,9 +86,6 @@ export function lifecycle({ stateRoot, host }) {
87
86
  if (await alive(session)) {
88
87
  const result = await health(session);
89
88
  if (session.scriptPath === scriptPath && session.lan && result?.status === 'ok' && result.resident) {
90
- const context = readRecord(contextPath) || {};
91
- const sourceThreadId = process.env.CODEX_THREAD_ID || process.env.CODEX_SESSION_ID || context.sourceThreadId;
92
- writeRecord(contextPath, { sourceThreadId });
93
89
  await controlRequest(session, 'POST', 'refresh-execution');
94
90
  return pair();
95
91
  }
@@ -102,9 +98,8 @@ export function lifecycle({ stateRoot, host }) {
102
98
  throw new Error('bridge-config.json 中端口或 Token 无效;不会自动重置凭据。');
103
99
  }
104
100
  const address = await host.lanAddress() || '127.0.0.1';
105
- const { executable, environment } = await host.desktopEnvironment(readRecord(contextPath), '');
106
- writeRecord(configPath, config);
107
- writeRecord(contextPath, { sourceThreadId: environment.CODEX_THREAD_ID || environment.CODEX_SESSION_ID });
101
+ const { executable, environment } = await host.bridgeEnvironment();
102
+ writeRecord(configPath, config);
108
103
  const instanceId = randomUUID();
109
104
  const controlSecret = randomBytes(32).toString('base64url');
110
105
  const readyPath = path.join(stateRoot, `bridge-ready-${instanceId}.json`);
@@ -32,7 +32,7 @@ export class SharedAppServer extends EventEmitter {
32
32
  constructor(url) {
33
33
  super();
34
34
  const address = new URL(url);
35
- if (address.protocol !== 'ws:' || address.hostname !== '127.0.0.1') throw new Error('完整控制必须使用本机回环 WebSocket。');
35
+ if (address.protocol !== 'ws:' || address.hostname !== '127.0.0.1') throw new Error('Codex 内核必须使用本机回环 WebSocket。');
36
36
  this.url = url;
37
37
  this.epoch = randomUUID();
38
38
  this.sequence = 0;
@@ -80,7 +80,7 @@ export class SharedAppServer extends EventEmitter {
80
80
  this.ready = false;
81
81
  for (const pending of this.pending.values()) {
82
82
  clearTimeout(pending.timer);
83
- pending.reject(new Error('完整控制连接断开;发送结果可能未确认,请检查对话后再操作。'));
83
+ pending.reject(new Error('Codex 内核连接断开;发送结果可能未确认,请检查对话后再操作。'));
84
84
  }
85
85
  this.pending.clear();
86
86
  this.serverRequests.reset(randomUUID());
@@ -128,19 +128,21 @@ export class SharedAppServer extends EventEmitter {
128
128
  await this.connect();
129
129
  this.requireWritable(args.threadId);
130
130
  // Reading the live thread checks this service owns it without resume changing settings.
131
- const { thread } = await this.request('thread/read', { threadId: args.threadId, includeTurns: false });
132
- const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
133
- if (status === 'active') {
134
- const live = await this.request('thread/read', { threadId: args.threadId, includeTurns: true });
135
- const active = live.thread.turns?.findLast(turn => turn.status === 'inProgress');
136
- if (!active) throw new Error('活动轮次已变化,请刷新后再发送。');
131
+ const { thread } = await this.request('thread/read', { threadId: args.threadId, includeTurns: false });
132
+ const status = typeof thread.status === 'string' ? thread.status : thread.status?.type;
133
+ if (status === 'active') {
134
+ const page = await this.request('thread/turns/list', {
135
+ threadId: args.threadId, limit: 1, sortDirection: 'desc', itemsView: 'summary',
136
+ });
137
+ const active = page.data?.[0];
138
+ if (active?.status !== 'inProgress') throw new Error('活动轮次已变化,请刷新后再发送。');
137
139
  return this.request('turn/steer', { threadId: args.threadId, expectedTurnId: active.id, input: args.input, ...(args.clientUserMessageId ? { clientUserMessageId: args.clientUserMessageId } : {}) });
138
140
  }
139
141
  return this.request('turn/start', args);
140
142
  }
141
143
 
142
144
  respondApproval(threadId, body) {
143
- if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('完整控制未连接');
145
+ if (!this.ready || this.socket?.readyState !== WebSocket.OPEN) throw new Error('Codex 内核未就绪');
144
146
  this.requireWritable(threadId);
145
147
  return this.serverRequests.respond(threadId, body, message => new Promise((resolve, reject) => {
146
148
  this.socket.send(JSON.stringify(message), error => error ? reject(error) : resolve());
@@ -221,7 +223,7 @@ export class SharedAppServer extends EventEmitter {
221
223
  result = null;
222
224
  }
223
225
  if (result) {
224
- if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('完整控制未返回分页历史。');
226
+ if (!Array.isArray(result.initialTurnsPage?.data)) throw new Error('Codex 内核未返回分页历史。');
225
227
  const compact = await Promise.all([...result.initialTurnsPage.data].reverse().map(turn => this.compactTurn(threadId, turn)));
226
228
  const thread = { ...result.thread, turns: compact };
227
229
  record = { thread, nextCursor: result.initialTurnsPage.nextCursor ?? null, sequence: 0,
@@ -329,7 +331,7 @@ export class SharedAppServer extends EventEmitter {
329
331
  threadId, turnId: turn.id, ...(cursor ? { cursor } : {}),
330
332
  limit: 100, sortDirection: 'desc',
331
333
  });
332
- if (!Array.isArray(page.data)) throw new Error('完整控制未返回单轮条目索引。');
334
+ if (!Array.isArray(page.data)) throw new Error('Codex 内核未返回单轮条目索引。');
333
335
  batches.push(page.data.map(entry => events.compactItem(entry.item)));
334
336
  cursor = page.nextCursor ?? null;
335
337
  } while (cursor);
@@ -19,7 +19,7 @@ const recoverySteps = () => [
19
19
  ['starting-app-server', '确认 Codex 内核'], ['starting-desktop', '重新打开 Codex'],
20
20
  ['checking-execution', '确认手机控制就绪'],
21
21
  ].map(([id, label]) => ({ id, label, status: 'pending' }));
22
- export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。手机完整控制和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
22
+ export const recoveryWarning = 'Codex Desktop 需要重新打开,才能接入同一个 Codex 内核。手机连接和 Codex 内核会保持运行;旧 Desktop 实例中的任务可能中断,未发送内容请先保存。切换不会清空项目、对话历史、设置或设备授权。';
23
23
 
24
24
  export async function recoveryStatus(stateRoot, host = desktopHost) {
25
25
  const record = readRecord(recordPath(stateRoot));
@@ -44,7 +44,7 @@ export async function planRecovery(stateRoot, host, available = portAvailable) {
44
44
  const record = readRecord(path.join(stateRoot, 'managed-app-server.json')) || readRecord(path.join(stateRoot, 'shared-desktop.json'));
45
45
  const actual = record ? await host.process(record.pid) : null;
46
46
  if (actual && (!sameProcess(record, actual) || !actual.command?.includes('app-server') || !actual.command.includes('ws://127.0.0.1:45839'))) {
47
- throw new Error('完整控制进程身份与记录不符,未结束任何进程。');
47
+ throw new Error('Codex 内核进程身份与记录不符,未结束任何进程。');
48
48
  }
49
49
  if (!actual && !await available()) throw new Error('45839 被未识别进程占用,未关闭 Desktop 或结束任何进程。');
50
50
  return { desktop: info.desktop, action };
@@ -63,7 +63,7 @@ export async function requestRecovery(stateRoot, { confirmed, operationId = rand
63
63
  catch (error) { if (error.code === 'EEXIST') throw new Error('已有恢复操作,请先查看恢复状态。'); throw error; }
64
64
  fs.writeFileSync(lock, JSON.stringify({ operationId })); fs.closeSync(lock);
65
65
  const record = { operationId, status: 'scheduled', stage: 'prepared', startedAt: new Date().toISOString(),
66
- message: '完整控制请求已接受,电脑管理连接保持运行。', steps: recoverySteps(), ...planned };
66
+ message: 'Codex 连接请求已接受,电脑管理连接保持运行。', steps: recoverySteps(), ...planned };
67
67
  try {
68
68
  writeRecord(recordPath(stateRoot), record);
69
69
  const log = path.join(stateRoot, 'shared-recovery.log');
@@ -109,9 +109,9 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
109
109
  await waitFor(async () => {
110
110
  const health = await service.status();
111
111
  return health.running && health.mode === 'lan-shared' && health.execution?.ready === true;
112
- }, '完整控制未就绪;Bridge 和中继管理连接仍保留。');
112
+ }, 'Codex 内核未就绪;Bridge 和中继管理连接仍保留。');
113
113
  record = { ...record, status: 'completed', stage: 'ready', steps: record.steps.map(step => ({ ...step, status: 'completed' })),
114
- message: 'Codex Desktop 已接入 Codex 内核;手机完整控制保持就绪,原任务未自动重发。' };
114
+ message: 'Codex Desktop 已接入 Codex 内核;手机控制保持就绪,原任务未自动重发。' };
115
115
  } catch (error) { record = { ...record, status: 'failed', error: error.message,
116
116
  steps: record.steps.map(step => ({ ...step, status: step.status === 'running' ? 'failed' : step.status })),
117
117
  message: 'Codex Desktop 未完成接入,已停止后续步骤。' }; }
@@ -43,7 +43,7 @@ export async function streamSharedThread(response, threadId, client, normalize,
43
43
  if (!threadListNotificationMethods.has(method)) return;
44
44
  emit('thread-list-changed', { method, threadId: params.threadId || params.thread?.id || '' });
45
45
  };
46
- const onDisconnected = () => { emit('bridge-error', { error: '完整控制已断开,请重新连接。' }); response.end(); finish(); };
46
+ const onDisconnected = () => { emit('bridge-error', { error: 'Codex 内核连接已断开,请重新连接。' }); response.end(); finish(); };
47
47
  response.once('close', () => { closed = true; finish(); });
48
48
  client.on('turn', onTurn); client.on('settings', onSettings); client.on('disconnected', onDisconnected);
49
49
  client.on('queue-changed', onQueue);
@@ -4,14 +4,10 @@
4
4
  })(globalThis, function () {
5
5
  // Execution semantics belong to a connection/task, never to a client OS.
6
6
  const profiles = {
7
- 'lan-legacy': { input: 'markdown-path', history: 'recent', settings: 'next-message', updates: 'snapshot-poll',
8
- stop: 'none', queueOwner: 'bridge' },
9
7
  'lan-shared': { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
10
8
  stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, sharedRecovery: true },
11
9
  relay: { input: 'inline-image', history: 'paged', settings: 'immediate', updates: 'events',
12
10
  stop: 'primary', queueOwner: 'codex', archive: true, approvals: true, syncBeforeWrite: true, historyCache: true, remoteImages: true, remoteFiles: true, managedConnection: true },
13
- 'app-server': { input: 'inline-image', history: 'recent', settings: 'next-message', updates: 'events',
14
- stop: 'separate', queueOwner: 'bridge' },
15
11
  };
16
12
  const unknown = Object.freeze({ input: 'unknown', history: 'unknown', settings: 'unknown', updates: 'unknown',
17
13
  stop: 'none', queueOwner: 'none' });
@@ -25,13 +21,10 @@
25
21
  case 'queue': return p.queueOwner !== 'none';
26
22
  case 'nativeInput': return ['local-image', 'inline-image'].includes(p.input);
27
23
  case 'primaryStop': return p.stop === 'primary';
28
- case 'separateStop': return p.stop === 'separate';
29
24
  case 'immediateSettings': return p.settings === 'immediate';
30
25
  case 'pagedHistory': return p.history === 'paged';
31
- case 'recentHistory': return p.history === 'recent';
32
26
  case 'archive': return p.archive === true;
33
- // The independent adapter keeps its existing snapshot-based UI contract.
34
- case 'incrementalEvents': return p.updates === 'events' && mode !== 'app-server';
27
+ case 'incrementalEvents': return p.updates === 'events';
35
28
  default: return p[feature] === true;
36
29
  }
37
30
  }
@@ -8,12 +8,12 @@
8
8
  : root.codexLanConfig?.workstationScope || '';
9
9
 
10
10
  class LanTransport extends (selfRelay ? root.CodexSelfRelaySession : root.CodexLanSession) {
11
- constructor(token) { super(token); this.mode = root.codexLanConfig?.transportMode || 'lan-legacy'; }
11
+ constructor(token) { super(token); this.mode = root.codexLanConfig?.transportMode || 'unavailable'; }
12
12
  connectionState() { return this.ready ? 'connected' : 'disconnected'; }
13
13
  async api(path, options = {}) {
14
14
  const body = await this.request(path, options);
15
15
  if (path.startsWith('/api/health')) {
16
- this.mode = body.transportMode || 'lan-legacy';
16
+ this.mode = body.transportMode || 'unavailable';
17
17
  this.recoveryAvailable = body.sharedRecoveryAvailable === true;
18
18
  this.execution = body.execution;
19
19
  this.managedAppServer = body.managedAppServer;
@@ -98,9 +98,7 @@
98
98
  const adapter = relay || new LanTransport(token);
99
99
  const modeFor = thread => {
100
100
  if (relay) return 'relay';
101
- if (thread?.transport === 'app-server') return 'app-server';
102
- if (thread?.hostId && thread.hostId !== 'local') return 'lan-legacy';
103
- return adapter.mode;
101
+ return adapter.mode;
104
102
  };
105
103
  const mode = () => modeFor(currentThread());
106
104
  const serviceAdvisory = () => {
@@ -116,13 +114,9 @@
116
114
  };
117
115
  if (adapter.execution && (!adapter.execution.ready || adapter.execution.state === 'recovering')) return {
118
116
  kind: 'execution-unavailable',
119
- title: adapter.execution.state === 'recovering' ? 'Codex 内核正在恢复' : 'Codex 内核暂不可用',
117
+ title: adapter.execution.state === 'recovering' ? 'Codex 内核正在恢复' : 'Codex 内核未就绪',
120
118
  detail: '工作站连接正常;点击查看当前状况、影响和可执行操作。'
121
119
  };
122
- if (adapter.mode === 'lan-legacy') return {
123
- kind: 'basic', title: '当前为基础控制',
124
- detail: '部分实时控制不可用;点击查看影响和恢复完整控制。'
125
- };
126
120
  return null;
127
121
  };
128
122
  return {
@@ -134,7 +128,7 @@
134
128
  return adapter.subscribe('/api/shared-recovery/events');
135
129
  },
136
130
  recoveryStatus: () => {
137
- if (relay) throw new Error('OpenAI 官方中继不支持从手机启用完整控制');
131
+ if (relay) throw new Error('OpenAI 官方中继不支持管理工作站 Codex 内核');
138
132
  return adapter.api('/api/shared-recovery');
139
133
  },
140
134
  recoverShared: operationId => {
@@ -146,7 +140,7 @@
146
140
  connectionLabel: (compact = false) => {
147
141
  if (relay) return compact ? 'OpenAI中继' : 'OpenAI 中继';
148
142
  const network = selfRelay ? compact ? 'CodeRelax中继' : 'Code Relax 中继' : '局域网';
149
- return serviceAdvisory() ? network : `${network} · ${adapter.mode === 'lan-shared' ? '完整控制' : '基础控制'}`;
143
+ return network;
150
144
  },
151
145
  draftScope: () => relay ? adapter.draftScope() : stationScope || (selfRelay ? `${lanOrigin}/${selfRelay.room}` : lanOrigin),
152
146
  capabilities: () => capabilities.describe(mode()),
package/dist/web/chat.css CHANGED
@@ -134,7 +134,8 @@
134
134
  .message-body .code-block-copy:hover { color: #e8eee9; background: #ffffff0d; }
135
135
  .message-body .code-block-copy[data-copy-state="copied"] { color: #e8eee9; }
136
136
  .message-body .code-block-copy-icon { display: block; width: 16px; height: 16px; }
137
- .message-body .code-block pre { margin: 0; padding: 0 16px 12px; border: 0; border-radius: 0; background: transparent; }
137
+ .message-body .code-block pre { margin: 0; padding: 0 16px 12px; border: 0; border-radius: 0; background: transparent; scrollbar-width: none; }
138
+ .message-body .code-block pre::-webkit-scrollbar { display: none; }
138
139
  .message-body .code-block code.hljs { color: #d9e3dc; }
139
140
  .message-body .hljs-comment, .message-body .hljs-quote { color: #789083; font-style: italic; }
140
141
  .message-body .hljs-keyword, .message-body .hljs-selector-tag, .message-body .hljs-literal,
@@ -153,7 +154,8 @@
153
154
  .message-body .hljs-strong { font-weight: 700; }
154
155
  .message-body a { color: #73d99a; text-decoration: none; }
155
156
  .message-body a:hover { text-decoration: underline; }
156
- .message-body table { display: block; max-width: 100%; overflow: auto; border-collapse: collapse; }
157
+ .message-body table { display: block; max-width: 100%; overflow: auto; scrollbar-width: none; border-collapse: collapse; }
158
+ .message-body table::-webkit-scrollbar { display: none; }
157
159
  .message-body th, .message-body td { border: 1px solid #35433a; padding: 6px 8px; }
158
160
  .message-body hr { border: 0; border-top: 1px solid #35433a; }
159
161
  .user-attachments { display: flex; align-self: flex-end; max-width: 88%; flex-wrap: wrap; justify-content: flex-end; gap: 7px; }
@@ -188,7 +190,8 @@
188
190
  .command-shell-command code { min-width: 0; padding: 3px 0; overflow-wrap: anywhere; white-space: pre-wrap; color: inherit; background: transparent; }
189
191
  .command-shell-prompt { padding-top: 3px; color: #76d395; user-select: none; }
190
192
  .command-shell-output { position: relative; overflow: hidden; padding: 9px 36px 9px 10px; }
191
- .command-shell-output pre { min-width: 0; min-height: 16px; max-height: 182px; padding: 0; overflow-x: hidden; overflow-y: auto; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; color: #c2ccc5; font-size: 10.5px; }
193
+ .command-shell-output pre { min-width: 0; min-height: 16px; max-height: 182px; padding: 0; overflow-x: hidden; overflow-y: auto; scrollbar-width: none; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; color: #c2ccc5; font-size: 10.5px; }
194
+ .command-shell-output pre::-webkit-scrollbar { display: none; }
192
195
  .command-shell-output pre.empty { color: #758279; font-style: italic; white-space: pre-wrap; }
193
196
  .command-shell-output .activity-copy { position: absolute; top: 5px; right: 5px; }
194
197
  .command-shell-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 28px; padding: 5px 9px 5px 10px; border-top: 1px solid #2a352e; color: #7f8d84; font-size: 10.5px; }
@@ -201,7 +204,8 @@
201
204
  .tool-detail-fields dt { color: #7f8d84; }
202
205
  .tool-detail-fields dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
203
206
  .tool-detail-fields dd > .tool-detail-fields { margin-top: 4px; }
204
- .tool-detail .tool-detail-text { max-height: 200px; margin: 0; padding: 7px 8px; overflow: auto; white-space: pre-wrap; word-break: break-word; border-radius: 5px; background: #0b100d; color: #c2ccc5; font: 10.5px/1.5 "Cascadia Mono", Consolas, monospace; }
207
+ .tool-detail .tool-detail-text { max-height: 200px; margin: 0; padding: 7px 8px; overflow: auto; scrollbar-width: none; white-space: pre-wrap; word-break: break-word; border-radius: 5px; background: #0b100d; color: #c2ccc5; font: 10.5px/1.5 "Cascadia Mono", Consolas, monospace; }
208
+ .tool-detail .tool-detail-text::-webkit-scrollbar { display: none; }
205
209
  .tool-detail-list { display: flex; flex-direction: column; gap: 4px; margin: 0; padding-left: 18px; }
206
210
  .tool-detail-section.error { color: #ec8b83; }
207
211
  .tool-detail-footer { padding-top: 3px; border-top: 1px solid #2a352e; color: #7f8d84; text-align: right; font-size: 10px; }
@@ -270,7 +274,8 @@
270
274
  .clear-pending-settings { display: grid; place-items: center; width: 18px; height: 18px; flex: none; border: 0; border-radius: 5px; padding: 0; color: #829087; background: transparent; font-size: 16px; line-height: 1; }
271
275
  .clear-pending-settings:hover { color: #e8eee9; background: #303632; }
272
276
  textarea { width: 100%; min-height: 62px; resize: none; border: 0; outline: 0; padding: 3px 1px; color: #eef4ef; background: transparent; font-size: 13px; }
273
- #prompt { width:calc(100% - 40px); height:48px; min-height:48px; overflow-y:auto; font-size:12px; line-height:1.5; }
277
+ #prompt { width:calc(100% - 40px); height:48px; min-height:48px; overflow-y:auto; scrollbar-width:none; font-size:12px; line-height:1.5; }
278
+ #prompt::-webkit-scrollbar { display:none; }
274
279
  .voice-stop-popover { position:absolute; top:-48px; left:12px; z-index:20; padding:4px; border:1px solid #465149; border-radius:12px; background:#252d28; box-shadow:0 4px 16px #0003; }
275
280
  .voice-stop-popover[hidden] { display:none; }
276
281
  #stopVoiceToEdit { min-width:112px; min-height:36px; padding:6px 16px; border:0; border-radius:8px; background:transparent; color:#dce3de; font-size:13px; }
@@ -398,7 +403,6 @@
398
403
  .effort-panel.ultra .effort-option:not(:disabled)::before, .effort-panel.fast .effort-option.reached::before { opacity:0; }
399
404
  .setting-select { display: none; }
400
405
  .composer-icon-button { flex-shrink: 0; }
401
- #turnActions[hidden] { display: none; }
402
406
  .send-control { position:relative; display:grid; place-items:center; width:26px; height:26px; flex:none; }
403
407
  .queue-send-icon { position:absolute; right:0; bottom:calc(100% + 16px); color:#243029; background:#d4ded7; }
404
408
  .queue-send-icon[hidden] { display:none; }
@@ -481,7 +485,7 @@
481
485
  .embedded-terminal .status { margin-left:0; overflow:hidden; }
482
486
  .embedded-terminal #status { overflow:hidden; text-overflow:ellipsis; }
483
487
  .embedded-terminal .project-actions { opacity:1; pointer-events:auto; }
484
- #connectionSettings, #usageButton, #announcementButton, #communityButton, #returnToComputer, #installedAppVersion, #debugMenu, .app-actions { display:none; }
488
+ #connectionSettings, #usageButton, #announcementButton, #returnToComputer, #installedAppVersion, #debugMenu, .app-actions { display:none; }
485
489
  .embedded-terminal .app-actions { position:relative; display:flex; align-items:stretch; gap:4px; flex-shrink:0; min-width:0; }
486
490
  .embedded-terminal .app-actions > :is(#returnToComputer:not([hidden]), #debugMenu:not([hidden])) { flex:0 0 auto; min-width:0; width:auto; }
487
491
  .embedded-terminal #installedAppVersion:not([hidden]) { display:block; flex:1 1 auto; align-self:center; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@@ -497,7 +501,7 @@
497
501
  #sharedRecoveryConfirm button { width:100%; margin-top:6px; text-align:left; }
498
502
  .embedded-terminal aside { display:flex; flex-direction:column; overflow:hidden; padding:2px 8px 6px; font-size:12px; }
499
503
  .embedded-terminal #tasks { flex:1; min-height:0; overflow-y:auto; overflow-anchor:none; }
500
- .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton:not([hidden]), #communityButton, #archivedThreads:not([hidden]), #returnToComputer:not([hidden])) { display:flex; align-items:center; gap:9px; flex-shrink:0; width:100%; padding:5px 8px; border:0; border-radius:0; background:transparent; color:#9eaca3; text-align:left; font-size:12px; }
504
+ .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton, #archivedThreads:not([hidden]), #returnToComputer:not([hidden])) { display:flex; align-items:center; gap:9px; flex-shrink:0; width:100%; padding:5px 8px; border:0; border-radius:0; background:transparent; color:#9eaca3; text-align:left; font-size:12px; }
501
505
  .embedded-terminal #archivedThreads { margin-top:3px; padding-top:5px; border-top:1px solid #ffffff12; }
502
506
  .embedded-terminal #usageButton { margin-top:0; }
503
507
  .embedded-terminal #archivedThreads[hidden] + #usageButton { margin-top:3px; padding-top:5px; border-top:1px solid #ffffff12; }
@@ -511,11 +515,8 @@
511
515
  .connection-control-mode { font-weight:600; }
512
516
  .connection-control-mode.complete { color:#42d17b; }
513
517
  .connection-control-mode.basic { color:#dc9b57; }
514
- .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton, #communityButton, #archivedThreads, #returnToComputer):hover { color:#e8eee9; }
515
518
  #usageButton > span { flex:1; }
516
519
  #announcementTitle { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
517
- #communityButton > span { min-width:0; flex:1; }
518
- .embedded-terminal #communityButton[aria-current="page"] { color:#e8eee9; background:#ffffff0a; }
519
520
  #usageSummary { display:flex; align-items:center; justify-content:space-between; gap:8px; min-width:0; }
520
521
  #returnToComputer { margin-top:0; }
521
522
  .usage-heading { display:flex; align-items:center; justify-content:space-between; }
@@ -558,14 +559,12 @@
558
559
  .usage-dialog-close { width:32px; height:32px; padding:0; border:0; background:transparent; font-size:20px; }
559
560
  #usageRows { display:grid; gap:9px; min-height:18px; color:#aeb9b2; font-size:12px; }
560
561
  @media (prefers-color-scheme:light) {
561
- .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton, #communityButton, #archivedThreads, #returnToComputer) { color:#596c60; }
562
+ .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton, #archivedThreads, #returnToComputer) { color:#596c60; }
562
563
  .embedded-terminal #archivedThreads { border-top-color:#17212b18; }
563
564
  .embedded-terminal #archivedThreads[hidden] + #usageButton { border-top-color:#17212b18; }
564
565
  .embedded-terminal .shared-service-notice:not([hidden]) { color:#8b5318; background:#d8994914; border-color:#b66b2c45; }
565
566
  .embedded-terminal .shared-service-notice .shared-service-copy > span,
566
567
  .embedded-terminal .shared-service-notice > .codex-icon { color:#76512c; }
567
- .embedded-terminal :is(#connectionSettings, #usageButton, #announcementButton, #communityButton, #archivedThreads, #returnToComputer):hover { color:#17212b; }
568
- .embedded-terminal #communityButton[aria-current="page"] { color:#17212b; background:#17212b0a; }
569
568
  .composer-add-menu { color:#34443a; background:#f5f7f5; border-color:#d5ddd8; box-shadow:0 10px 28px #17212b24; }
570
569
  .file-action-menu { color:#17212b; background:#fff; border-color:#cfd8d2; box-shadow:0 10px 28px #17212b24; }
571
570
  .file-action-name { color:#65736a; border-color:#dce3df; }
package/dist/web/chat.js CHANGED
@@ -329,7 +329,6 @@
329
329
  const approvalFlagOverrides = new Map();
330
330
  const archivedThreadsEntryVisible = false;
331
331
  const el = id => document.getElementById(id);
332
- let communityView;
333
332
  timelineScroll = window.createTimelineScrollController({
334
333
  document,
335
334
  state,
@@ -383,7 +382,7 @@
383
382
  });
384
383
  let enteredConversation = false;
385
384
  let entryWork, entryRetryTimer, entryAttempts = 0;
386
- const terminalConnectionCodes = new Set([1008, 4401, 4406, 4408, 4409, 4428, 4429]);
385
+ const terminalConnectionCodes = new Set([1008, 4401, 4406, 4408, 4409, 4428, 4429, 'full_control_required']);
387
386
  const isTerminalConnectionError = error => terminalConnectionCodes.has(error?.code);
388
387
  const stationConnection = window.createStationConnectionController({
389
388
  connect: () => transport.has('managedConnection') ? transport.resume() : api('/api/health'),
@@ -453,7 +452,7 @@
453
452
  const label = transport.connectionLabel(true);
454
453
  const mode = transport.transportMode?.();
455
454
  const separator = label.lastIndexOf(' · ');
456
- if (!['lan-shared', 'lan-legacy'].includes(mode) || separator < 0) {
455
+ if (mode !== 'lan-shared' || separator < 0) {
457
456
  target.textContent = label;
458
457
  return;
459
458
  }
@@ -548,14 +547,13 @@
548
547
  closeEventStream(); threadContext.stop(); closeModelMenu(); closePermissionMenu();
549
548
  conversation.clear({ resetSettings: true });
550
549
  el('prompt').value = ''; state.draftImages = []; renderDraftImages(); queue.render([]);
551
- el('selected').hidden = true; el('empty').hidden = false;
552
- setConversationTitle('选择聊天');
550
+ el('newChat').click();
553
551
  }
554
552
 
555
553
  async function changeThreadArchived(thread, archived) {
556
554
  if (archiveBusy.has(thread.id)) return;
557
555
  if (state.selectedId === thread.id && threadReadOnly()) { showActionToast('该对话正在另一个 Codex 实例中使用,当前只能查看'); return; }
558
- if (!archiveSupported(thread)) { showActionToast('当前连接不支持归档,请启用完整控制'); return; }
556
+ if (!archiveSupported(thread)) { showActionToast('当前连接不支持归档,请检查 Codex 内核状态'); return; }
559
557
  if (!archived && isThreadRunning(thread)) { showActionToast('请先停止当前任务'); return; }
560
558
  if (!archived && state.selectedId === thread.id && (el('prompt').value.trim() || state.draftImages.length)) {
561
559
  showActionToast('输入框有未发送内容,请先发送或清空'); return;
@@ -1018,7 +1016,6 @@
1018
1016
  async function selectThread(thread, initialMessage = null) {
1019
1017
  const syncStarted = performance.now();
1020
1018
  if (platform.embedded && platform.voice.active) return;
1021
- communityView?.close();
1022
1019
  const selectionPlan = conversation.planSelection(thread, isNewThread());
1023
1020
  const retained = selectionPlan.retainedTimeline;
1024
1021
  history.cancelSave();
@@ -1428,7 +1425,6 @@
1428
1425
  const timelineElement = el('timeline');
1429
1426
  state.timelineAtBottom = syncTimelinePosition();
1430
1427
  const reduced = CodexTimelineReducer.reduceTimelineUpdate(state.timeline, update, {
1431
- recentHistoryLimit: transport.has('recentHistory') ? 10 : 0,
1432
1428
  });
1433
1429
  if (reduced.duplicate) return;
1434
1430
  state.timeline = reduced.timeline;
@@ -2961,7 +2957,7 @@
2961
2957
  const button = el('fastMode');
2962
2958
  button.disabled = !supported || connectionBlocked() || threadReadOnly() || state.historyLoading || state.timeline?.cached || applyingServerSettings;
2963
2959
  button.setAttribute('aria-checked', String(enabled));
2964
- button.title = supported ? '快速模式,开启会增加用量' : transport.has('immediateSettings') ? '当前模型暂不支持快速模式' : '基础控制暂不支持修改快速模式';
2960
+ button.title = supported ? '快速模式,开启会增加用量' : transport.has('immediateSettings') ? '当前模型暂不支持快速模式' : '当前连接不支持修改快速模式';
2965
2961
  button.setAttribute('aria-label', button.title);
2966
2962
  }
2967
2963
 
@@ -2988,7 +2984,6 @@
2988
2984
  el('refresh').onclick = refreshTaskList;
2989
2985
  el('archivedThreads').onclick = () => void showArchivedThreads();
2990
2986
  el('newChat').onclick = () => {
2991
- communityView?.close();
2992
2987
  if (state.showingArchived) { state.showingArchived = false; renderTasks(); }
2993
2988
  const projectId = state.selectedThread?.projectId
2994
2989
  || (isNewThread() ? state.newThreadProject?.projectId : null) || '__unassigned__';
@@ -3024,7 +3019,6 @@
3024
3019
  } catch (error) { setStatus(`停止失败:${error.message}`); }
3025
3020
  finally { stoppingThreads.delete(threadId); updateComposerMode(); }
3026
3021
  }
3027
- el('interruptTurn').onclick = interruptCurrentTurn;
3028
3022
  function renderDraftImages() {
3029
3023
  el('draftImages').replaceChildren();
3030
3024
  for (const image of state.draftImages) {
@@ -3318,10 +3312,6 @@
3318
3312
  if (!taskListLocked()) document.body.classList.remove('sidebar-open');
3319
3313
  return true;
3320
3314
  }
3321
- if (communityView?.isOpen()) {
3322
- communityView.close();
3323
- return true;
3324
- }
3325
3315
  if (platform.voice.active) return true;
3326
3316
  if (matchMedia('(max-width:599px)').matches) {
3327
3317
  showTaskList();
@@ -3375,27 +3365,16 @@
3375
3365
  document.addEventListener('visibilitychange', refreshVisibleList);
3376
3366
  refreshVisibleList();
3377
3367
  renderConnectionLabel();
3378
- communityView = window.createCommunityView({
3379
- button: el('communityButton'),
3380
- page: el('communityPage'),
3381
- onOpen: () => {
3382
- document.body.classList.remove('sidebar-open');
3383
- el('terminalChatTitle').textContent = 'Code Relax 社区';
3384
- },
3385
- onClose: () => {
3386
- el('terminalChatTitle').textContent = isNewThread() ? '新对话' : (el('selectedTitle').textContent || 'Code Relax');
3387
- },
3388
- });
3389
3368
  el('usageButton').onclick = () => usage.open();
3390
3369
  const updateAnnouncement = value => {
3391
3370
  const button = el('announcementButton');
3392
3371
  const title = value && typeof value === 'object' && typeof value.title === 'string' ? value.title.trim() : '';
3393
- el('announcementTitle').textContent = title;
3394
- button.hidden = !title;
3395
- button.setAttribute('aria-label', title ? `系统公告:${title}` : '查看最新公告');
3372
+ el('announcementTitle').textContent = title || '公告';
3373
+ button.setAttribute('aria-label', title ? `公告:${title}` : '查看公告');
3396
3374
  };
3397
3375
  platform.callbacks.announcementChanged = updateAnnouncement;
3398
3376
  el('announcementButton').onclick = () => {
3377
+ document.body.classList.remove('sidebar-open');
3399
3378
  if (platform.has('openAnnouncement')) platform.openAnnouncement();
3400
3379
  };
3401
3380
  el('usageDialogClose').onclick = () => el('usageDialog').close();
@@ -3468,7 +3447,7 @@
3468
3447
  const showRecoveryResult = value => { recoveryResult.hidden = false; recoveryResult.textContent = value; };
3469
3448
  recoveryButton.onclick = () => {
3470
3449
  if (globalThis.codexNative?.openComputerMode) { globalThis.codexNative.openComputerMode(); return; }
3471
- if (!transport.recoverySupported()) { showRecoveryResult('需要新版 Bridge,并使用局域网或 Code Relax 中继;也可进入手机工作站详情启用完整控制。'); return; }
3450
+ if (!transport.recoverySupported()) { showRecoveryResult('请进入手机工作站详情检查 Codex 内核状态。'); return; }
3472
3451
  if (!recoveryEvents) {
3473
3452
  recoveryEvents = transport.recoveryEvents();
3474
3453
  recoveryEvents.addEventListener('progress', event => {
@@ -3746,7 +3725,7 @@
3746
3725
  if (retryable) showConnectionProgress(1, '正在恢复连接', error.message);
3747
3726
  else {
3748
3727
  el('entryProgress').dataset.state = 'error';
3749
- el('entryStatus').textContent = '连接未完成';
3728
+ el('entryStatus').textContent = '连接失败';
3750
3729
  el('entryDetail').textContent = error.message;
3751
3730
  el('entryRetry').hidden = false;
3752
3731
  }
@@ -3769,7 +3748,12 @@
3769
3748
  if (!state.selectedThread) await restoreStartupView().catch(cacheError);
3770
3749
  const initialSelection = conversation.generation();
3771
3750
  showConnectionProgress(3, '正在连接工作站', '正在建立连接并确认工作站状态。');
3772
- await api('/api/health');
3751
+ const health = await api('/api/health');
3752
+ if (!window.codexTransport && (health.transportMode !== 'lan-shared' || health.execution?.ready !== true)) {
3753
+ const unavailable = new Error('Codex 内核未就绪。请在电脑上重启 Codex;若仍失败,运行 relax doctor。');
3754
+ unavailable.code = 'full_control_required';
3755
+ throw unavailable;
3756
+ }
3773
3757
  if (!entryCurrent()) return;
3774
3758
  showConnectionProgress(4, '正在载入工作站任务', '连接已建立,正在读取任务列表。');
3775
3759
  // Official relay identity and LAN execution mode are authoritative after health.
@@ -3796,13 +3780,13 @@
3796
3780
  const thread = state.threads.find(item => item.id === last) || state.selectedThread;
3797
3781
  if (thread) {
3798
3782
  rememberVisitedThread(thread);
3799
- if (state.selectedId === thread.id && state.timeline) { state.selectedThread = thread; await restoreRelayConversation(); }
3800
- else await selectThread(thread);
3801
- } else {
3802
- conversation.clear();
3803
- el('selected').hidden = true; el('empty').hidden = false; showTaskList();
3804
- }
3805
- }
3783
+ if (state.selectedId === thread.id && state.timeline) { state.selectedThread = thread; await restoreRelayConversation(); }
3784
+ else await selectThread(thread);
3785
+ }
3786
+ }
3787
+ if (!state.selectedThread && !isNewThread() && conversation.isCurrent(initialSelection)) {
3788
+ el('newChat').click();
3789
+ }
3806
3790
  } catch (error) {
3807
3791
  if (!entryCurrent()) return;
3808
3792
  setConversationConnection('disconnected');
@@ -3822,8 +3806,6 @@
3822
3806
  })();
3823
3807
  return entryWork;
3824
3808
  }
3825
- el('firstChooseTask').onclick = showTaskList;
3826
- el('firstNewChat').onclick = () => el('newChat').click();
3827
3809
  el('entryRetry').onclick = () => enterConversation();
3828
3810
  el('entryBack').hidden = !platform.embedded;
3829
3811
  el('entryBack').onclick = () => platform.openSettings();
@@ -27,13 +27,17 @@
27
27
  .community-question-hot > span i { border-color:#745b35; color:#d5a85f; background:#d5a85f0d; }
28
28
  .community-question strong { color:#dce5df; font-size:13px; font-weight:550; line-height:1.5; }
29
29
  .community-question-featured strong { max-width:500px; font-size:19px; }
30
- .community-agent-board { display:grid; width:100%; grid-template-columns:auto minmax(0, 1fr) auto; align-items:center; gap:13px; margin-top:13px; padding:14px 15px; border:1px dashed #564b70; border-radius:14px; background:linear-gradient(135deg, #211e29dd, #171a18dd); text-align:left; }
30
+ .community-agent-board { width:100%; margin-top:20px; padding:14px 15px; border:1px dashed #564b70; border-radius:14px; background:linear-gradient(135deg, #211e29dd, #171a18dd); text-align:left; }
31
+ .community-agent-board summary { display:grid; grid-template-columns:auto minmax(0, 1fr) auto; align-items:center; gap:13px; cursor:pointer; list-style:none; }
32
+ .community-agent-board summary::-webkit-details-marker { display:none; }
31
33
  .community-agent-board-mark { display:grid; place-items:center; width:38px; height:38px; border:1px solid #756592; border-radius:12px; color:#c5aaef; background:#ad7bf912; font-size:15px; font-weight:750; }
32
- .community-agent-board > div { display:flex; min-width:0; flex-direction:column; }
33
- .community-agent-board > div > span { color:#927daf; font-size:9px; font-weight:700; letter-spacing:.1em; }
34
+ .community-agent-board-copy { display:flex; min-width:0; flex-direction:column; }
35
+ .community-agent-board-copy > span { color:#927daf; font-size:9px; font-weight:700; letter-spacing:.1em; }
34
36
  .community-agent-board strong { margin-top:2px; color:#e1d8ef; font-size:13px; }
35
- .community-agent-board p { margin:3px 0 0; color:#948b9f; font-size:11px; line-height:1.45; }
36
- .community-agent-board > small { align-self:start; flex:none; padding:3px 7px; border:1px solid #665779; border-radius:999px; color:#aa95c3; font-size:9px; }
37
+ .community-agent-board-copy small { margin-top:2px; color:#a99eb4; font-size:11px; line-height:1.45; }
38
+ .community-agent-board p { margin:12px 0 0; padding-top:12px; border-top:1px solid #66577966; color:#a99eb4; font-size:11px; line-height:1.55; }
39
+ .community-agent-board-action { align-self:start; flex:none; padding:3px 7px; border:1px solid #665779; border-radius:999px; color:#c2aadf; font-size:9px; }
40
+ .community-agent-board[open] .community-agent-board-action { color:#aa95c3; }
37
41
  .community-status { display:flex; width:min(380px, 100%); align-items:center; gap:11px; margin-top:25px; padding:13px 15px; border:1px solid #344239; border-radius:13px; background:#171c19e6; box-shadow:0 12px 30px #0002; text-align:left; }
38
42
  .community-status-dot { width:9px; height:9px; flex:none; border-radius:50%; background:#76d395; box-shadow:0 0 0 5px #76d39516, 0 0 16px #76d39555; }
39
43
  .community-status > span:last-child { display:flex; min-width:0; flex:1; align-items:center; justify-content:space-between; gap:12px; }
@@ -46,8 +50,8 @@
46
50
  .community-questions { grid-template-columns:1fr; }
47
51
  .community-question-featured, .community-question-wide { grid-column:auto; }
48
52
  .community-question-wide { min-height:92px; flex-direction:column; align-items:stretch; }
49
- .community-agent-board { grid-template-columns:auto minmax(0, 1fr); }
50
- .community-agent-board > small { display:none; }
53
+ .community-agent-board summary { grid-template-columns:auto minmax(0, 1fr); }
54
+ .community-agent-board-action { grid-column:2; justify-self:start; }
51
55
  .community-status { margin-top:22px; }
52
56
  }
53
57
  @media (prefers-color-scheme:light) {
@@ -67,10 +71,11 @@
67
71
  .community-question strong { color:#26332b; }
68
72
  .community-agent-board { border-color:#cbbdde; background:linear-gradient(135deg, #f4effb, #fff); }
69
73
  .community-agent-board-mark { border-color:#c6b4dd; color:#765b99; background:#ad7bf912; }
70
- .community-agent-board > div > span { color:#80669f; }
74
+ .community-agent-board-copy > span { color:#80669f; }
71
75
  .community-agent-board strong { color:#3b2d4d; }
72
- .community-agent-board p { color:#756b80; }
73
- .community-agent-board > small { border-color:#c6b4d5; color:#765f90; }
76
+ .community-agent-board-copy small { color:#756b80; }
77
+ .community-agent-board p { border-color:#c6b4d566; color:#756b80; }
78
+ .community-agent-board-action { border-color:#c6b4d5; color:#765f90; }
74
79
  .community-status { border-color:#d5ddd8; background:#ffffffd9; }
75
80
  .community-status strong { color:#26332b; }
76
81
  .community-status small { color:#77857d; }