@spzxg/cli-ui 0.6.7 → 0.6.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.
package/dist/cli-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-CB7ibAtF.js";
1
+ import { C as a, b as i, d as r, e as t, f as o, h as C, i as l, j as n, k as m, m as d, l as u, n as w, o as B, p as f } from "./cli-ui-embed-CtZDwP8y.js";
2
2
  export {
3
3
  a as CliBoard,
4
4
  i as CliBrowserView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spzxg/cli-ui",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "type": "module",
5
5
  "description": "相互联动的 CliMsgList、CliChat、CliBoard,支持任务中枢和完整 Operator 工作区。",
6
6
  "main": "src/index.ts",
@@ -28,6 +28,9 @@ import { QueuedMessages } from "../ui/QueuedMessages";
28
28
  import { ReferenceCard } from "../ui/ReferenceCard";
29
29
  import type { UploadDocument } from "../core/documentUpload";
30
30
  import { GroupChat, type GroupChatAdapter } from "./GroupChat";
31
+ import { RunControls } from "./RunControls";
32
+ import { useExecutionConfig } from "./useExecutionConfig";
33
+ import { CenterError } from "../core/client";
31
34
 
32
35
  /** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
33
36
  export interface CliChatProps extends CliStoreOptions, ResultHandlers {
@@ -229,7 +232,17 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
229
232
  const task = (shownTaskId ? job?.tasks?.find((t) => t.id === shownTaskId) : undefined) ?? (shownTaskId || batch ? undefined : job?.tasks?.[0]);
230
233
  const info = taskId ? job?.conversation?.conversations?.find((c) => c.task_id === taskId) : undefined;
231
234
  const agent = task?.agent ?? conv?.turns[conv.turns.length - 1]?.agent ?? null;
232
- const agentText = labelOf(agent);
235
+ const draftScope = store.draftScope ? `${store.draftScope}:${key ?? `new:${instanceId}`}` : key ?? `new:${instanceId}`;
236
+ const initialOptions = newConversation ?? store.getNewConversationOptions();
237
+ const execution = useExecutionConfig({ client: store.client, scope: draftScope,
238
+ target: key && !isNew ? key : null, running: running || batchPending,
239
+ enabled: showComposer && (cfg.composer.agentSelector || cfg.composer.executionMode),
240
+ initial: { ...(initialOptions?.agent ? { agent: initialOptions.agent } : {}), ...initialOptions?.executionConfig },
241
+ });
242
+ const agentText = labelOf(execution.config?.agent ?? agent);
243
+ const executionAgent = execution.config?.agents.find(item => item.id === execution.config?.agent);
244
+ const executionReadOnly = !!key && !isNew && !store.client.setExecutionConfig;
245
+ const settingsBusy = !!configurationBusy || execution.blocked;
233
246
  const jobStatus = conv?.job?.status ?? job?.status ?? null;
234
247
  // 对话式子任务没有「结束」:大任务结束了,再发一条会重新打开它。
235
248
  // 支持续派的普通子任务同理:大任务结束不挡输入框。
@@ -306,7 +319,11 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
306
319
  // 本组件的新建参数随每次发送带上:放进共享 Provider 时也用自己的,不依赖仓库是谁建的。
307
320
  const sendOpts = useRef({ newConversation, createConversation, businessIdFor, select: !controlled });
308
321
  useLayoutEffect(() => { sendOpts.current = { newConversation, createConversation, businessIdFor, select: !controlled }; });
322
+ const prepareExecution = useRef(execution.prepareSend);
323
+ const reloadExecution = useRef(execution.reload);
324
+ useLayoutEffect(() => { prepareExecution.current = execution.prepareSend; reloadExecution.current = execution.reload; });
309
325
  const send = useCallback(async (text: string) => {
326
+ const executionOptions = prepareExecution.current();
310
327
  // 一次性任务:发给正在看的那个子任务(接着它聊)。
311
328
  const id = latest.current.conv?.batch ? latest.current.runKey : latest.current.key;
312
329
  // 看板里引用到这个对话的东西:合成一个「看板上下文」块跟着这条消息发出去;发失败就放回去。
@@ -325,9 +342,10 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
325
342
  let target: string;
326
343
  try {
327
344
  if(charCount(full)>MAX_MESSAGE_CHARS)throw new Error("加上看板背景后消息过长,请精简消息内容后重试。");
328
- target = await store.send(id ?? null, full, sendOpts.current);
345
+ target = await store.send(id ?? null, full, { ...sendOpts.current, ...executionOptions });
329
346
  } catch (error) {
330
347
  if (refKey) { store.restoreRefs(refKey, refs); store.restoreViewing(refKey, viewing); }
348
+ if (error instanceof CenterError && error.code === "execution_config_conflict" && latest.current.key === refKey) await reloadExecution.current();
331
349
  throw error;
332
350
  }
333
351
  if (target !== id && latest.current.key === refKey) go(target);
@@ -814,21 +832,33 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
814
832
  </div>
815
833
  )}
816
834
  {slots?.beforeComposer}
835
+ {showComposer && execution.error && <div className="control-error" role="status">{execution.error} {store.client.getExecutionConfig && <button type="button" className="btn btn-ghost btn-sm" disabled={execution.loading || execution.busy} onClick={() => void execution.reload()}>重新读取</button>}</div>}
817
836
  {!showComposer && <QueuedMessages scopeKey={key ?? "new"} messages={queued} onCancel={handlers.onCancelQueued} attachmentUrl={handlers.attachmentUrl} />}
818
837
  {showComposer && (
819
838
  <Composer
820
839
  uploadDocument={uploadDocument}
821
840
  key={key ?? "new"}
822
- draft={store.draftScope ? `${store.draftScope}:${key ?? `new:${instanceId}`}` : key ?? `new:${instanceId}`}
841
+ draft={draftScope}
823
842
  context={(!!shownRefs?.length || !!viewing) && <div className="cli-refs composer-references" aria-label="下一条消息的引用">
824
843
  {viewing && <ReferenceCard key={`${key}:viewing:${viewing.id}`} className="reference-viewing" title={`正在看${viewing.label}`} text={viewing.text} disabled={composerDisabled || !!configurationBusy} removeLabel={`不带上 ${viewing.label}`} onRemove={() => { if (key) store.dismissViewing(key); chatRef.current?.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus({ preventScroll: true }); }} />}
825
844
  {(shownRefs ?? []).map(ref => <ReferenceCard key={`${key}:${ref.id}`} title={ref.kind === "message" && ref.label === "引用你的消息" ? "你" : ref.kind === "message" && ref.label === "引用智能体消息" ? "智能体" : ref.label} text={ref.text} disabled={composerDisabled || !!configurationBusy} removeLabel={`不带上 ${ref.label}`} onRemove={() => { if (key) store.removeRef(key, ref.id); chatRef.current?.querySelector<HTMLTextAreaElement>(".composer textarea")?.focus({ preventScroll: true }); }} />)}
826
845
  </div>}
827
846
  placeholder={fillTemplate(key && !isNew ? cfg.composer.placeholder : cfg.composer.placeholderNew, { title: convTitle, agent: agentText })}
828
- disabled={composerDisabled || !!configurationBusy}
847
+ disabled={composerDisabled || settingsBusy}
829
848
  disabledHint={cfg.composer.placeholderDisabled ? fillTemplate(cfg.composer.placeholderDisabled, { title: convTitle, agent: agentText }) : disabledHint}
830
849
  running={running || batchPending}
831
- toolbar={slots?.composerToolbar}
850
+ toolbar={<>
851
+ <RunControls task={{ id: draftScope, agent: execution.config?.agent ?? agent ?? initialOptions?.agent ?? undefined,
852
+ model: execution.config?.model, reasoning: execution.config?.reasoning, permission_mode: execution.config?.permission_mode }}
853
+ agents={execution.config?.agents} caps={executionAgent?.capabilities} defaults={executionAgent?.defaults}
854
+ showAgent={cfg.composer.agentSelector} showPermission={cfg.composer.executionMode}
855
+ unavailableReason={executionReadOnly ? "当前接入仅支持读取执行设置。" : execution.error ?? (execution.loading ? "正在读取执行设置…" : undefined)}
856
+ disabled={composerDisabled || running || batchPending || !!configurationBusy || execution.loading || executionReadOnly}
857
+ onSetAgent={agent => execution.save({ agent, model: null, reasoning: null, permission_mode: null })}
858
+ onSetModel={model => execution.save({ model })} onSetReasoning={reasoning => execution.save({ reasoning })}
859
+ onSetPermission={permission_mode => execution.save({ permission_mode })} />
860
+ {slots?.composerToolbar}
861
+ </>}
832
862
  queued={<QueuedMessages scopeKey={key ?? "new"} messages={queued} onCancel={handlers.onCancelQueued} attachmentUrl={handlers.attachmentUrl} />}
833
863
  onSend={async text => { if (text.trim() === "/clear" && onClear) { onClear(); return; } const sentFrom = key; await send(text); if (!running && !batchPending && latest.current.key === sentFrom) scrollToBottom("auto"); }}
834
864
  onStop={cfg.composer.stop && key && conv && runningTurn(conv) ? stop : undefined}
@@ -109,10 +109,11 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
109
109
  const tooLong = total > MAX_MESSAGE_CHARS;
110
110
  const reading = pending.some((p) => p.status === "reading");
111
111
  // 运行中不允许排队时,等这一轮结束再发。
112
- const canSend = !!message.trim() && !pending.length && !reading && !tooLong && !sending && (!running || options.queue);
112
+ const canSend = !!message.trim() && !pending.length && !reading && !tooLong && !sending && !stopping && (!running || options.queue);
113
+ const showStop = running && !!onStop && !sending && (stopping || disabled || !options.queue || !message.trim());
113
114
 
114
115
  const submit = async () => {
115
- if (!canSend || disabled || readComposerDraft(draft).sending) return;
116
+ if (!canSend || disabled || stopLock.current || readComposerDraft(draft).sending) return;
116
117
  const submitted = JSON.stringify([val, blocks]);
117
118
  setSending(true);
118
119
  setError(null);
@@ -138,7 +139,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
138
139
  stopLock.current = true; setStopping(true); setError(null);
139
140
  try { await onStop(); } catch (error) { stopLock.current = false; setStopping(false); setError(error instanceof Error ? error.message : "停止失败,请重试"); }
140
141
  }}
141
- disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"}>{Icon.stop()}</button>
142
+ disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"}>{stopping ? <span className="spinner" /> : Icon.stop()}</button>
142
143
  );
143
144
 
144
145
  return (
@@ -224,12 +225,11 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
224
225
  onClick={async () => { if (controlLock.current) return; controlLock.current = true; setControlling(true); try { await taskControl.onToggle(); } catch (error) { setError(error instanceof Error ? error.message : "执行控制失败,请重试"); } finally { controlLock.current = false; setControlling(false); } }}>
225
226
  {taskControl.paused ? Icon.play() : Icon.pause()}<span>{controlling ? "处理中…" : taskControl.paused ? "继续" : "暂停"}</span>
226
227
  </button>}
227
- {(!running || options.queue) && <button type="button" className={`send cli-send${running ? " queue" : ""}`} disabled={disabled || !canSend} aria-busy={sending}
228
+ {showStop ? stopButton : (!running || options.queue) && <button type="button" className={`send cli-send${running ? " queue" : ""}`} disabled={disabled || !canSend} aria-busy={sending}
228
229
  onClick={() => void submit()} aria-label={sending ? "正在发送" : running ? "排队发送" : "发送消息"}
229
- title={taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
230
- {Icon.send()}<span>{sending ? "发送中…" : running ? "排队发送" : "发送"}</span>
230
+ title={sending ? "发送中…" : taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
231
+ {sending ? <span className="spinner" /> : Icon.send()}
231
232
  </button>}
232
- {stopButton}
233
233
  </div>
234
234
  </div>
235
235
  </div>
@@ -21,6 +21,9 @@ export type RunControlProps = {
21
21
  caps?: RunCapabilities;
22
22
  defaults?: { model?: string | null; reasoning?: string | null; permission_mode?: string | null };
23
23
  disabled: boolean;
24
+ showAgent?: boolean;
25
+ showPermission?: boolean;
26
+ unavailableReason?: string;
24
27
  onSetModel: (value: string | null) => Promise<void>;
25
28
  onSetReasoning: (value: string | null) => Promise<void>;
26
29
  onSetPermission: (value: string | null) => Promise<void>;
@@ -29,7 +32,7 @@ export type RunControlProps = {
29
32
 
30
33
  // Both the pre-start view and composer use the same capability-driven controls.
31
34
  // Await persistence before allowing Send so the next turn uses the chosen value.
32
- export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled, onSetModel, onSetReasoning, onSetPermission, onBusyChange }: RunControlProps) {
35
+ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled, showAgent = true, showPermission = true, unavailableReason, onSetModel, onSetReasoning, onSetPermission, onBusyChange }: RunControlProps) {
33
36
  const [open, setOpen] = useState<"model" | "permission" | null>(null);
34
37
  const [modelPage, setModelPage] = useState<"agents" | "settings" | "models">("agents");
35
38
  const [switching, setSwitching] = useState<{ agent: string; saved: boolean } | null>(null);
@@ -79,6 +82,10 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
79
82
  finally { release(); onBusyChange?.(false); }
80
83
  };
81
84
  const locked = disabled || busy || !!switching || !caps;
85
+ const permissionAvailable = !!caps?.permissionModes.length;
86
+ useEffect(() => {
87
+ if ((open === "model" && (!showAgent || !caps)) || (open === "permission" && (!showPermission || !permissionAvailable))) setOpen(null);
88
+ }, [open, showAgent, showPermission, caps, permissionAvailable]);
82
89
  const pickAgent = async (agent: string) => {
83
90
  if (locked || !onSetAgent) return;
84
91
  setError(""); setCustom("");
@@ -108,11 +115,11 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
108
115
  return groups;
109
116
  }, []);
110
117
  return <>
111
- {!!caps?.permissionModes.length && <div className="comp-control">
112
- <button className={`comp-setting permission${effectivePermission === "bypassPermissions" ? " elevated" : ""}`} disabled={locked} aria-label="权限模式" aria-expanded={open === "permission"} aria-haspopup="dialog" title={disabled ? "本轮结束后可修改权限" : permission?.sub} onClick={(e) => { e.stopPropagation(); setError(""); setOpen(open === "permission" ? null : "permission"); }}>
118
+ {showPermission && <div className="comp-control">
119
+ <button className={`comp-setting permission${effectivePermission === "bypassPermissions" ? " elevated" : ""}`} disabled={locked || !permissionAvailable} aria-label="权限模式" aria-expanded={open === "permission"} aria-haspopup="dialog" title={unavailableReason || (!permissionAvailable ? "当前智能体暂未提供执行模式设置" : disabled ? "本轮结束后可修改权限" : permission?.sub)} onClick={(e) => { e.stopPropagation(); setError(""); setOpen(open === "permission" ? null : "permission"); }}>
113
120
  {Icon.lock()} <span>{permission?.value ? permission.label : "默认权限"}</span>{Icon.chevDown()}
114
121
  </button>
115
- {open === "permission" && <Popover onClose={() => setOpen(null)} className="composer-popover composer-permission-popover" ariaLabel="权限模式" placement="top" align="start">
122
+ {open === "permission" && permissionAvailable && <Popover onClose={() => setOpen(null)} className="composer-popover composer-permission-popover" ariaLabel="权限模式" placement="top" align="start">
116
123
  <div className="composer-menu-heading">权限模式</div>
117
124
  <p className="composer-menu-note">保存后用于下一轮</p>
118
125
  {permissions.map((p) => <button key={p.value ?? "default"} className="pop-item composer-option run-permission-option" disabled={locked} aria-pressed={(task.permission_mode ?? null) === p.value} onClick={() => void change(() => onSetPermission(p.value))}>
@@ -124,11 +131,11 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
124
131
  {error && <div className="control-error" role="alert">{error}</div>}
125
132
  </Popover>}
126
133
  </div>}
127
- <div className="comp-control comp-model">
128
- <button className="comp-setting" disabled={locked} aria-label="智能体设置" aria-expanded={open === "model"} aria-haspopup="dialog" title={disabled ? "本轮结束后可修改智能体设置" : `当前智能体:${currentAgent}`} onClick={(e) => { e.stopPropagation(); setError(""); setCustom(""); setModelPage(hasAgentPicker ? "agents" : "settings"); setOpen(open === "model" ? null : "model"); }}>
134
+ {showAgent && <div className="comp-control comp-model">
135
+ <button className="comp-setting" disabled={locked} aria-label="智能体设置" aria-expanded={open === "model"} aria-haspopup="dialog" title={unavailableReason || (!caps ? "当前服务暂未提供智能体设置" : disabled ? "本轮结束后可修改智能体设置" : `当前智能体:${currentAgent}`)} onClick={(e) => { e.stopPropagation(); setError(""); setCustom(""); setModelPage(hasAgentPicker ? "agents" : "settings"); setOpen(open === "model" ? null : "model"); }}>
129
136
  <span className="comp-model-name">{currentAgent}</span>{Icon.chevDown()}
130
137
  </button>
131
- {open === "model" && <Popover onClose={() => setOpen(null)} className="composer-popover composer-agent-popover" ariaLabel={modelPage === "agents" ? "选择智能体" : modelPage === "models" ? "选择模型" : "模型与推理设置"} placement="top" align="end">
138
+ {open === "model" && caps && <Popover onClose={() => setOpen(null)} className="composer-popover composer-agent-popover" ariaLabel={modelPage === "agents" ? "选择智能体" : modelPage === "models" ? "选择模型" : "模型与推理设置"} placement="top" align="end">
132
139
  {modelPage === "agents" ? <div ref={agentListRef} className="run-agent-list">
133
140
  <div className="composer-menu-heading">选择智能体</div>
134
141
  {availableAgents.map(agent => <button key={agent.id} className="pop-item composer-option run-agent-option" disabled={locked || agent.disabled} aria-pressed={task.agent === agent.id} onClick={() => void pickAgent(agent.id)}>
@@ -184,6 +191,6 @@ export function RunControls({ task, agents, onSetAgent, caps, defaults, disabled
184
191
  {(busy || switching) && <div className="composer-menu-note" role="status">{switching ? "正在切换智能体…" : "正在保存…"}</div>}
185
192
  {error && <div className="control-error" role="alert">{error}</div>}
186
193
  </Popover>}
187
- </div>
194
+ </div>}
188
195
  </>;
189
196
  }
package/src/chat/draft.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { useSyncExternalStore } from "react";
2
2
  import type { TextBlock } from "../core/blocks";
3
3
  import type { UploadPhase } from "../core/documentUpload";
4
+ import type { ExecutionOptions } from "../core/execution";
4
5
  export type PendingFile = { key: string; name: string; status: "reading" | "error"; error?: string; progress?: string; phase?: UploadPhase; bytes?: number; size?: number };
5
- export interface ComposerDraft { text: string; blocks: Array<TextBlock & { key: string }>; pending: PendingFile[]; sending: boolean; error: string | null; revision: number }
6
+ export interface ComposerDraft { text: string; blocks: Array<TextBlock & { key: string }>; pending: PendingFile[]; sending: boolean; error: string | null; revision: number; executionConfig?: ExecutionOptions; executionUncertain?: boolean }
6
7
  const empty = (): ComposerDraft => ({ text: "", blocks: [], pending: [], sending: false, error: null, revision: 0 });
7
8
  const server = empty();
8
9
  const entries = new Map<string, { state: ComposerDraft; stamp: string }>();
@@ -18,6 +19,10 @@ export function readComposerDraft(id: string): ComposerDraft {
18
19
  state.text = localStorage.getItem(textKey(id)) ?? "";
19
20
  const data = JSON.parse(localStorage.getItem(dataKey(id)) ?? "null");
20
21
  if (data?.version === 1) {
22
+ if (data.executionUncertain === true) state.executionUncertain = true;
23
+ if (data.executionConfig && typeof data.executionConfig === "object") state.executionConfig = Object.fromEntries(
24
+ Object.entries(data.executionConfig).filter(([key, value]) => ["agent", "model", "reasoning", "permission_mode"].includes(key) && (typeof value === "string" || value === null)),
25
+ );
21
26
  state.blocks = Array.isArray(data.blocks) ? data.blocks.filter((b: TextBlock & { key: string }) => typeof b?.text === "string" && typeof b?.key === "string") : [];
22
27
  state.pending = Array.isArray(data.pending) ? data.pending.map((p: PendingFile) => ({ ...p, status: "error", error: p.phase
23
28
  ? `${p.error || "页面重载中断了上传。"}${p.error?.includes("重新选择文件") ? "" : " 请重新选择文件。"}`
@@ -32,7 +37,7 @@ export function updateComposerDraft(id: string, update: (state: ComposerDraft) =
32
37
  const state = { ...update(previous), revision: previous.revision + 1 };
33
38
  try {
34
39
  if (state.text) localStorage.setItem(textKey(id), state.text); else localStorage.removeItem(textKey(id));
35
- if (state.blocks.length || state.pending.length) localStorage.setItem(dataKey(id), JSON.stringify({ version: 1, blocks: state.blocks, pending: state.pending })); else localStorage.removeItem(dataKey(id));
40
+ if (state.blocks.length || state.pending.length || state.executionConfig || state.executionUncertain) localStorage.setItem(dataKey(id), JSON.stringify({ version: 1, blocks: state.blocks, pending: state.pending, executionConfig: state.executionConfig, executionUncertain: state.executionUncertain })); else localStorage.removeItem(dataKey(id));
36
41
  } catch { /* Keep the in-memory draft even if storage is full. */ }
37
42
  entries.set(id, { state, stamp: stamp(id) });
38
43
  listeners.forEach(listener => listener());
@@ -0,0 +1,93 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { CenterError, type ConversationBackend } from "../core/client";
3
+ import type { ExecutionConfiguration, ExecutionOptions } from "../core/execution";
4
+ import { isConfigurationBusy, useConfigurationBusy } from "./configuration";
5
+ import { readComposerDraft, updateComposerDraft, useComposerDraft } from "./draft";
6
+
7
+ const errorText = (error: unknown) => error instanceof CenterError && [404, 405, 501].includes(error.status)
8
+ ? "当前服务尚未支持执行设置,请升级配套服务。"
9
+ : error instanceof Error ? error.message : "执行设置读取失败,请重试。";
10
+
11
+ function validate(result: ExecutionConfiguration) {
12
+ if (!result || typeof result.agent !== "string" || !Number.isSafeInteger(result.revision) || result.revision < 0 || !Array.isArray(result.agents)
13
+ || result.agents.some(agent => typeof agent?.id !== "string" || typeof agent.label !== "string" || !agent.capabilities
14
+ || [agent.capabilities.models, agent.capabilities.reasoningOptions, agent.capabilities.permissionModes].some(options => !Array.isArray(options)
15
+ || options.some(option => typeof option?.value !== "string" || typeof option.label !== "string")))) {
16
+ throw new Error("服务未返回有效的执行设置,请检查配套服务版本。");
17
+ }
18
+ }
19
+
20
+ /** 按对话隔离读取与保存,不轮询;换对话后的迟到响应不覆盖当前选择。 */
21
+ export function useExecutionConfig({ client, scope, target, enabled, initial, running }: {
22
+ client: ConversationBackend; scope: string; target: string | null; enabled: boolean;
23
+ initial?: ExecutionOptions; running: boolean;
24
+ }) {
25
+ const [view, setView] = useState<{ scope: string; config?: ExecutionConfiguration; error?: string; loading: boolean; saveFailed?: boolean }>({ scope, loading: false });
26
+ const current = useRef(scope);
27
+ const request = useRef(0);
28
+ const mounted = useRef(true);
29
+ useLayoutEffect(() => { current.current = scope; request.current++; }, [scope]);
30
+ useEffect(() => { mounted.current = true; return () => { mounted.current = false; request.current++; }; }, []);
31
+ const draft = useComposerDraft(scope);
32
+ const busy = useConfigurationBusy(scope);
33
+ const config = view.scope === scope ? view.config : undefined;
34
+ const reload = useCallback(async () => {
35
+ const serial = ++request.current;
36
+ const active = () => mounted.current && current.current === scope && request.current === serial;
37
+ if (!enabled && !readComposerDraft(scope).executionUncertain) return;
38
+ if (!client.getExecutionConfig) {
39
+ setView({ scope, loading: false, error: "当前接入尚未提供执行设置。" });
40
+ return;
41
+ }
42
+ setView(value => ({ ...(value.scope === scope ? value : {}), scope, loading: true }));
43
+ try {
44
+ const result = await client.getExecutionConfig(target);
45
+ validate(result);
46
+ if (active()) {
47
+ if (!isConfigurationBusy(scope) && readComposerDraft(scope).executionUncertain) updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: false }));
48
+ setView({ scope, config: result, loading: false });
49
+ }
50
+ } catch (error) {
51
+ if (active()) setView(value => ({ ...(value.scope === scope ? value : {}), scope, error: errorText(error), loading: false }));
52
+ }
53
+ }, [client, scope, target, enabled]);
54
+ useEffect(() => { void reload(); return () => { request.current++; }; }, [reload]);
55
+ const wasRunning = useRef(running);
56
+ useEffect(() => {
57
+ if (wasRunning.current && !running && !isConfigurationBusy(scope)) void reload();
58
+ wasRunning.current = running;
59
+ }, [running, reload, scope]);
60
+
61
+ const local = target === null ? { ...initial, ...draft.executionConfig } : undefined;
62
+ const selected = config ? { ...config, ...local } : undefined;
63
+ const save = async (changes: ExecutionOptions) => {
64
+ if (!config || !enabled || running) throw new Error("当前无法修改执行设置,请稍后重试。");
65
+ if (target === null) {
66
+ updateComposerDraft(scope, value => ({ ...value, executionConfig: { agent: config.agent, ...initial, ...value.executionConfig, ...changes } }));
67
+ return;
68
+ }
69
+ if (!client.setExecutionConfig) throw new Error("当前接入尚未提供保存执行设置的接口。");
70
+ // An earlier GET must not overwrite the response to this save.
71
+ const serial = ++request.current;
72
+ updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: true }));
73
+ try {
74
+ const result = await client.setExecutionConfig(target, changes, config.revision);
75
+ validate(result);
76
+ updateComposerDraft(scope, draft => ({ ...draft, executionUncertain: false }));
77
+ if (mounted.current && current.current === scope && request.current === serial) setView({ scope, config: result, loading: false });
78
+ } catch (error) {
79
+ if (mounted.current && current.current === scope && request.current === serial) setView({ scope, config, loading: false, saveFailed: true, error: `${errorText(error)} 请重新读取设置后再发送。` });
80
+ throw error;
81
+ }
82
+ };
83
+ return {
84
+ config: selected, busy, loading: view.scope === scope && view.loading,
85
+ error: view.scope === scope && (enabled || view.saveFailed || draft.executionUncertain) ? view.error ?? (draft.executionUncertain && !busy ? "执行设置尚未确认,请重新读取后再发送。" : undefined) : undefined,
86
+ blocked: busy || !!draft.executionUncertain || (view.scope === scope && !!view.saveFailed), reload, save,
87
+ /** Read at submit time, including changes committed before React paints. */
88
+ prepareSend: () => {
89
+ if (isConfigurationBusy(scope) || readComposerDraft(scope).executionUncertain || (view.scope === scope && view.saveFailed)) throw new Error("请等待执行设置保存或重新读取设置后再发送。");
90
+ return target === null ? { executionConfig: readComposerDraft(scope).executionConfig } : { expectedExecutionRevision: config?.revision };
91
+ },
92
+ };
93
+ }
@@ -9,5 +9,5 @@ export interface CliMsgListProps extends CenterMsgListProps { operator?: Compone
9
9
  export interface CliChatProps extends CenterChatProps { operator?: ComponentProps<typeof SessionView> }
10
10
  export interface CliBoardProps extends CenterBoardProps { operator?: ComponentProps<typeof SessionRail> }
11
11
  export function CliMsgList(props: CliMsgListProps) { return props.operator ? <TasksColumn {...props.operator}/> : <CenterMsgList {...props}/>; }
12
- export function CliChat(props: CliChatProps) { return props.operator ? <SessionView {...props.operator} board={{...props.operator.board,autoContext:props.operator.board?.autoContext??props.passiveContext,onFilesChange:props.operator.board?.onFilesChange??props.onFilesChange,onFilesWatch:props.operator.board?.onFilesWatch??props.onFilesWatch,onBoardContextChange:props.operator.board?.onBoardContextChange??props.onBoardContextChange}}/> : <CenterChat {...props}/>; }
12
+ export function CliChat(props: CliChatProps) { return props.operator ? <SessionView {...props.operator} composer={props.config?.composer ?? props.operator.composer} board={{...props.operator.board,autoContext:props.operator.board?.autoContext??props.passiveContext,onFilesChange:props.operator.board?.onFilesChange??props.onFilesChange,onFilesWatch:props.operator.board?.onFilesWatch??props.onFilesWatch,onBoardContextChange:props.operator.board?.onBoardContextChange??props.onBoardContextChange}}/> : <CenterChat {...props}/>; }
13
13
  export function CliBoard(props: CliBoardProps) { return props.operator ? <SessionRail key={props.operator.task.id} {...props.operator} tabs={props.operator.tabs ?? props.tabs} availableTabs={props.operator.availableTabs ?? props.availableTabs} defaultTab={props.operator.defaultTab ?? props.defaultTab} autoContext={props.operator.autoContext ?? props.autoQuoteOpenFile} watchFiles={props.operator.watchFiles ?? props.watchFiles} fileWatchIntervalMs={props.operator.fileWatchIntervalMs ?? props.fileWatchIntervalMs} onFilesChange={props.operator.onFilesChange ?? props.onFilesChange} onFilesWatch={props.operator.onFilesWatch ?? props.onFilesWatch} onBoardContextChange={props.operator.onBoardContextChange ?? props.onBoardContextChange} onEvent={props.operator.onEvent ?? props.onEvent}/> : <CenterBoard {...props}/>; }
@@ -8,6 +8,7 @@ import { decodeText } from "./textDecode";
8
8
  import { createDocumentUploader, extractDocumentReferences, documentRequest, isTaskUploadPath, type UploadDocument, type DocumentUploadOptions, type DocumentReceipt } from "./documentUpload";
9
9
  import { syncFile, type FileSyncTransport } from "../board/fileSync";
10
10
  import { createFileSyncTransport } from "../board/fileSyncTransport";
11
+ import type { ExecutionConfiguration, ExecutionOptions } from "./execution";
11
12
 
12
13
  /**
13
14
  * center 服务(cli-task-center)对话任务接口的浏览器端客户端。组件的数据全部从这里来。
@@ -95,6 +96,10 @@ const ERROR_TEXT: Record<string, string> = {
95
96
  * 带 ? 的是可选能力:后端不提供,界面就按 Operator 在「没有这个能力」时的样子显示(例如提问的回答改走下一条消息)。
96
97
  */
97
98
  export interface ConversationBackend {
99
+ /** 无 key 时读取新对话可用设置;有 key 时读取该对话的已保存设置。 */
100
+ getExecutionConfig?(key?: string | null): Promise<ExecutionConfiguration>;
101
+ /** 保存成功才切换界面;服务端用 revision 防止并发覆盖。 */
102
+ setExecutionConfig?(key: string, changes: ExecutionOptions, expectedRevision: number): Promise<ExecutionConfiguration>;
98
103
  /** Upload original HTML/Markdown without embedding document bytes in a message. */
99
104
  uploadDocument?: UploadDocument;
100
105
  baseUrl: string;
@@ -109,7 +114,7 @@ export interface ConversationBackend {
109
114
  history?(jobId: string, remote: string, before: string, limit?: number): Promise<{ messages: Message[]; older: number }>;
110
115
  createConversation(firstMessage: string, opts?: NewConversationOptions & { businessId?: string }): Promise<{ id: string; job_id: string; status: string; task_ids?: string[]; turn: CenterTurn }>;
111
116
  /** taskId:发给对话任务里的哪个对话,不给 = 第一个。 */
112
- sendMessage(jobId: string, text: string, opts?: { businessId?: string; timeoutSeconds?: number; taskId?: string }): Promise<SendResult>;
117
+ sendMessage(jobId: string, text: string, opts?: { businessId?: string; timeoutSeconds?: number; taskId?: string; expectedExecutionRevision?: number }): Promise<SendResult>;
113
118
  /**
114
119
  * 一次性任务里的对话式子任务接着聊:续派一条 mode=conversation、continue_from 这个子任务的子任务(接在同一个 Agent 会话里)。
115
120
  * 返回新子任务的 id。
@@ -119,7 +124,7 @@ export interface ConversationBackend {
119
124
  pauseTask?(taskId: string): Promise<CenterTask>;
120
125
  resumeTask?(taskId: string): Promise<CenterTask>;
121
126
  /** 在对话任务里新开一个对话(同一节点、同一工作区),text 是它的第一条消息。 */
122
- openConversation?(jobId: string, text: string, opts?: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number }): Promise<SendResult>;
127
+ openConversation?(jobId: string, text: string, opts?: { title?: string; agent?: string; executionConfig?: ExecutionOptions; browser?: boolean; businessId?: string; timeoutSeconds?: number }): Promise<SendResult>;
123
128
  cancelTurn(jobId: string, turnId: string): Promise<CenterTurn>;
124
129
  closeConversation(jobId: string): Promise<{ id: string; status: string }>;
125
130
  /** 只关掉对话任务里的一个对话(别的对话和对话任务照常)。 */
@@ -297,9 +302,18 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
297
302
 
298
303
  const post = <T>(path: string, body: unknown = {}) => request<T>(path, { method: "POST", body: JSON.stringify(body) });
299
304
  const id = encodeURIComponent;
305
+ const executionPath = (key?: string | null) => {
306
+ if (!key) return "/api/execution-config";
307
+ const ref = parseConvKey(key);
308
+ return `/api/jobs/${id(ref.jobId)}/execution-config${ref.taskId && ref.taskId !== NEW_TASK ? `?task_id=${id(ref.taskId)}` : ""}`;
309
+ };
300
310
 
301
311
  return {
302
312
  baseUrl: base,
313
+ getExecutionConfig: key => request<ExecutionConfiguration>(executionPath(key)),
314
+ setExecutionConfig: (key, changes, expectedRevision) => request<ExecutionConfiguration>(executionPath(key), {
315
+ method: "PATCH", body: JSON.stringify({ ...changes, expected_revision: expectedRevision }),
316
+ }),
303
317
  uploadDocument: createDocumentUploader(doFetch, `${base}/api/file-sync`, headers),
304
318
  getJob: (jobId: string) => request<CenterJob>(`/api/jobs/${id(jobId)}`),
305
319
  listTurns: (jobId: string, query: { afterSeq?: number; limit?: number; taskId?: string } = {}) => {
@@ -345,6 +359,7 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
345
359
  ...(extractDocumentReferences(firstMessage).attachments ? { attachments: extractDocumentReferences(firstMessage).attachments } : {}),
346
360
  ...(opts.businessId ? { business_id: opts.businessId } : {}),
347
361
  ...(opts.agent ? { agent: opts.agent } : {}),
362
+ ...(opts.executionConfig ? { execution_config: opts.executionConfig } : {}),
348
363
  ...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
349
364
  ...(opts.browser !== undefined ? { browser: opts.browser } : {}),
350
365
  ...(opts.browserPlaybook !== undefined ? { browser_playbook: opts.browserPlaybook } : {}),
@@ -353,10 +368,11 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
353
368
  });
354
369
  },
355
370
  /** 带 business_id 时 center 按它去重,网络超时可以放心重发同一条。 */
356
- sendMessage: (jobId: string, text: string, opts: { businessId?: string; timeoutSeconds?: number; taskId?: string } = {}) =>
371
+ sendMessage: (jobId: string, text: string, opts: { businessId?: string; timeoutSeconds?: number; taskId?: string; expectedExecutionRevision?: number } = {}) =>
357
372
  post<SendResult>(`/api/jobs/${id(jobId)}/messages`, {
358
373
  ...extractDocumentReferences(text),
359
374
  ...(opts.taskId ? { task_id: opts.taskId } : {}),
375
+ ...(opts.expectedExecutionRevision !== undefined ? { expected_execution_revision: opts.expectedExecutionRevision } : {}),
360
376
  ...(opts.businessId ? { business_id: opts.businessId } : {}),
361
377
  ...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
362
378
  }),
@@ -370,11 +386,12 @@ export function createCenterClient(options: CenterClientOptions = {}): Conversat
370
386
  pauseTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/pause`, {}),
371
387
  resumeTask: (taskId: string) => post<CenterTask>(`/api/tasks/${id(taskId)}/resume`, {}),
372
388
  /** 同一个 business_id 重发只开一个对话。 */
373
- openConversation: (jobId: string, text: string, opts: { title?: string; agent?: string; browser?: boolean; businessId?: string; timeoutSeconds?: number } = {}) =>
389
+ openConversation: (jobId: string, text: string, opts: { title?: string; agent?: string; executionConfig?: ExecutionOptions; browser?: boolean; businessId?: string; timeoutSeconds?: number } = {}) =>
374
390
  post<SendResult>(`/api/jobs/${id(jobId)}/conversations`, {
375
391
  ...extractDocumentReferences(text),
376
392
  ...(opts.title ? { title: opts.title } : {}),
377
393
  ...(opts.agent ? { agent: opts.agent } : {}),
394
+ ...(opts.executionConfig ? { execution_config: opts.executionConfig } : {}),
378
395
  ...(opts.browser !== undefined ? { browser: opts.browser } : {}),
379
396
  ...(opts.businessId ? { business_id: opts.businessId } : {}),
380
397
  ...(opts.timeoutSeconds ? { timeout_seconds: opts.timeoutSeconds } : {}),
@@ -45,6 +45,10 @@ export interface CliChatConfig {
45
45
  charCount?: boolean;
46
46
  /** 文件上传(按钮、拖拽、粘贴文件)。只收文档:Word、Excel、Markdown、TXT、HTML、CSV。 */
47
47
  fileUpload?: boolean;
48
+ /** 智能体选择入口,默认显示;缺少服务端能力时禁用。 */
49
+ agentSelector?: boolean;
50
+ /** 执行模式选择入口,默认显示;缺少服务端能力时禁用。 */
51
+ executionMode?: boolean;
48
52
  /** 「文件」按钮的文字。 */
49
53
  fileButtonText?: string;
50
54
  /** 执行控制:普通对话停止当前轮次,一次性子任务暂停 / 继续。 */
@@ -87,7 +91,7 @@ export interface ResolvedChatConfig {
87
91
  header: { show: boolean; crumb: boolean; export: boolean; title: boolean; agent: boolean; usage: boolean; model: boolean; status: boolean; close: boolean };
88
92
  composer: {
89
93
  show: boolean; placeholder: string; placeholderNew: string; placeholderRunning: string; placeholderDisabled: string | null;
90
- hints: boolean; charCount: boolean; fileUpload: boolean; fileButtonText: string; stop: boolean; queue: boolean;
94
+ hints: boolean; charCount: boolean; fileUpload: boolean; agentSelector: boolean; executionMode: boolean; fileButtonText: string; stop: boolean; queue: boolean;
91
95
  };
92
96
  transcript: { nav: boolean; sessionLabel: boolean; timestamps: boolean; initialPrompt: boolean; initialBadge: boolean; tools: boolean; runningIndicator: boolean; status: boolean };
93
97
  labels: { user: string; agent: string };
@@ -108,7 +112,7 @@ export const DEFAULT_CHAT_CONFIG: ResolvedChatConfig = {
108
112
  placeholderNew: "输入消息,开始新对话…",
109
113
  placeholderRunning: "排队一条后续消息…(本轮结束后发送)",
110
114
  placeholderDisabled: null,
111
- hints: true, charCount: true, fileUpload: true, fileButtonText: "文件", stop: true, queue: true,
115
+ hints: false, charCount: false, fileUpload: true, agentSelector: true, executionMode: true, fileButtonText: "", stop: true, queue: true,
112
116
  },
113
117
  transcript: { nav: true, sessionLabel: true, timestamps: true, initialPrompt: false, initialBadge: true, tools: true, runningIndicator: true, status: false },
114
118
  labels: { user: "你", agent: "智能体" },
@@ -0,0 +1,24 @@
1
+ import type { RunCapabilities } from "../chat/RunControls";
2
+
3
+ /** 用户选择的下一轮执行设置;不包含账号或凭据。null 表示使用服务端默认值。 */
4
+ export interface ExecutionOptions {
5
+ agent?: string;
6
+ model?: string | null;
7
+ reasoning?: string | null;
8
+ permission_mode?: string | null;
9
+ }
10
+
11
+ export interface ExecutionConfiguration {
12
+ revision: number;
13
+ agent: string;
14
+ model: string | null;
15
+ reasoning: string | null;
16
+ permission_mode: string | null;
17
+ agents: Array<{
18
+ id: string;
19
+ label: string;
20
+ disabled?: boolean;
21
+ capabilities: RunCapabilities;
22
+ defaults?: Omit<ExecutionOptions, "agent">;
23
+ }>;
24
+ }
package/src/core/store.ts CHANGED
@@ -153,7 +153,7 @@ export interface CliStoreOptions extends CenterClientOptions {
153
153
  */
154
154
  newConversation?: NewConversationOptions;
155
155
  /** 自己建对话(例如经业务后端建,以便把回调密钥留在服务端):返回 center 的 job_id。 */
156
- createConversation?: (firstMessage: string) => Promise<string>;
156
+ createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
157
157
  /**
158
158
  * 每条消息的 business_id(center 按它去重,回调里原样带回)。缺省 `cli-<uuid>`。
159
159
  * kind = "create" 是第一条(新建对话),"message" 是之后的每条。
@@ -163,10 +163,12 @@ export interface CliStoreOptions extends CenterClientOptions {
163
163
 
164
164
  /** 发一条消息时可以单独指定的新建参数(组件自己的 props),优先于仓库的。 */
165
165
  export interface SendOptions {
166
+ expectedExecutionRevision?: number;
167
+ executionConfig?: import("./execution").ExecutionOptions;
166
168
  /** 新建对话后是否改仓库的选中(缺省改)。 */
167
169
  select?: boolean;
168
170
  newConversation?: NewConversationOptions;
169
- createConversation?: (firstMessage: string) => Promise<string>;
171
+ createConversation?: (firstMessage: string, options?: NewConversationOptions) => Promise<string>;
170
172
  businessIdFor?: (text: string, kind: "create" | "message") => string;
171
173
  }
172
174
 
@@ -601,7 +603,8 @@ export function createCliStore(options: CliStoreOptions = {}) {
601
603
 
602
604
  async function sendWithText(key: string | null, text: string, overrides: SendOptions): Promise<string> {
603
605
  // 发消息的组件自己的参数优先(同一个仓库里可以有几个对话组件,各带各的新建参数),没有再用仓库的。
604
- const newConversation = overrides.newConversation ?? options.newConversation;
606
+ const originalOptions = overrides.newConversation ?? options.newConversation;
607
+ const newConversation = overrides.executionConfig ? { ...originalOptions, agent: overrides.executionConfig.agent ?? originalOptions?.agent, executionConfig: overrides.executionConfig } : originalOptions;
605
608
  const createConversation = overrides.createConversation ?? options.createConversation;
606
609
  const businessIdFor = overrides.businessIdFor ?? options.businessIdFor;
607
610
  const ref = key ? parseConvKey(key) : null;
@@ -610,7 +613,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
610
613
  if (ref?.taskId === NEW_TASK) {
611
614
  // 在对话任务里新开一个对话:标题取第一行,智能体 / 浏览器模式沿用对话任务的第一个对话(center 决定)。
612
615
  if (!client.openConversation) throw new CenterError("当前后端不支持在对话任务里新开对话", "UNSUPPORTED", 0);
613
- const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId }));
616
+ const result = await retryOnce(() => client.openConversation!(ref.jobId, text, { title: firstLine(text, 60), businessId, agent: newConversation?.agent, executionConfig: newConversation?.executionConfig, browser: newConversation?.browser, timeoutSeconds: newConversation?.timeoutSeconds }));
614
617
  const target = convKey(result.job_id, result.task_id ?? result.turn.task_id);
615
618
  patchConv(target, (conv) => ({ ...conv, turns: addTurnsIfAbsent(conv.turns, [{ ...result.turn, queue_position: result.queue_position }]) }));
616
619
  addJobIfMissing(result.job_id);
@@ -623,7 +626,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
623
626
  const selectedBeforeCreate = state.selected;
624
627
  let newId: string;
625
628
  if (createConversation) {
626
- newId = await createConversation(text);
629
+ newId = newConversation?.executionConfig ? await createConversation(text, newConversation) : await createConversation(text);
627
630
  } else {
628
631
  // 新建只发一次:center 的 POST /api/jobs 没有幂等,建好了却没收到响应时重发会建出第二个对话。
629
632
  const created = await client.createConversation(text, { ...newConversation, title: newConversation?.title || firstLine(text), businessId: newConversation?.businessId || businessId });
@@ -653,7 +656,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
653
656
  void refreshJob(ref.jobId, true);
654
657
  return key;
655
658
  }
656
- const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}) }));
659
+ const result = await retryOnce(() => client.sendMessage(ref.jobId, text, { businessId, ...(ref.taskId ? { taskId: ref.taskId } : {}), ...(overrides.expectedExecutionRevision !== undefined ? { expectedExecutionRevision: overrides.expectedExecutionRevision } : {}) }));
657
660
  const jobTarget = result.job_id || ref.jobId;
658
661
  // 指定了对话的:接续后跟到新对话任务里对应的那个对话;只有 job_id 的照旧只换 job。
659
662
  const target = ref.taskId ? convKey(jobTarget, result.task_id ?? result.turn.task_id ?? ref.taskId) : jobTarget;
@@ -959,6 +962,7 @@ export function createCliStore(options: CliStoreOptions = {}) {
959
962
 
960
963
  return {
961
964
  client,
965
+ getNewConversationOptions: () => options.newConversation,
962
966
  draftScope: connectionDraftScope(options),
963
967
  getState: () => state,
964
968
  /** 在看板里打开文件 / 浏览器 / 改动。返回有没有看板接住。 */
package/src/core/types.ts CHANGED
@@ -131,6 +131,8 @@ export interface BrowserPacing {
131
131
 
132
132
  /** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
133
133
  export interface NewConversationOptions {
134
+ /** 首轮执行配置。由服务端校验、持久化,并用于后续派发和重试。 */
135
+ executionConfig?: import("./execution").ExecutionOptions;
134
136
  title?: string;
135
137
  /** 业务自己的 ID(tasks[0].business_id):第一轮的回调里原样带回,用来和业务数据对应。 */
136
138
  businessId?: string;
package/src/index.ts CHANGED
@@ -43,6 +43,7 @@ export {
43
43
  type TextBlock, type MessagePart,
44
44
  } from "./core/blocks";
45
45
  export type * from "./core/types";
46
+ export type { ExecutionOptions, ExecutionConfiguration } from "./core/execution";
46
47
 
47
48
 
48
49
  export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";