@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.
@@ -18,6 +18,7 @@ import { BranchHistory, useBranchHistory } from "./BranchHistory";
18
18
  import { BranchNavigator } from "./BranchNavigator";
19
19
  import type { Quote } from "./composerDraft";
20
20
  import type { RunControlProps } from "./RunControls";
21
+ import type { ResolvedChatConfig } from "../../core/config";
21
22
  const QUIET_AFTER_MS = 180_000;
22
23
  function RunningIndicator({ agent, startedAt, lastActivityRef }: { agent: string | null; startedAt: number | null; lastActivityRef: React.MutableRefObject<number> }) {
23
24
  const [now, setNow] = useState(() => Date.now());
@@ -68,6 +69,7 @@ export type ChatProps = {
68
69
  running: boolean; blockedBy?: string[]; transcriptLoading?: boolean;
69
70
  older?: number; onLoadOlder?: (beforeApply?: () => void) => Promise<boolean>;
70
71
  controls: RunControlProps; configBusy: boolean; active?: boolean;
72
+ composer?: Pick<ResolvedChatConfig["composer"], "show" | "fileUpload">;
71
73
  onSelectTask: (id: string) => void; onFork: (text: string, sourceIds: string[], requestId: string) => Promise<boolean>;
72
74
  onSend: (text: string) => Promise<boolean>; onStop: () => Promise<boolean>;
73
75
  onStart: () => void; onEdit: () => void; onClear: () => void; onSetSendContext: (v: boolean) => void;
@@ -76,7 +78,7 @@ export type ChatProps = {
76
78
  };
77
79
  export function Chat(props: ChatProps) { return <ChatSession key={props.task.id} {...props} />; }
78
80
  function ChatSession({ project, task, tasks, agents, messages, running, transcriptLoading, older=0, onLoadOlder,
79
- controls, configBusy, active=true, onSelectTask, onFork, onSend, onStop, onClear, onReconnect, onAnswer, onCancelQueued }: ChatProps) {
81
+ controls, configBusy, composer, active=true, onSelectTask, onFork, onSend, onStop, onClear, onReconnect, onAnswer, onCancelQueued }: ChatProps) {
80
82
  const scrollRef = useRef<HTMLDivElement>(null);
81
83
  useChatScrollbar(scrollRef, task.id);
82
84
  const [quoteRequest, setQuoteRequest] = useState<Quote|null>(null);
@@ -439,6 +441,7 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
439
441
  pinned.current=false; setAtBottom(false); flowPositioned.current=true;
440
442
  },[active,flow.ready,task.parent_task_id]);
441
443
  useEffect(()=>{if(!task.parent_task_id && active) scrollToBottom("auto");},[active,task.id]);
444
+ const queued = <QueuedMessages scopeKey={task.id} messages={messages.filter(message => message.role === "queued")} onCancel={message => stableCancelQueued(message.id)} attachmentUrl={attachment => `/api/tasks/${encodeURIComponent(attachment.taskId)}/uploads/${encodeURIComponent(attachment.name)}`} renderMessage={message => <MessageView m={message} initial={false} hideWho readOnly />} />;
442
445
  return (<FileLinkContext.Provider value={fileScope}>
443
446
  <>
444
447
  <div className="transcript-wrap">
@@ -508,8 +511,8 @@ function ChatSession({ project, task, tasks, agents, messages, running, transcri
508
511
  )}
509
512
  </div>
510
513
  </div>
511
- <Composer tasks={tasks} onFork={onFork} task={task} agentLabel={agentLabel(agents, task.agent)} disabled={task.started!==1} configBusy={configBusy} running={running} onSend={async text => { const sent = await onSend(text); if (sent && !running) scrollToBottom("auto"); return sent; }} onStop={onStop} onClear={onClear} quoteRequest={quoteRequest} onQuoteHandled={()=>setQuoteRequest(null)} controls={controls}
512
- queued={<QueuedMessages scopeKey={task.id} messages={messages.filter(message => message.role === "queued")} onCancel={message => stableCancelQueued(message.id)} attachmentUrl={attachment => `/api/tasks/${encodeURIComponent(attachment.taskId)}/uploads/${encodeURIComponent(attachment.name)}`} renderMessage={message => <MessageView m={message} initial={false} hideWho readOnly />} />} />
514
+ {composer?.show !== false ? <Composer tasks={tasks} onFork={onFork} task={task} agentLabel={agentLabel(agents, task.agent)} disabled={task.started!==1} configBusy={configBusy} running={running} onSend={async text => { const sent = await onSend(text); if (sent && !running) scrollToBottom("auto"); return sent; }} onStop={onStop} onClear={onClear} quoteRequest={quoteRequest} onQuoteHandled={()=>setQuoteRequest(null)} controls={controls}
515
+ fileUpload={composer?.fileUpload} queued={queued} /> : queued}
513
516
  </></FileLinkContext.Provider>
514
517
  );
515
518
 
@@ -21,12 +21,13 @@ import { acknowledgeDraft, DRAFT_EVENT, isTaskSending, setTaskSending, readDraft
21
21
  import { getOperatorBoardBackend } from "./boardBackend";
22
22
  import { OperatorBoardContext, openOperatorBoard } from "./boardInteraction";
23
23
 
24
- export function Composer({ task, agentLabel, disabled, running, onSend, onStop, onClear, quoteRequest, onQuoteHandled, controls, configBusy, tasks, onFork, queued }: {
24
+ export function Composer({ task, agentLabel, disabled, running, onSend, onStop, onClear, quoteRequest, onQuoteHandled, controls, configBusy, tasks, onFork, queued, fileUpload = true }: {
25
25
  task: TaskRow; agentLabel: string; disabled: boolean; running: boolean;
26
26
  tasks: TaskRow[]; onFork: (text: string, sourceIds: string[], requestId: string) => Promise<boolean>;
27
27
  onSend: (text: string) => Promise<boolean>; onStop: () => Promise<boolean>; onClear: () => void;
28
28
  configBusy: boolean; quoteRequest: Quote | null; onQuoteHandled: () => void; controls: RunControlProps;
29
29
  queued?: ReactNode;
30
+ fileUpload?: boolean;
30
31
  }) {
31
32
  const boardView=useBoardState(task.id);
32
33
  const [draft, setDraft] = useState(() => readDraft(task.id));
@@ -116,7 +117,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
116
117
  const sharedConfigBusy = useConfigurationBusy(task.id);
117
118
  const locked = disabled || sending || configBusy || sharedConfigBusy;
118
119
  const startUpload = (key: string, file: File) => {
119
- if (!backend.uploadFile || locked || uploads.current.has(key)) return;
120
+ if (!fileUpload || !backend.uploadFile || locked || uploads.current.has(key)) return;
120
121
  const taskId = task.id;
121
122
  const preview = draftRef.current.attachments.find(a => a.key === key)?.preview;
122
123
  const controller = new AbortController();
@@ -143,7 +144,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
143
144
  });
144
145
  };
145
146
  const addFiles = (files: File[]) => {
146
- if (locked) return;
147
+ if (locked || !fileUpload) return;
147
148
  setError("");
148
149
  if (!backend.uploadFile) { setError("当前服务暂不支持附件上传。"); return; }
149
150
  for (const f of files) {
@@ -195,14 +196,15 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
195
196
  const pendingFork=sources.length&&draft.forkRequest?.signature===currentSignature?draft.forkRequest:null;
196
197
  const payload=pendingFork?pendingFork.payload??ownPayload:ownPayload&&!isClear?appendBoardContext(ownPayload,boardView,task.id):ownPayload;
197
198
  const tooLong = payload.length > MAX_MESSAGE_CHARS;
198
- const canSend = !!payload && !uploading && !failed && !tooLong && !locked;
199
+ const canSend = !!payload && !uploading && !failed && !tooLong && !locked && !stopping;
200
+ const showStop = running && !sending && (stopping || locked || !payload);
199
201
  const clear = () => {
200
202
  if (running || locked || task.started !== 1) return;
201
203
  onClear(); setSlash(false);
202
204
  if (draft.text.trim() === "/clear") update((d) => ({ ...d, text: "" }));
203
205
  };
204
206
  const submit = async () => {
205
- 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;
206
208
  if (draft.text.trim() === "/clear" && !draft.attachments.length && !draft.quote) { clear(); return; }
207
209
  let submittedDraft = draftRef.current;
208
210
  let requestId = "";
@@ -249,7 +251,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
249
251
  {slash && !running && !locked && "/clear".startsWith(draft.text.trim()) && <div className="slash"><button className="slash-item" onClick={clear}><span className="cmd">/clear</span><span className="cd">保存摘要 · 开启新上下文</span></button></div>}
250
252
  {queued}
251
253
  <div className={`comp-box${dragging ? " dropping" : ""}`}
252
- onDragEnter={(e) => { if (hasFileDrag(e)) { e.preventDefault(); if (!locked) { dragDepth.current++; setDragging(true); } } }}
254
+ onDragEnter={(e) => { if (hasFileDrag(e)) { e.preventDefault(); if (fileUpload && !locked) { dragDepth.current++; setDragging(true); } } }}
253
255
  onDragOver={(e) => { if (hasFileDrag(e)) e.preventDefault(); }}
254
256
  onDragLeave={() => { if (dragDepth.current > 0 && --dragDepth.current === 0) setDragging(false); }}
255
257
  onDrop={(e) => { if (!hasFileDrag(e)) return; e.preventDefault(); dragDepth.current = 0; setDragging(false); addFiles(Array.from(e.dataTransfer.files)); }}>
@@ -258,7 +260,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
258
260
  {draft.attachments.some(a => a.status !== "ready") && <div className="composer-upload-list">{draft.attachments.filter(a => a.status !== "ready").map(a => <UploadProgress key={a.key} name={a.name}
259
261
  status={a.status === "error" ? "error" : a.phase ?? "queued"} bytes={a.bytes} total={a.size} disabled={locked}
260
262
  error={`${a.error || "上传失败,请重试。"}${uploadFiles.current.has(a.key) || a.error?.includes("重新选择文件") ? "" : " 请重新选择文件。"}`}
261
- onCancel={() => removeAtt(a.key)} onRemove={() => removeAtt(a.key)} onRetry={uploadFiles.current.has(a.key) ? () => startUpload(a.key, uploadFiles.current.get(a.key)!) : undefined} />)}</div>}
263
+ onCancel={() => removeAtt(a.key)} onRemove={() => removeAtt(a.key)} onRetry={fileUpload && uploadFiles.current.has(a.key) ? () => startUpload(a.key, uploadFiles.current.get(a.key)!) : undefined} />)}</div>}
262
264
  {readyAttachments.length > 0 && <div className="attach-row">{readyAttachments.map((a) => <div key={a.key} className={`attach-chip ${a.kind} ${a.status}`} title={a.name}>
263
265
  {a.kind === "image" && a.preview ? <img src={a.preview} alt={a.name} /> : <span className="attach-file">{Icon.clip()} {a.name}</span>}
264
266
  <button className="attach-x" disabled={locked} aria-label={`移除 ${a.name}`} onClick={() => removeAtt(a.key)}>×</button>
@@ -282,16 +284,16 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
282
284
  const files = Array.from(e.clipboardData.files);
283
285
  if (files.length) { e.preventDefault(); addFiles(files); return; }
284
286
  const text = e.clipboardData.getData("text/plain");
285
- if (text.length > PASTE_ATTACH_THRESHOLD) { e.preventDefault(); addFiles([new File([text], "pasted-text.txt", { type: "text/plain" })]); }
287
+ if (fileUpload && text.length > PASTE_ATTACH_THRESHOLD) { e.preventDefault(); addFiles([new File([text], "pasted-text.txt", { type: "text/plain" })]); }
286
288
  }} /></div>
287
289
  <div className="comp-toolbar">
288
- <input ref={fileRef} type="file" accept={FILE_UPLOAD_ACCEPT} multiple hidden onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }} />
289
- <div className="comp-control"><button className={`comp-add${addOpen ? " on" : ""}`} disabled={locked} title="附件、派生任务与新上下文" aria-label="添加内容" aria-haspopup="dialog" aria-expanded={addOpen} onClick={(e) => { e.stopPropagation(); setAddOpen(!addOpen); }}>{Icon.plus()}</button>
290
+ {fileUpload && <input ref={fileRef} type="file" accept={FILE_UPLOAD_ACCEPT} multiple hidden onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }} />}
291
+ <div className="comp-control"><button className={`comp-add${addOpen ? " on" : ""}`} disabled={locked} title={fileUpload ? "附件、派生任务与新上下文" : "派生任务与新上下文"} aria-label="添加内容" aria-haspopup="dialog" aria-expanded={addOpen} onClick={(e) => { e.stopPropagation(); setAddOpen(!addOpen); }}>{Icon.plus()}</button>
290
292
  {addOpen && <Popover className="composer-popover composer-add-popover" ariaLabel="添加内容" placement="top" align="start" onClose={() => setAddOpen(false)}>
291
293
  <div className="composer-menu-heading">添加内容</div>
292
- <button className="pop-item composer-option" disabled={locked} onClick={() => { setAddOpen(false); fileRef.current?.click(); }}>
294
+ {fileUpload && <button className="pop-item composer-option" disabled={locked} onClick={() => { setAddOpen(false); fileRef.current?.click(); }}>
293
295
  <span className="composer-option-icon">{Icon.clip()}</span><span className="composer-option-copy"><span className="composer-option-title">添加附件</span><span className="composer-option-sub">HTML、Markdown 最多 100 MiB,其他文件最多 10 MiB</span></span>
294
- </button>
296
+ </button>}
295
297
  <button className="pop-item composer-option" disabled={locked} onClick={openMention}>
296
298
  <span className="composer-option-icon">{Icon.git()}</span><span className="composer-option-copy"><span className="composer-option-title">从已完成任务派生</span><span className="composer-option-sub">带上已完成的任务,在新分支继续工作</span></span><span className="composer-option-trailing composer-menu-key">@</span>
297
299
  </button>
@@ -302,8 +304,8 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
302
304
  </div>
303
305
  <RunControls {...controls} disabled={controls.disabled || sending} />
304
306
  <div className="send-group">
305
- {(!running || !!payload) && <button className={`send${running ? " queue" : ""}`} disabled={!canSend} aria-busy={sending} aria-label={sources.length ? `派生 ${sources.length} 个分支` : running ? "排队发送" : "发送消息"} title={sending ? "发送中…" : running ? "本轮结束后发送" : "发送消息(Enter)"} onClick={() => void submit()}>{sending ? <span className="spinner" /> : Icon.send()}</button>}
306
- {running && <button className="send stop" disabled={stopping} aria-busy={stopping} aria-label="停止当前轮次" 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>}
307
309
  </div>
308
310
  </div>
309
311
  </div>
@@ -30,6 +30,7 @@ import { ColResize } from "./Layout";
30
30
  import { WorkspaceTabs } from "../../ui/WorkspaceTabs";
31
31
  import { WorkspaceDrawer, useWorkspaceDrawer } from "../../ui/WorkspaceDrawer";
32
32
  import { jget, downloadExport } from "./api";
33
+ import { resolveChatConfig, type CliChatConfig } from "../../core/config";
33
34
 
34
35
  // Non-blocking banner shown when a reopened task's worktree is behind its base
35
36
  // branch. Computed (read-only) on open; the actual git op fires only when the user
@@ -92,8 +93,9 @@ function SyncBanner({ taskId, running, pinned, onResolveWithAI, onSwitchToChat }
92
93
  }
93
94
 
94
95
  export function SessionView(props: Parameters<typeof SessionViewInner>[0]) { return <SessionViewInner key={props.task.id} {...props}/>; }
95
- function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents, messages, running, blockedBy, transcriptLoading, older=0, onLoadOlder, onSend, onStart, onStop, onClear, clearConfirming, onConfirmClear, onCancelClear, onEdit, onReconnect, onSetStatus, onSetPriority, onSetModel, onSetAgent, onSetReasoning, onSetPermission, onSetSendContext, onResolveWithAI, onMerged, onPrCreated, onAnswer, onCancelQueued, onBack, mobile, railW, onRailWidth, onRailReset, railCollapsed, onRailCollapse, onRailExpand, board }: {
96
+ function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents, messages, running, blockedBy, transcriptLoading, older=0, onLoadOlder, onSend, onStart, onStop, onClear, clearConfirming, onConfirmClear, onCancelClear, onEdit, onReconnect, onSetStatus, onSetPriority, onSetModel, onSetAgent, onSetReasoning, onSetPermission, onSetSendContext, onResolveWithAI, onMerged, onPrCreated, onAnswer, onCancelQueued, onBack, mobile, railW, onRailWidth, onRailReset, railCollapsed, onRailCollapse, onRailExpand, board, composer }: {
96
97
  board?: OperatorBoardOptions;
98
+ composer?: CliChatConfig["composer"];
97
99
  tasks: TaskRow[]; onSelectTask: (id: string) => void; onFork: (text: string, sourceIds: string[], requestId: string) => Promise<boolean>;
98
100
  project: ProjectRow; task: TaskRow; agents: AgentsBundle; messages: Msg[]; running: boolean; blockedBy?: string[]; transcriptLoading?: boolean;
99
101
  older?: number; onLoadOlder?: (beforeApply?: () => void) => Promise<boolean>;
@@ -181,7 +183,8 @@ function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents,
181
183
  // Run-control pickers + feature gates come from this task's agent capabilities,
182
184
  // never a hardcoded list — so the options always match the agent it runs under.
183
185
  const caps = capsFor(agents, task.agent);
184
- const controls = { task, caps, agents:agents.agents.map(a=>({id:a.id,label:a.label,disabled:!a.authenticated||a.installed===false})), onSetAgent, defaults: findAgent(agents, task.agent)?.defaults, disabled: running, onSetModel, onSetReasoning, onSetPermission };
186
+ const composerConfig = resolveChatConfig({ composer }).composer;
187
+ const controls = { task, caps, agents:agents.agents.map(a=>({id:a.id,label:a.label,disabled:!a.authenticated||a.installed===false})), onSetAgent, defaults: findAgent(agents, task.agent)?.defaults, disabled: running, onSetModel, onSetReasoning, onSetPermission, showAgent: composerConfig.agentSelector, showPermission: composerConfig.executionMode };
185
188
  // Usage chip: tokens split into fresh work vs re-read cache (the raw total is
186
189
  // mostly cache reads and wildly overstates what ran), and a dollar figure whose
187
190
  // presentation follows how this agent is signed in — a subscription login's
@@ -259,7 +262,7 @@ function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents,
259
262
  )}
260
263
 
261
264
  {!hasSession ? (
262
- <><div className="hero-controls"><RunControls {...controls} /></div><TaskHero task={task} project={project} onStart={onStart} onEdit={onEdit} onSetSendContext={onSetSendContext} running={running || configBusy} blockedBy={blockedBy} /></>
265
+ <>{composerConfig.show && <div className="hero-controls"><RunControls {...controls} /></div>}<TaskHero task={task} project={project} onStart={onStart} onEdit={onEdit} onSetSendContext={onSetSendContext} running={running || configBusy} blockedBy={blockedBy} /></>
263
266
  ) : (
264
267
  // Keep the chat at the same React position across all widths. Moving
265
268
  // it between different parents remounted the composer during resize
@@ -267,7 +270,7 @@ function SessionViewInner({ project, task, tasks, onSelectTask, onFork, agents,
267
270
  <div className={tabMode||drawerMode ? "sess-responsive" : "sess-split"} style={tabMode||drawerMode ? undefined : { gridTemplateColumns: `minmax(0,1fr) 0px ${railW}px` }}>
268
271
  <div className={`sess-main${tabMode||drawerMode ? " compact-chat" : ""}`} data-chat-layout={tabMode||drawerMode?"centered":"wide"} hidden={tabMode && view !== "chat"}><Chat project={project} task={task} tasks={tasks} agents={agents} messages={messages}
269
272
  running={running} blockedBy={blockedBy} transcriptLoading={transcriptLoading} older={older} onLoadOlder={onLoadOlder}
270
- controls={controls} configBusy={configBusy} active={!tabMode&&!drawerMode || view === "chat"}
273
+ controls={controls} configBusy={configBusy} composer={composerConfig} active={!tabMode&&!drawerMode || view === "chat"}
271
274
  onSelectTask={onSelectTask} onFork={onFork} onSend={onSend} onStop={onStop}
272
275
  onStart={onStart} onEdit={onEdit} onClear={onClear} onSetSendContext={onSetSendContext}
273
276
  onReconnect={onReconnect} onAnswer={onAnswer} onCancelQueued={onCancelQueued} /></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,14 +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:36px;height:36px;flex:none;border-radius:50%;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s;}
1033
- .send:hover{filter:brightness(1.07);}
1034
- .send:disabled{opacity:.35;cursor:default;filter:none;}
1035
- .send.stop{background:var(--blue);color:#fff;}
1036
- .send.stop svg{fill:currentColor;width:15px;height:15px;}
1037
- /* mid-turn: queue button + stop button sit side by side */
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;}
1034
+ .send:hover:not(:disabled){filter:brightness(1.08);}
1035
+ .send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
1036
+ .send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
1037
+ .send[aria-busy="true"]{opacity:.7;cursor:progress;}
1038
+ .send.stop{background-color:var(--blue);color:var(--on-accent);}
1039
+ .send.stop svg{fill:currentColor;width:20px;height:20px;}
1040
+ .send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
1041
+ @media(prefers-reduced-motion:reduce){.send{transition:none;}.send .spinner{animation:none;}}
1042
+ /* Send, queue and stop share one primary action slot. */
1038
1043
  .send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
1039
- .send svg{width:17px;height:17px;}
1044
+ .send svg{width:22px;height:22px;}
1040
1045
  .comp-foot{display:flex;align-items:center;gap:10px;padding:9px 14px;border-top:1px solid var(--line-2);background:var(--surface-2);}
1041
1046
  .comp-foot .hint{font-family:var(--mono);font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px;}
1042
1047
  .comp-foot .hint svg{width:13px;height:13px;}
@@ -1053,7 +1058,6 @@ button.tpeek-more:hover{color:var(--accent-ink);}
1053
1058
  @container (max-width:420px){.comp-foot .hint .kbd{display:none;}.run-sw{max-width:130px;}}
1054
1059
 
1055
1060
  .send-group{display:flex;align-items:center;gap:6px;flex:none;}
1056
- .send svg{width:19px;height:19px;}
1057
1061
  .comp-error,.control-error{padding:8px 10px;font-size:12px;line-height:1.5;color:var(--red);overflow-wrap:anywhere;}
1058
1062
  .comp-upload-status{padding:6px 10px;font-size:11px;color:var(--ink-3);}
1059
1063
  .comp-quote{margin:12px 14px 0;}
@@ -1411,8 +1415,8 @@ button.pop-item:hover{background:var(--surface-2);}
1411
1415
  .comp-add.on svg{transform:rotate(45deg);}
1412
1416
  .composer .comp-setting{min-height:38px;border-radius:6px;padding:7px 10px;background:transparent;}
1413
1417
  .composer .comp-add{width:38px;height:38px;border-radius:6px;background:transparent;}
1414
- .composer .comp-setting:hover:not(:disabled),.composer .comp-add:hover:not(:disabled){background:var(--surface-2);}
1415
- .composer .send{width:38px;height:38px;}
1418
+ @media(hover:hover) and (pointer:fine){.composer .comp-setting:hover:not(:disabled),.composer .comp-add:hover:not(:disabled){background:var(--surface-2);}}
1419
+ .composer .send{width:44px;height:44px;}
1416
1420
  .composer .comp-box{border-radius:26px;border-color:var(--line);box-shadow:0 4px 20px rgba(0,0,0,.035);}
1417
1421
  .composer .comp-box:focus-within{border-color:var(--line-strong);box-shadow:0 6px 24px rgba(0,0,0,.055);}
1418
1422
  .composer .comp-toolbar{gap:6px;padding:6px 12px 12px;}
@@ -1434,12 +1438,11 @@ button.pop-item:hover{background:var(--surface-2);}
1434
1438
  .composer .comp-toolbar{flex-wrap:nowrap;padding-left:8px;padding-right:8px;}
1435
1439
  .composer .comp-toolbar>.comp-control{flex:0 0 auto;}
1436
1440
  .composer .comp-toolbar>.comp-model{flex:0 1 auto;min-width:44px;margin-left:auto;}
1437
- .composer .comp-setting{font-size:11px;gap:0;padding-left:6px;padding-right:6px;}
1438
- .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;}
1439
1443
  .composer .comp-model-name{max-width:42px;}
1440
- .composer .comp-toolbar:not(:has(.send.stop)) .comp-setting{gap:4px;font-size:12px;}
1441
- .composer .comp-toolbar:not(:has(.send.stop)) .permission>svg:first-child{display:block;width:14px;height:14px;}
1442
- .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;}
1443
1446
  .composer .comp-foot{flex-wrap:wrap;gap:6px;}
1444
1447
  .composer .comp-foot>.send-group{margin-left:auto;}
1445
1448
  }
@@ -1478,7 +1481,6 @@ button.pop-item:hover{background:var(--surface-2);}
1478
1481
  /* generic separators / utility */
1479
1482
  .divider{height:1px;background:var(--line);margin:6px 0;}
1480
1483
 
1481
- .send svg{width:17px;height:17px;}
1482
1484
  .sug-btn svg,.btn-sm svg{width:13px;height:13px;}
1483
1485
 
1484
1486
  /* status label (opt-in via .slabel consumers; hidden by default) */
@@ -1549,7 +1551,7 @@ button.pop-item:hover{background:var(--surface-2);}
1549
1551
  the home indicator. The keyboard hints are mouse-era noise on a phone. */
1550
1552
  .app.mobile .composer{padding:10px 12px calc(12px + env(safe-area-inset-bottom));}
1551
1553
  .app.mobile .comp-area textarea{font-size:16px;}
1552
- .app.mobile .send{width:40px;height:40px;}
1554
+ .app.mobile .send{width:44px;height:44px;}
1553
1555
 
1554
1556
  /* Ask card options are tap targets — give them room. */
1555
1557
  .app.mobile .ask-opt{padding:10px 13px;}
@@ -1838,7 +1840,7 @@ button.pop-item:hover{background:var(--surface-2);}
1838
1840
  [data-chat-layout]>.composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1839
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;}
1840
1842
  @container chat-column (max-width:900px){[data-chat-layout]>*{--cli-chat-content-max:100%;}}
1841
- @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);}}
1842
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;}
1843
1845
  .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1844
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,14 +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:36px;height:36px;flex:none;border-radius:50%;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;transition:filter .12s,opacity .12s;}
1033
- .cli-ui .send:hover{filter:brightness(1.07);}
1034
- .cli-ui .send:disabled{opacity:.35;cursor:default;filter:none;}
1035
- .cli-ui .send.stop{background:var(--blue);color:#fff;}
1036
- .cli-ui .send.stop svg{fill:currentColor;width:15px;height:15px;}
1037
- /* mid-turn: queue button + stop button sit side by side */
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;}
1034
+ .cli-ui .send:hover:not(:disabled){filter:brightness(1.08);}
1035
+ .cli-ui .send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
1036
+ .cli-ui .send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
1037
+ .cli-ui .send[aria-busy="true"]{opacity:.7;cursor:progress;}
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;}
1040
+ .cli-ui .send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
1041
+ @media(prefers-reduced-motion:reduce){.cli-ui .send{transition:none;}.cli-ui .send .spinner{animation:none;}}
1042
+ /* Send, queue and stop share one primary action slot. */
1038
1043
  .cli-ui .send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
1039
- .cli-ui .send svg{width:17px;height:17px;}
1044
+ .cli-ui .send svg{width:22px;height:22px;}
1040
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);}
1041
1046
  .cli-ui .comp-foot .hint{font-family:var(--mono);font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px;}
1042
1047
  .cli-ui .comp-foot .hint svg{width:13px;height:13px;}
@@ -1053,7 +1058,6 @@
1053
1058
  @container (max-width:420px){.cli-ui .comp-foot .hint .kbd{display:none;}.cli-ui .run-sw{max-width:130px;}}
1054
1059
 
1055
1060
  .cli-ui .send-group{display:flex;align-items:center;gap:6px;flex:none;}
1056
- .cli-ui .send svg{width:19px;height:19px;}
1057
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;}
1058
1062
  .cli-ui .comp-upload-status{padding:6px 10px;font-size:11px;color:var(--ink-3);}
1059
1063
  .cli-ui .comp-quote{margin:12px 14px 0;}
@@ -1411,8 +1415,8 @@
1411
1415
  .cli-ui .comp-add.on svg{transform:rotate(45deg);}
1412
1416
  .cli-ui .composer .comp-setting{min-height:38px;border-radius:6px;padding:7px 10px;background:transparent;}
1413
1417
  .cli-ui .composer .comp-add{width:38px;height:38px;border-radius:6px;background:transparent;}
1414
- .cli-ui .composer .comp-setting:hover:not(:disabled),.cli-ui .composer .comp-add:hover:not(:disabled){background:var(--surface-2);}
1415
- .cli-ui .composer .send{width:38px;height:38px;}
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
+ .cli-ui .composer .send{width:44px;height:44px;}
1416
1420
  .cli-ui .composer .comp-box{border-radius:26px;border-color:var(--line);box-shadow:0 4px 20px rgba(0,0,0,.035);}
1417
1421
  .cli-ui .composer .comp-box:focus-within{border-color:var(--line-strong);box-shadow:0 6px 24px rgba(0,0,0,.055);}
1418
1422
  .cli-ui .composer .comp-toolbar{gap:6px;padding:6px 12px 12px;}
@@ -1434,12 +1438,11 @@
1434
1438
  .cli-ui .composer .comp-toolbar{flex-wrap:nowrap;padding-left:8px;padding-right:8px;}
1435
1439
  .cli-ui .composer .comp-toolbar>.comp-control{flex:0 0 auto;}
1436
1440
  .cli-ui .composer .comp-toolbar>.comp-model{flex:0 1 auto;min-width:44px;margin-left:auto;}
1437
- .cli-ui .composer .comp-setting{font-size:11px;gap:0;padding-left:6px;padding-right:6px;}
1438
- .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;}
1439
1443
  .cli-ui .composer .comp-model-name{max-width:42px;}
1440
- .cli-ui .composer .comp-toolbar:not(:has(.send.stop)) .comp-setting{gap:4px;font-size:12px;}
1441
- .cli-ui .composer .comp-toolbar:not(:has(.send.stop)) .permission>svg:first-child{display:block;width:14px;height:14px;}
1442
- .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;}
1443
1446
  .cli-ui .composer .comp-foot{flex-wrap:wrap;gap:6px;}
1444
1447
  .cli-ui .composer .comp-foot>.send-group{margin-left:auto;}
1445
1448
  }
@@ -1478,7 +1481,6 @@
1478
1481
  /* generic separators / utility */
1479
1482
  .cli-ui .divider{height:1px;background:var(--line);margin:6px 0;}
1480
1483
 
1481
- .cli-ui .send svg{width:17px;height:17px;}
1482
1484
  .cli-ui .sug-btn svg,.cli-ui .btn-sm svg{width:13px;height:13px;}
1483
1485
 
1484
1486
  /* status label (opt-in via .slabel consumers; hidden by default) */
@@ -1549,7 +1551,7 @@
1549
1551
  the home indicator. The keyboard hints are mouse-era noise on a phone. */
1550
1552
  .cli-ui .app.mobile .composer{padding:10px 12px calc(12px + env(safe-area-inset-bottom));}
1551
1553
  .cli-ui .app.mobile .comp-area textarea{font-size:16px;}
1552
- .cli-ui .app.mobile .send{width:40px;height:40px;}
1554
+ .cli-ui .app.mobile .send{width:44px;height:44px;}
1553
1555
 
1554
1556
  /* Ask card options are tap targets — give them room. */
1555
1557
  .cli-ui .app.mobile .ask-opt{padding:10px 13px;}
@@ -1838,7 +1840,7 @@
1838
1840
  .cli-ui [data-chat-layout]>.composer>.composer-inner{width:100%;max-width:var(--cli-chat-content-max);margin-inline:auto;}
1839
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;}
1840
1842
  @container chat-column (max-width:900px){.cli-ui [data-chat-layout]>*{--cli-chat-content-max:100%;}}
1841
- @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);}}
1842
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;}
1843
1845
  .cli-ui .fork-sources{display:flex;flex-wrap:wrap;gap:6px;padding:12px 14px 0;}
1844
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
@@ -54,12 +54,12 @@
54
54
  .cli-ui .cli-export{white-space:nowrap;}
55
55
  .cli-ui .comp-area textarea{min-width:0;}
56
56
  .cli-ui .comp-foot{gap:8px;padding:8px 10px;background:transparent;border-top:0;flex-wrap:wrap;}
57
- .cli-ui .cli-attach{display:inline-flex;align-items:center;gap:5px;min-height:32px;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);}
59
- .cli-ui .cli-attach svg{width:15px;height:15px;}
60
- .cli-ui .send.cli-send{display:inline-flex;justify-content:center;gap:5px;width:auto;min-width:76px;padding:0 12px;font-size:12.5px;font-weight:600;}
61
- .cli-ui .send.cli-send:disabled{background:var(--surface-2);color:var(--ink-4);box-shadow:none;opacity:1;}
62
- .cli-ui .cli-export:focus-visible,.cli-ui .cli-attach:focus-visible,.cli-ui .send:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
57
+ .cli-ui .comp-foot>.comp-control{flex:0 0 auto;}
58
+ .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;}
59
+ @media(hover:hover) and (pointer:fine){.cli-ui .cli-attach:hover{background:var(--surface-2);color:var(--ink);}}
60
+ .cli-ui .cli-attach svg{width:22px;height:22px;}
61
+ .cli-ui .comp-foot:has(.comp-model)>.spacer{display:none;}
62
+ .cli-ui .cli-export:focus-visible,.cli-ui .cli-attach:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
63
63
  .cli-ui .cli-task-control{height:34px;padding:0 10px;white-space:nowrap;}
64
64
  .cli-ui .cli-task-control:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
65
65
  @container cli-chat (max-width:560px){
@@ -455,19 +455,20 @@ cli-split{display:block;}
455
455
  .cli-ui .cli-auto-background pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:220px;overflow:auto;}
456
456
 
457
457
  /* The live queue sits just above the composer; expanded content stays in a viewport-bound card. */
458
- .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);}
459
- .cli-ui .cli-queue-row{display:flex;align-items:center;gap:8px;min-width:0;min-height:46px;}
458
+ .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);}
459
+ .cli-ui .cli-queue-row{display:flex;align-items:center;gap:8px;min-width:0;min-height:44px;}
460
460
  .cli-ui .cli-queue-row+.cli-queue-row{border-top:1px solid var(--line-2);}
461
461
  .cli-ui .cli-queue-icon{display:grid;place-items:center;flex:none;width:20px;color:var(--ink-4);}
462
462
  .cli-ui .cli-queue-icon svg{width:16px;height:16px;}
463
463
  .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);}
464
464
  .cli-ui .cli-queue-thumb{flex:none;width:32px;height:32px;object-fit:cover;border:1px solid var(--line);border-radius:7px;}
465
465
  .cli-ui .cli-queue-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:1.5;}
466
- .cli-ui .cli-queue-state{flex:none;color:var(--ink-4);font-size:10px;white-space:nowrap;}
467
466
  .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);}
468
467
  .cli-ui .cli-queue-remove svg,.cli-ui .cli-queue-more svg{width:17px;height:17px;}
469
- .cli-ui .cli-queue-remove:hover,.cli-ui .cli-queue-more:hover{background:var(--surface-2);color:var(--ink);}
470
- .cli-ui .cli-queue-remove:hover{color:var(--red);}
468
+ @media(hover:hover) and (pointer:fine){
469
+ .cli-ui .cli-queue-remove:hover,.cli-ui .cli-queue-more:hover{background:var(--surface-2);color:var(--ink);}
470
+ .cli-ui .cli-queue-remove:hover{color:var(--red);}
471
+ }
471
472
  .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;}
472
473
  .cli-ui .cli-queue-popover{width:420px;}
473
474
  .cli-ui .cli-queue-detail{max-height:min(44vh,320px);overflow:auto;overscroll-behavior:contain;overflow-wrap:anywhere;}
@@ -480,11 +481,10 @@ cli-split{display:block;}
480
481
  .cli-ui .cli-queue-feedback[role="alert"]{color:var(--red);}
481
482
  .cli-ui .session>.cli-queue{width:calc(100% - 48px);max-width:800px;align-self:center;margin:8px 24px 16px;border-radius:16px;}
482
483
  @container(max-width:420px){
483
- .cli-ui .cli-queue{margin:0 8px -1px;padding:4px 6px 7px;max-height:112px;}
484
+ .cli-ui .cli-queue{margin:0 10px -12px;padding:4px 6px 16px;max-height:112px;}
484
485
  .cli-ui .cli-queue-row{gap:5px;}
485
- .cli-ui .cli-queue-state{display:none;}
486
486
  }
487
- @media(pointer:coarse){.cli-ui .cli-queue-remove,.cli-ui .cli-queue-more{width:36px;height:40px;}}
487
+ @media(pointer:coarse){.cli-ui .cli-queue-remove,.cli-ui .cli-queue-more{width:44px;height:44px;}}
488
488
  @media(max-height:500px){.cli-ui .cli-queue{max-height:70px;}}
489
489
 
490
490
  /* Keep controls reachable when a keyboard, split host or landscape view leaves little height. */
@@ -495,7 +495,8 @@ cli-split{display:block;}
495
495
  .cli-ui .comp-area textarea,.cli-ui .ask-other,.cli-ui .bp-addr,.cli-ui .bp-dialog-in{font-size:16px;}
496
496
  .cli-ui .rail-ftab-x{width:32px;height:36px;opacity:1;}
497
497
  .cli-ui .cli-ref-x{width:32px;height:32px;}
498
- .cli-ui .cli-fv-btn,.cli-ui .cli-fv-back,.cli-ui .cli-fv-icon,.cli-ui .bp-nav,.cli-ui .cli-attach{min-height:36px;min-width:36px;}
498
+ .cli-ui .cli-fv-btn,.cli-ui .cli-fv-back,.cli-ui .cli-fv-icon,.cli-ui .bp-nav{min-height:36px;min-width:36px;}
499
+ .cli-ui .cli-attach{min-height:44px;min-width:44px;}
499
500
  .cli-ui .rail-collapse{min-width:36px;min-height:36px;}
500
501
  .cli-ui .cli-crumb{min-height:36px;}
501
502
  .cli-ui .cli-fv-selq{min-height:40px;}
@@ -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)}>