ai_co_discuss 0.2.34 → 0.2.35

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.
@@ -263,7 +263,7 @@ async function claude_delivery(config, binding, task, spawn_process = spawn) {
263
263
  try {
264
264
  child = spawn_process(config.executable, [...args, '--', task.prompt], {
265
265
  cwd: launch_directory(config, task), windowsHide: true, shell: false, stdio: ['ignore', 'pipe', 'pipe'],
266
- env: standalone_claude_env(),
266
+ env: { ...standalone_claude_env(), DISCUSS_ACCESS_FILE: task.access_path },
267
267
  });
268
268
  } catch (error) { throw not_sent(error); }
269
269
  if (typeof child.kill === 'function') {
@@ -371,6 +371,7 @@ function cli_invocation(config, binding, task, user_home) {
371
371
  args.push('--mode', 'accept-edits', '--project', project.project_id);
372
372
  if (binding.session_id) args.push('--conversation', binding.session_id);
373
373
  if (config.model) args.push('--model', config.model);
374
+ if (config.effort) args.push('--effort', config.effort);
374
375
  args.push('-p', task.prompt);
375
376
  return { args, project };
376
377
  }
@@ -492,6 +493,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
492
493
  try {
493
494
  child = spawn_process(config.executable, invocation.args, {
494
495
  cwd: launch_directory(config, task), windowsHide: true, shell: false, stdio: ['pipe', 'pipe', 'pipe'],
496
+ env: { ...process.env, DISCUSS_ACCESS_FILE: task.access_path },
495
497
  });
496
498
  } catch (error) { throw not_sent(error); }
497
499
  const state = monitor_cli(child, config, binding, task);
@@ -9,7 +9,7 @@ import { pathToFileURL } from 'node:url';
9
9
  import { parseArgs } from 'node:util';
10
10
 
11
11
  import { atomic_json } from './discussion_adapters.mjs';
12
- import { software_models } from './discussion_models.mjs';
12
+ import { software_effort, software_models } from './discussion_models.mjs';
13
13
  import { API_PROVIDERS, api_check, api_models, save_api_key, saved_api_keys, validate_api_profile } from './discussion_api.mjs';
14
14
  import { SCHEDULER_PROVIDERS, scheduler_check } from './scheduler.mjs';
15
15
  import { acquire_lock, api, CODEX_ADAPTERS, DiscussionStore, service_drafts, task_files, validate_config } from './discussion_runner.mjs';
@@ -30,8 +30,9 @@ export function next_instance(platform, taken) {
30
30
 
31
31
  /** Supported local clients of an instance's platform. */
32
32
  function clients_of(name) { return CLIENTS[platform_of(name)]; }
33
- // Reasoning effort is a separate CLI argument only for Codex and Claude; agy encodes it in the model name.
34
- const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max'] };
33
+ // The UI discovers each model's supported subset; omitted values keep the CLI's own default.
34
+ const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max'],
35
+ antigravity_cli: ['low', 'medium', 'high', 'xhigh', 'max'] };
35
36
  const PROFILE_FIELDS = ['adapter', 'executable', 'model', 'effort', 'source_thread_id', 'ipc_directory', 'allowed_tools', 'cwd', 'project_name', 'project_id', 'base_url', 'api_protocol', 'api_key_ref'];
36
37
  const MAX_MINUTES = 1440;
37
38
  const SESSION_ID_PATTERN = /^[0-9A-Za-z_-]{8,128}$/;
@@ -106,22 +107,31 @@ export function pick_directory({ platform = process.platform, spawn_process = sp
106
107
  const MODEL_CACHE_MS = 24 * 60 * 60 * 1000;
107
108
 
108
109
  /** Return the software catalog, served from the private service home when still valid. */
109
- export async function cached_software_models(home, profile, { refresh = false, query = software_models } = {}) {
110
+ export async function cached_software_models(home, profile, { refresh = false, query = software_models, query_effort = software_effort } = {}) {
110
111
  const path = join(home, 'model-catalogs.json');
111
112
  let stamp = '';
112
113
  try { const file = statSync(profile.executable); stamp = `${file.size}:${file.mtimeMs}`; } catch {}
113
114
  const key = JSON.stringify([profile.adapter, profile.executable || '', stamp]);
115
+ const effort_key = JSON.stringify([profile.adapter, profile.executable || '', stamp, 'effort', profile.model || '']);
114
116
  let cache = {};
115
117
  try { cache = JSON.parse(readFileSync(path, 'utf8')); } catch {}
116
- const hit = cache[key];
117
- if (!refresh && hit && Date.now() - hit.at < MODEL_CACHE_MS) return hit.result;
118
- const result = await query(profile);
119
- if (!result.error && result.models.length) {
118
+ const cached = entry => !refresh && entry && Date.now() - entry.at < MODEL_CACHE_MS ? entry.result : null;
119
+ const [result, effort] = await Promise.all([cached(cache[key]) || query(profile),
120
+ profile.adapter === 'antigravity_cli' ? cached(cache[effort_key]) || query_effort(profile) : null]);
121
+ const updates = {};
122
+ if (!cached(cache[key]) && !result.error && result.models.length) {
123
+ updates[key] = { at: Date.now(), result };
124
+ }
125
+ if (effort && !effort.error && !cached(cache[effort_key])) {
126
+ updates[effort_key] = { at: Date.now(), result: effort };
127
+ }
128
+ if (Object.keys(updates).length) {
129
+ try { cache = JSON.parse(readFileSync(path, 'utf8')); } catch {}
120
130
  const prefix = JSON.stringify([profile.adapter, profile.executable || '']).slice(0, -1);
121
- for (const old of Object.keys(cache)) if (old.startsWith(prefix)) delete cache[old];
122
- atomic_json(path, { ...cache, [key]: { at: Date.now(), result } });
131
+ for (const old of Object.keys(cache)) if (old.startsWith(prefix) && !old.startsWith(key.slice(0, -1))) delete cache[old];
132
+ atomic_json(path, { ...cache, ...updates });
123
133
  }
124
- return result;
134
+ return effort ? { ...result, efforts: effort.levels, ...(effort.error ? { effort_error: effort.error } : {}) } : result;
125
135
  }
126
136
 
127
137
  /** Read private defaults without selecting a global room or identity. */
@@ -180,7 +190,7 @@ function validate_profile(name, profile, binding = false) {
180
190
  if (profile.adapter === 'api') validate_api_profile(profile);
181
191
  if (profile.effort && profile.adapter !== 'api' && !EFFORT_LEVELS[profile.adapter]?.includes(profile.effort)) {
182
192
  throw new Error(EFFORT_LEVELS[profile.adapter] ? `${name}.effort 只能是 ${EFFORT_LEVELS[profile.adapter].join(' / ')}`
183
- : `${name} 的思考档位不能单独设置;agy 请选择带档位的模型名,App 在软件内选择`);
193
+ : `${name} 的思考档位在原软件中选择`);
184
194
  }
185
195
  }
186
196
 
@@ -411,19 +421,29 @@ export async function room_member(home, raw, locked = false) {
411
421
  } finally { release(); }
412
422
  }
413
423
 
424
+ /** A Codex desktop chat is resumed through the app, any other Codex session through the CLI. */
425
+ function codex_session_adapter(session_id, env = process.env) {
426
+ const sessions = join(env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
427
+ const file = session_files(sessions).find(path => path.endsWith(`-${session_id}.jsonl`));
428
+ const meta = file ? readFileSync(file, 'utf8').split('\n', 1)[0] : '';
429
+ return /"originator":"Codex Desktop"/.test(meta) ? 'codex' : 'codex_cli';
430
+ }
431
+
414
432
  /**
415
- * Register a manual Claude expert's own session as a hosted participant, so the service resumes it for later turns.
416
- * Throws when the local Claude software is not configured or the session is not found on this machine; the member then stays manual.
433
+ * Register a manual Claude or Codex expert's own session as a hosted participant, so the service resumes it for later turns.
434
+ * Returns the adapter used. Throws when the local software is not configured or the session is not found on this machine; the member then stays manual.
417
435
  */
418
- export function adopt_member_profile(home, path, name, session_id) {
436
+ export function adopt_member_profile(home, path, name, session_id, client = 'claude') {
419
437
  const release = acquire_lock(join(dirname(path), 'configuration-edit'));
420
438
  try {
421
439
  const config = configuration_snapshot(path).config;
422
- const input = { cwd: config.cwd, participants: { [name]: { adapter: 'claude', session_id } } };
440
+ const adapter = client === 'codex' ? codex_session_adapter(session_id) : 'claude';
441
+ const input = { cwd: config.cwd, participants: { [name]: { adapter, session_id } } };
423
442
  const profile = borrowed_session(name, participant_profile(name, launch_settings(home).participants, input, false), config.cwd);
424
443
  const next = { ...config, participants: { ...config.participants, [name]: profile } };
425
444
  validate_config(next, dirname(path));
426
445
  atomic_json(path, next);
446
+ return adapter;
427
447
  } finally { release(); }
428
448
  }
429
449
 
@@ -741,7 +761,8 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
741
761
  else if (positionals[0] === 'models' && input?.adapter === 'api') result = await api_models(home, input);
742
762
  else if (positionals[0] === 'models' && input) {
743
763
  const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
744
- result = await cached_software_models(home, { adapter: input.adapter, executable: input.executable || saved?.executable }, { refresh: input.refresh === true });
764
+ result = await cached_software_models(home, { adapter: input.adapter, executable: input.executable || saved?.executable,
765
+ ...(input.model ? { model: input.model } : {}) }, { refresh: input.refresh === true });
745
766
  }
746
767
  else if (positionals[0] === 'room-member' && input) result = await room_member(home, input);
747
768
  else if (positionals[0] === 'pick-directory') result = await pick_directory();
@@ -17,7 +17,8 @@ function model_entry(id, label, efforts) {
17
17
  }
18
18
 
19
19
  /** Keep all query processes noninteractive and avoid user/project startup hooks. */
20
- function query_arguments(adapter) {
20
+ function query_arguments({ adapter, model }, effort = false) {
21
+ if (effort) return ['-p', '/effort', '--output-format', 'json', ...(model ? ['--model', model] : [])];
21
22
  if (adapter === 'codex_cli') return ['app-server', '--listen', 'stdio://'];
22
23
  if (adapter === 'antigravity_cli') return ['models'];
23
24
  return ['--safe-mode', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json',
@@ -29,7 +30,7 @@ function query_catalog(profile, options) {
29
30
  return new Promise((resolve_result, reject) => {
30
31
  const env = { ...process.env };
31
32
  delete env.CLAUDECODE;
32
- const child = (options.spawn || spawn)(profile.executable, query_arguments(profile.adapter), {
33
+ const child = (options.spawn || spawn)(profile.executable, query_arguments(profile, options.effort), {
33
34
  cwd: tmpdir(), env, shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
34
35
  });
35
36
  let finished = false, bytes = 0;
@@ -41,7 +42,7 @@ function query_catalog(profile, options) {
41
42
  const timer = setTimeout(() => finish(new Error('读取模型列表超时,请检查软件登录和网络后重试')), options.timeout_ms ?? QUERY_TIMEOUT_MS);
42
43
  const lines = createInterface({ input: child.stdout });
43
44
  const send = value => child.stdin.write(`${JSON.stringify(value)}\n`);
44
- const receive = catalog_reader(profile.adapter, send, finish);
45
+ const receive = catalog_reader(profile.adapter, send, finish, options.effort);
45
46
  child.once('error', () => finish(new Error('无法启动模型查询,请检查 CLI 路径')));
46
47
  child.stdin.on('error', () => finish(new Error('模型查询连接已关闭')));
47
48
  child.stderr.on('data', () => {}); // CLI diagnostics can contain private account or configuration data.
@@ -93,7 +94,13 @@ function json_line(line) {
93
94
  }
94
95
 
95
96
  /** Claude initialization exposes its picker; agy emits tab-separated ids and labels. */
96
- function catalog_reader(adapter, send, finish) {
97
+ function catalog_reader(adapter, send, finish, effort = false) {
98
+ if (effort) return { start: () => {}, line: line => {
99
+ const reply = json_line(line), data = reply?.command?.data;
100
+ if (reply?.status === 'SUCCESS' && reply.command?.name === 'effort' && Array.isArray(data?.available)) {
101
+ finish(null, data.adjustable ? data.available.filter(level => typeof level === 'string') : []);
102
+ }
103
+ } };
97
104
  if (adapter === 'codex_cli') return codex_reader(send, finish);
98
105
  if (adapter === 'claude') return {
99
106
  start: () => send({ type: 'control_request', request_id: 'catalog', request: { subtype: 'initialize' } }),
@@ -125,3 +132,11 @@ export async function software_models(profile, options = {}) {
125
132
  } catch (error) { result.error = error.message; }
126
133
  return result;
127
134
  }
135
+
136
+ /** Read native effort capabilities for the selected agy model, without an agent turn or saved setting changes. */
137
+ export async function software_effort(profile, options = {}) {
138
+ if (profile.adapter !== 'antigravity_cli' || !isAbsolute(profile.executable || '')
139
+ || (profile.model && !MODEL_PATTERN.test(profile.model))) return { error: '无法读取所选模型的思考档位' };
140
+ try { return { levels: await query_catalog(profile, { ...options, effort: true }) }; }
141
+ catch { return { error: '无法读取原生思考档位;保留 CLI 默认值,或检查软件版本后重试' }; }
142
+ }
@@ -197,7 +197,7 @@ export function acquire_lock(directory) {
197
197
  }
198
198
 
199
199
  /** Write a non-secret task separately from its CLI-only credential file; suggestions are smart-scheduling hints for the host. */
200
- export function task_files(config, event, token, binding, suggestions = [], entered = false) {
200
+ export function task_files(config, event, token, binding, { suggestions = [], entered = false, process_access = false } = {}) {
201
201
  const id = `event-${event.id}`;
202
202
  const draft_directory = config.draft_directory || `${config.directory}-drafts`;
203
203
  mkdirSync(draft_directory, { recursive: true });
@@ -208,6 +208,9 @@ export function task_files(config, event, token, binding, suggestions = [], ente
208
208
  if (event.role === 'host') skills.push(join(config.project, '.agents/skills/discuss-host/SKILL.md'));
209
209
  const adapter = config.participants[event.participant];
210
210
  const auto_enter = entered || AUTO_ENTER_ADAPTERS.has(adapter.adapter);
211
+ const bound_access = process_access && AUTO_ENTER_ADAPTERS.has(adapter.adapter);
212
+ const access_rule = bound_access ? '本次凭据文件已通过进程环境 DISCUSS_ACCESS_FILE 绑定,codiscuss 自动读取,命令无需 --access-file。'
213
+ : '每次业务写入都带 --access-file。';
211
214
  const skills_directory = join(config.project, '.agents', 'skills');
212
215
  // Codex and agy run commands through PowerShell on Windows, whose execution policy blocks npm's codiscuss.ps1 shim.
213
216
  const cli = process.platform === 'win32' && adapter.adapter !== 'claude' ? 'codiscuss.cmd' : 'codiscuss';
@@ -217,8 +220,9 @@ export function task_files(config, event, token, binding, suggestions = [], ente
217
220
  atomic_json(task_path, { site: config.site, room: config.room, event_id: event.id, participant: event.participant,
218
221
  project_name: adapter.project_name || null, cwd: adapter.cwd || null, project_access: adapter.project_access,
219
222
  draft_directory, ...(suggestions.length ? { scheduler_suggestions: suggestions } : {}),
220
- role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS, access_file: access_path,
221
- instructions: `${adapter.adapter === 'antigravity_cli' ? `无人值守下 run_command 只能运行 ${cli} 的 subcommands 子命令,其他命令会被自动拒绝并使会话直接结束;浏览项目用 list_dir、find_by_name、grep_search、view_file。` : ''}读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;${auto_enter ? '接入程序已进入本次执行,不要再执行 execution enter。' : `使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。进入成功后才处理本次安排;`}凭据文件由 CLI 内部读取,不要打印或读取其 token。每次业务写入都带 --access-file。答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。${suggestion_rule}` });
223
+ role: event.role, turn_id: event.turn_id, skills, cli, subcommands: TASK_SUBCOMMANDS,
224
+ ...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
225
+ instructions: `${adapter.adapter === 'antigravity_cli' ? `无人值守下 run_command 只能运行 ${cli} 的 subcommands 子命令,其他命令会被自动拒绝并使会话直接结束;浏览项目用 list_dir、find_by_name、grep_search、view_file。` : ''}读取指定技能;网站命令只用 ${cli} 的 subcommands 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口;${auto_enter ? '接入程序已进入本次执行,不要再执行 execution enter。' : `使用 ${cli} execution enter 房间 执行编号 --access-file 凭据路径 进入。进入成功后才处理本次安排;`}凭据文件由 CLI 内部读取,不要打印或读取其 token。${access_rule}答案草稿只保存到 draft_directory,不修改任务、凭据、日志或调度数据库;${project_rule}只处理本次工作,不建定时任务。主持创建下一安排或收束即结束,专家提交原文后结束。无效或已进入的任务不要重复执行。${suggestion_rule}` });
222
226
  const read_rule = windows_rg() ? '读取含中文的技能或文本文件时用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会按系统编码读出乱码,需要反复重试)。' : '';
223
227
  const scope = `本次只放行任务内 subcommands 列出的 ${cli} 子命令,命令以 ${cli} 开头,不用 python discuss.py、cd、管道或其他入口。任务内 cwd 是讨论项目目录;${project_rule}项目读写仍受软件实际权限和议题授权约束。不读取凭据内容。${read_rule}`;
224
228
  const plan_rule = event.role === 'host' ? '安排专家回应时先写发言计划,用 turn create --plan-file 一次排满本轮每位专家,不要每次只排一位。' : '';
@@ -275,7 +279,7 @@ export class DiscussionRunner {
275
279
  let invoked = false;
276
280
  let entered = pre_entered;
277
281
  try {
278
- const task = task_files(this.config, event, row.token, binding, suggestions, pre_entered);
282
+ const task = task_files(this.config, event, row.token, binding, { suggestions, entered: pre_entered, process_access: true });
279
283
  invoked = true;
280
284
  const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
281
285
  api_context: { site: this.config.site, room: this.config.room, token: row.token, home: this.config.api_home,
@@ -220,7 +220,7 @@ class RoomWorker {
220
220
  }
221
221
  this.state.config_version = snapshot.version;
222
222
  }
223
- /** A manual Claude expert whose CLI reported its session is handed to the service, which resumes that session from the next turn on. */
223
+ /** A manual Claude or Codex expert whose CLI reported its session is handed to the service, which resumes that session from the next turn on. */
224
224
  async adopt_manual_members() {
225
225
  if (this.config.mode === 'free') return;
226
226
  for (const member of this.runner.members || []) {
@@ -228,8 +228,8 @@ class RoomWorker {
228
228
  if (member.access !== 'manual' || member.status !== 'active' || !member.reported_session || this.adopt_failed.has(key)) continue;
229
229
  try {
230
230
  const { adopt_member_profile } = await import('./discussion_launch.mjs');
231
- adopt_member_profile(this.home, this.path, member.name, member.reported_session);
232
- await api(this.config, `/members/${member.id}`, { action: 'adopt', adapter: 'claude', session_id: member.reported_session });
231
+ const adapter = adopt_member_profile(this.home, this.path, member.name, member.reported_session, member.reported_client);
232
+ await api(this.config, `/members/${member.id}`, { action: 'adopt', adapter, session_id: member.reported_session });
233
233
  this.log(`${this.config.room}: ${member.name} 已转为本机托管`);
234
234
  } catch (error) {
235
235
  this.adopt_failed.add(key);
@@ -62,6 +62,9 @@ export function free_task_files(config, member, execution, binding) {
62
62
  atomic_json(access_path, { site: config.site, room: config.room, token: config.members[member.name].token,
63
63
  session_id: binding.session_id || '' });
64
64
  const cli = process.platform === 'win32' && profile.adapter !== 'claude' ? 'codiscuss.cmd' : 'codiscuss';
65
+ const bound_access = ['claude', 'codex_cli', 'antigravity_cli'].includes(profile.adapter);
66
+ const access_arg = bound_access ? '' : ' --access-file 凭据路径';
67
+ const access_rule = bound_access ? '本次凭据文件已通过进程环境 DISCUSS_ACCESS_FILE 绑定,codiscuss 自动读取,命令无需 --access-file。' : '';
65
68
  const project_rule = !profile.cwd ? '未指定项目目录。'
66
69
  : profile.project_access === 'write' ? '用户允许按请求修改 cwd 中的项目文件。' : 'cwd 中的项目文件只读,不修改。';
67
70
  const messages = execution.requests.map(item => `#${item.message_id}`).join('、');
@@ -74,8 +77,8 @@ export function free_task_files(config, member, execution, binding) {
74
77
  request_purposes: execution.requests.map(item => item.purpose || 'ask'), role: member.role || 'member',
75
78
  read_after: execution.read_until, cwd: profile.cwd || null, project_access: profile.project_access, draft_directory,
76
79
  skills: [join(config.project, '.agents/skills/discuss-participant/SKILL.md')], cli, subcommands: TASK_SUBCOMMANDS,
77
- access_file: access_path,
78
- instructions: `自由协作房间:用户点名了你(${messages})。先用 ${cli} message list ${config.room} --after ${execution.read_until} --access-file 凭据路径 读取新消息,按时间顺序处理,后面的消息可能修正前面的,以最新指令为准。${host_rule}完成后用 ${cli} message post ${config.room} --as ${member.name} --kind chat --execution ${execution.id} --body-file 草稿 --submission-id 新UUID --access-file 凭据路径 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 --execution 改作普通发言。草稿只写入 draft_directory;${project_rule}不读取或打印凭据内容,不建定时任务。` });
80
+ ...(bound_access ? { access_env: 'DISCUSS_ACCESS_FILE' } : { access_file: access_path }),
81
+ instructions: `自由协作房间:用户点名了你(${messages})。${access_rule}先用 ${cli} message list ${config.room} --after ${execution.read_until}${access_arg} 读取新消息,按时间顺序处理,后面的消息可能修正前面的,以最新指令为准。${host_rule}完成后用 ${cli} message post ${config.room} --as ${member.name} --kind chat --execution ${execution.id} --body-file 草稿 --submission-id 新UUID${access_arg} 提交一次回答。提交被拒绝说明本次执行已被打断或过期:保留草稿并结束,不要去掉 --execution 改作普通发言。草稿只写入 draft_directory;${project_rule}不读取或打印凭据内容,不建定时任务。` });
79
82
  const prompt = `用户在自由协作房间点名了你(${member.name}),对应消息 ${messages}。请先读取任务文件:${task_path},按其中说明读取新消息、完成请求,并用 ${cli} message post 带 --execution ${execution.id} 提交一次回答后结束。命令只用任务内 subcommands 列出的 ${cli} 子命令;${project_rule}不读取凭据内容。`;
80
83
  return { id, participant: member.name, directory: config.directory, draft_directory,
81
84
  skills_directory: join(config.project, '.agents', 'skills'), access_path, prompt };
package/web/app.js CHANGED
@@ -914,13 +914,6 @@ async function change_member({ member_id, name, action, role }) {
914
914
  } catch (error) { if (state.room?.id === room_id) state.free_error = error.message; }
915
915
  }
916
916
 
917
- /** Composer stop in a free room: interrupt every member that is working or queued. */
918
- async function interrupt_free() {
919
- const busy = (state.room.free?.members || []).filter(member => member.pending
920
- || ["dispatched", "started"].includes(member.execution?.status));
921
- for (const member of busy) await change_member({ member_id: member.id, action: "interrupt" });
922
- }
923
-
924
917
  /** Invite an independent member, retaining its credential before either delivery path runs. */
925
918
  async function invite_member({ name, access, project_access = "read", session_id = "" }) {
926
919
  const room_id = state.room.id;
@@ -1609,7 +1602,7 @@ function participant_model(name, room, saved) {
1609
1602
  if (!profile) return "";
1610
1603
  if (["codex", "antigravity"].includes(profile.adapter)) return "模型与强度由 App 设置";
1611
1604
  const model = profile.model || "默认模型";
1612
- const effort = profile.adapter === "antigravity_cli" ? "强度随模型" : profile.effort || "默认强度";
1605
+ const effort = profile.effort || (profile.adapter === "antigravity_cli" ? "模型默认强度" : "默认强度");
1613
1606
  return `${model} · ${effort}`;
1614
1607
  }
1615
1608
 
@@ -1630,6 +1623,7 @@ const MODEL_SOURCE_LABELS = { "codex-app-server": "来自 Codex CLI 模型列表
1630
1623
  /** Keep discoveries separate for each software executable, shared by its participant instances. */
1631
1624
  function model_catalog_key(profile) {
1632
1625
  if (profile.adapter === "api") return JSON.stringify([profile.adapter, profile.base_url, profile.api_protocol, profile.api_key_ref]);
1626
+ if (profile.adapter === "antigravity_cli") return JSON.stringify([profile.adapter, profile.executable || "", profile.model || ""]);
1633
1627
  return JSON.stringify([profile.adapter, profile.executable || ""]);
1634
1628
  }
1635
1629
 
@@ -1667,8 +1661,8 @@ function launch_model_state(name, launch = state.launch) {
1667
1661
  const items = [{ label: "软件默认模型", value: "__default__" }, ...models.map(model => ({ label: model.label || model.id, value: model.id }))];
1668
1662
  if (profile.model && !selected) items.push({ label: `${profile.model}(已保存,列表未确认)`, value: profile.model, disabled: true });
1669
1663
  return { supported: MODEL_CLIENTS.includes(profile.adapter), items, loading: Boolean(catalog?.loading),
1670
- efforts: selected?.efforts, source: MODEL_SOURCE_LABELS[catalog?.source] || catalog?.source || "",
1671
- effort_note: profile.adapter === "antigravity_cli" ? "思考强度随模型选择。" : !selected
1664
+ efforts: profile.adapter === "antigravity_cli" ? catalog?.efforts : selected?.efforts, source: MODEL_SOURCE_LABELS[catalog?.source] || catalog?.source || "",
1665
+ effort_note: profile.adapter === "antigravity_cli" ? catalog?.effort_error || (catalog?.loading ? "正在读取原生思考档位…" : "默认强度由 CLI 决定。") : !selected
1672
1666
  ? "选择模型后显示可用思考强度;默认值由软件决定。" : "所选模型不提供单独思考强度。",
1673
1667
  error: catalog?.error || (!launch.available ? "连接本机服务后读取模型列表" : !catalog ? "尚未读取此软件的模型列表" : "") };
1674
1668
  }
@@ -1680,9 +1674,11 @@ async function load_launch_models(name, force = false) {
1680
1674
  if (profile.adapter === "api" && (!force || !profile.api_key_ref)) return;
1681
1675
  const catalogs = state.launch.model_catalogs ||= {}, key = model_catalog_key(profile);
1682
1676
  if (catalogs[key]?.loading || (!force && catalogs[key])) return;
1683
- const current = { models: [], loading: true, error: "", source: "" };
1677
+ const fallback = profile.adapter === "antigravity_cli" ? catalogs[model_catalog_key({ ...profile, model: "" })] : null;
1678
+ const current = { models: fallback?.models || [], loading: true, error: "", source: "" };
1684
1679
  const input = profile.adapter === "api" ? { adapter: "api", base_url: profile.base_url, api_protocol: profile.api_protocol, model: profile.model || "discovery", api_key_ref: profile.api_key_ref }
1685
- : { adapter: profile.adapter, ...(profile.executable ? { executable: profile.executable } : {}), ...(force ? { refresh: true } : {}) };
1680
+ : { adapter: profile.adapter, ...(profile.executable ? { executable: profile.executable } : {}),
1681
+ ...(profile.adapter === "antigravity_cli" && profile.model ? { model: profile.model } : {}), ...(force ? { refresh: true } : {}) };
1686
1682
  catalogs[key] = current;
1687
1683
  publish_view();
1688
1684
  try {
@@ -1690,6 +1686,8 @@ async function load_launch_models(name, force = false) {
1690
1686
  if (result.adapter !== input.adapter) throw new Error("模型列表与所选软件不一致");
1691
1687
  current.models = result.models || [];
1692
1688
  current.source = result.source || "";
1689
+ current.efforts = result.efforts;
1690
+ current.effort_error = result.effort_error || "";
1693
1691
  current.error = result.error || (!current.models.length ? "软件未返回可用模型,可使用默认模型" : "");
1694
1692
  } catch (error) { current.error = error.message; }
1695
1693
  finally {
@@ -1704,6 +1702,7 @@ function write_launch_model(name, value) {
1704
1702
  write_launch_field(name, "model", model);
1705
1703
  const levels = launch_model_state(name).efforts;
1706
1704
  if (!levels?.includes(state.launch.profiles[name].effort)) write_launch_field(name, "effort", "");
1705
+ if (state.launch.profiles[name].adapter === "antigravity_cli") load_launch_models(name);
1707
1706
  }
1708
1707
 
1709
1708
  /** Reuse the key last saved on this machine for the same API root instead of asking again. */
@@ -2206,7 +2205,7 @@ ${instruction}
2206
2205
  /** How a manual member learns it is its turn: free rooms are pulled by the member, discussions only on the host's assignment. */
2207
2206
  function manual_trigger_instruction(room) {
2208
2207
  return room.mode === "free" ? "首次复制后,可由人工或软件原生定时触发;自行提交,不必每轮复制回答。"
2209
- : "这是全自动讨论:只在主持人给你安排发言时才参与,先核对 turn current 是否轮到你,再按参与技能认领发言权并提交;没有安排就结束本次检查。首次提交后,若你运行在 Claude Code 中,本机服务会自动接管你的会话并在后续轮次唤醒你;未被接管时才需要你或软件原生定时触发。";
2208
+ : "这是全自动讨论:只在主持人给你安排发言时才参与,先核对 turn current 是否轮到你,再按参与技能认领发言权并提交;没有安排就结束本次检查。首次提交后,若你运行在 Claude Code 或 Codex 中,本机服务会自动接管你的会话并在后续轮次唤醒你;未被接管时才需要你或软件原生定时触发。";
2210
2209
  }
2211
2210
 
2212
2211
  /** Download a private per-room setup file for the independent local runner. */
@@ -3060,7 +3059,7 @@ async function execute_view_action(name, value) {
3060
3059
  copy_online: () => copy_context(), copy_cli: () => copy_context("cli"), copy_background,
3061
3060
  choose_runtime, export_runtime, download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_runtime, check_limits, pause_discussion, restart_discussion, force_close, conclude_discussion,
3062
3061
  save_launch_defaults, launch_current_room, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
3063
- load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, interrupt_free, set_proxy, copy_online_notice,
3062
+ load_room_configuration, save_room_configuration, interrupt_member, change_member, invite_member, set_proxy, copy_online_notice,
3064
3063
  prepare_brainstorm, return_conclusion, convert_suggestion, adopt_recommendation,
3065
3064
  busy: () => { throw new Error("上一个操作仍在处理中,请稍候;长时间无响应请刷新页面"); },
3066
3065
  copy_reading: () => copy_text(`${location.origin}/r/${state.room.id}`), copy: copy_text };