@spzxg/cli-ui 0.6.7 → 0.6.8
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/CHANGELOG.md +8 -1
- package/README.md +14 -4
- package/dist/{cli-ui-embed-CB7ibAtF.js → cli-ui-embed-Be9_d9jB.js} +15257 -15124
- package/dist/{cli-ui-index-BmwOLWFI.js → cli-ui-index-CzNfiynu.js} +2 -2
- package/dist/{cli-ui-jszip.min-Cq0WjNYb.js → cli-ui-jszip.min-BL0mSekN.js} +2 -2
- package/dist/{cli-ui-jszip.min-BKNOJnhm.js → cli-ui-jszip.min-DjCIAfse.js} +1 -1
- package/dist/{cli-ui-xlsx-Sl7ypvSe.js → cli-ui-xlsx-CscI4xk0.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +1 -1
- package/src/chat/CliChat.tsx +35 -5
- package/src/chat/Composer.tsx +4 -4
- package/src/chat/RunControls.tsx +15 -8
- package/src/chat/draft.ts +7 -2
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +1 -1
- package/src/core/client.ts +21 -4
- package/src/core/config.ts +6 -2
- package/src/core/execution.ts +24 -0
- package/src/core/store.ts +10 -6
- package/src/core/types.ts +2 -0
- package/src/index.ts +1 -0
- package/src/operator/node/Chat.tsx +6 -3
- package/src/operator/node/Composer.tsx +13 -12
- package/src/operator/node/SessionView.tsx +7 -4
- package/src/operator.base.css +10 -6
- package/src/operator.css +10 -6
- package/src/styles.css +6 -6
|
@@ -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
|
-
|
|
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) {
|
|
@@ -249,7 +250,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
|
|
|
249
250
|
{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
251
|
{queued}
|
|
251
252
|
<div className={`comp-box${dragging ? " dropping" : ""}`}
|
|
252
|
-
onDragEnter={(e) => { if (hasFileDrag(e)) { e.preventDefault(); if (!locked) { dragDepth.current++; setDragging(true); } } }}
|
|
253
|
+
onDragEnter={(e) => { if (hasFileDrag(e)) { e.preventDefault(); if (fileUpload && !locked) { dragDepth.current++; setDragging(true); } } }}
|
|
253
254
|
onDragOver={(e) => { if (hasFileDrag(e)) e.preventDefault(); }}
|
|
254
255
|
onDragLeave={() => { if (dragDepth.current > 0 && --dragDepth.current === 0) setDragging(false); }}
|
|
255
256
|
onDrop={(e) => { if (!hasFileDrag(e)) return; e.preventDefault(); dragDepth.current = 0; setDragging(false); addFiles(Array.from(e.dataTransfer.files)); }}>
|
|
@@ -258,7 +259,7 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
|
|
|
258
259
|
{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
260
|
status={a.status === "error" ? "error" : a.phase ?? "queued"} bytes={a.bytes} total={a.size} disabled={locked}
|
|
260
261
|
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>}
|
|
262
|
+
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
263
|
{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
264
|
{a.kind === "image" && a.preview ? <img src={a.preview} alt={a.name} /> : <span className="attach-file">{Icon.clip()} {a.name}</span>}
|
|
264
265
|
<button className="attach-x" disabled={locked} aria-label={`移除 ${a.name}`} onClick={() => removeAtt(a.key)}>×</button>
|
|
@@ -282,16 +283,16 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
|
|
|
282
283
|
const files = Array.from(e.clipboardData.files);
|
|
283
284
|
if (files.length) { e.preventDefault(); addFiles(files); return; }
|
|
284
285
|
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" })]); }
|
|
286
|
+
if (fileUpload && text.length > PASTE_ATTACH_THRESHOLD) { e.preventDefault(); addFiles([new File([text], "pasted-text.txt", { type: "text/plain" })]); }
|
|
286
287
|
}} /></div>
|
|
287
288
|
<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>
|
|
289
|
+
{fileUpload && <input ref={fileRef} type="file" accept={FILE_UPLOAD_ACCEPT} multiple hidden onChange={(e) => { addFiles(Array.from(e.target.files ?? [])); e.target.value = ""; }} />}
|
|
290
|
+
<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
291
|
{addOpen && <Popover className="composer-popover composer-add-popover" ariaLabel="添加内容" placement="top" align="start" onClose={() => setAddOpen(false)}>
|
|
291
292
|
<div className="composer-menu-heading">添加内容</div>
|
|
292
|
-
<button className="pop-item composer-option" disabled={locked} onClick={() => { setAddOpen(false); fileRef.current?.click(); }}>
|
|
293
|
+
{fileUpload && <button className="pop-item composer-option" disabled={locked} onClick={() => { setAddOpen(false); fileRef.current?.click(); }}>
|
|
293
294
|
<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>
|
|
295
|
+
</button>}
|
|
295
296
|
<button className="pop-item composer-option" disabled={locked} onClick={openMention}>
|
|
296
297
|
<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
298
|
</button>
|
|
@@ -302,8 +303,8 @@ export function Composer({ task, agentLabel, disabled, running, onSend, onStop,
|
|
|
302
303
|
</div>
|
|
303
304
|
<RunControls {...controls} disabled={controls.disabled || sending} />
|
|
304
305
|
<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>}
|
|
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
308
|
</div>
|
|
308
309
|
</div>
|
|
309
310
|
</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
|
|
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
|
-
|
|
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>
|
package/src/operator.base.css
CHANGED
|
@@ -1029,11 +1029,15 @@ 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:
|
|
1033
|
-
.send:hover{filter:brightness(1.
|
|
1034
|
-
.send:disabled{
|
|
1035
|
-
.send
|
|
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;}
|
|
1033
|
+
.send:hover:not(:disabled){filter:brightness(1.08);}
|
|
1034
|
+
.send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
|
|
1035
|
+
.send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
|
|
1036
|
+
.send[aria-busy="true"]{opacity:.7;cursor:progress;}
|
|
1037
|
+
.send.stop{background:var(--blue);color:var(--on-accent);}
|
|
1036
1038
|
.send.stop svg{fill:currentColor;width:15px;height:15px;}
|
|
1039
|
+
.send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
|
|
1040
|
+
@media(prefers-reduced-motion:reduce){.send{transition:none;}.send .spinner{animation:none;}}
|
|
1037
1041
|
/* mid-turn: queue button + stop button sit side by side */
|
|
1038
1042
|
.send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
|
|
1039
1043
|
.send svg{width:17px;height:17px;}
|
|
@@ -1412,7 +1416,7 @@ button.pop-item:hover{background:var(--surface-2);}
|
|
|
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
1418
|
.composer .comp-setting:hover:not(:disabled),.composer .comp-add:hover:not(:disabled){background:var(--surface-2);}
|
|
1415
|
-
.composer .send{width:
|
|
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;}
|
|
@@ -1549,7 +1553,7 @@ button.pop-item:hover{background:var(--surface-2);}
|
|
|
1549
1553
|
the home indicator. The keyboard hints are mouse-era noise on a phone. */
|
|
1550
1554
|
.app.mobile .composer{padding:10px 12px calc(12px + env(safe-area-inset-bottom));}
|
|
1551
1555
|
.app.mobile .comp-area textarea{font-size:16px;}
|
|
1552
|
-
.app.mobile .send{width:
|
|
1556
|
+
.app.mobile .send{width:44px;height:44px;}
|
|
1553
1557
|
|
|
1554
1558
|
/* Ask card options are tap targets — give them room. */
|
|
1555
1559
|
.app.mobile .ask-opt{padding:10px 13px;}
|
package/src/operator.css
CHANGED
|
@@ -1029,11 +1029,15 @@
|
|
|
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:
|
|
1033
|
-
.cli-ui .send:hover{filter:brightness(1.
|
|
1034
|
-
.cli-ui .send:disabled{
|
|
1035
|
-
.cli-ui .send
|
|
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;}
|
|
1033
|
+
.cli-ui .send:hover:not(:disabled){filter:brightness(1.08);}
|
|
1034
|
+
.cli-ui .send:active:not(:disabled){transform:scale(.96);filter:brightness(.94);}
|
|
1035
|
+
.cli-ui .send:disabled{opacity:.35;cursor:not-allowed;filter:none;}
|
|
1036
|
+
.cli-ui .send[aria-busy="true"]{opacity:.7;cursor:progress;}
|
|
1037
|
+
.cli-ui .send.stop{background:var(--blue);color:var(--on-accent);}
|
|
1036
1038
|
.cli-ui .send.stop svg{fill:currentColor;width:15px;height:15px;}
|
|
1039
|
+
.cli-ui .send .spinner{width:18px;height:18px;border-color:color-mix(in srgb,currentColor 30%,transparent);border-top-color:currentColor;}
|
|
1040
|
+
@media(prefers-reduced-motion:reduce){.cli-ui .send{transition:none;}.cli-ui .send .spinner{animation:none;}}
|
|
1037
1041
|
/* mid-turn: queue button + stop button sit side by side */
|
|
1038
1042
|
.cli-ui .send-group{display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;}
|
|
1039
1043
|
.cli-ui .send svg{width:17px;height:17px;}
|
|
@@ -1412,7 +1416,7 @@
|
|
|
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
1418
|
.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:
|
|
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;}
|
|
@@ -1549,7 +1553,7 @@
|
|
|
1549
1553
|
the home indicator. The keyboard hints are mouse-era noise on a phone. */
|
|
1550
1554
|
.cli-ui .app.mobile .composer{padding:10px 12px calc(12px + env(safe-area-inset-bottom));}
|
|
1551
1555
|
.cli-ui .app.mobile .comp-area textarea{font-size:16px;}
|
|
1552
|
-
.cli-ui .app.mobile .send{width:
|
|
1556
|
+
.cli-ui .app.mobile .send{width:44px;height:44px;}
|
|
1553
1557
|
|
|
1554
1558
|
/* Ask card options are tap targets — give them room. */
|
|
1555
1559
|
.cli-ui .app.mobile .ask-opt{padding:10px 13px;}
|
package/src/styles.css
CHANGED
|
@@ -54,12 +54,11 @@
|
|
|
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:
|
|
57
|
+
.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
58
|
.cli-ui .cli-attach:hover{background:var(--surface-2);color:var(--ink);}
|
|
59
|
-
.cli-ui .cli-attach svg{width:
|
|
60
|
-
.cli-ui .
|
|
61
|
-
.cli-ui .
|
|
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;}
|
|
59
|
+
.cli-ui .cli-attach svg{width:22px;height:22px;}
|
|
60
|
+
.cli-ui .comp-foot:has(.comp-model)>.spacer{display:none;}
|
|
61
|
+
.cli-ui .cli-export:focus-visible,.cli-ui .cli-attach:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
63
62
|
.cli-ui .cli-task-control{height:34px;padding:0 10px;white-space:nowrap;}
|
|
64
63
|
.cli-ui .cli-task-control:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
65
64
|
@container cli-chat (max-width:560px){
|
|
@@ -495,7 +494,8 @@ cli-split{display:block;}
|
|
|
495
494
|
.cli-ui .comp-area textarea,.cli-ui .ask-other,.cli-ui .bp-addr,.cli-ui .bp-dialog-in{font-size:16px;}
|
|
496
495
|
.cli-ui .rail-ftab-x{width:32px;height:36px;opacity:1;}
|
|
497
496
|
.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
|
|
497
|
+
.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;}
|
|
498
|
+
.cli-ui .cli-attach{min-height:44px;min-width:44px;}
|
|
499
499
|
.cli-ui .rail-collapse{min-width:36px;min-height:36px;}
|
|
500
500
|
.cli-ui .cli-crumb{min-height:36px;}
|
|
501
501
|
.cli-ui .cli-fv-selq{min-height:40px;}
|