@shendeguize/dsh-agent-sidecar 0.1.0 → 0.1.1

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.
@@ -30,9 +30,11 @@ import css from './inject.module.css'
30
30
  import {
31
31
  byteUsage,
32
32
  classifyExecuteResponse,
33
+ classifyPanelKey,
33
34
  classifyPrepareResponse,
34
35
  deriveEditorGate,
35
36
  errorCopy,
37
+ isDeliveredResult,
36
38
  initialPanelState,
37
39
  messageInvalidCopy,
38
40
  MODE_COPY,
@@ -82,6 +84,12 @@ export interface InjectPanelProps {
82
84
  onExecute(req: PanelExecuteRequest): Promise<InjectResultView | ApiErrorLike>
83
85
  /** Close/dismiss the panel (the hosting modal owns the lifecycle). */
84
86
  onClose?(): void
87
+ /**
88
+ * UX-05 observation loop: when given, the delivered result page offers a
89
+ * 「开启监听观察反应」 button (the host typically enables listen mode on
90
+ * the detail timeline and closes the panel).
91
+ */
92
+ onObserve?(): void
85
93
  /** Clock injection for the token countdown; defaults to Date.now. */
86
94
  nowMs?: () => number
87
95
  /** Locale seat override; defaults to the module-local table. */
@@ -240,6 +248,32 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
240
248
  dispatch(event)
241
249
  }
242
250
 
251
+ // Keyboard affordances (UX-10): Esc closes in every zone; Cmd/Ctrl+Enter
252
+ // triggers PREPARE only (classifyPanelKey refuses everything else, so no
253
+ // keyboard path can shortcut the explicit confirm click).
254
+ const handleKeyDown = (event: {
255
+ key: string
256
+ metaKey: boolean
257
+ ctrlKey: boolean
258
+ stopPropagation(): void
259
+ preventDefault(): void
260
+ }): void => {
261
+ const intent = classifyPanelKey({
262
+ key: event.key,
263
+ metaKey: event.metaKey,
264
+ ctrlKey: event.ctrlKey,
265
+ phase: state.phase,
266
+ canPrepare: gate.canPrepare,
267
+ })
268
+ if (intent === 'close' && props.onClose !== undefined) {
269
+ event.stopPropagation()
270
+ props.onClose()
271
+ } else if (intent === 'prepare') {
272
+ event.preventDefault()
273
+ void handlePrepare()
274
+ }
275
+ }
276
+
243
277
  const closeButton = props.onClose !== undefined
244
278
  ? (
245
279
  <button type="button" className={css['btn']} onClick={props.onClose}>
@@ -251,7 +285,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
251
285
  // ── whole-panel gate: injection capability off ─────────────────────────
252
286
  if (!props.capability.inject) {
253
287
  return (
254
- <section className={css['panel']} aria-label={t('inject.title')}>
288
+ <section className={css['panel']} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
255
289
  <header className={css['header']}>
256
290
  <h3 className={css['title']}>{t('inject.title')}</h3>
257
291
  </header>
@@ -275,7 +309,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
275
309
  ? css['resultFail']
276
310
  : css['resultUnknown']
277
311
  return (
278
- <section className={css['panel']} aria-label={t('inject.title')}>
312
+ <section className={css['panel']} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
279
313
  <header className={css['header']}>
280
314
  <h3 className={css['title']}>{t('inject.title')}</h3>
281
315
  </header>
@@ -293,6 +327,18 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
293
327
  : null}
294
328
  <div className={css['footer']}>
295
329
  {closeButton}
330
+ {isDeliveredResult(result) && props.onObserve !== undefined
331
+ ? (
332
+ <button
333
+ type="button"
334
+ className={css['btnPrimary']}
335
+ onClick={props.onObserve}
336
+ data-testid="agent-sidecar-inject-observe"
337
+ >
338
+ {t('inject.observeListen')}
339
+ </button>
340
+ )
341
+ : null}
296
342
  {actions.canReprepare
297
343
  ? (
298
344
  <button
@@ -328,7 +374,11 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
328
374
  ? tokenCountdown(state.expiresAt, clock)
329
375
  : null
330
376
  return (
331
- <section className={css['panel']} aria-label={t('inject.confirmTitle')}>
377
+ <section
378
+ className={css['panel']}
379
+ aria-label={t('inject.confirmTitle')}
380
+ onKeyDown={handleKeyDown}
381
+ >
332
382
  <header className={css['header']}>
333
383
  <h3 className={css['title']}>{t('inject.confirmTitle')}</h3>
334
384
  </header>
@@ -373,7 +423,7 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
373
423
  const showInvalid = !validation.ok && validation.code !== 'empty'
374
424
 
375
425
  return (
376
- <section className={css['panel']} aria-label={t('inject.title')}>
426
+ <section className={css['panel']} aria-label={t('inject.title')} onKeyDown={handleKeyDown}>
377
427
  <header className={css['header']}>
378
428
  <h3 className={css['title']}>{t('inject.title')}</h3>
379
429
  </header>
@@ -419,6 +469,9 @@ export function InjectPanel(props: InjectPanelProps): ReactNode {
419
469
  placeholder={t('inject.messagePlaceholder')}
420
470
  disabled={!canEdit}
421
471
  rows={5}
472
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- the panel is
473
+ // a modal whose single task starts in this field (UX-10).
474
+ autoFocus
422
475
  onChange={(event) => { setDraft(event.target.value) }}
423
476
  />
424
477
  <div className={css['byteRow']}>
@@ -1,13 +1,14 @@
1
1
  /* Inject panel (design §5.1 view 3). Every color rides a dsh --dsw-alias-*
2
- * design token, no literal colors same discipline as the settings card.
3
- * The confirm-injection button is the one danger affordance: eye-catching
4
- * (error-toned border/text, filled on hover) but restrained (no permanent
5
- * solid red block). */
2
+ * design token WITH a neutral fallback (UX-16: same token+fallback
3
+ * discipline as every other CSS module), and error tones ride the
4
+ * state-error-* family the board uses. The confirm-injection button is
5
+ * the one danger affordance: eye-catching (error-toned border/text,
6
+ * filled on hover) but restrained (no permanent solid red block). */
6
7
 
7
8
  .panel {
8
- border: 1px solid var(--dsw-alias-border-l2);
9
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
9
10
  border-radius: 12px;
10
- background: var(--dsw-alias-bg-layer-2);
11
+ background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
11
12
  max-width: 560px;
12
13
  display: flex;
13
14
  flex-direction: column;
@@ -20,7 +21,7 @@
20
21
  align-items: center;
21
22
  gap: 12px;
22
23
  padding: 14px 16px 10px;
23
- border-bottom: 1px solid var(--dsw-alias-border-l2);
24
+ border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
24
25
  }
25
26
 
26
27
  .title {
@@ -30,7 +31,7 @@
30
31
  font-size: 15px;
31
32
  font-weight: 600;
32
33
  line-height: 1.4;
33
- color: var(--dsw-alias-label-primary);
34
+ color: var(--dsw-alias-label-primary, #1f2328);
34
35
  }
35
36
 
36
37
  /* ── body ──────────────────────────────────────────────────────────────── */
@@ -48,8 +49,8 @@
48
49
  padding: 10px 12px;
49
50
  font-size: 13px;
50
51
  line-height: 1.6;
51
- background: var(--dsw-alias-bg-module-platform);
52
- color: var(--dsw-alias-label-secondary);
52
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
53
+ color: var(--dsw-alias-label-secondary, #57606a);
53
54
  }
54
55
 
55
56
  /* ── notices (editor banners after a failed/expired attempt) ───────────── */
@@ -61,17 +62,17 @@
61
62
  padding: 8px 10px;
62
63
  font-size: 12px;
63
64
  line-height: 1.6;
64
- background: var(--dsw-alias-bg-module-platform);
65
- color: var(--dsw-alias-label-secondary);
65
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
66
+ color: var(--dsw-alias-label-secondary, #57606a);
66
67
  }
67
68
 
68
69
  .noticeError {
69
- border: 1px solid var(--dsw-alias-label-error);
70
- color: var(--dsw-alias-label-error);
70
+ border: 1px solid var(--dsw-alias-state-error-primary, #cf222e);
71
+ color: var(--dsw-alias-state-error-primary, #cf222e);
71
72
  }
72
73
 
73
74
  .noticeDetail {
74
- color: var(--dsw-alias-label-tertiary);
75
+ color: var(--dsw-alias-label-tertiary, #6e7781);
75
76
  }
76
77
 
77
78
  /* ── target row ────────────────────────────────────────────────────────── */
@@ -86,7 +87,7 @@
86
87
  .noTarget {
87
88
  font-size: 12px;
88
89
  line-height: 1.5;
89
- color: var(--dsw-alias-label-tertiary);
90
+ color: var(--dsw-alias-label-tertiary, #6e7781);
90
91
  }
91
92
 
92
93
  .agentTag {
@@ -96,8 +97,8 @@
96
97
  font-size: 11px;
97
98
  line-height: 17px;
98
99
  font-weight: 500;
99
- background: var(--dsw-alias-bg-module-platform);
100
- color: var(--dsw-alias-label-secondary);
100
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
101
+ color: var(--dsw-alias-label-secondary, #57606a);
101
102
  }
102
103
 
103
104
  .planTitle {
@@ -107,7 +108,7 @@
107
108
  white-space: nowrap;
108
109
  font-size: 13px;
109
110
  line-height: 1.5;
110
- color: var(--dsw-alias-label-primary);
111
+ color: var(--dsw-alias-label-primary, #1f2328);
111
112
  }
112
113
 
113
114
  /* ── editor field ──────────────────────────────────────────────────────── */
@@ -121,30 +122,30 @@
121
122
  .label {
122
123
  font-size: 13px;
123
124
  line-height: 1.5;
124
- color: var(--dsw-alias-label-primary);
125
+ color: var(--dsw-alias-label-primary, #1f2328);
125
126
  }
126
127
 
127
128
  .textarea {
128
129
  appearance: none;
129
130
  resize: vertical;
130
131
  min-height: 96px;
131
- border: 1px solid var(--dsw-alias-border-l2);
132
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
132
133
  border-radius: 8px;
133
- background: var(--dsw-alias-bg-layer-3);
134
+ background: var(--dsw-alias-bg-layer-3, #ffffff);
134
135
  padding: 8px 10px;
135
136
  font: inherit;
136
137
  font-size: 13px;
137
138
  line-height: 1.6;
138
- color: var(--dsw-alias-label-primary);
139
+ color: var(--dsw-alias-label-primary, #1f2328);
139
140
  }
140
141
 
141
142
  .textarea:focus-visible {
142
143
  outline: none;
143
- border-color: var(--dsw-alias-brand-primary);
144
+ border-color: var(--dsw-alias-brand-primary, #4d6bfe);
144
145
  }
145
146
 
146
147
  .textarea::placeholder {
147
- color: var(--dsw-alias-label-tertiary);
148
+ color: var(--dsw-alias-label-tertiary, #6e7781);
148
149
  }
149
150
 
150
151
  .textarea:disabled {
@@ -163,20 +164,20 @@
163
164
  flex: 1;
164
165
  height: 3px;
165
166
  border-radius: 2px;
166
- background: var(--dsw-alias-bg-module-platform);
167
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
167
168
  overflow: hidden;
168
169
  }
169
170
 
170
171
  .byteFill {
171
172
  height: 100%;
172
173
  border-radius: 2px;
173
- background: var(--dsw-alias-brand-primary);
174
+ background: var(--dsw-alias-brand-primary, #4d6bfe);
174
175
  transition: width .12s;
175
176
  }
176
177
 
177
178
  /* Modifier applied ALONGSIDE .byteFill (no `composes`; see settings card). */
178
179
  .byteFillOver {
179
- background: var(--dsw-alias-label-error);
180
+ background: var(--dsw-alias-state-error-primary, #cf222e);
180
181
  }
181
182
 
182
183
  .byteText,
@@ -185,18 +186,18 @@
185
186
  font-size: 11px;
186
187
  line-height: 1.5;
187
188
  font-variant-numeric: tabular-nums;
188
- color: var(--dsw-alias-label-tertiary);
189
+ color: var(--dsw-alias-label-tertiary, #6e7781);
189
190
  }
190
191
 
191
192
  .byteTextOver {
192
- color: var(--dsw-alias-label-error);
193
+ color: var(--dsw-alias-state-error-primary, #cf222e);
193
194
  }
194
195
 
195
196
  .invalid {
196
197
  margin: 0;
197
198
  font-size: 12px;
198
199
  line-height: 1.5;
199
- color: var(--dsw-alias-label-error);
200
+ color: var(--dsw-alias-state-error-primary, #cf222e);
200
201
  }
201
202
 
202
203
  /* ── mode radios ───────────────────────────────────────────────────────── */
@@ -218,7 +219,7 @@
218
219
  }
219
220
 
220
221
  .modeOption input {
221
- accent-color: var(--dsw-alias-brand-primary);
222
+ accent-color: var(--dsw-alias-brand-primary, #4d6bfe);
222
223
  width: 14px;
223
224
  height: 14px;
224
225
  margin: 3px 0 0;
@@ -239,26 +240,26 @@
239
240
  .modeLabel {
240
241
  font-size: 13px;
241
242
  line-height: 1.5;
242
- color: var(--dsw-alias-label-primary);
243
+ color: var(--dsw-alias-label-primary, #1f2328);
243
244
  }
244
245
 
245
246
  .modeHint {
246
247
  font-size: 12px;
247
248
  line-height: 1.5;
248
- color: var(--dsw-alias-label-tertiary);
249
+ color: var(--dsw-alias-label-tertiary, #6e7781);
249
250
  }
250
251
 
251
252
  /* ── warnings & audit note ─────────────────────────────────────────────── */
252
253
 
253
254
  .warnBar {
254
255
  margin: 0;
255
- border: 1px solid var(--dsw-alias-label-error);
256
+ border: 1px solid var(--dsw-alias-state-error-primary, #cf222e);
256
257
  border-radius: 8px;
257
258
  padding: 8px 10px;
258
259
  font-size: 12px;
259
260
  line-height: 1.6;
260
- background: var(--dsw-alias-bg-module-platform);
261
- color: var(--dsw-alias-label-error);
261
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
262
+ color: var(--dsw-alias-state-error-primary, #cf222e);
262
263
  }
263
264
 
264
265
  .auditNote {
@@ -267,16 +268,16 @@
267
268
  padding: 8px 10px;
268
269
  font-size: 12px;
269
270
  line-height: 1.6;
270
- background: var(--dsw-alias-bg-module-platform);
271
- color: var(--dsw-alias-label-secondary);
271
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
272
+ color: var(--dsw-alias-label-secondary, #57606a);
272
273
  }
273
274
 
274
275
  /* ── confirm plan ──────────────────────────────────────────────────────── */
275
276
 
276
277
  .planBox {
277
- border: 1px solid var(--dsw-alias-border-l2);
278
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
278
279
  border-radius: 8px;
279
- background: var(--dsw-alias-bg-layer-3);
280
+ background: var(--dsw-alias-bg-layer-3, #ffffff);
280
281
  padding: 10px 12px;
281
282
  display: flex;
282
283
  flex-direction: column;
@@ -294,7 +295,7 @@
294
295
  flex: none;
295
296
  font-size: 12px;
296
297
  line-height: 1.5;
297
- color: var(--dsw-alias-label-tertiary);
298
+ color: var(--dsw-alias-label-tertiary, #6e7781);
298
299
  }
299
300
 
300
301
  .planValue {
@@ -304,14 +305,14 @@
304
305
  min-width: 0;
305
306
  font-size: 13px;
306
307
  line-height: 1.5;
307
- color: var(--dsw-alias-label-primary);
308
+ color: var(--dsw-alias-label-primary, #1f2328);
308
309
  }
309
310
 
310
311
  .observedNote {
311
312
  margin: 0;
312
313
  font-size: 11px;
313
314
  line-height: 1.5;
314
- color: var(--dsw-alias-label-tertiary);
315
+ color: var(--dsw-alias-label-tertiary, #6e7781);
315
316
  }
316
317
 
317
318
  .planPreview {
@@ -323,9 +324,9 @@
323
324
 
324
325
  .preview {
325
326
  margin: 0;
326
- border: 1px solid var(--dsw-alias-border-l2);
327
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
327
328
  border-radius: 6px;
328
- background: var(--dsw-alias-bg-layer-2);
329
+ background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
329
330
  padding: 8px 10px;
330
331
  max-height: 120px;
331
332
  overflow: auto;
@@ -333,7 +334,7 @@
333
334
  line-height: 1.6;
334
335
  white-space: pre-wrap;
335
336
  word-break: break-word;
336
- color: var(--dsw-alias-label-secondary);
337
+ color: var(--dsw-alias-label-secondary, #57606a);
337
338
  }
338
339
 
339
340
  .countdown {
@@ -341,7 +342,7 @@
341
342
  font-size: 12px;
342
343
  line-height: 1.5;
343
344
  font-variant-numeric: tabular-nums;
344
- color: var(--dsw-alias-label-secondary);
345
+ color: var(--dsw-alias-label-secondary, #57606a);
345
346
  }
346
347
 
347
348
  /* ── results ───────────────────────────────────────────────────────────── */
@@ -354,29 +355,29 @@
354
355
  padding: 10px 12px;
355
356
  font-size: 13px;
356
357
  line-height: 1.6;
357
- background: var(--dsw-alias-bg-module-platform);
358
- color: var(--dsw-alias-label-primary);
358
+ background: var(--dsw-alias-bg-module-platform, rgba(0, 0, 0, 0.03));
359
+ color: var(--dsw-alias-label-primary, #1f2328);
359
360
  }
360
361
 
361
362
  .resultOk {
362
- border: 1px solid var(--dsw-alias-brand-primary);
363
+ border: 1px solid var(--dsw-alias-brand-primary, #4d6bfe);
363
364
  }
364
365
 
365
366
  .resultFail {
366
- border: 1px solid var(--dsw-alias-label-error);
367
- color: var(--dsw-alias-label-error);
367
+ border: 1px solid var(--dsw-alias-state-error-primary, #cf222e);
368
+ color: var(--dsw-alias-state-error-primary, #cf222e);
368
369
  }
369
370
 
370
371
  .resultUnknown {
371
- border: 1px solid var(--dsw-alias-label-dimmed);
372
- color: var(--dsw-alias-label-secondary);
372
+ border: 1px solid var(--dsw-alias-label-dimmed, #8c959f);
373
+ color: var(--dsw-alias-label-secondary, #57606a);
373
374
  }
374
375
 
375
376
  .resultDetail {
376
377
  margin: 0;
377
378
  font-size: 12px;
378
379
  line-height: 1.6;
379
- color: var(--dsw-alias-label-tertiary);
380
+ color: var(--dsw-alias-label-tertiary, #6e7781);
380
381
  }
381
382
 
382
383
  /* ── footer buttons ────────────────────────────────────────────────────── */
@@ -403,32 +404,32 @@
403
404
  }
404
405
 
405
406
  .btn {
406
- border-color: var(--dsw-alias-border-l2);
407
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
407
408
  background: none;
408
- color: var(--dsw-alias-label-secondary);
409
+ color: var(--dsw-alias-label-secondary, #57606a);
409
410
  }
410
411
 
411
412
  .btn:hover:not(:disabled) {
412
- color: var(--dsw-alias-label-primary);
413
- border-color: var(--dsw-alias-label-dimmed);
413
+ color: var(--dsw-alias-label-primary, #1f2328);
414
+ border-color: var(--dsw-alias-label-dimmed, #8c959f);
414
415
  }
415
416
 
416
417
  .btnPrimary {
417
- background: var(--dsw-alias-label-primary);
418
- color: var(--dsw-alias-bg-layer-3);
418
+ background: var(--dsw-alias-label-primary, #1f2328);
419
+ color: var(--dsw-alias-bg-layer-3, #ffffff);
419
420
  }
420
421
 
421
422
  /* The danger action: outlined error tone at rest, fills on hover. */
422
423
  .btnDanger {
423
- border-color: var(--dsw-alias-label-error);
424
+ border-color: var(--dsw-alias-state-error-primary, #cf222e);
424
425
  background: none;
425
- color: var(--dsw-alias-label-error);
426
+ color: var(--dsw-alias-state-error-primary, #cf222e);
426
427
  font-weight: 600;
427
428
  }
428
429
 
429
430
  .btnDanger:hover:not(:disabled) {
430
- background: var(--dsw-alias-label-error);
431
- color: var(--dsw-alias-bg-layer-3);
431
+ background: var(--dsw-alias-state-error-primary, #cf222e);
432
+ color: var(--dsw-alias-bg-layer-3, #ffffff);
432
433
  }
433
434
 
434
435
  .btn:disabled,
@@ -441,6 +442,6 @@
441
442
  .btn:focus-visible,
442
443
  .btnPrimary:focus-visible,
443
444
  .btnDanger:focus-visible {
444
- outline: 2px solid var(--dsw-alias-brand-primary);
445
+ outline: 2px solid var(--dsw-alias-brand-primary, #4d6bfe);
445
446
  outline-offset: 1px;
446
447
  }
@@ -418,6 +418,33 @@ export function deriveEditorGate(input: {
418
418
  return { canPrepare: true, block: null }
419
419
  }
420
420
 
421
+ // ---------------------------------------------------------------------------
422
+ // Keyboard affordances (UX-10).
423
+ // ---------------------------------------------------------------------------
424
+
425
+ export type PanelKeyIntent = 'close' | 'prepare' | null
426
+
427
+ /**
428
+ * Map a keydown to a panel intent: Escape closes in every phase;
429
+ * Cmd/Ctrl+Enter submits the PREPARE step only (editor phase with a valid
430
+ * message). The confirm phase deliberately binds nothing — executing the
431
+ * injection stays an explicit click (two-phase discipline, §5.3), so no
432
+ * keyboard path can shortcut the confirmation.
433
+ */
434
+ export function classifyPanelKey(input: {
435
+ key: string
436
+ metaKey: boolean
437
+ ctrlKey: boolean
438
+ phase: InjectPhase
439
+ canPrepare: boolean
440
+ }): PanelKeyIntent {
441
+ if (input.key === 'Escape') return 'close'
442
+ if (input.key === 'Enter' && (input.metaKey || input.ctrlKey)) {
443
+ return input.phase === 'idle' && input.canPrepare ? 'prepare' : null
444
+ }
445
+ return null
446
+ }
447
+
421
448
  // ---------------------------------------------------------------------------
422
449
  // Result actions (S6: unknown is terminal).
423
450
  // ---------------------------------------------------------------------------
@@ -437,6 +464,17 @@ export function resultActions(outcome: InjectOutcome): ResultActions {
437
464
  }
438
465
  }
439
466
 
467
+ /**
468
+ * True when an execute response reports the message actually reached the
469
+ * target (UX-05 observation loop): a delivered outcome — including an
470
+ * idempotent replay of one — never an error envelope. failed/unknown must
471
+ * NOT trigger observation aids: unknown is a locked terminal state (S6)
472
+ * and any follow-up activity could read as "retrying is fine".
473
+ */
474
+ export function isDeliveredResult(value: InjectResultView | ApiErrorLike): boolean {
475
+ return !isApiErrorLike(value) && value.outcome === 'delivered'
476
+ }
477
+
440
478
  // ---------------------------------------------------------------------------
441
479
  // Copy mapping (keys live in the T2.3 locale table, inject.* domain).
442
480
  // ---------------------------------------------------------------------------
@@ -11,7 +11,7 @@
11
11
  align-items: center;
12
12
  justify-content: center;
13
13
  padding: 24px;
14
- background: var(--dsw-alias-bg-mask-1);
14
+ background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.45));
15
15
  }
16
16
 
17
17
  .dialog {
@@ -69,7 +69,7 @@ export const en = {
69
69
  'settings.sectionInject': 'Message injection',
70
70
  'settings.injectEnabledLabel': 'Enable injection',
71
71
  'settings.injectEnabledHint':
72
- 'When off, the board hides every inject affordance and the server rejects write actions.',
72
+ 'When off, the inject panel renders read-only and disabled, and the server rejects write actions.',
73
73
  'settings.injectDefaultModeLabel': 'Default injection mode',
74
74
  'settings.injectDefaultModeHint': 'The mode preselected when the inject panel opens.',
75
75
  'settings.injectModeQueue': 'queue (next turn)',
@@ -147,6 +147,7 @@ export const en = {
147
147
  'inject.resultReplayed':
148
148
  'Idempotent replay: this is the earlier result of the same request — no second injection happened.',
149
149
  'inject.reprepare': 'Prepare again',
150
+ 'inject.observeListen': 'Listen for the reaction',
150
151
 
151
152
  // ── inject panel: error vocabulary (gateway + transport) ─────────────
152
153
  'inject.errInjectDisabled': 'Injection is disabled on the server; enable it in Settings.',
@@ -178,6 +179,11 @@ export const en = {
178
179
  'detail.header.listenOn': 'Listening',
179
180
  'detail.header.listenOff': 'Listen',
180
181
  'detail.header.listenHint': 'New events append live and get highlighted while on',
182
+ 'detail.header.refresh': 'Refresh',
183
+ 'detail.header.refreshing': 'Refreshing…',
184
+ 'detail.header.refreshHint': 'Pull the newest timeline window',
185
+ 'detail.header.copyIdTitle': 'Click to copy the session ID',
186
+ 'detail.header.copied': 'Copied',
181
187
  'detail.header.untitled': '(untitled)',
182
188
  'detail.header.unknownProject': 'Unknown project',
183
189
  'detail.header.observedDisclaimer':
@@ -204,6 +210,9 @@ export const en = {
204
210
  'detail.kind.other': 'Event',
205
211
  'detail.gap.label':
206
212
  'Gap: about {n} events may be uncaptured (256-slot queue cap or not persisted)',
213
+ 'detail.filter.conversation': 'Conversation only',
214
+ 'detail.filter.all': 'All events',
215
+ 'detail.filter.hiddenNotice': '{n} protocol events hidden',
207
216
  'detail.timeline.loadMore': 'Load older history',
208
217
  'detail.timeline.loadingMore': 'Loading…',
209
218
  'detail.timeline.noMore': 'Start of the timeline',
@@ -213,6 +222,7 @@ export const en = {
213
222
  'detail.timeline.seq': 'seq {n}',
214
223
  'detail.timeline.hiddenNotice': '{n} earlier entries collapsed to stay smooth',
215
224
  'detail.timeline.showAll': 'Show all',
225
+ 'detail.timeline.chunkRun': '{n} streaming chunks',
216
226
  'detail.states.loadingTitle': 'Loading the timeline…',
217
227
  'detail.states.emptyTitle': 'No events yet',
218
228
  'detail.states.emptyHint': 'This session has no normalized events to show yet.',
@@ -233,6 +243,9 @@ export const en = {
233
243
  'detail.actions.analyze': 'AI analysis',
234
244
  'detail.actions.analyzeDisabledHint':
235
245
  'Enable "AI bypass analysis" in Settings to use this',
246
+ 'detail.tools.title': 'Lineage & search',
247
+ 'detail.tools.show': 'Show',
248
+ 'detail.tools.hide': 'Hide',
236
249
 
237
250
  // ── dsh deep-query tools ────────────────────────────────────────────────
238
251
  'dshtools.lineage.title': 'Session lineage',
@@ -287,6 +300,8 @@ export const en = {
287
300
  'project.lastActive': 'Last active {time}',
288
301
  'project.liveChip': 'Live',
289
302
  'project.untitled': '(untitled)',
303
+ 'project.showAllSessions': 'Show all {n} sessions',
304
+ 'project.showLessSessions': 'Show first {n} only',
290
305
  'project.empty.title': 'No project correlation yet',
291
306
  'project.empty.hint':
292
307
  'No cross-agent project activity within the time window; projects appear here once an agent works inside a project directory.',
@@ -357,7 +372,7 @@ export const en = {
357
372
  'command.noSessions': 'No observed sessions yet',
358
373
  'command.unknownProject': 'Unknown project',
359
374
  'command.untitled': '(untitled)',
360
- 'command.truncated': '{n} more active sessions not listed',
375
+ 'command.truncated': '{n} more sessions not listed',
361
376
  'command.boardHint': 'Open the "Sidecar" tab in the conversation view for the full board',
362
377
  'command.unreachable': 'sidecar is not connected',
363
378
  'command.unreachableHint':