@fonlan/dsh-quick-commands 0.1.0
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/LICENSE +21 -0
- package/README.en.md +149 -0
- package/README.md +108 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1036 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +904 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/server/remote.d.ts +117 -0
- package/lib/types/server/rpc.d.ts +4 -0
- package/lib/types/server/runner.d.ts +114 -0
- package/lib/types/server/settings.d.ts +24 -0
- package/lib/types/shared/contract.d.ts +87 -0
- package/package.json +108 -0
- package/src/client/api.ts +76 -0
- package/src/client/header.tsx +534 -0
- package/src/client/index.tsx +50 -0
- package/src/client/locales.ts +106 -0
- package/src/client/quick-commands.css +580 -0
- package/src/client/run-state.ts +125 -0
- package/src/client/settings-card.tsx +209 -0
- package/src/client/slot-augment.d.ts +14 -0
- package/src/index.ts +61 -0
- package/src/server/remote.ts +304 -0
- package/src/server/rpc.ts +286 -0
- package/src/server/runner.ts +471 -0
- package/src/server/settings.ts +106 -0
- package/src/shared/contract.ts +92 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/* @fonlan/dsh-quick-commands client styles.
|
|
2
|
+
Uses theme CSS variables (--dsw-alias-*); falls back to neutral literals.
|
|
3
|
+
Constraint: this deployment's theme defines only these text tokens —
|
|
4
|
+
label-primary, label-secondary, label-tertiary, label-dimmed. READABLE
|
|
5
|
+
description copy uses label-secondary / label-tertiary (never label-dimmed,
|
|
6
|
+
which is intentionally near-invisible and reserved for disabled states). */
|
|
7
|
+
|
|
8
|
+
/* ── header button (icon-only, left of Session log pill) ─────────────────── */
|
|
9
|
+
.qc-head-wrap {
|
|
10
|
+
position: relative;
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
}
|
|
14
|
+
.qc-head-btn {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
width: 32px;
|
|
19
|
+
height: 32px;
|
|
20
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
21
|
+
border-radius: 18px;
|
|
22
|
+
background: transparent;
|
|
23
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
padding: 0;
|
|
26
|
+
}
|
|
27
|
+
.qc-head-btn:hover:not(:disabled) {
|
|
28
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
29
|
+
}
|
|
30
|
+
.qc-head-btn:disabled {
|
|
31
|
+
color: var(--dsw-alias-label-dimmed, #666);
|
|
32
|
+
cursor: not-allowed;
|
|
33
|
+
}
|
|
34
|
+
.qc-head-btn svg {
|
|
35
|
+
flex: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── command menu popover (anchored under the button) ────────────────────── */
|
|
39
|
+
.qc-menu {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: calc(100% + 6px);
|
|
42
|
+
right: 12px;
|
|
43
|
+
min-width: 280px;
|
|
44
|
+
max-height: 420px;
|
|
45
|
+
overflow: auto;
|
|
46
|
+
background: var(--dsw-alias-bg-overlay, #1b1b1c);
|
|
47
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
48
|
+
border-radius: 10px;
|
|
49
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
50
|
+
z-index: 50;
|
|
51
|
+
padding: 6px;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
}
|
|
55
|
+
.qc-menu-head {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 8px;
|
|
59
|
+
padding: 4px 8px 6px;
|
|
60
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
61
|
+
}
|
|
62
|
+
.qc-menu-title {
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
66
|
+
}
|
|
67
|
+
.qc-menu-ws {
|
|
68
|
+
flex: 1;
|
|
69
|
+
font-size: 11px;
|
|
70
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-overflow: ellipsis;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
text-align: right;
|
|
75
|
+
margin-right: 2px;
|
|
76
|
+
}
|
|
77
|
+
.qc-menu-remote {
|
|
78
|
+
flex: 0 0 auto;
|
|
79
|
+
font-size: 10px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
line-height: 1;
|
|
82
|
+
padding: 2px 5px;
|
|
83
|
+
border-radius: 5px;
|
|
84
|
+
color: var(--dsw-alias-fg-on-accent, #fff);
|
|
85
|
+
background: var(--dsw-alias-accent, #4a8dff);
|
|
86
|
+
letter-spacing: 0.04em;
|
|
87
|
+
}
|
|
88
|
+
.qc-menu-close {
|
|
89
|
+
border: 0;
|
|
90
|
+
background: transparent;
|
|
91
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
padding: 2px;
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
97
|
+
.qc-menu-empty {
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
100
|
+
padding: 10px 8px;
|
|
101
|
+
margin: 0;
|
|
102
|
+
}
|
|
103
|
+
.qc-menu-item {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
align-items: flex-start;
|
|
107
|
+
gap: 2px;
|
|
108
|
+
text-align: left;
|
|
109
|
+
border: 0;
|
|
110
|
+
background: transparent;
|
|
111
|
+
border-radius: 8px;
|
|
112
|
+
padding: 6px 8px;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
width: 100%;
|
|
115
|
+
}
|
|
116
|
+
.qc-menu-item:hover:not(:disabled) {
|
|
117
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
118
|
+
}
|
|
119
|
+
.qc-menu-item:disabled {
|
|
120
|
+
opacity: 0.6;
|
|
121
|
+
cursor: wait;
|
|
122
|
+
}
|
|
123
|
+
.qc-menu-item-name {
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
127
|
+
}
|
|
128
|
+
.qc-menu-item-cmd {
|
|
129
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
text-overflow: ellipsis;
|
|
134
|
+
white-space: nowrap;
|
|
135
|
+
max-width: 100%;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── live output popup ───────────────────────────────────────────────────── */
|
|
139
|
+
.qc-popup {
|
|
140
|
+
position: relative; /* anchors the absolutely-positioned resize grip */
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
background: var(--dsw-alias-bg-overlay, #1b1b1c);
|
|
144
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
145
|
+
border-radius: 12px;
|
|
146
|
+
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.45);
|
|
147
|
+
z-index: 60;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
}
|
|
150
|
+
.qc-popup-corner {
|
|
151
|
+
position: fixed;
|
|
152
|
+
right: 20px;
|
|
153
|
+
bottom: 20px;
|
|
154
|
+
width: 520px;
|
|
155
|
+
max-width: calc(100vw - 40px);
|
|
156
|
+
height: 340px;
|
|
157
|
+
max-height: calc(100vh - 120px);
|
|
158
|
+
}
|
|
159
|
+
.qc-popup-button {
|
|
160
|
+
position: absolute;
|
|
161
|
+
top: calc(100% + 6px);
|
|
162
|
+
right: 12px;
|
|
163
|
+
width: 440px;
|
|
164
|
+
max-width: calc(100vw - 40px);
|
|
165
|
+
height: 300px;
|
|
166
|
+
max-height: calc(100vh - 120px);
|
|
167
|
+
}
|
|
168
|
+
.qc-popup-head {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 8px;
|
|
172
|
+
padding: 8px 10px;
|
|
173
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
174
|
+
}
|
|
175
|
+
.qc-popup-title {
|
|
176
|
+
flex: 1;
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
text-overflow: ellipsis;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
.qc-popup-cmd {
|
|
185
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
text-overflow: ellipsis;
|
|
190
|
+
white-space: nowrap;
|
|
191
|
+
max-width: 40%;
|
|
192
|
+
}
|
|
193
|
+
.qc-popup-remote {
|
|
194
|
+
flex: 0 0 auto;
|
|
195
|
+
font-size: 10px;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
line-height: 1;
|
|
198
|
+
padding: 3px 6px;
|
|
199
|
+
border-radius: 6px;
|
|
200
|
+
color: var(--dsw-alias-fg-on-accent, #fff);
|
|
201
|
+
background: var(--dsw-alias-accent, #4a8dff);
|
|
202
|
+
letter-spacing: 0.04em;
|
|
203
|
+
}
|
|
204
|
+
.qc-popup-icon {
|
|
205
|
+
border: 0;
|
|
206
|
+
background: transparent;
|
|
207
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
padding: 4px;
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
border-radius: 6px;
|
|
213
|
+
}
|
|
214
|
+
.qc-popup-icon:hover:not(:disabled) {
|
|
215
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
216
|
+
}
|
|
217
|
+
.qc-popup-icon:disabled {
|
|
218
|
+
opacity: 0.4;
|
|
219
|
+
cursor: not-allowed;
|
|
220
|
+
}
|
|
221
|
+
.qc-popup-kill {
|
|
222
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
223
|
+
}
|
|
224
|
+
.qc-popup-confirm {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 8px;
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
padding: 8px 10px;
|
|
230
|
+
background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f5a623) 12%, transparent);
|
|
231
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
232
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
233
|
+
}
|
|
234
|
+
.qc-popup-btn {
|
|
235
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
236
|
+
border-radius: 8px;
|
|
237
|
+
background: transparent;
|
|
238
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
padding: 4px 10px;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
.qc-popup-btn:hover {
|
|
244
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
245
|
+
}
|
|
246
|
+
.qc-popup-btn-plain {
|
|
247
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
248
|
+
}
|
|
249
|
+
.qc-popup-tabs {
|
|
250
|
+
display: flex;
|
|
251
|
+
gap: 2px;
|
|
252
|
+
padding: 6px 10px 0;
|
|
253
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
254
|
+
}
|
|
255
|
+
.qc-popup-tab {
|
|
256
|
+
border: 0;
|
|
257
|
+
background: transparent;
|
|
258
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
259
|
+
font-size: 12px;
|
|
260
|
+
padding: 5px 10px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
border-bottom: 2px solid transparent;
|
|
263
|
+
}
|
|
264
|
+
.qc-popup-tab-active {
|
|
265
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
266
|
+
border-bottom-color: var(--dsw-alias-brand-primary, #4a9eff);
|
|
267
|
+
}
|
|
268
|
+
.qc-popup-output {
|
|
269
|
+
flex: 1;
|
|
270
|
+
overflow: auto;
|
|
271
|
+
padding: 8px 10px;
|
|
272
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
line-height: 1.5;
|
|
275
|
+
}
|
|
276
|
+
.qc-popup-pre {
|
|
277
|
+
margin: 0;
|
|
278
|
+
white-space: pre-wrap;
|
|
279
|
+
word-break: break-word;
|
|
280
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
281
|
+
}
|
|
282
|
+
.qc-popup-empty {
|
|
283
|
+
margin: 0;
|
|
284
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
285
|
+
font-size: 12px;
|
|
286
|
+
}
|
|
287
|
+
.qc-popup-error {
|
|
288
|
+
margin: 0 0 6px;
|
|
289
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
}
|
|
292
|
+
.qc-popup-foot {
|
|
293
|
+
display: flex;
|
|
294
|
+
align-items: center;
|
|
295
|
+
padding: 6px 10px;
|
|
296
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
|
|
297
|
+
font-size: 11px;
|
|
298
|
+
}
|
|
299
|
+
.qc-popup-state {
|
|
300
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* Resize handles. The popup pins its RIGHT edge (both anchors); width is
|
|
304
|
+
always dragged from the LEFT border. Height is dragged from the TOP border
|
|
305
|
+
(corner anchor, bottom pinned) or the BOTTOM border (button anchor, top
|
|
306
|
+
pinned below the button). The free corner (top-left / bottom-left) carries
|
|
307
|
+
the diagonal grip. Pointer capture lives in header.tsx; touch-action:none
|
|
308
|
+
lets touch drags resize instead of scrolling. */
|
|
309
|
+
.qc-popup-resize {
|
|
310
|
+
position: absolute;
|
|
311
|
+
z-index: 3;
|
|
312
|
+
touch-action: none;
|
|
313
|
+
user-select: none;
|
|
314
|
+
}
|
|
315
|
+
.qc-popup-resize-left {
|
|
316
|
+
left: 0;
|
|
317
|
+
top: 10px;
|
|
318
|
+
bottom: 10px;
|
|
319
|
+
width: 7px;
|
|
320
|
+
cursor: ew-resize;
|
|
321
|
+
}
|
|
322
|
+
.qc-popup-resize-top {
|
|
323
|
+
top: 0;
|
|
324
|
+
left: 10px;
|
|
325
|
+
right: 10px;
|
|
326
|
+
height: 7px;
|
|
327
|
+
cursor: ns-resize;
|
|
328
|
+
}
|
|
329
|
+
.qc-popup-resize-bottom {
|
|
330
|
+
bottom: 0;
|
|
331
|
+
left: 10px;
|
|
332
|
+
right: 10px;
|
|
333
|
+
height: 7px;
|
|
334
|
+
cursor: ns-resize;
|
|
335
|
+
}
|
|
336
|
+
.qc-popup-resize-left:hover,
|
|
337
|
+
.qc-popup-resize-top:hover,
|
|
338
|
+
.qc-popup-resize-bottom:hover {
|
|
339
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 14%, transparent);
|
|
340
|
+
}
|
|
341
|
+
.qc-popup-resize-corner {
|
|
342
|
+
width: 22px;
|
|
343
|
+
height: 22px;
|
|
344
|
+
z-index: 4;
|
|
345
|
+
display: flex;
|
|
346
|
+
padding: 3px;
|
|
347
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
348
|
+
opacity: 0.6;
|
|
349
|
+
transition: opacity .15s ease;
|
|
350
|
+
}
|
|
351
|
+
.qc-popup-resize-corner-tl {
|
|
352
|
+
top: 0;
|
|
353
|
+
left: 0;
|
|
354
|
+
cursor: nwse-resize;
|
|
355
|
+
align-items: flex-start;
|
|
356
|
+
justify-content: flex-start;
|
|
357
|
+
}
|
|
358
|
+
.qc-popup-resize-corner-bl {
|
|
359
|
+
bottom: 0;
|
|
360
|
+
left: 0;
|
|
361
|
+
cursor: nesw-resize;
|
|
362
|
+
align-items: flex-end;
|
|
363
|
+
justify-content: flex-start;
|
|
364
|
+
}
|
|
365
|
+
.qc-popup-resize-corner:hover,
|
|
366
|
+
.qc-popup-resize-corner:active {
|
|
367
|
+
opacity: 1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* ── settings card (设置 → 插件配置) — mirrors official PluginCard disclosure ── */
|
|
371
|
+
.qc-settings-card {
|
|
372
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
373
|
+
background: var(--dsw-alias-bg-layer-2, #1f1f21);
|
|
374
|
+
border-radius: 12px;
|
|
375
|
+
list-style: none;
|
|
376
|
+
transition: border-color .16s, background .16s;
|
|
377
|
+
}
|
|
378
|
+
.qc-settings-card:hover {
|
|
379
|
+
border-color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
380
|
+
}
|
|
381
|
+
.qc-settings-card-open {
|
|
382
|
+
background: var(--dsw-alias-bg-layer-1, #27272a);
|
|
383
|
+
border-color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
384
|
+
}
|
|
385
|
+
.qc-settings-header {
|
|
386
|
+
appearance: none;
|
|
387
|
+
width: 100%;
|
|
388
|
+
font: inherit;
|
|
389
|
+
color: inherit;
|
|
390
|
+
text-align: left;
|
|
391
|
+
cursor: pointer;
|
|
392
|
+
background: transparent;
|
|
393
|
+
border: 0;
|
|
394
|
+
border-radius: 12px;
|
|
395
|
+
align-items: center;
|
|
396
|
+
gap: 12px;
|
|
397
|
+
padding: 14px 16px;
|
|
398
|
+
display: flex;
|
|
399
|
+
}
|
|
400
|
+
.qc-settings-header:focus-visible {
|
|
401
|
+
outline: 2px solid var(--dsw-alias-brand-primary, #4a9eff);
|
|
402
|
+
outline-offset: -2px;
|
|
403
|
+
}
|
|
404
|
+
.qc-settings-head-text {
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
flex: 1;
|
|
407
|
+
gap: 4px;
|
|
408
|
+
min-width: 0;
|
|
409
|
+
display: flex;
|
|
410
|
+
}
|
|
411
|
+
.qc-settings-name {
|
|
412
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
413
|
+
font-size: 15px;
|
|
414
|
+
font-weight: 600;
|
|
415
|
+
line-height: 1.4;
|
|
416
|
+
}
|
|
417
|
+
.qc-settings-description {
|
|
418
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
419
|
+
font-size: 13px;
|
|
420
|
+
line-height: 1.5;
|
|
421
|
+
}
|
|
422
|
+
.qc-settings-chevron {
|
|
423
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
424
|
+
flex: none;
|
|
425
|
+
transition: transform .16s;
|
|
426
|
+
}
|
|
427
|
+
.qc-settings-chevron-open {
|
|
428
|
+
transform: rotate(180deg);
|
|
429
|
+
}
|
|
430
|
+
.qc-settings-body {
|
|
431
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
432
|
+
margin: 0 16px;
|
|
433
|
+
padding-bottom: 12px;
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-direction: column;
|
|
436
|
+
gap: 10px;
|
|
437
|
+
}
|
|
438
|
+
.qc-settings-intro {
|
|
439
|
+
font-size: 12px;
|
|
440
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
441
|
+
margin: 8px 0 0;
|
|
442
|
+
line-height: 1.5;
|
|
443
|
+
}
|
|
444
|
+
.qc-settings-anchor {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 12px;
|
|
448
|
+
font-size: 12px;
|
|
449
|
+
}
|
|
450
|
+
.qc-settings-anchor-label {
|
|
451
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
452
|
+
font-weight: 500;
|
|
453
|
+
}
|
|
454
|
+
.qc-settings-radio {
|
|
455
|
+
display: inline-flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
gap: 4px;
|
|
458
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
459
|
+
cursor: pointer;
|
|
460
|
+
}
|
|
461
|
+
.qc-settings-error {
|
|
462
|
+
font-size: 12px;
|
|
463
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
464
|
+
}
|
|
465
|
+
.qc-settings-empty {
|
|
466
|
+
font-size: 12px;
|
|
467
|
+
color: var(--dsw-alias-label-secondary, #cfd3d6);
|
|
468
|
+
margin: 0;
|
|
469
|
+
}
|
|
470
|
+
.qc-settings-workspace {
|
|
471
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
472
|
+
border-radius: 10px;
|
|
473
|
+
padding: 10px;
|
|
474
|
+
display: flex;
|
|
475
|
+
flex-direction: column;
|
|
476
|
+
gap: 8px;
|
|
477
|
+
}
|
|
478
|
+
.qc-settings-workspace-head {
|
|
479
|
+
display: flex;
|
|
480
|
+
align-items: baseline;
|
|
481
|
+
gap: 8px;
|
|
482
|
+
}
|
|
483
|
+
.qc-settings-workspace-title {
|
|
484
|
+
font-size: 13px;
|
|
485
|
+
font-weight: 600;
|
|
486
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
487
|
+
}
|
|
488
|
+
.qc-settings-workspace-path {
|
|
489
|
+
font-size: 11px;
|
|
490
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
491
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
text-overflow: ellipsis;
|
|
494
|
+
white-space: nowrap;
|
|
495
|
+
}
|
|
496
|
+
.qc-settings-no-commands {
|
|
497
|
+
font-size: 12px;
|
|
498
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
499
|
+
margin: 0;
|
|
500
|
+
}
|
|
501
|
+
.qc-settings-command {
|
|
502
|
+
display: flex;
|
|
503
|
+
gap: 6px;
|
|
504
|
+
align-items: center;
|
|
505
|
+
}
|
|
506
|
+
.qc-settings-field {
|
|
507
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
508
|
+
border-radius: 8px;
|
|
509
|
+
background: var(--dsw-alias-bg-layer-1, #2a2a2d);
|
|
510
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
511
|
+
font-size: 12px;
|
|
512
|
+
padding: 6px 8px;
|
|
513
|
+
outline: none;
|
|
514
|
+
}
|
|
515
|
+
.qc-settings-field::placeholder {
|
|
516
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
517
|
+
}
|
|
518
|
+
.qc-settings-field:focus {
|
|
519
|
+
border-color: var(--dsw-alias-brand-primary, #4a9eff);
|
|
520
|
+
}
|
|
521
|
+
.qc-settings-field-name {
|
|
522
|
+
width: 160px;
|
|
523
|
+
flex: none;
|
|
524
|
+
}
|
|
525
|
+
.qc-settings-field-command {
|
|
526
|
+
flex: 1;
|
|
527
|
+
font-family: var(--dsw-font-family-mono, monospace);
|
|
528
|
+
}
|
|
529
|
+
.qc-settings-icon-btn {
|
|
530
|
+
border: 0;
|
|
531
|
+
background: transparent;
|
|
532
|
+
color: var(--dsw-alias-label-tertiary, #adb2b8);
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
padding: 4px;
|
|
535
|
+
display: inline-flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
border-radius: 6px;
|
|
538
|
+
}
|
|
539
|
+
.qc-settings-icon-btn:hover {
|
|
540
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
541
|
+
color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
542
|
+
}
|
|
543
|
+
.qc-settings-add-btn {
|
|
544
|
+
align-self: flex-start;
|
|
545
|
+
display: inline-flex;
|
|
546
|
+
align-items: center;
|
|
547
|
+
gap: 4px;
|
|
548
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
|
|
549
|
+
border-radius: 8px;
|
|
550
|
+
background: transparent;
|
|
551
|
+
color: var(--dsw-alias-label-primary, #f2f2f2);
|
|
552
|
+
font-size: 12px;
|
|
553
|
+
padding: 4px 10px;
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
}
|
|
556
|
+
.qc-settings-add-btn:hover {
|
|
557
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary, #f2f2f2) 10%, transparent);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* ── mobile fallback (dsh-mobile-nav) ──────────────────────────────────────
|
|
561
|
+
The dsh-mobile-nav plugin (≤1023px) hides the ENTIRE header utilities seat
|
|
562
|
+
with `[data-mobile-nav="frame"] [data-phase] header > :first-child >
|
|
563
|
+
:last-child { display: none !important; }`, because the seat used to hold
|
|
564
|
+
only the session-log-export capsule (which it relocates to the drawer
|
|
565
|
+
footer). Our quick-run ▶ button rides that same seat, so it vanished on
|
|
566
|
+
phones.
|
|
567
|
+
|
|
568
|
+
Re-show ONLY our button: restore the seat's flex layout (out-ranking the
|
|
569
|
+
host rule via `:has(.qc-head-wrap)` specificity (0,5,1) > (0,4,1)) and keep
|
|
570
|
+
every other seat occupant hidden (the session-log capsule stays relocated
|
|
571
|
+
to the drawer footer). Desktop (no [data-mobile-nav]) is untouched. */
|
|
572
|
+
[data-mobile-nav="frame"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap) {
|
|
573
|
+
display: flex !important;
|
|
574
|
+
margin-left: 4px !important;
|
|
575
|
+
gap: 0 !important;
|
|
576
|
+
}
|
|
577
|
+
[data-mobile-nav="frame"] [data-phase] header > :first-child > :last-child:has(.qc-head-wrap)
|
|
578
|
+
[data-slot="conversation.session.header.utilities"] > :not(.qc-head-wrap) {
|
|
579
|
+
display: none !important;
|
|
580
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMR-safe shared run state for the quick-commands header button and output
|
|
3
|
+
* popup. Module-level fields live on one globalThis-backed record keyed with
|
|
4
|
+
* Symbol.for so every client-bundle copy (client-hmr hot swap re-evaluates the
|
|
5
|
+
* bundle without reloading the page) reads/writes the same state cell.
|
|
6
|
+
*
|
|
7
|
+
* The popup handles ONE run at a time (serial per workspace by design); a
|
|
8
|
+
* second workspace can run in parallel with its own popup, so the store keys
|
|
9
|
+
* popups by runId.
|
|
10
|
+
*/
|
|
11
|
+
import { useSyncExternalStore } from 'react'
|
|
12
|
+
|
|
13
|
+
const STATE_KEY = Symbol.for('@fonlan/dsh-quick-commands/run-state')
|
|
14
|
+
|
|
15
|
+
/** One popup slot: menu open flag + the live run the popup streams. */
|
|
16
|
+
export interface RunPopoverState {
|
|
17
|
+
/** Whether the header command menu is open. */
|
|
18
|
+
menuOpen: boolean
|
|
19
|
+
/** Active run id (undefined when no run is open). */
|
|
20
|
+
runId: string | null
|
|
21
|
+
/** Workspace id owning the active run (for kill/cwd display). */
|
|
22
|
+
workspaceId: string | null
|
|
23
|
+
/** Command display name. */
|
|
24
|
+
commandName: string | null
|
|
25
|
+
/** SSH host id when the run executes on a remote workspace (else null). */
|
|
26
|
+
remoteHost: string | null
|
|
27
|
+
/** Whether the run popup should stream (false while minimized). */
|
|
28
|
+
streaming: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RunPopoverStateInternal extends RunPopoverState {
|
|
32
|
+
listeners: Set<() => void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface QuickCommandsState {
|
|
36
|
+
popover: RunPopoverStateInternal
|
|
37
|
+
/** Cached published snapshot — STABLE reference; rebuilt only after a patch. */
|
|
38
|
+
snapshot: RunPopoverState | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getState(): QuickCommandsState {
|
|
42
|
+
const g = globalThis as unknown as Record<symbol, QuickCommandsState | undefined>
|
|
43
|
+
let state = g[STATE_KEY]
|
|
44
|
+
if (state === undefined) {
|
|
45
|
+
state = {
|
|
46
|
+
popover: {
|
|
47
|
+
menuOpen: false,
|
|
48
|
+
runId: null,
|
|
49
|
+
workspaceId: null,
|
|
50
|
+
commandName: null,
|
|
51
|
+
remoteHost: null,
|
|
52
|
+
streaming: true,
|
|
53
|
+
listeners: new Set(),
|
|
54
|
+
},
|
|
55
|
+
// Seed the cache with values our equality check will see as unchanged.
|
|
56
|
+
snapshot: {
|
|
57
|
+
menuOpen: false,
|
|
58
|
+
runId: null,
|
|
59
|
+
workspaceId: null,
|
|
60
|
+
commandName: null,
|
|
61
|
+
remoteHost: null,
|
|
62
|
+
streaming: true,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
g[STATE_KEY] = state
|
|
66
|
+
}
|
|
67
|
+
return state
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function notify(): void {
|
|
71
|
+
// Invalidate the cached snapshot FIRST: useSyncExternalStore reads
|
|
72
|
+
// getSnapshot after notification, so a new stable object must be ready.
|
|
73
|
+
getState().snapshot = null
|
|
74
|
+
for (const listener of getState().popover.listeners) listener()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function subscribe(cb: () => void): () => void {
|
|
78
|
+
const listeners = getState().popover.listeners
|
|
79
|
+
listeners.add(cb)
|
|
80
|
+
return () => { listeners.delete(cb) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* getSnapshot for useSyncExternalStore. MUST return a stable reference while
|
|
85
|
+
* the underlying fields are unchanged (React compares with Object.is) — return
|
|
86
|
+
* the CACHED object; only rebuild it when a patch invalidated it.
|
|
87
|
+
*/
|
|
88
|
+
function snapshot(): RunPopoverState {
|
|
89
|
+
const state = getState()
|
|
90
|
+
if (state.snapshot !== null) return state.snapshot
|
|
91
|
+
const p = state.popover
|
|
92
|
+
state.snapshot = {
|
|
93
|
+
menuOpen: p.menuOpen,
|
|
94
|
+
runId: p.runId,
|
|
95
|
+
workspaceId: p.workspaceId,
|
|
96
|
+
commandName: p.commandName,
|
|
97
|
+
remoteHost: p.remoteHost,
|
|
98
|
+
streaming: p.streaming,
|
|
99
|
+
}
|
|
100
|
+
return state.snapshot
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function patch(partial: Partial<RunPopoverState>): void {
|
|
104
|
+
const state = getState()
|
|
105
|
+
Object.assign(state.popover, partial)
|
|
106
|
+
// Invalidate the cached snapshot so equality flips exactly on real changes.
|
|
107
|
+
state.snapshot = null
|
|
108
|
+
notify()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useRunPopover(): RunPopoverState {
|
|
112
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function openMenu(): void { patch({ menuOpen: true }) }
|
|
116
|
+
export function closeMenu(): void { patch({ menuOpen: false }) }
|
|
117
|
+
export function toggleMenu(open: boolean): void { patch({ menuOpen: open }) }
|
|
118
|
+
|
|
119
|
+
export function openRun(runId: string, workspaceId: string, commandName: string, remoteHost?: string): void {
|
|
120
|
+
patch({ runId, workspaceId, commandName, remoteHost: remoteHost ?? null, menuOpen: false, streaming: true })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function closeRun(): void {
|
|
124
|
+
patch({ runId: null, workspaceId: null, commandName: null, remoteHost: null, streaming: false })
|
|
125
|
+
}
|