@shendeguize/dsh-agent-sidecar 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.md +167 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +8062 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +396 -0
- package/lib/index.js +4166 -0
- package/package.json +101 -0
- package/src/analysis.ts +782 -0
- package/src/bridge.ts +841 -0
- package/src/client/analysis/AnalysisPanel.tsx +191 -0
- package/src/client/analysis/analysis.module.css +183 -0
- package/src/client/analysis-glue.ts +331 -0
- package/src/client/api.ts +380 -0
- package/src/client/board/Board.tsx +214 -0
- package/src/client/board/board.module.css +302 -0
- package/src/client/board/logic.ts +556 -0
- package/src/client/board/project-view-logic.ts +361 -0
- package/src/client/board/project-view.module.css +307 -0
- package/src/client/board/project-view.tsx +189 -0
- package/src/client/board/strings.ts +112 -0
- package/src/client/commands.ts +484 -0
- package/src/client/controller.ts +360 -0
- package/src/client/css-modules.d.ts +11 -0
- package/src/client/detail/SessionDetail.tsx +270 -0
- package/src/client/detail/detail.module.css +433 -0
- package/src/client/detail/logic.ts +779 -0
- package/src/client/detail/strings.ts +98 -0
- package/src/client/detail/transport.ts +175 -0
- package/src/client/detail-glue.ts +397 -0
- package/src/client/detail-view.module.css +79 -0
- package/src/client/detail-view.tsx +233 -0
- package/src/client/dsh-tools/LineageTree.tsx +210 -0
- package/src/client/dsh-tools/SearchPanel.tsx +169 -0
- package/src/client/dsh-tools/dsh-tools.module.css +374 -0
- package/src/client/dsh-tools/logic.ts +596 -0
- package/src/client/dsh-tools/strings.ts +90 -0
- package/src/client/index.ts +315 -0
- package/src/client/inject/InjectPanel.tsx +482 -0
- package/src/client/inject/inject.module.css +446 -0
- package/src/client/inject/logic.ts +516 -0
- package/src/client/inject/overlay.module.css +22 -0
- package/src/client/inject-glue.ts +171 -0
- package/src/client/locales/command.ts +48 -0
- package/src/client/locales/en.ts +385 -0
- package/src/client/locales/index.ts +123 -0
- package/src/client/locales/zh.ts +402 -0
- package/src/client/m3-transport.ts +151 -0
- package/src/client/mount.tsx +307 -0
- package/src/client/project-glue.ts +134 -0
- package/src/client/search-glue.ts +143 -0
- package/src/client/settings-card.module.css +359 -0
- package/src/client/settings-card.tsx +565 -0
- package/src/client/settings-glue.ts +130 -0
- package/src/client/sidebar-tab.tsx +494 -0
- package/src/client/sse.ts +366 -0
- package/src/client/widget.tsx +80 -0
- package/src/config.ts +193 -0
- package/src/dsh-inject.ts +240 -0
- package/src/fusion.ts +988 -0
- package/src/guard.ts +274 -0
- package/src/index.ts +950 -0
- package/src/inject-gateway.ts +574 -0
- package/src/routes.ts +1133 -0
- package/src/send-cli.ts +340 -0
- package/src/session-store.ts +184 -0
- package/src/skills-provider.ts +293 -0
- package/src/supervisor.ts +463 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject panel (design §5.1 view 3): the two-phase confirmation UI for
|
|
3
|
+
* message injection. Fully controlled and presentational — the component
|
|
4
|
+
* does NO data fetching; the integration layer supplies `onPrepare` /
|
|
5
|
+
* `onExecute` callbacks that speak to `POST <prefix>/action` and resolve
|
|
6
|
+
* with either the wire success shape or the data layer's normalized
|
|
7
|
+
* ApiError (matched structurally, never imported).
|
|
8
|
+
*
|
|
9
|
+
* Three zones, driven by the pure reducer in ./logic.ts:
|
|
10
|
+
*
|
|
11
|
+
* 1. editor — message textarea + live UTF-8 byte counter, queue/steer mode
|
|
12
|
+
* radios, target row, cursor-cli process-list warning (§4.d/S7) and the
|
|
13
|
+
* audit-fingerprint note (§5.3);
|
|
14
|
+
* 2. confirm — the prepare plan (live target-status snapshot + message
|
|
15
|
+
* digest), the 60s confirmToken countdown, and the deliberately
|
|
16
|
+
* restrained danger button;
|
|
17
|
+
* 3. result — delivered / failed (re-prepare offered) / unknown (terminal:
|
|
18
|
+
* the reducer itself refuses a reset, so no retry affordance can exist —
|
|
19
|
+
* S6 — and the copy points at the session to verify).
|
|
20
|
+
*
|
|
21
|
+
* `capability.inject === false` disables the whole panel with the
|
|
22
|
+
* "enable injection in Settings" note.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useEffect, useId, useReducer, useState } from 'react'
|
|
26
|
+
import type { ReactNode } from 'react'
|
|
27
|
+
import { t as defaultT } from '../locales/index.ts'
|
|
28
|
+
import type { SidecarLocaleKey } from '../locales/index.ts'
|
|
29
|
+
import css from './inject.module.css'
|
|
30
|
+
import {
|
|
31
|
+
byteUsage,
|
|
32
|
+
classifyExecuteResponse,
|
|
33
|
+
classifyPrepareResponse,
|
|
34
|
+
deriveEditorGate,
|
|
35
|
+
errorCopy,
|
|
36
|
+
initialPanelState,
|
|
37
|
+
messageInvalidCopy,
|
|
38
|
+
MODE_COPY,
|
|
39
|
+
noticeCopy,
|
|
40
|
+
reducePanel,
|
|
41
|
+
RESULT_COPY,
|
|
42
|
+
resultActions,
|
|
43
|
+
showsProcessListWarning,
|
|
44
|
+
tokenCountdown,
|
|
45
|
+
validateMessage,
|
|
46
|
+
} from './logic.ts'
|
|
47
|
+
import type {
|
|
48
|
+
ApiErrorLike,
|
|
49
|
+
CopyRef,
|
|
50
|
+
InjectMode,
|
|
51
|
+
InjectPlanView,
|
|
52
|
+
InjectResultView,
|
|
53
|
+
PanelEvent,
|
|
54
|
+
PanelExecuteRequest,
|
|
55
|
+
PanelPrepareRequest,
|
|
56
|
+
PrepareSuccess,
|
|
57
|
+
} from './logic.ts'
|
|
58
|
+
|
|
59
|
+
/** Translate seat the panel consumes (module-local t by default). */
|
|
60
|
+
export type InjectTranslate = (
|
|
61
|
+
key: SidecarLocaleKey,
|
|
62
|
+
params?: Record<string, unknown>,
|
|
63
|
+
) => string
|
|
64
|
+
|
|
65
|
+
/** The session the panel injects into, as picked on the board/detail view. */
|
|
66
|
+
export interface InjectPanelTarget {
|
|
67
|
+
agent: string
|
|
68
|
+
sessionId: string
|
|
69
|
+
title?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface InjectPanelProps {
|
|
73
|
+
/** Server capability bits (from the state snapshot). */
|
|
74
|
+
capability: { inject: boolean }
|
|
75
|
+
/** Injection target; null renders the editor disabled with a hint. */
|
|
76
|
+
target: InjectPanelTarget | null
|
|
77
|
+
/** Mode preselected on mount (the inject.default-mode setting). */
|
|
78
|
+
defaultMode: InjectMode
|
|
79
|
+
/** Phase one: POST inject.prepare (resolve errors as values, don't throw). */
|
|
80
|
+
onPrepare(req: PanelPrepareRequest): Promise<PrepareSuccess | ApiErrorLike>
|
|
81
|
+
/** Phase two: POST inject.execute (resolve errors as values, don't throw). */
|
|
82
|
+
onExecute(req: PanelExecuteRequest): Promise<InjectResultView | ApiErrorLike>
|
|
83
|
+
/** Close/dismiss the panel (the hosting modal owns the lifecycle). */
|
|
84
|
+
onClose?(): void
|
|
85
|
+
/** Clock injection for the token countdown; defaults to Date.now. */
|
|
86
|
+
nowMs?: () => number
|
|
87
|
+
/** Locale seat override; defaults to the module-local table. */
|
|
88
|
+
t?: InjectTranslate
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Countdown re-render cadence while a confirm token is live. */
|
|
92
|
+
const TICK_MS = 500
|
|
93
|
+
|
|
94
|
+
function renderCopy(t: InjectTranslate, copy: CopyRef): string {
|
|
95
|
+
return t(copy.key, copy.params)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Zone building blocks.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
interface WarningsProps {
|
|
103
|
+
t: InjectTranslate
|
|
104
|
+
agent: string | null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** cursor-cli process-list warning (gated) + the always-on audit note. */
|
|
108
|
+
function Warnings(props: WarningsProps): ReactNode {
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
{props.agent !== null && showsProcessListWarning(props.agent)
|
|
112
|
+
? <p className={css['warnBar']} role="alert">{props.t('inject.argvWarning')}</p>
|
|
113
|
+
: null}
|
|
114
|
+
<p className={css['auditNote']}>{props.t('inject.auditNote')}</p>
|
|
115
|
+
</>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface PlanBoxProps {
|
|
120
|
+
t: InjectTranslate
|
|
121
|
+
plan: InjectPlanView
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Confirm-phase plan: live target snapshot + message digest. */
|
|
125
|
+
function PlanBox(props: PlanBoxProps): ReactNode {
|
|
126
|
+
const { t, plan } = props
|
|
127
|
+
const status = plan.targetStatus
|
|
128
|
+
const targetName = status.title !== undefined && status.title !== ''
|
|
129
|
+
? status.title
|
|
130
|
+
: plan.target.sessionId
|
|
131
|
+
return (
|
|
132
|
+
<div className={css['planBox']}>
|
|
133
|
+
<div className={css['planRow']}>
|
|
134
|
+
<span className={css['planKey']}>{t('inject.planTargetLabel')}</span>
|
|
135
|
+
<span className={css['planValue']}>
|
|
136
|
+
<span className={css['agentTag']}>{plan.target.agent}</span>
|
|
137
|
+
<span className={css['planTitle']} title={plan.target.sessionId}>{targetName}</span>
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div className={css['planRow']}>
|
|
141
|
+
<span className={css['planKey']}>
|
|
142
|
+
{t('inject.planStatus', { status: status.status })}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
<p className={css['observedNote']}>{t('inject.statusObservedNote')}</p>
|
|
146
|
+
<div className={css['planRow']}>
|
|
147
|
+
<span className={css['planKey']}>{t('inject.planModeLabel')}</span>
|
|
148
|
+
<span className={css['planValue']}>{t(MODE_COPY[plan.mode].label)}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className={css['planPreview']}>
|
|
151
|
+
<span className={css['planKey']}>
|
|
152
|
+
{t('inject.planPreviewLabel', { bytes: plan.messagePreview.bytes })}
|
|
153
|
+
</span>
|
|
154
|
+
<pre className={css['preview']}>{plan.messagePreview.head}</pre>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Panel.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the two-phase inject panel.
|
|
166
|
+
* @param props - capability, target, and the integration callbacks.
|
|
167
|
+
* @returns the panel.
|
|
168
|
+
*/
|
|
169
|
+
export function InjectPanel(props: InjectPanelProps): ReactNode {
|
|
170
|
+
const t = props.t ?? defaultT
|
|
171
|
+
const now = props.nowMs ?? Date.now
|
|
172
|
+
const [state, dispatch] = useReducer(reducePanel, undefined, initialPanelState)
|
|
173
|
+
const [draft, setDraft] = useState('')
|
|
174
|
+
const [mode, setMode] = useState<InjectMode>(props.defaultMode)
|
|
175
|
+
const [clock, setClock] = useState(() => now())
|
|
176
|
+
const textareaId = useId()
|
|
177
|
+
const modeGroup = useId()
|
|
178
|
+
|
|
179
|
+
// Token countdown: refresh the display clock and let the reducer decide
|
|
180
|
+
// expiry, only while a confirm token is live.
|
|
181
|
+
const inConfirm = state.phase === 'confirm'
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!inConfirm) return
|
|
184
|
+
setClock(now())
|
|
185
|
+
const id = setInterval(() => {
|
|
186
|
+
const nowValue = now()
|
|
187
|
+
setClock(nowValue)
|
|
188
|
+
dispatch({ type: 'TICK', nowMs: nowValue })
|
|
189
|
+
}, TICK_MS)
|
|
190
|
+
return () => { clearInterval(id) }
|
|
191
|
+
// `now` is a stable prop (or Date.now); phase entry is the real trigger.
|
|
192
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
193
|
+
}, [inConfirm])
|
|
194
|
+
|
|
195
|
+
const validation = validateMessage(draft)
|
|
196
|
+
const gate = deriveEditorGate({
|
|
197
|
+
injectEnabled: props.capability.inject,
|
|
198
|
+
hasTarget: props.target !== null,
|
|
199
|
+
phase: state.phase,
|
|
200
|
+
validation,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const handlePrepare = async (): Promise<void> => {
|
|
204
|
+
const target = props.target
|
|
205
|
+
if (target === null || !gate.canPrepare) return
|
|
206
|
+
const message = draft
|
|
207
|
+
dispatch({ type: 'PREPARE_START', message, mode })
|
|
208
|
+
let event: PanelEvent
|
|
209
|
+
try {
|
|
210
|
+
const response = await props.onPrepare({
|
|
211
|
+
target: { agent: target.agent, sessionId: target.sessionId },
|
|
212
|
+
mode,
|
|
213
|
+
message,
|
|
214
|
+
})
|
|
215
|
+
event = classifyPrepareResponse(response)
|
|
216
|
+
} catch {
|
|
217
|
+
// Contract says resolve errors as values; a throw is still mapped to
|
|
218
|
+
// a retryable transport notice rather than a crashed panel.
|
|
219
|
+
event = { type: 'PREPARE_ERROR', code: 'unexpected_error' }
|
|
220
|
+
}
|
|
221
|
+
dispatch(event)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const handleExecute = async (): Promise<void> => {
|
|
225
|
+
if (state.phase !== 'confirm') return
|
|
226
|
+
const { requestId, confirmToken, message } = state
|
|
227
|
+
dispatch({ type: 'EXECUTE_START' })
|
|
228
|
+
let event: PanelEvent
|
|
229
|
+
try {
|
|
230
|
+
const response = await props.onExecute({ requestId, confirmToken, message })
|
|
231
|
+
event = classifyExecuteResponse(response)
|
|
232
|
+
} catch {
|
|
233
|
+
// The execute may already have been dispatched server-side: honest
|
|
234
|
+
// verdict is unknown — terminal, no retry (S6).
|
|
235
|
+
event = {
|
|
236
|
+
type: 'EXECUTE_RESULT',
|
|
237
|
+
result: { outcome: 'unknown', errorCode: 'unexpected_error' },
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
dispatch(event)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const closeButton = props.onClose !== undefined
|
|
244
|
+
? (
|
|
245
|
+
<button type="button" className={css['btn']} onClick={props.onClose}>
|
|
246
|
+
{t('inject.close')}
|
|
247
|
+
</button>
|
|
248
|
+
)
|
|
249
|
+
: null
|
|
250
|
+
|
|
251
|
+
// ── whole-panel gate: injection capability off ─────────────────────────
|
|
252
|
+
if (!props.capability.inject) {
|
|
253
|
+
return (
|
|
254
|
+
<section className={css['panel']} aria-label={t('inject.title')}>
|
|
255
|
+
<header className={css['header']}>
|
|
256
|
+
<h3 className={css['title']}>{t('inject.title')}</h3>
|
|
257
|
+
</header>
|
|
258
|
+
<div className={css['body']}>
|
|
259
|
+
<p className={css['capabilityOff']} role="status">{t('inject.capabilityOff')}</p>
|
|
260
|
+
{closeButton !== null
|
|
261
|
+
? <div className={css['footer']}>{closeButton}</div>
|
|
262
|
+
: null}
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── zone 3: result ──────────────────────────────────────────────────────
|
|
269
|
+
if (state.phase === 'result') {
|
|
270
|
+
const { result } = state
|
|
271
|
+
const actions = resultActions(result.outcome)
|
|
272
|
+
const toneClass = result.outcome === 'delivered'
|
|
273
|
+
? css['resultOk']
|
|
274
|
+
: result.outcome === 'failed'
|
|
275
|
+
? css['resultFail']
|
|
276
|
+
: css['resultUnknown']
|
|
277
|
+
return (
|
|
278
|
+
<section className={css['panel']} aria-label={t('inject.title')}>
|
|
279
|
+
<header className={css['header']}>
|
|
280
|
+
<h3 className={css['title']}>{t('inject.title')}</h3>
|
|
281
|
+
</header>
|
|
282
|
+
<div className={css['body']}>
|
|
283
|
+
<p className={toneClass} role="status">{t(RESULT_COPY[result.outcome])}</p>
|
|
284
|
+
{result.outcome === 'failed' && result.errorCode !== undefined
|
|
285
|
+
? (
|
|
286
|
+
<p className={css['resultDetail']}>
|
|
287
|
+
{renderCopy(t, errorCopy(result.errorCode))}
|
|
288
|
+
</p>
|
|
289
|
+
)
|
|
290
|
+
: null}
|
|
291
|
+
{result.replayed === true
|
|
292
|
+
? <p className={css['resultDetail']}>{t('inject.resultReplayed')}</p>
|
|
293
|
+
: null}
|
|
294
|
+
<div className={css['footer']}>
|
|
295
|
+
{closeButton}
|
|
296
|
+
{actions.canReprepare
|
|
297
|
+
? (
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
className={css['btnPrimary']}
|
|
301
|
+
onClick={() => { dispatch({ type: 'RESET' }) }}
|
|
302
|
+
>
|
|
303
|
+
{t('inject.reprepare')}
|
|
304
|
+
</button>
|
|
305
|
+
)
|
|
306
|
+
: null}
|
|
307
|
+
{result.outcome === 'delivered' && props.onClose === undefined
|
|
308
|
+
? (
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className={css['btnPrimary']}
|
|
312
|
+
onClick={() => { dispatch({ type: 'RESET' }) }}
|
|
313
|
+
>
|
|
314
|
+
{t('inject.done')}
|
|
315
|
+
</button>
|
|
316
|
+
)
|
|
317
|
+
: null}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</section>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── zone 2: confirm / executing ─────────────────────────────────────────
|
|
325
|
+
if (state.phase === 'confirm' || state.phase === 'executing') {
|
|
326
|
+
const executing = state.phase === 'executing'
|
|
327
|
+
const countdown = state.phase === 'confirm'
|
|
328
|
+
? tokenCountdown(state.expiresAt, clock)
|
|
329
|
+
: null
|
|
330
|
+
return (
|
|
331
|
+
<section className={css['panel']} aria-label={t('inject.confirmTitle')}>
|
|
332
|
+
<header className={css['header']}>
|
|
333
|
+
<h3 className={css['title']}>{t('inject.confirmTitle')}</h3>
|
|
334
|
+
</header>
|
|
335
|
+
<div className={css['body']}>
|
|
336
|
+
<PlanBox t={t} plan={state.plan} />
|
|
337
|
+
<Warnings t={t} agent={state.plan.target.agent} />
|
|
338
|
+
{countdown !== null
|
|
339
|
+
? (
|
|
340
|
+
<p className={css['countdown']} role="timer">
|
|
341
|
+
{t('inject.countdown', { seconds: countdown.seconds })}
|
|
342
|
+
</p>
|
|
343
|
+
)
|
|
344
|
+
: null}
|
|
345
|
+
<div className={css['footer']}>
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
className={css['btn']}
|
|
349
|
+
disabled={executing}
|
|
350
|
+
onClick={() => { dispatch({ type: 'CANCEL' }) }}
|
|
351
|
+
>
|
|
352
|
+
{t('inject.cancel')}
|
|
353
|
+
</button>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
className={css['btnDanger']}
|
|
357
|
+
disabled={executing}
|
|
358
|
+
onClick={() => { void handleExecute() }}
|
|
359
|
+
>
|
|
360
|
+
{t(executing ? 'inject.executing' : 'inject.confirmExecute')}
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</section>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── zone 1: editor (idle / preparing) ───────────────────────────────────
|
|
369
|
+
const preparing = state.phase === 'preparing'
|
|
370
|
+
const canEdit = props.target !== null && !preparing
|
|
371
|
+
const usage = byteUsage(validation.bytes)
|
|
372
|
+
const notice = state.phase === 'idle' ? state.notice : null
|
|
373
|
+
const showInvalid = !validation.ok && validation.code !== 'empty'
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<section className={css['panel']} aria-label={t('inject.title')}>
|
|
377
|
+
<header className={css['header']}>
|
|
378
|
+
<h3 className={css['title']}>{t('inject.title')}</h3>
|
|
379
|
+
</header>
|
|
380
|
+
<div className={css['body']}>
|
|
381
|
+
{notice !== null
|
|
382
|
+
? (
|
|
383
|
+
<p
|
|
384
|
+
className={notice.kind === 'token_expired' ? css['noticeWarn'] : css['noticeError']}
|
|
385
|
+
role="alert"
|
|
386
|
+
>
|
|
387
|
+
{renderCopy(t, noticeCopy(notice))}
|
|
388
|
+
{notice.kind === 'prepare_rejected' && notice.detail !== undefined
|
|
389
|
+
? <span className={css['noticeDetail']}> {notice.detail}</span>
|
|
390
|
+
: null}
|
|
391
|
+
</p>
|
|
392
|
+
)
|
|
393
|
+
: null}
|
|
394
|
+
|
|
395
|
+
<div className={css['targetRow']}>
|
|
396
|
+
<span className={css['planKey']}>{t('inject.targetLabel')}</span>
|
|
397
|
+
{props.target !== null
|
|
398
|
+
? (
|
|
399
|
+
<span className={css['planValue']}>
|
|
400
|
+
<span className={css['agentTag']}>{props.target.agent}</span>
|
|
401
|
+
<span className={css['planTitle']} title={props.target.sessionId}>
|
|
402
|
+
{props.target.title !== undefined && props.target.title !== ''
|
|
403
|
+
? props.target.title
|
|
404
|
+
: props.target.sessionId}
|
|
405
|
+
</span>
|
|
406
|
+
</span>
|
|
407
|
+
)
|
|
408
|
+
: <span className={css['noTarget']}>{t('inject.noTarget')}</span>}
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div className={css['field']}>
|
|
412
|
+
<label className={css['label']} htmlFor={textareaId}>
|
|
413
|
+
{t('inject.messageLabel')}
|
|
414
|
+
</label>
|
|
415
|
+
<textarea
|
|
416
|
+
id={textareaId}
|
|
417
|
+
className={css['textarea']}
|
|
418
|
+
value={draft}
|
|
419
|
+
placeholder={t('inject.messagePlaceholder')}
|
|
420
|
+
disabled={!canEdit}
|
|
421
|
+
rows={5}
|
|
422
|
+
onChange={(event) => { setDraft(event.target.value) }}
|
|
423
|
+
/>
|
|
424
|
+
<div className={css['byteRow']}>
|
|
425
|
+
<div className={css['byteBar']} aria-hidden>
|
|
426
|
+
<div
|
|
427
|
+
className={usage.over
|
|
428
|
+
? `${css['byteFill']} ${css['byteFillOver']}`
|
|
429
|
+
: css['byteFill']}
|
|
430
|
+
style={{ width: `${usage.ratio * 100}%` }}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
<span className={usage.over ? css['byteTextOver'] : css['byteText']}>
|
|
434
|
+
{t('inject.byteCount', { bytes: usage.bytes, limit: usage.limit })}
|
|
435
|
+
</span>
|
|
436
|
+
</div>
|
|
437
|
+
{showInvalid && !validation.ok
|
|
438
|
+
? (
|
|
439
|
+
<p className={css['invalid']} role="alert">
|
|
440
|
+
{renderCopy(t, messageInvalidCopy(validation))}
|
|
441
|
+
</p>
|
|
442
|
+
)
|
|
443
|
+
: null}
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<fieldset className={css['modes']}>
|
|
447
|
+
<legend className={css['label']}>{t('inject.modeLabel')}</legend>
|
|
448
|
+
{(['queue', 'steer'] as const).map(option => (
|
|
449
|
+
<label key={option} className={css['modeOption']}>
|
|
450
|
+
<input
|
|
451
|
+
type="radio"
|
|
452
|
+
name={modeGroup}
|
|
453
|
+
value={option}
|
|
454
|
+
checked={mode === option}
|
|
455
|
+
disabled={!canEdit}
|
|
456
|
+
onChange={() => { setMode(option) }}
|
|
457
|
+
/>
|
|
458
|
+
<span className={css['modeText']}>
|
|
459
|
+
<span className={css['modeLabel']}>{t(MODE_COPY[option].label)}</span>
|
|
460
|
+
<span className={css['modeHint']}>{t(MODE_COPY[option].hint)}</span>
|
|
461
|
+
</span>
|
|
462
|
+
</label>
|
|
463
|
+
))}
|
|
464
|
+
</fieldset>
|
|
465
|
+
|
|
466
|
+
<Warnings t={t} agent={props.target?.agent ?? null} />
|
|
467
|
+
|
|
468
|
+
<div className={css['footer']}>
|
|
469
|
+
{closeButton}
|
|
470
|
+
<button
|
|
471
|
+
type="button"
|
|
472
|
+
className={css['btnPrimary']}
|
|
473
|
+
disabled={!gate.canPrepare}
|
|
474
|
+
onClick={() => { void handlePrepare() }}
|
|
475
|
+
>
|
|
476
|
+
{t(preparing ? 'inject.preparing' : 'inject.prepare')}
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</section>
|
|
481
|
+
)
|
|
482
|
+
}
|