ai_co_discuss 0.3.50 → 0.3.51

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.
package/README.md CHANGED
@@ -30,7 +30,7 @@ Most multi-agent frameworks spin up fresh agents inside their own runtime. AI Co
30
30
  - **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
31
31
  - **Goal mode** — give the host an objective with `/goal`; whenever the room goes idle the host is woken to carry on, until it confirms the goal is complete, gets blocked, a budget runs out or you end it.
32
32
  - **Smart Scheduling** — optionally let a decision model handle routine routing instead of waking the host.
33
- - **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards, share it as a folder, and select it when creating a room or inviting members; after you confirm the members, the host follows the playbook. It brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
33
+ - **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Browse and edit workflows when creating a room; start one later by asking the host in the message box or selecting it with `/` and confirming its members. It brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
34
34
  - **Mix any participant** — local CLIs and desktop apps, model APIs and manually connected sessions in one room.
35
35
  - **Shared workspace** — point a room at a project directory, grant each local agent read-only or writable access, and browse files with text, Markdown and static HTML previews from the room.
36
36
  - **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
@@ -48,7 +48,7 @@ A Goal keeps the host pursuing one objective: when the room goes idle, the local
48
48
 
49
49
  - **Start**: send `/goal <objective>` and the host starts it as written; with a bare `/goal` the host draws the objective from the recent conversation and declines when it cannot. You can also click **Start Goal** (开启 Goal) in the member panel, or choose **Start Goal** under **After creating** (创建后) when creating a room. Typing `/` at the start of the input box lists Goal and Discussion in a **Modes** (模式) group; while one is running, it lists its pause, resume and end commands.
50
50
  - **Progress**: after the host's dispatched work ends and the room stays idle for 30 seconds (not counting when you just spoke, a member is still working or messages are queued), the host is woken once, shown as "Goal 续跑 #N". If the host then hands out no work (it only answers or asks you), it is not woken again until you reply.
51
- - **Budget**: unlimited by default. You can set a wake-up count or time limit when starting or via **Edit**; at the limit the host wraps up once and the Goal ends. Three failed wake-ups in a row stop it.
51
+ - **Budget**: unlimited by default. Starting or editing a Goal only requires an objective; expand **Advanced settings** (高级设置) to set an optional wake-up count or time limit. At the limit the host wraps up once and the Goal ends. Three failed wake-ups in a row stop it.
52
52
  - **End**: the host declares it complete after checking the actual results, or blocked when the same obstacle persists for three wake-ups. You can `/goal pause`, `/goal resume`, `/goal clear` or `/goal edit <new objective>` at any time, or use the panel buttons. **Stop all** also pauses the Goal. An end card in the conversation offers **Continue Goal** with the same objective.
53
53
 
54
54
  A Goal grants no extra permissions: the host stays within members' project access and the chain-round limit, and does not invite members, commit, push or deploy on its own.
package/docs/runner.md CHANGED
@@ -37,6 +37,8 @@ codiscuss setup
37
37
 
38
38
  运行 `codiscuss local open`,点击“新建房间”,添加成员并指定主持人。启动失败时刷新页面,可沿用原请求继续,不会重复建房。
39
39
 
40
+ 话题名称、项目目录和本次成员每次重新填写;智能调度、协作方式、讨论与 Goal 上限、连续派活轮数等选项自动记住上一次选择,取消建房也保留。偏好保存在运行服务的电脑上,其他已配对设备使用同一份;讨论与 Goal 的目标仍需每次填写。工作流只记住浏览选中项,不会因此启动。
41
+
40
42
  ### 浏览项目文件
41
43
 
42
44
  房间设置了项目目录后,左栏“房间”下方的“文件”页可浏览电脑上的文件夹与文件,查看大小和修改时间。点击文件打开悬浮预览,支持文本、Markdown 渲染与 HTML 静态渲染,并可切换源码。未设置项目目录时入口灰显。
@@ -117,7 +119,7 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
117
119
 
118
120
  ## 工作流(可选)
119
121
 
120
- 工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent 卡片(每张就是一个专家预设)。建房或邀请时在“工作流”一行选中它,确认卡片后作为成员加入;主持人此后按说明组织工作。它只带来做法和成员,不保证工序,具体怎么做仍由主持人判断,你随时可以 @ 任何成员。
122
+ 工作流把一种多人协作的做法存下来,供别的房间复用:一份给主持人读的做法说明(SKILL.md),加一组 Agent 卡片(每张就是一个专家预设)。建房或普通邀请时在“工作流”一行选中它,只供查看和编辑,不添加成员,也不随建房启动。建房后,在输入框明确要求主持人启动(权限见下文),或打 `/` 选择工作流、确认成员后启用;主持人此后按说明组织工作。它只带来做法和成员,不保证工序,具体怎么做仍由主持人判断,你随时可以 @ 任何成员。
121
123
 
122
124
  - **保存**:在“工作流”一行点“添加”,选“新建”,或导入别人分享的 zip 或文件夹(手机、平板上同样可用)。导入的内容先在编辑页预览,点保存才写入。选中一个工作流后,下方显示它的描述,可“查看说明”(做法说明与成员卡片)或“编辑”,删除在编辑页底部。编辑页可以“从当前房间成员导入”卡片,提示词保持原文。想让主持人起草,打开下面的“允许主持人自建工作流”。
123
125
  - **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 → 协作设置”勾选“允许主持人自建工作流”后,直接对主持人说要什么分工(或“把这个房间的做法存成工作流”),它会自己保存、在本房间启用,并邀请工作流里的成员,接着就按流程派活,不需要你导入或添加成员。之后让它改流程,改动从成员下一次执行起生效。主持人带来的成员项目权限最高为可写;只有本地软件担任的主持人能这样做,API 主持人不行。
package/docs/workflow.md CHANGED
@@ -58,7 +58,7 @@ Goal 让主持人持续推进一个目标:房间空闲时,本机服务会再
58
58
 
59
59
  - **开启**:在输入框发送 `/goal 目标`,主持人照原文开启;只发 `/goal` 时,主持人从最近的对话提炼目标,提炼不出就不开启并说明缺什么。也可以在成员栏点“开启 Goal”,或建房时在“创建后”选“开启 Goal”,直接填写目标。输入框开头打 `/` 时,菜单最上面一组“模式”列出 Goal 与讨论;进行中时改为列出暂停、继续、结束等命令。需要一位由本机唤醒的 AI 主持人;Goal 与讨论不能同时进行。
60
60
  - **推进**:主持人派出的活全部结束、房间空闲 30 秒后(你刚发言、还有成员在工作或有排队消息时不算空闲),本机服务唤醒主持人一次,消息流里显示“Goal 续跑 #N”。主持人这次没派活(只回复你或向你提问)时,不再自动唤醒,直到你回复。
61
- - **预算**:默认不限。可在开启时或成员栏“编辑”中设续跑次数或时长上限;到限后主持人收尾一次,Goal 结束。连续 3 次续跑执行失败时自动停止。
61
+ - **预算**:默认不限。开启或编辑 Goal 时只需填写目标;需要控制预算时,展开“高级设置”设续跑次数或时长上限。到限后主持人收尾一次,Goal 结束。连续 3 次续跑执行失败时自动停止。
62
62
  - **结束**:主持人对照实际结果核验后宣告完成,或同一阻碍连续 3 次无法解决时宣告卡住;你可以随时 `/goal pause`、`/goal resume`、`/goal clear`、`/goal edit 新目标`,或用成员栏的按钮。“全部停止”也会暂停 Goal。结束后消息流留一张结束卡,可“继续 Goal”沿用原目标重新开启。
63
63
 
64
64
  Goal 不扩大权限:主持人仍受成员项目权限和连续派活轮数限制,不会自行邀请成员、提交、推送或部署。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.3.50",
3
+ "version": "0.3.51",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -23,7 +23,7 @@ import { stt_part } from './stt.mjs';
23
23
  import { access_levels, BUILTIN_EXPERT_PRESETS, MAX_EXPERT_PROMPT, PROJECT_ACCESS } from './expert_prompts.mjs';
24
24
  import { acquire_lock, api, DiscussionStore, service_drafts, validate_config } from './discussion_runner.mjs';
25
25
  import { FreeRoomRunner, free_task_files } from './free_runner.mjs';
26
- import { bind_room_workflow, delete_workflow, get_workflow, list_workflows, load_room_workflow, parse_workflow_draft, preview_workflow, save_workflow } from './workflows.mjs';
26
+ import { bind_room_workflow, delete_workflow, get_workflow, list_workflows, load_room_workflow, parse_workflow_draft, preview_workflow, save_workflow, WORKFLOW_ID_PATTERN } from './workflows.mjs';
27
27
  import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
28
28
 
29
29
  // A platform is a type; `Claude-review` is another Claude instance with its own session, model and effort. An ID is
@@ -336,18 +336,66 @@ export async function room_files(home, raw) {
336
336
  return { room_id: input.room_id, ...read_project_files(config.cwd, input.path, input.offset, input.query) };
337
337
  }
338
338
 
339
- /** Save only reusable client settings; room credentials and session bindings are never defaults. */
339
+ const CREATE_PREFERENCE_RANGES = { max_rounds: [0, 100], max_minutes: [0, 1440], goal_max_continues: [0, 100], goal_max_minutes: [0, 1440], chain_rounds: [1, 20] };
340
+ const CREATE_PREFERENCE_FLAGS = ['named_followup', 'host_workflows', 'advanced_open'];
341
+ const MAX_PREFERENCE_MODEL_LENGTH = 200;
342
+ const SETTINGS_LOCK_WAIT_MS = 5000;
343
+
344
+ /** Validate only reusable form choices; room text, members, paths and credentials are never preferences. */
345
+ function validate_create_preferences(input) {
346
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('建房偏好须为对象');
347
+ for (const [field, value] of Object.entries(input)) {
348
+ let valid = false;
349
+ if (CREATE_PREFERENCE_FLAGS.includes(field)) valid = typeof value === 'boolean';
350
+ else if (Object.hasOwn(CREATE_PREFERENCE_RANGES, field)) {
351
+ const [minimum, maximum] = CREATE_PREFERENCE_RANGES[field];
352
+ valid = Number.isInteger(value) && value >= minimum && value <= maximum;
353
+ } else if (field === 'drive') valid = ['free', 'discussion', 'goal'].includes(value);
354
+ else if (field === 'workflow_id') valid = typeof value === 'string' && (!value || WORKFLOW_ID_PATTERN.test(value));
355
+ else if (field === 'scheduler') valid = valid_scheduler_preference(value);
356
+ if (!valid) throw new Error(`建房偏好字段无效:${field}`);
357
+ }
358
+ }
359
+
360
+ /** A scheduler preference carries selections, never a key or even a key reference. */
361
+ function valid_scheduler_preference(value) {
362
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
363
+ const fields = {
364
+ enabled: item => typeof item === 'boolean',
365
+ provider: item => typeof item === 'string' && (!item || Object.hasOwn(SCHEDULER_PROVIDERS, item)),
366
+ model: item => typeof item === 'string' && item.length <= MAX_PREFERENCE_MODEL_LENGTH,
367
+ mode: item => typeof item === 'string' && (!item || Object.hasOwn(SCHEDULER_MODES, item)),
368
+ };
369
+ return Object.entries(value).every(([field, item]) => Object.hasOwn(fields, field) && fields[field](item));
370
+ }
371
+
372
+ /** Update software defaults or form preferences independently, preserving the other section. */
340
373
  export function save_launch_settings(home, input) {
341
- const participants = {};
342
- for (const [name, profile] of Object.entries(input.participants || {})) {
343
- if (platform_of(name) !== name) throw new Error(`本机默认配置按平台保存;${name} 的模型与强度只用于本场讨论`);
344
- validate_profile(name, profile);
345
- participants[name] = profile;
374
+ const patch = {};
375
+ if (Object.hasOwn(input, 'participants')) {
376
+ const participants = {};
377
+ for (const [name, profile] of Object.entries(input.participants || {})) {
378
+ if (platform_of(name) !== name) throw new Error(`本机默认配置按平台保存;${name} 的模型与强度只用于本场讨论`);
379
+ validate_profile(name, profile);
380
+ participants[name] = profile;
381
+ }
382
+ patch.participants = participants;
383
+ }
384
+ if (Object.hasOwn(input, 'create_preferences')) {
385
+ validate_create_preferences(input.create_preferences);
386
+ patch.create_preferences = input.create_preferences;
346
387
  }
347
- const settings = { participants };
348
388
  mkdirSync(home, { recursive: true, mode: 0o700 });
349
- atomic_json(join(home, 'launch-settings.json'), settings);
350
- return settings;
389
+ const lock = new DatabaseSync(join(home, 'launch-settings-lock.sqlite3'));
390
+ try {
391
+ lock.exec(`PRAGMA busy_timeout=${SETTINGS_LOCK_WAIT_MS}; BEGIN IMMEDIATE`);
392
+ const previous = launch_settings(home);
393
+ const settings = { ...previous, ...patch };
394
+ if (patch.create_preferences) settings.create_preferences = { ...previous.create_preferences, ...patch.create_preferences };
395
+ atomic_json(join(home, 'launch-settings.json'), settings);
396
+ lock.exec('COMMIT');
397
+ return settings;
398
+ } finally { lock.close(); }
351
399
  }
352
400
 
353
401
  const PROJECT_NAME_LIMIT = 80;
package/web/app.js CHANGED
@@ -126,10 +126,9 @@ state.navigation_version = 0;
126
126
  state.admin = { loaded: false, local: false, registered: false, setup_available: false, logged_in: false, profile: null, identity_version: 0 };
127
127
  // needs: "" | "pairing" | "renew"; pairing_code arrives from a scanned link (#pair=CODE); prompt waits for the user's tap.
128
128
  state.device = { id: "", needs: "", pairing_code: "", prompt: null };
129
- state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], hidden_builtins: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, error: "", recoveries: [] };
130
- // The workflow chosen in the new-room form: { id, label, members: { card: instance } }; cards removed from the form drop out at submission.
131
- state.create_workflow = null;
129
+ state.launch = { available: false, remote: false, relay: null, connect_command: "", profiles: {}, projects: [], presets: [], builtin_presets: [], hidden_builtins: [], workflows: [], description_templates: {}, workspace: {}, scheduler: {}, create_preferences: {}, error: "", recoveries: [] };
132
130
  state.create_host_workflows = false;
131
+ let create_preferences_pending = {}, create_preferences_saving = null, create_preferences_version = 0;
133
132
  const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, { value: "codex", label: "Codex App" }],
134
133
  Claude: [{ value: "claude", label: "Claude Code CLI" }],
135
134
  Antigravity: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
@@ -1137,23 +1136,83 @@ function mentioned(text, name) {
1137
1136
  return new RegExp(`(^|[\\s((,,。;;::])@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "u").test(prose);
1138
1137
  }
1139
1138
 
1140
- /** Start every new form from defaults: room use, delivery, text, roster, host, limits and software overrides. */
1139
+ /** Clear this room's text, project and roster, then restore the user's last reusable choices. */
1141
1140
  function reset_create_form() {
1142
1141
  element("room-form").reset();
1143
1142
  render_expert_inputs("new-expert-modes", default_experts());
1144
1143
  render_host_options("new-room-host", [], "");
1145
1144
  state.launch.workspace = {};
1146
- state.launch.scheduler = default_scheduler();
1147
- state.named_followup = false;
1148
- state.create_discussion = false;
1149
1145
  state.create_goal = null;
1150
- state.create_chain_rounds = DEFAULT_CHAIN_ROUNDS;
1151
- state.create_workflow = null;
1152
- state.create_host_workflows = false;
1146
+ apply_create_preferences();
1153
1147
  state.launch.profiles = JSON.parse(JSON.stringify(state.launch.saved_profiles || {}));
1154
1148
  Object.values(state.launch.profiles).forEach(fill_api_key);
1155
1149
  render_launch_settings();
1156
1150
  publish_view();
1151
+ return refresh_create_preferences();
1152
+ }
1153
+
1154
+ /** Restore choices without touching the new room's title, project, members, host or goal text. */
1155
+ function apply_create_preferences() {
1156
+ const preferences = state.launch.create_preferences || {};
1157
+ const scheduler = preferences.scheduler;
1158
+ const base_url = state.launch.scheduler_providers?.[scheduler?.provider]?.base_url;
1159
+ state.launch.scheduler = scheduler ? { ...scheduler, api_key_ref: state.launch.api_keys?.[base_url] || "" } : default_scheduler();
1160
+ state.named_followup = preferences.named_followup ?? false;
1161
+ state.create_discussion = preferences.drive === "discussion";
1162
+ state.create_goal = preferences.drive === "goal" ? { objective: state.create_goal?.objective || "",
1163
+ max_continues: preferences.goal_max_continues ?? 0, max_minutes: preferences.goal_max_minutes ?? 0 } : null;
1164
+ state.create_chain_rounds = preferences.chain_rounds ?? DEFAULT_CHAIN_ROUNDS;
1165
+ state.create_host_workflows = preferences.host_workflows ?? false;
1166
+ for (const field of ["max_rounds", "max_minutes"]) {
1167
+ const control = element(`new-room-limit-${field}`);
1168
+ if (control && preferences[field] !== undefined) control.value = String(preferences[field]);
1169
+ }
1170
+ }
1171
+
1172
+ /** Remember only changed preference fields; cancellation of the form does not undo these choices. */
1173
+ function write_create_preferences(patch) {
1174
+ const current = state.launch.create_preferences || {};
1175
+ const changed = Object.fromEntries(Object.entries(patch).filter(([field, value]) => JSON.stringify(current[field]) !== JSON.stringify(value)));
1176
+ if (!Object.keys(changed).length) return;
1177
+ state.launch.create_preferences = { ...current, ...changed };
1178
+ create_preferences_pending = { ...create_preferences_pending, ...changed };
1179
+ create_preferences_version++;
1180
+ flush_create_preferences();
1181
+ publish_view();
1182
+ }
1183
+
1184
+ /** Serialize preference patches so a slower earlier save cannot overwrite the latest operation. */
1185
+ function flush_create_preferences() {
1186
+ if (create_preferences_saving) return create_preferences_saving;
1187
+ if (!state.launch.available || !Object.keys(create_preferences_pending).length) return Promise.resolve();
1188
+ const patch = create_preferences_pending;
1189
+ create_preferences_pending = {};
1190
+ create_preferences_saving = local_launch_request("settings", { create_preferences: patch }).catch(error => {
1191
+ create_preferences_pending = { ...patch, ...create_preferences_pending };
1192
+ notify("error", `建房偏好未保存:${error.message}`);
1193
+ return false;
1194
+ }).then(result => {
1195
+ create_preferences_saving = null;
1196
+ if (result !== false) return flush_create_preferences();
1197
+ });
1198
+ return create_preferences_saving;
1199
+ }
1200
+
1201
+ /** Reopen with the paired computer's latest choices, keeping edits made while the read was in flight. */
1202
+ async function refresh_create_preferences() {
1203
+ if (!state.launch.available) return;
1204
+ const version = create_preferences_version;
1205
+ await flush_create_preferences();
1206
+ try {
1207
+ const settings = await local_launch_request("settings");
1208
+ if (version !== create_preferences_version) return;
1209
+ for (const field of ["api_keys", "scheduler_providers", "scheduler_default_mode", "workflows"]) {
1210
+ if (settings[field] !== undefined) state.launch[field] = settings[field];
1211
+ }
1212
+ state.launch.create_preferences = { ...settings.create_preferences, ...create_preferences_pending };
1213
+ apply_create_preferences();
1214
+ publish_view();
1215
+ } catch (error) { notify("error", `建房偏好未读取:${error.message}`); }
1157
1216
  }
1158
1217
 
1159
1218
  /** Persist a member mutation before transmission and reuse its credential after an uncertain response. */
@@ -2666,6 +2725,7 @@ async function load_launch_settings() {
2666
2725
  const settings = await local_launch_request("settings");
2667
2726
  // Keep the saved machine defaults so every new form can start from them again.
2668
2727
  state.launch.saved_profiles = JSON.parse(JSON.stringify(settings.participants || {}));
2728
+ state.launch.create_preferences = { ...settings.create_preferences, ...create_preferences_pending };
2669
2729
  Object.assign(state.launch, { available: true, profiles: settings.participants, projects: settings.projects || [], api_providers: settings.api_providers || {}, api_keys: settings.api_keys || {},
2670
2730
  presets: settings.presets || [], builtin_presets: settings.builtin_presets || [], hidden_builtins: settings.hidden_builtins || [], workflows: settings.workflows || [], description_templates: settings.description_templates || {},
2671
2731
  scheduler_providers: settings.scheduler_providers || {}, scheduler_modes: settings.scheduler_modes || {}, scheduler_default_mode: settings.scheduler_default_mode || "", error: "",
@@ -2673,8 +2733,8 @@ async function load_launch_settings() {
2673
2733
  Object.values(state.launch.profiles).forEach(fill_api_key);
2674
2734
  if (local_connection && !relay_machine) pair_local_machine();
2675
2735
  move_browser_description_templates();
2676
- // Settings arrive after the form was reset; only an untouched draft gets the default.
2677
- if (!Object.keys(state.launch.scheduler).length) state.launch.scheduler = default_scheduler();
2736
+ apply_create_preferences();
2737
+ flush_create_preferences();
2678
2738
  load_room_models();
2679
2739
  } catch (error) {
2680
2740
  // An unpaired or expired device keeps its way to the computer: pairing and renewal go through it.
@@ -2978,20 +3038,13 @@ function launch_options() {
2978
3038
  function creation_launch_options() {
2979
3039
  const { enabled, provider, model, api_key_ref, mode } = state.launch.scheduler;
2980
3040
  return { ...launch_options(), ...(enabled && provider && model && api_key_ref ? { scheduler: { provider, model, api_key_ref, ...(mode ? { mode } : {}) } } : {}),
2981
- ...creation_workflow(), ...(state.create_host_workflows ? { host_workflows: true } : {}) };
2982
- }
2983
-
2984
- /** The new room's workflow, naming only the cards still in the form as woken members. */
2985
- function creation_workflow() {
2986
- if (!state.create_workflow) return {};
2987
- const chosen = read_expert_inputs("new-expert-modes");
2988
- const members = Object.fromEntries(Object.entries(state.create_workflow.members).filter(([, name]) => chosen[name] === "cli"));
2989
- return Object.keys(members).length ? { workflow: { id: state.create_workflow.id, members } } : {};
3041
+ ...(state.create_host_workflows ? { host_workflows: true } : {}) };
2990
3042
  }
2991
3043
 
2992
3044
  /** Whether the new room's host may save and enable workflows by itself, inviting their cards. */
2993
3045
  function write_create_host_workflows(value) {
2994
3046
  state.create_host_workflows = Boolean(value);
3047
+ write_create_preferences({ host_workflows: state.create_host_workflows });
2995
3048
  publish_view();
2996
3049
  }
2997
3050
 
@@ -3018,25 +3071,6 @@ function workflow_preset({ card, requested_access, needs_platform, ...preset })
3018
3071
  return preset;
3019
3072
  }
3020
3073
 
3021
- /**
3022
- * Fill the new-room form with a workflow's cards, each a preset-made instance, and remember which card each plays.
3023
- * Choosing another (or the same one again after editing) replaces the previous cards; clearing the choice keeps them as ordinary members.
3024
- */
3025
- async function choose_create_workflow(id) {
3026
- const workflow = id ? await local_launch_request("workflows", { action: "get", id }) : null;
3027
- if (!workflow) { state.create_workflow = null; return publish_view(); }
3028
- for (const name of Object.values(state.create_workflow?.members || {})) write_view_expert("new-expert-modes", name, "");
3029
- const members = {};
3030
- for (const agent of workflow.agents) members[agent.card] = add_preset_instance("new-expert-modes", workflow_preset(agent), "cli");
3031
- state.create_workflow = { id, label: workflow.label || id, members };
3032
- publish_view();
3033
- }
3034
-
3035
- /** The workflow a card in the new-room form or the invite dialog came from, for its read-only prompt. */
3036
- function create_workflow_label(name) {
3037
- return state.create_workflow && Object.values(state.create_workflow.members).includes(name) ? state.create_workflow.label : "";
3038
- }
3039
-
3040
3074
  /** Bind a workflow to this room after its cards were invited (or brought back); the host reads it from the next task. */
3041
3075
  async function enable_room_workflow({ id, members }) {
3042
3076
  const room_id = state.room.id;
@@ -3062,6 +3096,7 @@ async function move_workflow_members(action) {
3062
3096
  /** Choose whether the new discussion's host writes named follow-ups for each expert between rounds. */
3063
3097
  function write_named_followup(value) {
3064
3098
  state.named_followup = Boolean(value);
3099
+ write_create_preferences({ named_followup: state.named_followup });
3065
3100
  publish_view();
3066
3101
  }
3067
3102
 
@@ -3072,6 +3107,7 @@ state.create_chain_rounds = DEFAULT_CHAIN_ROUNDS;
3072
3107
  /** Change the new room's round limit; it is saved right after the room is created. */
3073
3108
  function write_create_chain_rounds(value) {
3074
3109
  state.create_chain_rounds = value;
3110
+ write_create_preferences({ chain_rounds: value });
3075
3111
  publish_view();
3076
3112
  }
3077
3113
 
@@ -3079,13 +3115,18 @@ function write_create_chain_rounds(value) {
3079
3115
  function write_create_discussion(value) {
3080
3116
  state.create_discussion = Boolean(value);
3081
3117
  if (state.create_discussion) state.create_goal = null;
3118
+ write_create_preferences({ drive: state.create_discussion ? "discussion" : state.create_goal ? "goal" : "free" });
3082
3119
  publish_view();
3083
3120
  }
3084
3121
 
3085
3122
  /** Turn the new room's goal on (with a patch of its fields) or off; a goal and a discussion exclude each other. */
3086
3123
  function write_create_goal(patch) {
3087
- state.create_goal = patch === null ? null : { objective: "", max_continues: 0, max_minutes: 0, ...(state.create_goal || {}), ...patch };
3124
+ const preferences = state.launch.create_preferences || {};
3125
+ state.create_goal = patch === null ? null : { objective: "", max_continues: preferences.goal_max_continues ?? 0,
3126
+ max_minutes: preferences.goal_max_minutes ?? 0, ...(state.create_goal || {}), ...patch };
3088
3127
  if (state.create_goal) state.create_discussion = false;
3128
+ write_create_preferences({ drive: state.create_goal ? "goal" : state.create_discussion ? "discussion" : "free",
3129
+ ...(state.create_goal ? { goal_max_continues: state.create_goal.max_continues, goal_max_minutes: state.create_goal.max_minutes } : {}) });
3089
3130
  publish_view();
3090
3131
  }
3091
3132
 
@@ -3108,6 +3149,8 @@ function write_scheduler(field, value) {
3108
3149
  scheduler.model = preset?.models[0] || "";
3109
3150
  scheduler.api_key_ref = preset ? state.launch.api_keys?.[preset.base_url] || "" : "";
3110
3151
  }
3152
+ const { enabled = false, provider = "", model = "", mode = "" } = scheduler;
3153
+ write_create_preferences({ scheduler: { enabled, provider, model, mode } });
3111
3154
  publish_view();
3112
3155
  }
3113
3156
 
@@ -4333,7 +4376,7 @@ function view_snapshot() {
4333
4376
  device: { id: state.device.id, needs: state.device.needs, pairing_code: state.device.pairing_code,
4334
4377
  prompt: state.device.prompt ? { title: state.device.prompt.title, description: state.device.prompt.description, confirm_label: state.device.prompt.confirm_label } : null },
4335
4378
  tab: state.tab, snapshot_id: state.snapshot_id, context_dirty: state.context_dirty,
4336
- theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion, create_goal: state.create_goal, create_chain_rounds: state.create_chain_rounds, create_workflow: state.create_workflow, create_host_workflows: state.create_host_workflows };
4379
+ theme: document.documentElement.dataset.theme, fields, experts, launch: structuredClone(state.launch), named_followup: state.named_followup, create_discussion: state.create_discussion, create_goal: state.create_goal, create_chain_rounds: state.create_chain_rounds, create_host_workflows: state.create_host_workflows };
4337
4380
  }
4338
4381
 
4339
4382
  /** Update a shared draft using the same input events as the original controls. */
@@ -4348,6 +4391,9 @@ function write_view_field(id, value) {
4348
4391
  control.value = value ?? "";
4349
4392
  control.dispatchEvent(new Event("input", { bubbles: true }));
4350
4393
  control.dispatchEvent(new Event("change", { bubbles: true }));
4394
+ if (["new-room-limit-max_rounds", "new-room-limit-max_minutes"].includes(id)) {
4395
+ write_create_preferences({ [id.slice("new-room-limit-".length)]: Number(control.value) });
4396
+ }
4351
4397
  publish_view();
4352
4398
  }
4353
4399
 
@@ -4546,7 +4592,7 @@ async function execute_view_action(name, value) {
4546
4592
  download_member_access, copy_member_access_text, copy_member_join, copy_member_launch: name => copy_member_join(name, true), pause_discussion, restart_discussion, start_room_discussion, end_room_discussion, start_room_goal, update_room_goal, goal_control,
4547
4593
  save_launch_defaults, resume_launch, discard_launch, retry_launch_connection, check_launch_connection: load_launch_settings,
4548
4594
  load_room_configuration, save_room_configuration, interrupt_member, retry_delivery, retract_message, retract_latest, stop_room, rewind_and_send, fork_room, change_member, reorder_speakers, invite_member,
4549
- choose_create_workflow, enable_room_workflow, move_workflow_members, set_room_host_workflows,
4595
+ enable_room_workflow, move_workflow_members, set_room_host_workflows,
4550
4596
  adopt_recommendation, rate_decision,
4551
4597
  queue_send: id => queue_request({ action: "send", id }), queue_delete: id => queue_request({ action: "delete", id }),
4552
4598
  queue_reorder: ids => queue_request({ action: "reorder", ids }), queue_resume: () => queue_request({ action: "resume" }),
@@ -4568,13 +4614,13 @@ function change_view_theme(theme) {
4568
4614
  try { localStorage.setItem(THEME_STORAGE_KEY, document.documentElement.dataset.theme); } catch { /* Keep the session preference. */ }
4569
4615
  }
4570
4616
 
4571
- window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, notify, load_decision_input,
4617
+ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field, write_create_preferences, notify, load_decision_input,
4572
4618
  write_launch: write_launch_field, default_launch_entry, description_template, save_description_template, description_draft, write_description_draft, describe_member, set_chain_rounds, continue_chain, expert_presets, apply_expert_preset, unique_instance, member_label, default_name, label_draft, write_label_draft, preset_identity, save_expert_preset, delete_expert_preset, write_room_connection, write_room_adapter, launch_clients: LAUNCH_CLIENTS,
4573
4619
  launch_model_state, load_launch_models, write_launch_model, save_launch_api_key, write_scheduler, save_scheduler_api_key, check_scheduler, write_named_followup, write_create_discussion, write_create_goal, write_create_chain_rounds, check_launch_api, launch_summary, launch_can_set_access, pick_launch_directory, browse_local_directory, list_remote_directories, save_launch_projects, restore_launch_profile, reset_create_form, empty_discussion_message,
4574
4620
  start_drag: start_directory_drag, reconnect: reconnect_live_updates, stop_label, local_request: local_launch_request,
4575
4621
  attachment_markup, attachment_size, attachment_file_icon, drop_attachments, paste_attachments,
4576
4622
  write_expert: write_view_expert, add_instance: add_view_instance, add_preset: add_preset_instance, platform_of, execute: execute_view_action, theme: change_view_theme,
4577
- reload_workflows, workflow_preset, create_workflow_label, write_create_host_workflows,
4623
+ reload_workflows, workflow_preset, write_create_host_workflows,
4578
4624
  admin: { register: register_admin, login: login_admin, logout: logout_admin,
4579
4625
  update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form,
4580
4626
  sessions: admin_sessions, revoke: revoke_admin_sessions, change_password: change_admin_password, login_with_passkey,