@spzxg/cli-ui 0.6.8 → 0.6.10

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-Be9_d9jB.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-CbxWozLO.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.8",
3
+ "version": "0.6.10",
4
4
  "type": "module",
5
5
  "description": "相互联动的 CliMsgList、CliChat、CliBoard,支持任务中枢和完整 Operator 工作区。",
6
6
  "main": "src/index.ts",
@@ -234,9 +234,11 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
234
234
  const agent = task?.agent ?? conv?.turns[conv.turns.length - 1]?.agent ?? null;
235
235
  const draftScope = store.draftScope ? `${store.draftScope}:${key ?? `new:${instanceId}`}` : key ?? `new:${instanceId}`;
236
236
  const initialOptions = newConversation ?? store.getNewConversationOptions();
237
+ const executionReady = !batch && (!key || !!job);
238
+ const executionEnabled = showComposer && executionReady && (cfg.composer.agentSelector || cfg.composer.executionMode);
237
239
  const execution = useExecutionConfig({ client: store.client, scope: draftScope,
238
240
  target: key && !isNew ? key : null, running: running || batchPending,
239
- enabled: showComposer && (cfg.composer.agentSelector || cfg.composer.executionMode),
241
+ enabled: executionEnabled, ready: executionReady,
240
242
  initial: { ...(initialOptions?.agent ? { agent: initialOptions.agent } : {}), ...initialOptions?.executionConfig },
241
243
  });
242
244
  const agentText = labelOf(execution.config?.agent ?? agent);
@@ -852,8 +854,8 @@ function ChatInner({ slots, passiveContext = true, configurationBusy, onClear, j
852
854
  model: execution.config?.model, reasoning: execution.config?.reasoning, permission_mode: execution.config?.permission_mode }}
853
855
  agents={execution.config?.agents} caps={executionAgent?.capabilities} defaults={executionAgent?.defaults}
854
856
  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
+ unavailableReason={batch ? "一次性任务沿用任务执行设置,不支持在对话中修改。" : executionReadOnly ? "当前接入仅支持读取执行设置。" : execution.error ?? (!executionEnabled || execution.loading ? "正在读取执行设置…" : undefined)}
858
+ disabled={!executionEnabled || composerDisabled || running || batchPending || !!configurationBusy || execution.loading || executionReadOnly}
857
859
  onSetAgent={agent => execution.save({ agent, model: null, reasoning: null, permission_mode: null })}
858
860
  onSetModel={model => execution.save({ model })} onSetReasoning={reasoning => execution.save({ reasoning })}
859
861
  onSetPermission={permission_mode => execution.save({ permission_mode })} />
@@ -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);
@@ -210,7 +211,7 @@ export function Composer({ draft, placeholder, disabled, disabledHint, running,
210
211
  {options.fileUpload && !disabled && <>
211
212
  <input ref={fileRef} type="file" accept={uploadDocument ? FILE_UPLOAD_ACCEPT : ACCEPT_ATTR} multiple hidden onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }} />
212
213
  <button type="button" className="cli-attach" disabled={sending} aria-label={uploadDocument ? "添加文件" : "添加文档"} title={uploadDocument ? "添加文件(也可拖拽进来);HTML、Markdown 最大 100 MiB,其他支持类型最大 10 MiB" : `添加文档:${ACCEPTED_LABEL}(也可拖拽进来)`}
213
- onClick={() => fileRef.current?.click()}>{Icon.plus()} <span>{options.fileButtonText}</span></button>
214
+ onClick={() => fileRef.current?.click()}>{Icon.plus()}{options.fileButtonText && <span>{options.fileButtonText}</span>}</button>
214
215
  </>}
215
216
  {toolbar}
216
217
  {options.hints && !disabled && <span className="hint comp-hint-extra">Shift + Enter 换行</span>}
@@ -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 && (!!message.trim() || sending || !onStop))) && <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
230
  title={sending ? "发送中…" : taskControl?.paused ? "排队发送,继续任务后依次执行" : running ? "排队发送,当前轮次结束后执行" : "发送消息(Enter)"}>
230
231
  {sending ? <span className="spinner" /> : Icon.send()}
231
232
  </button>}
232
- {stopButton}
233
233
  </div>
234
234
  </div>
235
235
  </div>
@@ -18,9 +18,9 @@ function validate(result: ExecutionConfiguration) {
18
18
  }
19
19
 
20
20
  /** 按对话隔离读取与保存,不轮询;换对话后的迟到响应不覆盖当前选择。 */
21
- export function useExecutionConfig({ client, scope, target, enabled, initial, running }: {
21
+ export function useExecutionConfig({ client, scope, target, enabled, ready = true, initial, running }: {
22
22
  client: ConversationBackend; scope: string; target: string | null; enabled: boolean;
23
- initial?: ExecutionOptions; running: boolean;
23
+ initial?: ExecutionOptions; running: boolean; ready?: boolean;
24
24
  }) {
25
25
  const [view, setView] = useState<{ scope: string; config?: ExecutionConfiguration; error?: string; loading: boolean; saveFailed?: boolean }>({ scope, loading: false });
26
26
  const current = useRef(scope);
@@ -34,7 +34,7 @@ export function useExecutionConfig({ client, scope, target, enabled, initial, ru
34
34
  const reload = useCallback(async () => {
35
35
  const serial = ++request.current;
36
36
  const active = () => mounted.current && current.current === scope && request.current === serial;
37
- if (!enabled && !readComposerDraft(scope).executionUncertain) return;
37
+ if (!ready || (!enabled && !readComposerDraft(scope).executionUncertain)) return;
38
38
  if (!client.getExecutionConfig) {
39
39
  setView({ scope, loading: false, error: "当前接入尚未提供执行设置。" });
40
40
  return;
@@ -50,7 +50,7 @@ export function useExecutionConfig({ client, scope, target, enabled, initial, ru
50
50
  } catch (error) {
51
51
  if (active()) setView(value => ({ ...(value.scope === scope ? value : {}), scope, error: errorText(error), loading: false }));
52
52
  }
53
- }, [client, scope, target, enabled]);
53
+ }, [client, scope, target, enabled, ready]);
54
54
  useEffect(() => { void reload(); return () => { request.current++; }; }, [reload]);
55
55
  const wasRunning = useRef(running);
56
56
  useEffect(() => {
@@ -196,14 +196,15 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
196
196
  const pendingFork=sources.length&&draft.forkRequest?.signature===currentSignature?draft.forkRequest:null;
197
197
  const payload=pendingFork?pendingFork.payload??ownPayload:ownPayload&&!isClear?appendBoardContext(ownPayload,boardView,task.id):ownPayload;
198
198
  const tooLong = payload.length > MAX_MESSAGE_CHARS;
199
- const canSend = !!payload && !uploading && !failed && !tooLong && !locked;
199
+ const canSend = !!payload && !uploading && !failed && !tooLong && !locked && !stopping;
200
+ const showStop = running && !sending && (stopping || locked || !payload);
200
201
  const clear = () => {
201
202
  if (running || locked || task.started !== 1) return;
202
203
  onClear(); setSlash(false);
203
204
  if (draft.text.trim() === "/clear") update((d) => ({ ...d, text: "" }));
204
205
  };
205
206
  const submit = async () => {
206
- if (!canSend || sendingRef.current || isTaskSending(task.id) || isConfigurationBusy(task.id)) return;
207
+ if (!canSend || stoppingRef.current || sendingRef.current || isTaskSending(task.id) || isConfigurationBusy(task.id)) return;
207
208
  if (draft.text.trim() === "/clear" && !draft.attachments.length && !draft.quote) { clear(); return; }
208
209
  let submittedDraft = draftRef.current;
209
210
  let requestId = "";
@@ -303,8 +304,8 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
303
304
  </div>
304
305
  <RunControls {...controls} disabled={controls.disabled || sending} />
305
306
  <div className="send-group">
306
- {(!running || !!payload || sending) && <button type="button" className={`send${running ? " queue" : ""}`} disabled={!canSend} aria-busy={sending} aria-label={sending ? "正在发送" : sources.length ? `派生 ${sources.length} 个分支` : running ? "排队发送" : "发送消息"} title={sending ? "发送中…" : running ? "本轮结束后发送" : "发送消息(Enter)"} onClick={() => void submit()}>{sending ? <span className="spinner" /> : Icon.send()}</button>}
307
- {running && <button type="button" className="send stop" disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"} onClick={() => void stop()}>{stopping ? <span className="spinner" /> : Icon.stop()}</button>}
307
+ {showStop ? <button type="button" className="send stop" disabled={stopping} aria-busy={stopping} aria-label={stopping ? "正在停止" : "停止当前轮次"} title={stopping ? "正在停止…" : "停止当前轮次"} onClick={() => void stop()}>{stopping ? <span className="spinner" /> : Icon.stop()}</button>
308
+ : <button type="button" className={`send${running ? " queue" : ""}`} disabled={!canSend} aria-busy={sending} aria-label={sending ? "正在发送" : sources.length ? `派生 ${sources.length} 个分支` : running ? "排队发送" : "发送消息"} title={sending ? "发送中…" : running ? "本轮结束后发送" : "发送消息(Enter)"} onClick={() => void submit()}>{sending ? <span className="spinner" /> : Icon.send()}</button>}
308
309
  </div>
309
310
  </div>
310
311
  </div>
@@ -1021,7 +1021,7 @@ button.tpeek-more:hover{color:var(--accent-ink);}
1021
1021
  .comp-toolbar{display:flex;align-items:center;gap:7px;padding:4px 12px 12px;min-width:0;}
1022
1022
  .comp-add{width:36px;height:36px;display:grid;place-items:center;border-radius:50%;flex:none;color:var(--ink-2);background:transparent;}
1023
1023
  .comp-add svg{width:22px;height:22px;}
1024
- .comp-add:hover,.comp-setting:hover{background:var(--surface-2);}
1024
+ @media(hover:hover) and (pointer:fine){.comp-add:hover,.comp-setting:hover{background:var(--surface-2);}}
1025
1025
  .comp-control{position:relative;min-width:0;}
1026
1026
  .comp-model{margin-left:auto;}
1027
1027
  .comp-setting{display:flex;align-items:center;gap:6px;min-height:36px;max-width:100%;padding:6px 8px;border-radius:10px;color:var(--ink-2);background:transparent;font-size:12px;white-space:nowrap;}
@@ -1029,18 +1029,19 @@ button.tpeek-more:hover{color:var(--accent-ink);}
1029
1029
  .comp-setting.elevated{color:var(--amber);}
1030
1030
  .comp-model-name{min-width:0;max-width:220px;overflow:hidden;text-overflow:ellipsis;}
1031
1031
  .comp-setting:disabled,.comp-add:disabled{opacity:.5;cursor:default;}
1032
- .send{width:44px;height:44px;min-width:44px;min-height:44px;flex:none;padding:0;border-radius:50%;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s,transform .12s;}
1032
+ /* A 32px visual circle inside a 44px touch target. */
1033
+ .send{width:44px;height:44px;min-width:44px;min-height:44px;flex:none;padding:6px;border-radius:50%;background:var(--accent);background-clip:content-box;color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s,transform .12s;}
1033
1034
  .send:hover:not(:disabled){filter:brightness(1.08);}
1034
1035
  .send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
1035
1036
  .send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
1036
1037
  .send[aria-busy="true"]{opacity:.7;cursor:progress;}
1037
- .send.stop{background:var(--blue);color:var(--on-accent);}
1038
- .send.stop svg{fill:currentColor;width:15px;height:15px;}
1038
+ .send.stop{background-color:var(--blue);color:var(--on-accent);}
1039
+ .send.stop svg{fill:currentColor;width:20px;height:20px;}
1039
1040
  .send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
1040
1041
  @media(prefers-reduced-motion:reduce){.send{transition:none;}.send .spinner{animation:none;}}
1041
- /* mid-turn: queue button + stop button sit side by side */
1042
+ /* Send, queue and stop share one primary action slot. */
1042
1043
  .send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
1043
- .send svg{width:17px;height:17px;}
1044
+ .send svg{width:22px;height:22px;}
1044
1045
  .comp-foot{display:flex;align-items:center;gap:10px;padding:9px 14px;border-top:1px solid var(--line-2);background:var(--surface-2);}
1045
1046
  .comp-foot .hint{font-family:var(--mono);font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px;}
1046
1047
  .comp-foot .hint svg{width:13px;height:13px;}
@@ -1057,7 +1058,6 @@ button.tpeek-more:hover{color:var(--accent-ink);}
1057
1058
  @container (max-width:420px){.comp-foot .hint .kbd{display:none;}.run-sw{max-width:130px;}}
1058
1059
 
1059
1060
  .send-group{display:flex;align-items:center;gap:6px;flex:none;}
1060
- .send svg{width:19px;height:19px;}
1061
1061
  .comp-error,.control-error{padding:8px 10px;font-size:12px;line-height:1.5;color:var(--red);overflow-wrap:anywhere;}
1062
1062
  .comp-upload-status{padding:6px 10px;font-size:11px;color:var(--ink-3);}
1063
1063
  .comp-quote{margin:12px 14px 0;}
@@ -1415,12 +1415,12 @@ button.pop-item:hover{background:var(--surface-2);}
1415
1415
  .comp-add.on svg{transform:rotate(45deg);}
1416
1416
  .composer .comp-setting{min-height:38px;border-radius:6px;padding:7px 10px;background:transparent;}
1417
1417
  .composer .comp-add{width:38px;height:38px;border-radius:6px;background:transparent;}
1418
- .composer .comp-setting:hover:not(:disabled),.composer .comp-add:hover:not(:disabled){background:var(--surface-2);}
1418
+ @media(hover:hover) and (pointer:fine){.composer .comp-setting:hover:not(:disabled),.composer .comp-add:hover:not(:disabled){background:var(--surface-2);}}
1419
1419
  .composer .send{width:44px;height:44px;}
1420
1420
  .composer .comp-box{border-radius:26px;border-color:var(--line);box-shadow:0 4px 20px rgba(0,0,0,.035);}
1421
1421
  .composer .comp-box:focus-within{border-color:var(--line-strong);box-shadow:0 6px 24px rgba(0,0,0,.055);}
1422
1422
  .composer .comp-toolbar{gap:6px;padding:6px 12px 12px;}
1423
- .composer .comp-area{padding:18px 20px 12px;}
1423
+ .composer .comp-area{padding:12px 20px 4px;}
1424
1424
  .composer .attach-chip.image{width:108px;height:88px;border-radius:12px;}
1425
1425
  .composer .attach-chip.image>img{height:100%;object-fit:cover;}
1426
1426
  .composer .attach-chip .attach-x{width:26px;height:26px;top:4px;right:4px;font-size:16px;}
@@ -1429,7 +1429,7 @@ button.pop-item:hover{background:var(--surface-2);}
1429
1429
  .composer .attach-row .attach-chip{flex:none;}
1430
1430
  .composer .cli-blocks{max-height:min(30vh,180px);}
1431
1431
  @container(max-width:420px){
1432
- .composer .comp-area{padding:14px 16px 8px;}
1432
+ .composer .comp-area{padding:12px 16px 4px;}
1433
1433
  .composer .comp-toolbar{gap:4px;padding:6px 10px 10px;}
1434
1434
  .composer .comp-setting{padding:6px 9px;}
1435
1435
  .composer .attach-chip.image{width:86px;height:76px;}
@@ -1438,12 +1438,11 @@ button.pop-item:hover{background:var(--surface-2);}
1438
1438
  .composer .comp-toolbar{flex-wrap:nowrap;padding-left:8px;padding-right:8px;}
1439
1439
  .composer .comp-toolbar>.comp-control{flex:0 0 auto;}
1440
1440
  .composer .comp-toolbar>.comp-model{flex:0 1 auto;min-width:44px;margin-left:auto;}
1441
- .composer .comp-setting{font-size:11px;gap:0;padding-left:6px;padding-right:6px;}
1442
- .composer .comp-setting>svg{display:none;}
1441
+ .composer .comp-setting{font-size:12px;gap:4px;padding-left:4px;padding-right:4px;}
1442
+ .composer .comp-setting>svg{flex:none;}
1443
1443
  .composer .comp-model-name{max-width:42px;}
1444
- .composer .comp-toolbar:not(:has(.send.stop)) .comp-setting{gap:4px;font-size:12px;}
1445
- .composer .comp-toolbar:not(:has(.send.stop)) .permission>svg:first-child{display:block;width:14px;height:14px;}
1446
- .composer .comp-toolbar:not(:has(.send.stop)) .comp-model .comp-setting>svg{display:block;width:12px;height:12px;}
1444
+ .composer .permission>svg:first-child{width:14px;height:14px;}
1445
+ .composer .comp-model .comp-setting>svg{width:12px;height:12px;}
1447
1446
  .composer .comp-foot{flex-wrap:wrap;gap:6px;}
1448
1447
  .composer .comp-foot>.send-group{margin-left:auto;}
1449
1448
  }
@@ -1482,7 +1481,6 @@ button.pop-item:hover{background:var(--surface-2);}
1482
1481
  /* generic separators / utility */
1483
1482
  .divider{height:1px;background:var(--line);margin:6px 0;}
1484
1483
 
1485
- .send svg{width:17px;height:17px;}
1486
1484
  .sug-btn svg,.btn-sm svg{width:13px;height:13px;}
1487
1485
 
1488
1486
  /* status label (opt-in via .slabel consumers; hidden by default) */
@@ -1842,7 +1840,7 @@ button.pop-item:hover{background:var(--surface-2);}
1842
1840
  [data-chat-layout]>.composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1843
1841
  [data-chat-layout]>.cli-refs,[data-chat-layout]>.cli-queue{width:calc(100% - 2 * var(--cli-chat-gutter) - var(--cli-chat-scrollbar,0px));max-width:var(--cli-chat-content-max);margin-inline:auto;margin-inline-start:max(var(--cli-chat-gutter),calc((100% - var(--cli-chat-scrollbar,0px) - var(--cli-chat-content-max)) / 2));flex-shrink:0;}
1844
1842
  @container chat-column (max-width:900px){[data-chat-layout]>*{--cli-chat-content-max:100%;}}
1845
- @container chat-column (max-width:640px){[data-chat-layout]>*{--cli-chat-gutter:12px;}}
1843
+ @container chat-column (max-width:640px){[data-chat-layout]>*{--cli-chat-gutter:12px;}.session [data-chat-layout]>.composer{padding-inline-end:var(--cli-chat-gutter);}}
1846
1844
  .task-origin{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--ink-3);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1847
1845
  .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1848
1846
  .fork-sources>span{max-width:100%;display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--line);background:var(--surface-2);border-radius:8px;font-size:11px;color:var(--ink-2);overflow-wrap:anywhere;}
package/src/operator.css CHANGED
@@ -1021,7 +1021,7 @@
1021
1021
  .cli-ui .comp-toolbar{display:flex;align-items:center;gap:7px;padding:4px 12px 12px;min-width:0;}
1022
1022
  .cli-ui .comp-add{width:36px;height:36px;display:grid;place-items:center;border-radius:50%;flex:none;color:var(--ink-2);background:transparent;}
1023
1023
  .cli-ui .comp-add svg{width:22px;height:22px;}
1024
- .cli-ui .comp-add:hover,.cli-ui .comp-setting:hover{background:var(--surface-2);}
1024
+ @media(hover:hover) and (pointer:fine){.cli-ui .comp-add:hover,.cli-ui .comp-setting:hover{background:var(--surface-2);}}
1025
1025
  .cli-ui .comp-control{position:relative;min-width:0;}
1026
1026
  .cli-ui .comp-model{margin-left:auto;}
1027
1027
  .cli-ui .comp-setting{display:flex;align-items:center;gap:6px;min-height:36px;max-width:100%;padding:6px 8px;border-radius:10px;color:var(--ink-2);background:transparent;font-size:12px;white-space:nowrap;}
@@ -1029,18 +1029,19 @@
1029
1029
  .cli-ui .comp-setting.elevated{color:var(--amber);}
1030
1030
  .cli-ui .comp-model-name{min-width:0;max-width:220px;overflow:hidden;text-overflow:ellipsis;}
1031
1031
  .cli-ui .comp-setting:disabled,.cli-ui .comp-add:disabled{opacity:.5;cursor:default;}
1032
- .cli-ui .send{width:44px;height:44px;min-width:44px;min-height:44px;flex:none;padding:0;border-radius:50%;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s,transform .12s;}
1032
+ /* A 32px visual circle inside a 44px touch target. */
1033
+ .cli-ui .send{width:44px;height:44px;min-width:44px;min-height:44px;flex:none;padding:6px;border-radius:50%;background:var(--accent);background-clip:content-box;color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s,transform .12s;}
1033
1034
  .cli-ui .send:hover:not(:disabled){filter:brightness(1.08);}
1034
1035
  .cli-ui .send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
1035
1036
  .cli-ui .send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
1036
1037
  .cli-ui .send[aria-busy="true"]{opacity:.7;cursor:progress;}
1037
- .cli-ui .send.stop{background:var(--blue);color:var(--on-accent);}
1038
- .cli-ui .send.stop svg{fill:currentColor;width:15px;height:15px;}
1038
+ .cli-ui .send.stop{background-color:var(--blue);color:var(--on-accent);}
1039
+ .cli-ui .send.stop svg{fill:currentColor;width:20px;height:20px;}
1039
1040
  .cli-ui .send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
1040
1041
  @media(prefers-reduced-motion:reduce){.cli-ui .send{transition:none;}.cli-ui .send .spinner{animation:none;}}
1041
- /* mid-turn: queue button + stop button sit side by side */
1042
+ /* Send, queue and stop share one primary action slot. */
1042
1043
  .cli-ui .send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
1043
- .cli-ui .send svg{width:17px;height:17px;}
1044
+ .cli-ui .send svg{width:22px;height:22px;}
1044
1045
  .cli-ui .comp-foot{display:flex;align-items:center;gap:10px;padding:9px 14px;border-top:1px solid var(--line-2);background:var(--surface-2);}
1045
1046
  .cli-ui .comp-foot .hint{font-family:var(--mono);font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px;}
1046
1047
  .cli-ui .comp-foot .hint svg{width:13px;height:13px;}
@@ -1057,7 +1058,6 @@
1057
1058
  @container (max-width:420px){.cli-ui .comp-foot .hint .kbd{display:none;}.cli-ui .run-sw{max-width:130px;}}
1058
1059
 
1059
1060
  .cli-ui .send-group{display:flex;align-items:center;gap:6px;flex:none;}
1060
- .cli-ui .send svg{width:19px;height:19px;}
1061
1061
  .cli-ui .comp-error,.cli-ui .control-error{padding:8px 10px;font-size:12px;line-height:1.5;color:var(--red);overflow-wrap:anywhere;}
1062
1062
  .cli-ui .comp-upload-status{padding:6px 10px;font-size:11px;color:var(--ink-3);}
1063
1063
  .cli-ui .comp-quote{margin:12px 14px 0;}
@@ -1415,12 +1415,12 @@
1415
1415
  .cli-ui .comp-add.on svg{transform:rotate(45deg);}
1416
1416
  .cli-ui .composer .comp-setting{min-height:38px;border-radius:6px;padding:7px 10px;background:transparent;}
1417
1417
  .cli-ui .composer .comp-add{width:38px;height:38px;border-radius:6px;background:transparent;}
1418
- .cli-ui .composer .comp-setting:hover:not(:disabled),.cli-ui .composer .comp-add:hover:not(:disabled){background:var(--surface-2);}
1418
+ @media(hover:hover) and (pointer:fine){.cli-ui .composer .comp-setting:hover:not(:disabled),.cli-ui .composer .comp-add:hover:not(:disabled){background:var(--surface-2);}}
1419
1419
  .cli-ui .composer .send{width:44px;height:44px;}
1420
1420
  .cli-ui .composer .comp-box{border-radius:26px;border-color:var(--line);box-shadow:0 4px 20px rgba(0,0,0,.035);}
1421
1421
  .cli-ui .composer .comp-box:focus-within{border-color:var(--line-strong);box-shadow:0 6px 24px rgba(0,0,0,.055);}
1422
1422
  .cli-ui .composer .comp-toolbar{gap:6px;padding:6px 12px 12px;}
1423
- .cli-ui .composer .comp-area{padding:18px 20px 12px;}
1423
+ .cli-ui .composer .comp-area{padding:12px 20px 4px;}
1424
1424
  .cli-ui .composer .attach-chip.image{width:108px;height:88px;border-radius:12px;}
1425
1425
  .cli-ui .composer .attach-chip.image>img{height:100%;object-fit:cover;}
1426
1426
  .cli-ui .composer .attach-chip .attach-x{width:26px;height:26px;top:4px;right:4px;font-size:16px;}
@@ -1429,7 +1429,7 @@
1429
1429
  .cli-ui .composer .attach-row .attach-chip{flex:none;}
1430
1430
  .cli-ui .composer .cli-blocks{max-height:min(30vh,180px);}
1431
1431
  @container(max-width:420px){
1432
- .cli-ui .composer .comp-area{padding:14px 16px 8px;}
1432
+ .cli-ui .composer .comp-area{padding:12px 16px 4px;}
1433
1433
  .cli-ui .composer .comp-toolbar{gap:4px;padding:6px 10px 10px;}
1434
1434
  .cli-ui .composer .comp-setting{padding:6px 9px;}
1435
1435
  .cli-ui .composer .attach-chip.image{width:86px;height:76px;}
@@ -1438,12 +1438,11 @@
1438
1438
  .cli-ui .composer .comp-toolbar{flex-wrap:nowrap;padding-left:8px;padding-right:8px;}
1439
1439
  .cli-ui .composer .comp-toolbar>.comp-control{flex:0 0 auto;}
1440
1440
  .cli-ui .composer .comp-toolbar>.comp-model{flex:0 1 auto;min-width:44px;margin-left:auto;}
1441
- .cli-ui .composer .comp-setting{font-size:11px;gap:0;padding-left:6px;padding-right:6px;}
1442
- .cli-ui .composer .comp-setting>svg{display:none;}
1441
+ .cli-ui .composer .comp-setting{font-size:12px;gap:4px;padding-left:4px;padding-right:4px;}
1442
+ .cli-ui .composer .comp-setting>svg{flex:none;}
1443
1443
  .cli-ui .composer .comp-model-name{max-width:42px;}
1444
- .cli-ui .composer .comp-toolbar:not(:has(.send.stop)) .comp-setting{gap:4px;font-size:12px;}
1445
- .cli-ui .composer .comp-toolbar:not(:has(.send.stop)) .permission>svg:first-child{display:block;width:14px;height:14px;}
1446
- .cli-ui .composer .comp-toolbar:not(:has(.send.stop)) .comp-model .comp-setting>svg{display:block;width:12px;height:12px;}
1444
+ .cli-ui .composer .permission>svg:first-child{width:14px;height:14px;}
1445
+ .cli-ui .composer .comp-model .comp-setting>svg{width:12px;height:12px;}
1447
1446
  .cli-ui .composer .comp-foot{flex-wrap:wrap;gap:6px;}
1448
1447
  .cli-ui .composer .comp-foot>.send-group{margin-left:auto;}
1449
1448
  }
@@ -1482,7 +1481,6 @@
1482
1481
  /* generic separators / utility */
1483
1482
  .cli-ui .divider{height:1px;background:var(--line);margin:6px 0;}
1484
1483
 
1485
- .cli-ui .send svg{width:17px;height:17px;}
1486
1484
  .cli-ui .sug-btn svg,.cli-ui .btn-sm svg{width:13px;height:13px;}
1487
1485
 
1488
1486
  /* status label (opt-in via .slabel consumers; hidden by default) */
@@ -1842,7 +1840,7 @@
1842
1840
  .cli-ui [data-chat-layout]>.composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1843
1841
  .cli-ui [data-chat-layout]>.cli-refs,.cli-ui [data-chat-layout]>.cli-queue{width:calc(100% - 2 * var(--cli-chat-gutter) - var(--cli-chat-scrollbar,0px));max-width:var(--cli-chat-content-max);margin-inline:auto;margin-inline-start:max(var(--cli-chat-gutter),calc((100% - var(--cli-chat-scrollbar,0px) - var(--cli-chat-content-max)) / 2));flex-shrink:0;}
1844
1842
  @container chat-column (max-width:900px){.cli-ui [data-chat-layout]>*{--cli-chat-content-max:100%;}}
1845
- @container chat-column (max-width:640px){.cli-ui [data-chat-layout]>*{--cli-chat-gutter:12px;}}
1843
+ @container chat-column (max-width:640px){.cli-ui [data-chat-layout]>*{--cli-chat-gutter:12px;}.cli-ui .session [data-chat-layout]>.composer{padding-inline-end:var(--cli-chat-gutter);}}
1846
1844
  .cli-ui .task-origin{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--ink-3);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
1847
1845
  .cli-ui .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1848
1846
  .cli-ui .fork-sources>span{max-width:100%;display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--line);background:var(--surface-2);border-radius:8px;font-size:11px;color:var(--ink-2);overflow-wrap:anywhere;}
package/src/styles.css CHANGED
@@ -53,9 +53,11 @@
53
53
  .cli-ui .sess-head .crumb{overflow-wrap:anywhere;}
54
54
  .cli-ui .cli-export{white-space:nowrap;}
55
55
  .cli-ui .comp-area textarea{min-width:0;}
56
- .cli-ui .comp-foot{gap:8px;padding:8px 10px;background:transparent;border-top:0;flex-wrap:wrap;}
56
+ .cli-ui .composer .comp-foot{gap:4px;padding:4px 10px;background:transparent;border-top:0;flex-wrap:wrap;}
57
+ .cli-ui .comp-foot>.comp-control{flex:0 0 auto;}
58
+ .cli-ui .comp-foot .permission{padding-inline:6px;}
57
59
  .cli-ui .cli-attach{display:inline-flex;justify-content:center;align-items:center;gap:5px;min-width:44px;min-height:44px;padding:0 8px;border-radius:7px;color:var(--ink-3);font-size:12px;}
58
- .cli-ui .cli-attach:hover{background:var(--surface-2);color:var(--ink);}
60
+ @media(hover:hover) and (pointer:fine){.cli-ui .cli-attach:hover{background:var(--surface-2);color:var(--ink);}}
59
61
  .cli-ui .cli-attach svg{width:22px;height:22px;}
60
62
  .cli-ui .comp-foot:has(.comp-model)>.spacer{display:none;}
61
63
  .cli-ui .cli-export:focus-visible,.cli-ui .cli-attach:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
@@ -454,19 +456,20 @@ cli-split{display:block;}
454
456
  .cli-ui .cli-auto-background pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:220px;overflow:auto;}
455
457
 
456
458
  /* The live queue sits just above the composer; expanded content stays in a viewport-bound card. */
457
- .cli-ui .cli-queue{flex:none;min-width:0;max-height:154px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;margin:0 12px -1px;padding:4px 8px 8px;border:1px solid var(--line-strong);border-radius:18px 18px 0 0;background:var(--raise);}
458
- .cli-ui .cli-queue-row{display:flex;align-items:center;gap:8px;min-width:0;min-height:46px;}
459
+ .cli-ui .cli-queue{flex:none;min-width:0;max-height:154px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;margin:0 16px -12px;padding:4px 10px 16px;border:1px solid var(--line);border-radius:18px 18px 0 0;background:var(--raise);}
460
+ .cli-ui .cli-queue-row{display:flex;align-items:center;gap:8px;min-width:0;min-height:44px;}
459
461
  .cli-ui .cli-queue-row+.cli-queue-row{border-top:1px solid var(--line-2);}
460
462
  .cli-ui .cli-queue-icon{display:grid;place-items:center;flex:none;width:20px;color:var(--ink-4);}
461
463
  .cli-ui .cli-queue-icon svg{width:16px;height:16px;}
462
464
  .cli-ui .cli-queue-summary{display:flex;align-items:center;gap:8px;flex:1;min-width:0;min-height:40px;padding:4px 0;text-align:left;color:var(--ink-2);}
463
465
  .cli-ui .cli-queue-thumb{flex:none;width:32px;height:32px;object-fit:cover;border:1px solid var(--line);border-radius:7px;}
464
466
  .cli-ui .cli-queue-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:1.5;}
465
- .cli-ui .cli-queue-state{flex:none;color:var(--ink-4);font-size:10px;white-space:nowrap;}
466
467
  .cli-ui .cli-queue-remove,.cli-ui .cli-queue-more{display:grid;place-items:center;flex:none;width:30px;height:34px;border-radius:9px;color:var(--ink-3);}
467
468
  .cli-ui .cli-queue-remove svg,.cli-ui .cli-queue-more svg{width:17px;height:17px;}
468
- .cli-ui .cli-queue-remove:hover,.cli-ui .cli-queue-more:hover{background:var(--surface-2);color:var(--ink);}
469
- .cli-ui .cli-queue-remove:hover{color:var(--red);}
469
+ @media(hover:hover) and (pointer:fine){
470
+ .cli-ui .cli-queue-remove:hover,.cli-ui .cli-queue-more:hover{background:var(--surface-2);color:var(--ink);}
471
+ .cli-ui .cli-queue-remove:hover{color:var(--red);}
472
+ }
470
473
  .cli-ui .cli-queue-summary:focus-visible,.cli-ui .cli-queue-remove:focus-visible,.cli-ui .cli-queue-more:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:8px;}
471
474
  .cli-ui .cli-queue-popover{width:420px;}
472
475
  .cli-ui .cli-queue-detail{max-height:min(44vh,320px);overflow:auto;overscroll-behavior:contain;overflow-wrap:anywhere;}
@@ -479,11 +482,10 @@ cli-split{display:block;}
479
482
  .cli-ui .cli-queue-feedback[role="alert"]{color:var(--red);}
480
483
  .cli-ui .session>.cli-queue{width:calc(100% - 48px);max-width:800px;align-self:center;margin:8px 24px 16px;border-radius:16px;}
481
484
  @container(max-width:420px){
482
- .cli-ui .cli-queue{margin:0 8px -1px;padding:4px 6px 7px;max-height:112px;}
485
+ .cli-ui .cli-queue{margin:0 10px -12px;padding:4px 6px 16px;max-height:112px;}
483
486
  .cli-ui .cli-queue-row{gap:5px;}
484
- .cli-ui .cli-queue-state{display:none;}
485
487
  }
486
- @media(pointer:coarse){.cli-ui .cli-queue-remove,.cli-ui .cli-queue-more{width:36px;height:40px;}}
488
+ @media(pointer:coarse){.cli-ui .cli-queue-remove,.cli-ui .cli-queue-more{width:44px;height:44px;}}
487
489
  @media(max-height:500px){.cli-ui .cli-queue{max-height:70px;}}
488
490
 
489
491
  /* Keep controls reachable when a keyboard, split host or landscape view leaves little height. */
@@ -55,7 +55,6 @@ function QueuedMessage({ message, index, onCancel, attachmentUrl, renderMessage
55
55
  {thumbnail?.url && <img className="cli-queue-thumb" src={thumbnail.url} alt={thumbnail.attachment.name} loading="lazy" onError={event => { event.currentTarget.hidden = true; }} />}
56
56
  <span className="cli-queue-text">{preview.summary}</span>
57
57
  </button>
58
- <span className="cli-queue-state">排队中</span>
59
58
  {onCancel && <button className="cli-queue-remove" title="从队列中移除" aria-label={`移除第 ${index + 1} 条排队消息`} onClick={() => onCancel(message)}>{Icon.x()}</button>}
60
59
  <button className="cli-queue-more" title="更多排队消息操作" aria-label={`第 ${index + 1} 条排队消息的更多操作`} aria-expanded={open} aria-haspopup="dialog" onClick={toggle}>{Icon.dots()}</button>
61
60
  {open && <Popover className="composer-popover cli-queue-popover" ariaLabel="排队消息内容" placement="top" align="start" returnFocusRef={returnFocusRef} onClose={() => setOpen(false)}>