@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +8062 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.d.ts +396 -0
  7. package/lib/index.js +4166 -0
  8. package/package.json +101 -0
  9. package/src/analysis.ts +782 -0
  10. package/src/bridge.ts +841 -0
  11. package/src/client/analysis/AnalysisPanel.tsx +191 -0
  12. package/src/client/analysis/analysis.module.css +183 -0
  13. package/src/client/analysis-glue.ts +331 -0
  14. package/src/client/api.ts +380 -0
  15. package/src/client/board/Board.tsx +214 -0
  16. package/src/client/board/board.module.css +302 -0
  17. package/src/client/board/logic.ts +556 -0
  18. package/src/client/board/project-view-logic.ts +361 -0
  19. package/src/client/board/project-view.module.css +307 -0
  20. package/src/client/board/project-view.tsx +189 -0
  21. package/src/client/board/strings.ts +112 -0
  22. package/src/client/commands.ts +484 -0
  23. package/src/client/controller.ts +360 -0
  24. package/src/client/css-modules.d.ts +11 -0
  25. package/src/client/detail/SessionDetail.tsx +270 -0
  26. package/src/client/detail/detail.module.css +433 -0
  27. package/src/client/detail/logic.ts +779 -0
  28. package/src/client/detail/strings.ts +98 -0
  29. package/src/client/detail/transport.ts +175 -0
  30. package/src/client/detail-glue.ts +397 -0
  31. package/src/client/detail-view.module.css +79 -0
  32. package/src/client/detail-view.tsx +233 -0
  33. package/src/client/dsh-tools/LineageTree.tsx +210 -0
  34. package/src/client/dsh-tools/SearchPanel.tsx +169 -0
  35. package/src/client/dsh-tools/dsh-tools.module.css +374 -0
  36. package/src/client/dsh-tools/logic.ts +596 -0
  37. package/src/client/dsh-tools/strings.ts +90 -0
  38. package/src/client/index.ts +315 -0
  39. package/src/client/inject/InjectPanel.tsx +482 -0
  40. package/src/client/inject/inject.module.css +446 -0
  41. package/src/client/inject/logic.ts +516 -0
  42. package/src/client/inject/overlay.module.css +22 -0
  43. package/src/client/inject-glue.ts +171 -0
  44. package/src/client/locales/command.ts +48 -0
  45. package/src/client/locales/en.ts +385 -0
  46. package/src/client/locales/index.ts +123 -0
  47. package/src/client/locales/zh.ts +402 -0
  48. package/src/client/m3-transport.ts +151 -0
  49. package/src/client/mount.tsx +307 -0
  50. package/src/client/project-glue.ts +134 -0
  51. package/src/client/search-glue.ts +143 -0
  52. package/src/client/settings-card.module.css +359 -0
  53. package/src/client/settings-card.tsx +565 -0
  54. package/src/client/settings-glue.ts +130 -0
  55. package/src/client/sidebar-tab.tsx +494 -0
  56. package/src/client/sse.ts +366 -0
  57. package/src/client/widget.tsx +80 -0
  58. package/src/config.ts +193 -0
  59. package/src/dsh-inject.ts +240 -0
  60. package/src/fusion.ts +988 -0
  61. package/src/guard.ts +274 -0
  62. package/src/index.ts +950 -0
  63. package/src/inject-gateway.ts +574 -0
  64. package/src/routes.ts +1133 -0
  65. package/src/send-cli.ts +340 -0
  66. package/src/session-store.ts +184 -0
  67. package/src/skills-provider.ts +293 -0
  68. package/src/supervisor.ts +463 -0
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Pure logic for the inject panel (design §5.1 view 3, §5.3 confirmation
3
+ * boundary, §8 S5-S7 wording). No React, no I/O, no imports from the data
4
+ * layer — everything arrives and leaves as plain values, so this module is
5
+ * unit-testable in a bare node environment (same posture as board/logic.ts).
6
+ *
7
+ * Decoupling contract (T4.5 ↔ S5 integration): the wire-facing types below
8
+ * (PrepareSuccess / InjectResultView / InjectPlanView / ApiErrorLike) are
9
+ * this module's OWN mirrors of the host action contract (src/routes.ts +
10
+ * src/inject-gateway.ts) and of the data layer's normalized error
11
+ * (client/api.ts ApiError, matched structurally so real instances satisfy
12
+ * {@link ApiErrorLike} without an import). The integration layer feeds the
13
+ * panel through the `onPrepare`/`onExecute` props; this module classifies
14
+ * whatever comes back.
15
+ *
16
+ * Two-phase state machine (§4.f.5):
17
+ *
18
+ * idle ──PREPARE_START──▶ preparing ──PREPARE_OK──▶ confirm
19
+ * ▲ │ │
20
+ * │◀──PREPARE_REJECTED/ERROR─┘ TICK(expired) / CANCEL
21
+ * │◀──────────────────────────────────────────────────┤
22
+ * │ EXECUTE_START
23
+ * │ ▼
24
+ * │◀──RESET (delivered/failed only)── result ◀── executing
25
+ *
26
+ * `outcome: 'unknown'` is terminal: the reducer refuses RESET out of an
27
+ * unknown result, so there is NO machine path that could re-drive an
28
+ * execute after an unknown delivery (S6).
29
+ *
30
+ * @module
31
+ */
32
+
33
+ import type { SidecarLocaleKey } from '../locales/index.ts'
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Wire-facing mirrors (host source of truth noted per type).
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Injection mode, aligned with dsh `session.prompt` semantics (host: inject-gateway.ts). */
40
+ export type InjectMode = 'queue' | 'steer'
41
+
42
+ /** Terminal outcome vocabulary (host: inject-gateway.ts). */
43
+ export type InjectOutcome = 'delivered' | 'failed' | 'unknown'
44
+
45
+ /** Injection target reference as sent in the prepare envelope. */
46
+ export interface InjectTargetRef {
47
+ agent: string
48
+ sessionId: string
49
+ }
50
+
51
+ /** Live target snapshot captured by the server-side prepare re-check. */
52
+ export interface TargetStatusView {
53
+ agent: string
54
+ sessionId: string
55
+ status: string
56
+ title?: string
57
+ project?: string
58
+ }
59
+
60
+ /** Confirmation plan echoed by `inject.prepare` (host: inject-gateway.ts). */
61
+ export interface InjectPlanView {
62
+ target: InjectTargetRef
63
+ mode: InjectMode
64
+ targetStatus: TargetStatusView
65
+ messagePreview: { bytes: number; head: string }
66
+ }
67
+
68
+ /** Success body of `POST action {type:'inject.prepare'}` (host: routes.ts). */
69
+ export interface PrepareSuccess {
70
+ requestId: string
71
+ confirmToken: string
72
+ plan: InjectPlanView
73
+ /** Epoch ms after which the confirmToken is dead. */
74
+ expiresAt: number
75
+ }
76
+
77
+ /** Body of `POST action {type:'inject.execute'}` answers (host: routes.ts). */
78
+ export interface InjectResultView {
79
+ outcome: InjectOutcome
80
+ errorCode?: string
81
+ detail?: string
82
+ /** True when the gateway replayed a cached first result (idempotency). */
83
+ replayed?: boolean
84
+ }
85
+
86
+ /**
87
+ * Structural mirror of the data layer's ApiError (client/api.ts): real
88
+ * instances satisfy this shape, so the panel can classify them without
89
+ * importing the data layer.
90
+ */
91
+ export interface ApiErrorLike {
92
+ kind: 'timeout' | 'aborted' | 'network' | 'http' | 'parse'
93
+ /** Server `{reason}` code for kind 'http', a stable local code otherwise. */
94
+ reason: string
95
+ status: number | null
96
+ }
97
+
98
+ /** What the panel hands the integration's `onPrepare` callback. */
99
+ export interface PanelPrepareRequest {
100
+ target: InjectTargetRef
101
+ mode: InjectMode
102
+ message: string
103
+ }
104
+
105
+ /** What the panel hands the integration's `onExecute` callback. */
106
+ export interface PanelExecuteRequest {
107
+ requestId: string
108
+ confirmToken: string
109
+ message: string
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Byte counting and message validation (mirrors the host gate so the UI
114
+ // refuses locally exactly what the server would refuse remotely).
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /** Message byte cap; must mirror inject-gateway.ts (pinned by test). */
118
+ export const MAX_MESSAGE_BYTES = 16 * 1024
119
+
120
+ const utf8 = new TextEncoder()
121
+
122
+ /**
123
+ * UTF-8 byte size of a message. TextEncoder agrees with the host's
124
+ * `Buffer.byteLength(message, 'utf8')` for every well-formed string, so the
125
+ * 16 KiB verdict is identical on both sides.
126
+ */
127
+ export function messageBytes(message: string): number {
128
+ return utf8.encode(message).length
129
+ }
130
+
131
+ export type MessageInvalidCode = 'empty' | 'nul' | 'too_large'
132
+
133
+ export type MessageValidation =
134
+ | { ok: true; bytes: number }
135
+ | { ok: false; code: MessageInvalidCode; bytes: number }
136
+
137
+ /** Local pre-validation, same checks and order as the host gateway. */
138
+ export function validateMessage(message: string): MessageValidation {
139
+ const bytes = messageBytes(message)
140
+ if (bytes === 0) return { ok: false, code: 'empty', bytes }
141
+ if (message.includes('\u0000')) return { ok: false, code: 'nul', bytes }
142
+ if (bytes > MAX_MESSAGE_BYTES) return { ok: false, code: 'too_large', bytes }
143
+ return { ok: true, bytes }
144
+ }
145
+
146
+ /** Render model for the live byte counter / limit progress bar. */
147
+ export interface ByteUsage {
148
+ bytes: number
149
+ limit: number
150
+ /** Fill fraction for the progress bar, clamped to [0, 1]. */
151
+ ratio: number
152
+ over: boolean
153
+ }
154
+
155
+ /** Derive the byte counter view from a byte count. */
156
+ export function byteUsage(bytes: number): ByteUsage {
157
+ return {
158
+ bytes,
159
+ limit: MAX_MESSAGE_BYTES,
160
+ ratio: Math.min(1, Math.max(0, bytes / MAX_MESSAGE_BYTES)),
161
+ over: bytes > MAX_MESSAGE_BYTES,
162
+ }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Target visibility warning (design §4.d / S7).
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Whether the process-list visibility warning applies. Only cursor-cli
171
+ * qualifies: its upstream contract puts the prompt on the native
172
+ * subprocess argv. claude / codex / dsh do not warn — after S4a the
173
+ * sidecar's own argv (send --message-stdin) and the dsh in-process path
174
+ * both keep the body off the process list.
175
+ */
176
+ export function showsProcessListWarning(agent: string): boolean {
177
+ return agent.trim().toLowerCase() === 'cursor-cli'
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Token countdown (60s TTL is server-owned; the UI only reads expiresAt).
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export interface TokenCountdown {
185
+ remainingMs: number
186
+ /** Whole seconds for display, rounded up so "1s" never shows as "0s". */
187
+ seconds: number
188
+ expired: boolean
189
+ }
190
+
191
+ /** Countdown view for a server-issued expiresAt (epoch ms). */
192
+ export function tokenCountdown(expiresAt: number, nowMs: number): TokenCountdown {
193
+ const remainingMs = Math.max(0, expiresAt - nowMs)
194
+ return {
195
+ remainingMs,
196
+ seconds: Math.ceil(remainingMs / 1000),
197
+ expired: nowMs >= expiresAt,
198
+ }
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Two-phase state machine.
203
+ // ---------------------------------------------------------------------------
204
+
205
+ /** Non-blocking notice shown on the editor after a failed/expired attempt. */
206
+ export type PanelNotice =
207
+ | { kind: 'token_expired' }
208
+ /** Server vocabulary rejection (prepare answered with an error status). */
209
+ | { kind: 'prepare_rejected'; code: string; detail?: string }
210
+ /** Transport-level prepare failure (timeout/network/…); safe to retry. */
211
+ | { kind: 'prepare_error'; code: string }
212
+
213
+ export type PanelState =
214
+ | { phase: 'idle'; notice: PanelNotice | null }
215
+ | { phase: 'preparing'; message: string; mode: InjectMode }
216
+ | {
217
+ phase: 'confirm'
218
+ message: string
219
+ mode: InjectMode
220
+ requestId: string
221
+ confirmToken: string
222
+ plan: InjectPlanView
223
+ expiresAt: number
224
+ }
225
+ | {
226
+ phase: 'executing'
227
+ message: string
228
+ mode: InjectMode
229
+ requestId: string
230
+ confirmToken: string
231
+ plan: InjectPlanView
232
+ }
233
+ | { phase: 'result'; result: InjectResultView; plan: InjectPlanView | null }
234
+
235
+ export type InjectPhase = PanelState['phase']
236
+
237
+ export type PanelEvent =
238
+ | { type: 'PREPARE_START'; message: string; mode: InjectMode }
239
+ | { type: 'PREPARE_OK'; response: PrepareSuccess }
240
+ | { type: 'PREPARE_REJECTED'; code: string; detail?: string }
241
+ | { type: 'PREPARE_ERROR'; code: string }
242
+ | { type: 'TICK'; nowMs: number }
243
+ | { type: 'CANCEL' }
244
+ | { type: 'EXECUTE_START' }
245
+ | { type: 'EXECUTE_RESULT'; result: InjectResultView }
246
+ | { type: 'RESET' }
247
+
248
+ /** Fresh machine state (factory, so callers can never share a mutable seed). */
249
+ export function initialPanelState(): PanelState {
250
+ return { phase: 'idle', notice: null }
251
+ }
252
+
253
+ /**
254
+ * Pure transition function. Events that do not apply to the current phase
255
+ * return the state unchanged (same reference), which both makes stale
256
+ * async callbacks harmless and gives React a free bail-out.
257
+ */
258
+ export function reducePanel(state: PanelState, event: PanelEvent): PanelState {
259
+ switch (event.type) {
260
+ case 'PREPARE_START':
261
+ if (state.phase !== 'idle') return state
262
+ return { phase: 'preparing', message: event.message, mode: event.mode }
263
+
264
+ case 'PREPARE_OK':
265
+ if (state.phase !== 'preparing') return state
266
+ return {
267
+ phase: 'confirm',
268
+ message: state.message,
269
+ mode: state.mode,
270
+ requestId: event.response.requestId,
271
+ confirmToken: event.response.confirmToken,
272
+ plan: event.response.plan,
273
+ expiresAt: event.response.expiresAt,
274
+ }
275
+
276
+ case 'PREPARE_REJECTED':
277
+ if (state.phase !== 'preparing') return state
278
+ return {
279
+ phase: 'idle',
280
+ notice: {
281
+ kind: 'prepare_rejected',
282
+ code: event.code,
283
+ ...(event.detail !== undefined ? { detail: event.detail } : {}),
284
+ },
285
+ }
286
+
287
+ case 'PREPARE_ERROR':
288
+ if (state.phase !== 'preparing') return state
289
+ return { phase: 'idle', notice: { kind: 'prepare_error', code: event.code } }
290
+
291
+ case 'TICK':
292
+ if (state.phase !== 'confirm') return state
293
+ if (event.nowMs < state.expiresAt) return state
294
+ // Token TTL elapsed while waiting for the click: back to the editor
295
+ // with a "prepare again" notice (§4.f.5, 60s server TTL).
296
+ return { phase: 'idle', notice: { kind: 'token_expired' } }
297
+
298
+ case 'CANCEL':
299
+ if (state.phase !== 'confirm') return state
300
+ return { phase: 'idle', notice: null }
301
+
302
+ case 'EXECUTE_START':
303
+ if (state.phase !== 'confirm') return state
304
+ return {
305
+ phase: 'executing',
306
+ message: state.message,
307
+ mode: state.mode,
308
+ requestId: state.requestId,
309
+ confirmToken: state.confirmToken,
310
+ plan: state.plan,
311
+ }
312
+
313
+ case 'EXECUTE_RESULT':
314
+ if (state.phase !== 'executing') return state
315
+ return { phase: 'result', result: event.result, plan: state.plan }
316
+
317
+ case 'RESET':
318
+ if (state.phase !== 'result') return state
319
+ // 'unknown' is terminal (S6): no machine path leads back to the
320
+ // editor, so no UI built on this reducer can offer a retry.
321
+ if (state.result.outcome === 'unknown') return state
322
+ return { phase: 'idle', notice: null }
323
+ }
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Response classification (PrepareSuccess/InjectResult vs normalized error).
328
+ // ---------------------------------------------------------------------------
329
+
330
+ const API_ERROR_KINDS: ReadonlySet<string> = new Set([
331
+ 'timeout',
332
+ 'aborted',
333
+ 'network',
334
+ 'http',
335
+ 'parse',
336
+ ])
337
+
338
+ /** Structural guard matching client/api.ts ApiError instances. */
339
+ export function isApiErrorLike(value: unknown): value is ApiErrorLike {
340
+ if (typeof value !== 'object' || value === null) return false
341
+ const v = value as Record<string, unknown>
342
+ return (
343
+ typeof v['kind'] === 'string' &&
344
+ API_ERROR_KINDS.has(v['kind']) &&
345
+ typeof v['reason'] === 'string' &&
346
+ !('outcome' in v)
347
+ )
348
+ }
349
+
350
+ /**
351
+ * Classify what `onPrepare` resolved with. An HTTP-kind error is a server
352
+ * vocabulary rejection (the reason carries the errorCode routes mapped to
353
+ * the status); any other kind is a transport failure — harmless for
354
+ * prepare, which is side-effect-free beyond a token that expires on its
355
+ * own, so the editor may simply offer another attempt.
356
+ */
357
+ export function classifyPrepareResponse(
358
+ value: PrepareSuccess | ApiErrorLike,
359
+ ): PanelEvent {
360
+ if (isApiErrorLike(value)) {
361
+ return value.kind === 'http'
362
+ ? { type: 'PREPARE_REJECTED', code: value.reason }
363
+ : { type: 'PREPARE_ERROR', code: value.reason }
364
+ }
365
+ return { type: 'PREPARE_OK', response: value }
366
+ }
367
+
368
+ /**
369
+ * Classify what `onExecute` resolved with. An HTTP-kind error is the
370
+ * routes-mapped failed outcome. Everything else (timeout/network/parse/
371
+ * aborted) happened AFTER the execute may already have been dispatched, so
372
+ * the honest verdict is `outcome: 'unknown'` — terminal, no retry (S6);
373
+ * the user is pointed at the target session to verify.
374
+ */
375
+ export function classifyExecuteResponse(
376
+ value: InjectResultView | ApiErrorLike,
377
+ ): PanelEvent {
378
+ if (isApiErrorLike(value)) {
379
+ if (value.kind === 'http') {
380
+ return {
381
+ type: 'EXECUTE_RESULT',
382
+ result: { outcome: 'failed', errorCode: value.reason },
383
+ }
384
+ }
385
+ return {
386
+ type: 'EXECUTE_RESULT',
387
+ result: { outcome: 'unknown', errorCode: value.reason },
388
+ }
389
+ }
390
+ return { type: 'EXECUTE_RESULT', result: value }
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Enable/disable gate for the editor (design: 置灰规则).
395
+ // ---------------------------------------------------------------------------
396
+
397
+ export type EditorBlock = 'inject_off' | 'no_target' | 'busy' | 'invalid_message'
398
+
399
+ export interface EditorGate {
400
+ canPrepare: boolean
401
+ block: EditorBlock | null
402
+ }
403
+
404
+ /**
405
+ * Why (if at all) the prepare action is unavailable, in priority order:
406
+ * capability off > no target > a phase already in flight > invalid message.
407
+ */
408
+ export function deriveEditorGate(input: {
409
+ injectEnabled: boolean
410
+ hasTarget: boolean
411
+ phase: InjectPhase
412
+ validation: MessageValidation
413
+ }): EditorGate {
414
+ if (!input.injectEnabled) return { canPrepare: false, block: 'inject_off' }
415
+ if (!input.hasTarget) return { canPrepare: false, block: 'no_target' }
416
+ if (input.phase !== 'idle') return { canPrepare: false, block: 'busy' }
417
+ if (!input.validation.ok) return { canPrepare: false, block: 'invalid_message' }
418
+ return { canPrepare: true, block: null }
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Result actions (S6: unknown is terminal).
423
+ // ---------------------------------------------------------------------------
424
+
425
+ export interface ResultActions {
426
+ /** True only for 'failed': a fresh prepare (fresh confirmation) is offered. */
427
+ canReprepare: boolean
428
+ /** True only for 'unknown': point the user at the session to verify. */
429
+ showCheckSessionHint: boolean
430
+ }
431
+
432
+ /** Which follow-up affordances a terminal outcome earns. */
433
+ export function resultActions(outcome: InjectOutcome): ResultActions {
434
+ return {
435
+ canReprepare: outcome === 'failed',
436
+ showCheckSessionHint: outcome === 'unknown',
437
+ }
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Copy mapping (keys live in the T2.3 locale table, inject.* domain).
442
+ // ---------------------------------------------------------------------------
443
+
444
+ /** A locale key plus its `{name}` template params, ready for `t()`. */
445
+ export interface CopyRef {
446
+ key: SidecarLocaleKey
447
+ params?: Record<string, unknown>
448
+ }
449
+
450
+ /** Mode radio copy (label + semantics hint) per injection mode. */
451
+ export const MODE_COPY = {
452
+ queue: { label: 'inject.modeQueue', hint: 'inject.modeQueueHint' },
453
+ steer: { label: 'inject.modeSteer', hint: 'inject.modeSteerHint' },
454
+ } as const satisfies Record<InjectMode, { label: SidecarLocaleKey; hint: SidecarLocaleKey }>
455
+
456
+ /** Local validation verdict → copy key. */
457
+ export const MESSAGE_INVALID_COPY = {
458
+ empty: 'inject.msgEmpty',
459
+ nul: 'inject.msgNul',
460
+ too_large: 'inject.msgTooLarge',
461
+ } as const satisfies Record<MessageInvalidCode, SidecarLocaleKey>
462
+
463
+ /** Validation verdict → renderable copy (too_large carries bytes/limit). */
464
+ export function messageInvalidCopy(
465
+ validation: Extract<MessageValidation, { ok: false }>,
466
+ ): CopyRef {
467
+ if (validation.code === 'too_large') {
468
+ return {
469
+ key: MESSAGE_INVALID_COPY.too_large,
470
+ params: { bytes: validation.bytes, limit: MAX_MESSAGE_BYTES },
471
+ }
472
+ }
473
+ return { key: MESSAGE_INVALID_COPY[validation.code] }
474
+ }
475
+
476
+ /** Terminal outcome → headline copy key. */
477
+ export const RESULT_COPY = {
478
+ delivered: 'inject.resultDelivered',
479
+ failed: 'inject.resultFailed',
480
+ unknown: 'inject.resultUnknown',
481
+ } as const satisfies Record<InjectOutcome, SidecarLocaleKey>
482
+
483
+ /**
484
+ * Error vocabulary → copy key: the gateway codes (inject-gateway.ts), plus
485
+ * the data layer's transport reasons (api.ts). Unlisted codes fall back to
486
+ * the generic template via {@link errorCopy}.
487
+ */
488
+ export const ERROR_COPY: Readonly<Record<string, SidecarLocaleKey>> = {
489
+ inject_disabled: 'inject.errInjectDisabled',
490
+ invalid_message: 'inject.errInvalidMessage',
491
+ target_not_found: 'inject.errTargetNotFound',
492
+ target_dead: 'inject.errTargetDead',
493
+ too_many_pending: 'inject.errTooManyPending',
494
+ token_missing: 'inject.errTokenMissing',
495
+ token_expired: 'inject.errTokenExpired',
496
+ token_reused: 'inject.errTokenReused',
497
+ token_mismatch: 'inject.errTokenMismatch',
498
+ unsupported_agent: 'inject.errUnsupportedAgent',
499
+ executor_error: 'inject.errExecutorError',
500
+ request_timeout: 'inject.errTimeout',
501
+ request_aborted: 'inject.errAborted',
502
+ network_error: 'inject.errNetwork',
503
+ invalid_json: 'inject.errParse',
504
+ }
505
+
506
+ /** Error code → renderable copy; unknown codes get the generic template. */
507
+ export function errorCopy(code: string): CopyRef {
508
+ const key = ERROR_COPY[code]
509
+ return key === undefined ? { key: 'inject.errGeneric', params: { code } } : { key }
510
+ }
511
+
512
+ /** Editor notice → renderable copy. */
513
+ export function noticeCopy(notice: PanelNotice): CopyRef {
514
+ if (notice.kind === 'token_expired') return { key: 'inject.tokenExpired' }
515
+ return errorCopy(notice.code)
516
+ }
@@ -0,0 +1,22 @@
1
+ /* Modal host for the inject panel (S5 wiring): a centered dialog over the
2
+ * board, opened by selecting a session card. Backdrop rides the dsh mask
3
+ * token --dsw-alias-bg-mask-1 (the ui-attachment / ui-settings-general
4
+ * modal precedent) — same no-literal-colors discipline as the panel. */
5
+
6
+ .backdrop {
7
+ position: fixed;
8
+ inset: 0;
9
+ z-index: 1000;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ padding: 24px;
14
+ background: var(--dsw-alias-bg-mask-1);
15
+ }
16
+
17
+ .dialog {
18
+ width: min(560px, 100%);
19
+ max-height: min(85vh, 720px);
20
+ overflow: auto;
21
+ border-radius: 12px;
22
+ }