dsh-sidenote 0.3.1 → 0.3.2
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/lib/client.js +724 -95
- package/lib/types/client/chat/cards.d.ts +4 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/sidechat/PendingCard.d.ts +4 -0
- package/lib/types/client/sidechat/notify.d.ts +16 -0
- package/lib/types/client/sidechat/pending.d.ts +71 -0
- package/package.json +1 -1
- package/src/client/chat/cards.ts +186 -4
- package/src/client/chat/transcript.ts +1 -0
- package/src/client/locales.ts +22 -2
- package/src/client/sidechat/PendingCard.tsx +145 -0
- package/src/client/sidechat/SideChatPanel.tsx +27 -1
- package/src/client/sidechat/index.tsx +3 -0
- package/src/client/sidechat/notify.ts +91 -0
- package/src/client/sidechat/pending.ts +238 -0
- package/src/client/sidechat/sidechat.module.css +142 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 完成通知(WI-03,L3 常驻监听层):侧聊子会话 running true→false 翻转时
|
|
3
|
+
* toast 提醒——用户焦点在主对话时侧聊长任务跑完能被看见(Zed
|
|
4
|
+
* notify_when_agent_waiting 先例)。
|
|
5
|
+
*
|
|
6
|
+
* 事件源是 session.subscribe 快照推送(非轮询);宿主 notifier 入账不依赖
|
|
7
|
+
* 订阅者——面板不可见(SideChatPanel 的 uSES 断订阅)期间事件不丢。
|
|
8
|
+
* 本层是常驻 ctx.effect:Tab 开闭由 betterSidebar.subscribeState 重扫驱动。
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '../host/contracts.ts'
|
|
11
|
+
import { collectSideTabs, parseSideChatMeta } from './model.ts'
|
|
12
|
+
import { openSessionWindow } from './lifecycle.ts'
|
|
13
|
+
import { showToast } from './toast.tsx'
|
|
14
|
+
import { t } from '../locales.ts'
|
|
15
|
+
|
|
16
|
+
interface Watcher {
|
|
17
|
+
unsub: () => void
|
|
18
|
+
wasRunning: boolean
|
|
19
|
+
/** 建 watcher 的时间——挂接瞬间的 running 翻转(残影)不报。 */
|
|
20
|
+
since: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 挂接保护窗:watcher 建立后该窗口内的翻转不视为「一个真实 turn 跑完」。 */
|
|
24
|
+
const ATTACH_GRACE_MS = 800
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 翻转侦测核心(抽出来纯可测):running true→false 且距挂接超过保护窗
|
|
28
|
+
* 时返回 true。调用方持状态。
|
|
29
|
+
*/
|
|
30
|
+
export function shouldNotify(wasRunning: boolean, running: boolean, attachedAt: number, now: number): boolean {
|
|
31
|
+
return wasRunning && !running && now - attachedAt > ATTACH_GRACE_MS
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function registerCompletionNotify(ctx: Context): void {
|
|
35
|
+
ctx.effect(() => {
|
|
36
|
+
const watchers = new Map<string, Watcher>()
|
|
37
|
+
|
|
38
|
+
const rescan = (): void => {
|
|
39
|
+
let tabs: ReturnType<typeof collectSideTabs> = []
|
|
40
|
+
try {
|
|
41
|
+
tabs = collectSideTabs(ctx.betterSidebar.getSnapshot())
|
|
42
|
+
} catch {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const alive = new Set<string>()
|
|
46
|
+
for (const tab of tabs) {
|
|
47
|
+
const meta = parseSideChatMeta(tab.meta)
|
|
48
|
+
const childId = meta.childId
|
|
49
|
+
if (childId === undefined) continue
|
|
50
|
+
alive.add(childId)
|
|
51
|
+
if (watchers.has(childId)) continue
|
|
52
|
+
// 新侧聊:binding 可能 throw(会话还没就绪)——本轮跳过,下轮重扫补。
|
|
53
|
+
try {
|
|
54
|
+
const session = ctx.sessions.binding(childId)?.session
|
|
55
|
+
if (session === undefined) continue
|
|
56
|
+
openSessionWindow(session)
|
|
57
|
+
const title = tab.title
|
|
58
|
+
const watcher: Watcher = { unsub: () => {}, wasRunning: false, since: Date.now() }
|
|
59
|
+
watcher.unsub = session.subscribe(() => {
|
|
60
|
+
const snap = session.getSnapshot() as { running?: unknown } | null
|
|
61
|
+
const running = snap?.running === true
|
|
62
|
+
if (shouldNotify(watcher.wasRunning, running, watcher.since, Date.now())) {
|
|
63
|
+
showToast(t('sideChatDone', { title }))
|
|
64
|
+
}
|
|
65
|
+
watcher.wasRunning = running
|
|
66
|
+
})
|
|
67
|
+
// 挂接即取一次初值(正在跑的会话稍后跑完时报)。
|
|
68
|
+
watcher.wasRunning = (session.getSnapshot() as { running?: unknown } | null)?.running === true
|
|
69
|
+
watchers.set(childId, watcher)
|
|
70
|
+
} catch {
|
|
71
|
+
// 下轮重扫再试。
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 清理已关闭 Tab 的 watcher(× 即焚的会话不再报)。
|
|
75
|
+
for (const [id, w] of watchers) {
|
|
76
|
+
if (!alive.has(id)) {
|
|
77
|
+
w.unsub()
|
|
78
|
+
watchers.delete(id)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const offState = ctx.betterSidebar.subscribeState(rescan)
|
|
84
|
+
rescan()
|
|
85
|
+
return () => {
|
|
86
|
+
offState()
|
|
87
|
+
for (const w of watchers.values()) w.unsub()
|
|
88
|
+
watchers.clear()
|
|
89
|
+
}
|
|
90
|
+
}, 'dsh-sidenote: completion notify')
|
|
91
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R8 侧边审批/提问答复(L0/L1:双版本数据源探测链 + 视图模型归一)。
|
|
3
|
+
*
|
|
4
|
+
* 侦察依据(.vibe Delivery_04/Workitem_02 design.md):
|
|
5
|
+
* - pending 不在 projections.faceOf 体系——0.1.1 在 Session 快照顶层
|
|
6
|
+
* `pending: PendingWait[]`(mux 帧铸造,重连重放 rpcId 不变);0.1.2 移入
|
|
7
|
+
* `ctx.get('uiSession').pendingInteractions`(Map<SessionId, Pending>,
|
|
8
|
+
* 每会话至多一项,precedence 仲裁)。
|
|
9
|
+
* - 写路径都是公开对象的公开方法:0.1.1 `wait.respond({ok,value})`(审批
|
|
10
|
+
* value={sessionId, approvalId, outcome};提问整批 {sessionId, answer},
|
|
11
|
+
* 取消走 {ok:false,error:{code:'cancelled',…}});0.1.2 域对象
|
|
12
|
+
* `answer(outcome|answer)` / `cancel()`。
|
|
13
|
+
* - 竞答安全:先答先得,settle 幂等——后答方 throw,UI catch 吞掉并回退
|
|
14
|
+
* 按钮态(宿主同款 `.catch(() => setAnswered(false))`)。
|
|
15
|
+
*
|
|
16
|
+
* off-face 纪律:两面都 feature-check;全缺席返回 undefined(面板回退
|
|
17
|
+
* pendingBar 跳转行为,不崩)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ── 视图模型(视图层唯一消费形状;双版本在此归一)──
|
|
21
|
+
|
|
22
|
+
export interface PendingApprovalView {
|
|
23
|
+
kind: 'approval'
|
|
24
|
+
key: string
|
|
25
|
+
toolName: string
|
|
26
|
+
reason?: string | undefined
|
|
27
|
+
answer: (outcome: 'allowed-once' | 'rejected') => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PendingQuestionOption {
|
|
31
|
+
label: string
|
|
32
|
+
description?: string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PendingQuestionView {
|
|
36
|
+
kind: 'question'
|
|
37
|
+
key: string
|
|
38
|
+
/** plan-review 按通用提问流降级(提交值逐字回传 asker 原 label)。 */
|
|
39
|
+
planReview: boolean
|
|
40
|
+
questions: readonly {
|
|
41
|
+
id: string
|
|
42
|
+
question: string
|
|
43
|
+
header?: string | undefined
|
|
44
|
+
detail?: string | undefined
|
|
45
|
+
options: readonly PendingQuestionOption[]
|
|
46
|
+
multiSelect: boolean
|
|
47
|
+
}[]
|
|
48
|
+
/** 整批提交(不可拆单题):answers = [{id, selected: label[], custom?}]。 */
|
|
49
|
+
answer: (answers: readonly { id: string; selected: readonly string[]; custom?: string }[]) => Promise<void>
|
|
50
|
+
cancel: () => Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type PendingItem = PendingApprovalView | PendingQuestionView
|
|
54
|
+
|
|
55
|
+
export interface PendingStore {
|
|
56
|
+
readonly available: boolean
|
|
57
|
+
subscribe(fn: () => void): () => void
|
|
58
|
+
/** 缓存快照:底层引用不变则返回同一数组(uSES 纪律)。 */
|
|
59
|
+
getSnapshot(): readonly PendingItem[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CtxLike {
|
|
63
|
+
get?: (key: string) => unknown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SessionLike {
|
|
67
|
+
subscribe(fn: () => void): () => void
|
|
68
|
+
getSnapshot(): unknown
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 0.1.2 适配(uiSession.pendingInteractions 域对象)──
|
|
72
|
+
|
|
73
|
+
function adapt012(pending: unknown): readonly PendingItem[] {
|
|
74
|
+
if (typeof pending !== 'object' || pending === null) return []
|
|
75
|
+
const o = pending as Record<string, unknown>
|
|
76
|
+
if (o.kind === 'approval' && typeof o.answer === 'function') {
|
|
77
|
+
return [{
|
|
78
|
+
kind: 'approval',
|
|
79
|
+
key: typeof o.key === 'string' ? o.key : 'approval',
|
|
80
|
+
toolName: typeof o.toolName === 'string' ? o.toolName : '',
|
|
81
|
+
...(typeof o.reason === 'string' ? { reason: o.reason } : {}),
|
|
82
|
+
answer: (outcome) => (o.answer as (x: string) => Promise<void>).call(o, outcome),
|
|
83
|
+
}]
|
|
84
|
+
}
|
|
85
|
+
if ((o.kind === 'question' || o.kind === 'plan-review') && typeof o.answer === 'function' && typeof o.cancel === 'function') {
|
|
86
|
+
const questions = normalizeQuestions(o.questions ?? (o.payload as { questions?: unknown } | undefined)?.questions)
|
|
87
|
+
if (questions === undefined) return []
|
|
88
|
+
return [{
|
|
89
|
+
kind: 'question',
|
|
90
|
+
key: typeof o.key === 'string' ? o.key : 'question',
|
|
91
|
+
planReview: o.kind === 'plan-review',
|
|
92
|
+
questions,
|
|
93
|
+
answer: (answers) => (o.answer as (a: unknown) => Promise<void>).call(o, { answers }),
|
|
94
|
+
cancel: () => (o.cancel as () => Promise<void>).call(o),
|
|
95
|
+
}]
|
|
96
|
+
}
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 0.1.1 适配(Session 快照 pending: PendingWait[])──
|
|
101
|
+
|
|
102
|
+
interface PendingWaitLike {
|
|
103
|
+
kind?: unknown
|
|
104
|
+
key?: unknown
|
|
105
|
+
sessionId?: unknown
|
|
106
|
+
payload?: unknown
|
|
107
|
+
respond?: unknown
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function adapt011(pending: readonly unknown[]): readonly PendingItem[] {
|
|
111
|
+
const items: PendingItem[] = []
|
|
112
|
+
for (const raw of pending) {
|
|
113
|
+
const w = raw as PendingWaitLike
|
|
114
|
+
if (typeof w.respond !== 'function') continue
|
|
115
|
+
const respond = w.respond as (r: unknown) => Promise<{ accepted?: boolean }>
|
|
116
|
+
const sessionId = typeof w.sessionId === 'string' ? w.sessionId : undefined
|
|
117
|
+
if (sessionId === undefined) continue
|
|
118
|
+
const payload = (w.payload ?? {}) as Record<string, unknown>
|
|
119
|
+
const key = typeof w.key === 'string' ? w.key : `pending:${items.length}`
|
|
120
|
+
if (w.kind === 'approval') {
|
|
121
|
+
const approvalId = payload.approvalId
|
|
122
|
+
if (typeof approvalId !== 'string') continue
|
|
123
|
+
items.push({
|
|
124
|
+
kind: 'approval',
|
|
125
|
+
key,
|
|
126
|
+
toolName: typeof payload.toolName === 'string' ? payload.toolName : '',
|
|
127
|
+
...(typeof payload.reason === 'string' ? { reason: payload.reason } : {}),
|
|
128
|
+
answer: async (outcome) => {
|
|
129
|
+
const receipt = await respond({ ok: true, value: { sessionId, approvalId, outcome } })
|
|
130
|
+
if (receipt.accepted === false) throw new Error('approval response rejected (not pending)')
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
} else if (w.kind === 'question') {
|
|
134
|
+
const questions = normalizeQuestions(payload.questions)
|
|
135
|
+
if (questions === undefined) continue
|
|
136
|
+
items.push({
|
|
137
|
+
kind: 'question',
|
|
138
|
+
key,
|
|
139
|
+
planReview: false,
|
|
140
|
+
questions,
|
|
141
|
+
answer: async (answers) => {
|
|
142
|
+
const receipt = await respond({ ok: true, value: { sessionId, answer: { answers } } })
|
|
143
|
+
if (receipt.accepted === false) throw new Error('question response rejected (not pending)')
|
|
144
|
+
},
|
|
145
|
+
cancel: async () => {
|
|
146
|
+
await respond({ ok: false, error: { code: 'cancelled', message: 'the user closed this question request', details: {} } })
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return items
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** questions 字段归一(选项 {label,description?} 与纯字符串两形都收)。 */
|
|
155
|
+
function normalizeQuestions(raw: unknown): PendingQuestionView['questions'] | undefined {
|
|
156
|
+
if (!Array.isArray(raw)) return undefined
|
|
157
|
+
const out: PendingQuestionView['questions'][number][] = []
|
|
158
|
+
for (const rawQ of raw) {
|
|
159
|
+
if (typeof rawQ !== 'object' || rawQ === null) return undefined
|
|
160
|
+
const q = rawQ as Record<string, unknown>
|
|
161
|
+
if (typeof q.question !== 'string') return undefined
|
|
162
|
+
const options: PendingQuestionOption[] = []
|
|
163
|
+
if (Array.isArray(q.options)) {
|
|
164
|
+
for (const rawO of q.options) {
|
|
165
|
+
if (typeof rawO === 'string') options.push({ label: rawO })
|
|
166
|
+
else if (typeof rawO === 'object' && rawO !== null && typeof (rawO as { label?: unknown }).label === 'string') {
|
|
167
|
+
const oo = rawO as { label: string; description?: unknown }
|
|
168
|
+
options.push({ label: oo.label, ...(typeof oo.description === 'string' ? { description: oo.description } : {}) })
|
|
169
|
+
} else return undefined
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
out.push({
|
|
173
|
+
id: typeof q.id === 'string' ? q.id : `q${out.length}`,
|
|
174
|
+
question: q.question,
|
|
175
|
+
...(typeof q.header === 'string' ? { header: q.header } : {}),
|
|
176
|
+
...(typeof q.detail === 'string' ? { detail: q.detail } : {}),
|
|
177
|
+
options,
|
|
178
|
+
multiSelect: q.multiSelect === true,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 双版本探测链(chatSourceOf 同款纪律):0.1.2 uiSession.pendingInteractions
|
|
186
|
+
* (getSnapshot() 是 Map 才认)优先;0.1.1 Session 快照 pending 数组回退;
|
|
187
|
+
* 双缺席 undefined(面板回退 pendingBar 跳转)。
|
|
188
|
+
*/
|
|
189
|
+
export function createPendingStore(ctx: CtxLike, session: SessionLike | undefined, childId: string): PendingStore | undefined {
|
|
190
|
+
// 面 1(0.1.2+):uiSession 服务的 pendingInteractions observable。
|
|
191
|
+
try {
|
|
192
|
+
const ui = typeof ctx.get === 'function' ? ctx.get('uiSession') : undefined
|
|
193
|
+
const pim = (ui as { pendingInteractions?: { subscribe?: unknown; getSnapshot?: unknown } } | undefined)?.pendingInteractions
|
|
194
|
+
if (pim !== undefined && typeof pim.subscribe === 'function' && typeof pim.getSnapshot === 'function') {
|
|
195
|
+
const obs = pim as { subscribe(fn: () => void): () => void; getSnapshot(): unknown }
|
|
196
|
+
if (obs.getSnapshot() instanceof Map) {
|
|
197
|
+
let cachedMap: unknown = null
|
|
198
|
+
let cachedItems: readonly PendingItem[] = []
|
|
199
|
+
return {
|
|
200
|
+
available: true,
|
|
201
|
+
subscribe: (fn) => obs.subscribe(fn),
|
|
202
|
+
getSnapshot: () => {
|
|
203
|
+
const map = obs.getSnapshot()
|
|
204
|
+
if (map === cachedMap) return cachedItems
|
|
205
|
+
cachedMap = map
|
|
206
|
+
cachedItems = map instanceof Map ? adapt012(map.get(childId)) : []
|
|
207
|
+
return cachedItems
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// 落面 2。
|
|
214
|
+
}
|
|
215
|
+
// 面 2(0.1.1):Session 快照顶层 pending 数组。
|
|
216
|
+
if (session === undefined) return undefined
|
|
217
|
+
try {
|
|
218
|
+
const snap = session.getSnapshot()
|
|
219
|
+
if (Array.isArray((snap as { pending?: unknown } | null)?.pending)) {
|
|
220
|
+
let cachedArr: unknown = null
|
|
221
|
+
let cachedItems: readonly PendingItem[] = []
|
|
222
|
+
return {
|
|
223
|
+
available: true,
|
|
224
|
+
subscribe: (fn) => session.subscribe(fn),
|
|
225
|
+
getSnapshot: () => {
|
|
226
|
+
const arr = (session.getSnapshot() as { pending?: unknown } | null)?.pending
|
|
227
|
+
if (arr === cachedArr) return cachedItems
|
|
228
|
+
cachedArr = arr
|
|
229
|
+
cachedItems = Array.isArray(arr) ? adapt011(arr) : []
|
|
230
|
+
return cachedItems
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// 双缺席。
|
|
236
|
+
}
|
|
237
|
+
return undefined
|
|
238
|
+
}
|
|
@@ -626,7 +626,148 @@
|
|
|
626
626
|
background: var(--dsw-alias-bg-layer-2);
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
/* R8
|
|
629
|
+
/* R8 审批/提问答复卡(钉在 composer 上方;警示色左边条 + 圆角卡——
|
|
630
|
+
材质与胶囊 composer 同族)。 */
|
|
631
|
+
.pendingCard {
|
|
632
|
+
margin: 0 10px 6px;
|
|
633
|
+
padding: 10px 12px;
|
|
634
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
635
|
+
border-left: 3px solid var(--dsw-alias-state-warning-primary, #e8a13a);
|
|
636
|
+
border-radius: 10px;
|
|
637
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
638
|
+
display: flex;
|
|
639
|
+
flex-direction: column;
|
|
640
|
+
gap: 6px;
|
|
641
|
+
font-size: 12px;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.pendingHead {
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
gap: 6px;
|
|
648
|
+
color: var(--dsw-alias-state-warning-primary, #e8a13a);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.pendingTitle {
|
|
652
|
+
font-weight: 600;
|
|
653
|
+
color: var(--dsw-alias-label-primary);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.pendingDesc {
|
|
657
|
+
color: var(--dsw-alias-label-secondary);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.pendingReason {
|
|
661
|
+
color: var(--dsw-alias-label-tertiary);
|
|
662
|
+
white-space: pre-wrap;
|
|
663
|
+
word-break: break-word;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.pendingQuestion {
|
|
667
|
+
display: flex;
|
|
668
|
+
flex-direction: column;
|
|
669
|
+
gap: 4px;
|
|
670
|
+
padding: 4px 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.pendingQHeader {
|
|
674
|
+
font-weight: 600;
|
|
675
|
+
color: var(--dsw-alias-label-primary);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.pendingQText {
|
|
679
|
+
color: var(--dsw-alias-label-secondary);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.pendingOption {
|
|
683
|
+
display: flex;
|
|
684
|
+
flex-direction: column;
|
|
685
|
+
align-items: flex-start;
|
|
686
|
+
gap: 2px;
|
|
687
|
+
text-align: left;
|
|
688
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
689
|
+
border-radius: 6px;
|
|
690
|
+
background: none;
|
|
691
|
+
color: var(--dsw-alias-label-secondary);
|
|
692
|
+
padding: 5px 8px;
|
|
693
|
+
font-size: 12px;
|
|
694
|
+
cursor: pointer;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.pendingOption:hover {
|
|
698
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* 选中态:与 .pendingOption 同结构(CSS 层 composes 依赖构建管道,不冒进)。 */
|
|
702
|
+
.pendingOptionActive {
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-direction: column;
|
|
705
|
+
align-items: flex-start;
|
|
706
|
+
gap: 2px;
|
|
707
|
+
text-align: left;
|
|
708
|
+
border: 1px solid var(--dsw-alias-state-business-primary);
|
|
709
|
+
border-radius: 6px;
|
|
710
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
711
|
+
color: var(--dsw-alias-label-primary);
|
|
712
|
+
padding: 5px 8px;
|
|
713
|
+
font-size: 12px;
|
|
714
|
+
cursor: pointer;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.pendingOptionDesc {
|
|
718
|
+
color: var(--dsw-alias-label-dimmed);
|
|
719
|
+
font-size: 11px;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.pendingCustom {
|
|
723
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
724
|
+
border-radius: 6px;
|
|
725
|
+
background: transparent;
|
|
726
|
+
color: var(--dsw-alias-label-primary);
|
|
727
|
+
padding: 5px 8px;
|
|
728
|
+
font-size: 12px;
|
|
729
|
+
font: inherit;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.pendingCustom:focus {
|
|
733
|
+
outline: none;
|
|
734
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.pendingActions {
|
|
738
|
+
display: flex;
|
|
739
|
+
justify-content: flex-end;
|
|
740
|
+
gap: 8px;
|
|
741
|
+
margin-top: 2px;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.pendingBtnGhost {
|
|
745
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
746
|
+
border-radius: 6px;
|
|
747
|
+
background: none;
|
|
748
|
+
color: var(--dsw-alias-label-secondary);
|
|
749
|
+
padding: 4px 12px;
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
cursor: pointer;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.pendingBtnPrimary {
|
|
755
|
+
border: none;
|
|
756
|
+
border-radius: 6px;
|
|
757
|
+
background: var(--dsw-alias-button-info-fill);
|
|
758
|
+
color: #fff;
|
|
759
|
+
padding: 4px 12px;
|
|
760
|
+
font-size: 12px;
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.pendingBtnGhost:disabled,
|
|
765
|
+
.pendingBtnPrimary:disabled {
|
|
766
|
+
opacity: 0.5;
|
|
767
|
+
cursor: default;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/* R8 审批提示条(钉在 composer 上方)——面缺席时的回退跳转条。 */
|
|
630
771
|
.pendingBar {
|
|
631
772
|
display: block;
|
|
632
773
|
width: calc(100% - 16px);
|