dsh-pentester 2.2.0 → 2.3.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/lib/client.js CHANGED
@@ -16117,170 +16117,587 @@ var SEVERITY_COLORS = {
16117
16117
  low: "#3b82f6",
16118
16118
  info: "#6b7280"
16119
16119
  };
16120
- function ExpandableSection(props) {
16121
- const [open, setOpen] = (0, import_react10.useState)(props.defaultOpen);
16122
- return import_react10.default.createElement(
16123
- "div",
16124
- { style: { marginBottom: "4px" } },
16125
- import_react10.default.createElement(
16126
- "div",
16127
- {
16128
- onClick: () => setOpen(!open),
16129
- style: {
16130
- display: "flex",
16131
- alignItems: "center",
16132
- gap: "6px",
16133
- padding: "6px 8px",
16134
- cursor: "pointer",
16135
- borderRadius: "4px",
16136
- fontSize: "13px",
16137
- fontWeight: 600,
16138
- color: "var(--dsw-fg)",
16139
- userSelect: "none"
16140
- }
16141
- },
16142
- import_react10.default.createElement("span", {
16143
- style: { fontSize: "12px", transition: "transform 0.15s", transform: open ? "rotate(90deg)" : "rotate(0deg)" }
16144
- }, "\u25B6"),
16145
- import_react10.default.createElement("span", null, props.title)
16146
- ),
16147
- open && import_react10.default.createElement("div", { style: { paddingLeft: "20px" } }, props.children)
16148
- );
16149
- }
16120
+ var FILTER_TABS = [
16121
+ { id: "all", label: "All" },
16122
+ ...SEVERITY_ORDER.map((s) => ({ id: s, label: SEVERITY_LABELS[s] }))
16123
+ ];
16150
16124
  function FindingsView(props) {
16151
- const { snapshot, command, sessionId } = props;
16152
- const [selectedFinding, setSelectedFinding] = (0, import_react10.useState)(null);
16125
+ const { snapshot, command, sessionId, onNavigate } = props;
16126
+ const findings = snapshot?.findings ?? [];
16127
+ const [selectedId, setSelectedId] = (0, import_react10.useState)(null);
16153
16128
  const [detailContent, setDetailContent] = (0, import_react10.useState)(null);
16154
16129
  const [detailLoading, setDetailLoading] = (0, import_react10.useState)(false);
16155
- const findings = snapshot?.findings ?? [];
16156
- const grouped = (0, import_react10.useMemo)(() => {
16157
- const groups = /* @__PURE__ */ new Map();
16158
- for (const severity of SEVERITY_ORDER) {
16159
- groups.set(severity, []);
16160
- }
16161
- for (const f of findings) {
16162
- const list = groups.get(f.severity);
16163
- if (list !== void 0) list.push(f);
16164
- }
16165
- return groups;
16130
+ const [detailError, setDetailError] = (0, import_react10.useState)(false);
16131
+ const [filter, setFilter] = (0, import_react10.useState)("all");
16132
+ const [search, setSearch] = (0, import_react10.useState)("");
16133
+ const [focusDone, setFocusDone] = (0, import_react10.useState)(false);
16134
+ const loadSeqRef = (0, import_react10.useRef)(0);
16135
+ const severityCounts = (0, import_react10.useMemo)(() => {
16136
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
16137
+ for (const f of findings) counts[f.severity]++;
16138
+ return counts;
16166
16139
  }, [findings]);
16167
- const openDetail = (0, import_react10.useCallback)(async (finding) => {
16168
- setSelectedFinding(finding);
16140
+ const filtered = (0, import_react10.useMemo)(() => {
16141
+ let list = [...findings];
16142
+ if (filter !== "all") list = list.filter((f) => f.severity === filter);
16143
+ if (search.trim() !== "") {
16144
+ const q = search.toLowerCase();
16145
+ list = list.filter(
16146
+ (f) => f.title.toLowerCase().includes(q) || (f.affectedAsset?.toLowerCase().includes(q) ?? false) || f.sourceStage.toLowerCase().includes(q) || f.sourceDelegationId.toLowerCase().includes(q)
16147
+ );
16148
+ }
16149
+ const orderMap = new Map(SEVERITY_ORDER.map((s, i) => [s, i]));
16150
+ list.sort((a, b) => {
16151
+ const oa = orderMap.get(a.severity) ?? 99;
16152
+ const ob = orderMap.get(b.severity) ?? 99;
16153
+ if (oa !== ob) return oa - ob;
16154
+ return 0;
16155
+ });
16156
+ return list;
16157
+ }, [findings, filter, search]);
16158
+ const selectedFinding = (0, import_react10.useMemo)(
16159
+ () => selectedId !== null ? findings.find((f) => f.id === selectedId) ?? null : null,
16160
+ [findings, selectedId]
16161
+ );
16162
+ (0, import_react10.useEffect)(() => {
16163
+ if (props.focusFinding !== void 0 && !focusDone) {
16164
+ const match = findings.find((f) => f.id === props.focusFinding);
16165
+ if (match !== void 0) {
16166
+ setSelectedId(match.id);
16167
+ setFilter("all");
16168
+ setSearch("");
16169
+ setFocusDone(true);
16170
+ }
16171
+ }
16172
+ }, [props.focusFinding, focusDone, findings]);
16173
+ (0, import_react10.useEffect)(() => {
16174
+ setFocusDone(false);
16175
+ }, [props.focusFinding]);
16176
+ const loadDetail = (0, import_react10.useCallback)(async (finding) => {
16177
+ setSelectedId(finding.id);
16169
16178
  setDetailContent(null);
16170
16179
  setDetailLoading(true);
16180
+ setDetailError(false);
16181
+ const seq = ++loadSeqRef.current;
16171
16182
  try {
16172
16183
  const result = await command({
16173
16184
  kind: "readFinding",
16174
16185
  detailFile: finding.detailFile,
16175
16186
  sessionId
16176
16187
  });
16188
+ if (seq !== loadSeqRef.current) return;
16177
16189
  if (result.ok) {
16178
16190
  const fileResult = result.value;
16179
16191
  setDetailContent(fileResult.content ?? "");
16192
+ setDetailError(false);
16180
16193
  } else {
16181
- setDetailContent("*Unable to load finding detail.*");
16194
+ setDetailContent(null);
16195
+ setDetailError(true);
16182
16196
  }
16183
16197
  } catch {
16184
- setDetailContent("*Error loading finding detail.*");
16198
+ if (seq !== loadSeqRef.current) return;
16199
+ setDetailContent(null);
16200
+ setDetailError(true);
16185
16201
  } finally {
16186
- setDetailLoading(false);
16202
+ if (seq === loadSeqRef.current) setDetailLoading(false);
16187
16203
  }
16188
16204
  }, [command, sessionId]);
16189
- const closeDetail = (0, import_react10.useCallback)(() => {
16190
- setSelectedFinding(null);
16191
- setDetailContent(null);
16192
- }, []);
16193
- if (findings.length === 0) {
16194
- return import_react10.default.createElement("div", {
16195
- style: { padding: "24px", color: "var(--dsw-fg-muted)", fontSize: "14px" }
16196
- }, "No findings yet");
16197
- }
16198
- const severitySections = SEVERITY_ORDER.map((severity) => {
16199
- const items = grouped.get(severity) ?? [];
16200
- if (items.length === 0) return null;
16201
- const findingRows = items.map(
16202
- (finding) => import_react10.default.createElement(
16205
+ return import_react10.default.createElement(
16206
+ "div",
16207
+ {
16208
+ style: {
16209
+ display: "flex",
16210
+ flexDirection: "column",
16211
+ height: "100%",
16212
+ minHeight: 0,
16213
+ overflow: "hidden"
16214
+ }
16215
+ },
16216
+ // ── Top: severity summary strip ──────────────────────────────────────
16217
+ import_react10.default.createElement(SeveritySummary, {
16218
+ total: findings.length,
16219
+ counts: severityCounts,
16220
+ activeFilter: filter,
16221
+ onFilter: setFilter
16222
+ }),
16223
+ // ── Bottom: split view ───────────────────────────────────────────────
16224
+ import_react10.default.createElement(
16225
+ "div",
16226
+ {
16227
+ style: {
16228
+ flex: 1,
16229
+ minHeight: 0,
16230
+ display: "flex",
16231
+ overflow: "hidden"
16232
+ }
16233
+ },
16234
+ // ── Left: Navigator ───────────────────────────────────────────────
16235
+ import_react10.default.createElement(
16236
+ "div",
16237
+ {
16238
+ style: {
16239
+ flex: "0 0 340px",
16240
+ minWidth: "280px",
16241
+ maxWidth: "380px",
16242
+ minHeight: 0,
16243
+ display: "flex",
16244
+ flexDirection: "column",
16245
+ overflow: "hidden",
16246
+ borderRight: "1px solid var(--dsw-alias-border-l2)"
16247
+ }
16248
+ },
16249
+ // Search + filter (fixed header)
16250
+ import_react10.default.createElement(
16251
+ "div",
16252
+ {
16253
+ style: {
16254
+ flex: "none",
16255
+ padding: "8px 12px 4px"
16256
+ }
16257
+ },
16258
+ import_react10.default.createElement("input", {
16259
+ type: "text",
16260
+ placeholder: "Search findings...",
16261
+ value: search,
16262
+ onChange: (e) => setSearch(e.target.value),
16263
+ style: {
16264
+ width: "100%",
16265
+ boxSizing: "border-box",
16266
+ padding: "6px 10px",
16267
+ border: "1px solid var(--dsw-alias-border-l2)",
16268
+ borderRadius: "6px",
16269
+ background: "var(--dsw-alias-bg-base)",
16270
+ color: "var(--dsw-alias-label-primary)",
16271
+ fontSize: "13px",
16272
+ outline: "none",
16273
+ marginBottom: "4px"
16274
+ }
16275
+ }),
16276
+ import_react10.default.createElement(PentesterTabBar, {
16277
+ items: FILTER_TABS,
16278
+ active: filter,
16279
+ onSelect: (id) => setFilter(id),
16280
+ variant: "compact"
16281
+ })
16282
+ ),
16283
+ // FindingList (scrollable)
16284
+ import_react10.default.createElement(
16285
+ "div",
16286
+ {
16287
+ style: {
16288
+ flex: 1,
16289
+ minHeight: 0,
16290
+ overflowY: "auto",
16291
+ overflowX: "hidden",
16292
+ overscrollBehaviorY: "contain",
16293
+ scrollbarGutter: "stable"
16294
+ }
16295
+ },
16296
+ filtered.length === 0 ? import_react10.default.createElement("div", {
16297
+ style: {
16298
+ padding: "24px 12px",
16299
+ color: "var(--dsw-alias-label-tertiary)",
16300
+ fontSize: "13px",
16301
+ textAlign: "center"
16302
+ }
16303
+ }, findings.length === 0 ? "No findings yet" : "No matching findings") : filtered.map(
16304
+ (finding) => import_react10.default.createElement(FindingRow, {
16305
+ key: finding.id,
16306
+ finding,
16307
+ isSelected: finding.id === selectedId,
16308
+ onClick: () => loadDetail(finding)
16309
+ })
16310
+ )
16311
+ )
16312
+ ),
16313
+ // ── Right: Detail ──────────────────────────────────────────────────
16314
+ import_react10.default.createElement(
16203
16315
  "div",
16204
16316
  {
16205
- key: finding.id,
16206
16317
  style: {
16318
+ flex: 1,
16319
+ minWidth: 0,
16320
+ minHeight: 0,
16207
16321
  display: "flex",
16322
+ flexDirection: "column",
16323
+ overflow: "hidden"
16324
+ }
16325
+ },
16326
+ selectedFinding === null ? import_react10.default.createElement(
16327
+ "div",
16328
+ {
16329
+ style: {
16330
+ flex: 1,
16331
+ display: "flex",
16332
+ alignItems: "center",
16333
+ justifyContent: "center",
16334
+ flexDirection: "column",
16335
+ gap: "8px",
16336
+ color: "var(--dsw-alias-label-tertiary)",
16337
+ fontSize: "13px"
16338
+ }
16339
+ },
16340
+ findings.length === 0 ? import_react10.default.createElement(
16341
+ import_react10.default.Fragment,
16342
+ null,
16343
+ import_react10.default.createElement("div", { style: { fontWeight: 500, color: "var(--dsw-alias-label-primary)" } }, "No findings yet"),
16344
+ import_react10.default.createElement("div", null, "Validated findings will appear here as the assessment progresses.")
16345
+ ) : "Select a finding to inspect"
16346
+ ) : import_react10.default.createElement(FindingDetail, {
16347
+ finding: selectedFinding,
16348
+ content: detailContent,
16349
+ loading: detailLoading,
16350
+ error: detailError,
16351
+ onNavigate
16352
+ })
16353
+ )
16354
+ )
16355
+ );
16356
+ }
16357
+ function SeveritySummary(props) {
16358
+ const { total, counts, activeFilter, onFilter } = props;
16359
+ const items = [
16360
+ { key: "all", label: "Total", count: total },
16361
+ ...SEVERITY_ORDER.map((s) => ({
16362
+ key: s,
16363
+ label: SEVERITY_LABELS[s],
16364
+ count: counts[s],
16365
+ color: SEVERITY_COLORS[s]
16366
+ }))
16367
+ ];
16368
+ return import_react10.default.createElement(
16369
+ "div",
16370
+ {
16371
+ style: {
16372
+ flex: "none",
16373
+ display: "flex",
16374
+ alignItems: "center",
16375
+ gap: "4px",
16376
+ padding: "8px 12px",
16377
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
16378
+ flexWrap: "wrap",
16379
+ fontSize: "13px",
16380
+ color: "var(--dsw-alias-label-secondary)"
16381
+ }
16382
+ },
16383
+ ...items.map((item) => {
16384
+ const isActive = activeFilter === item.key;
16385
+ return import_react10.default.createElement(
16386
+ "button",
16387
+ {
16388
+ key: item.key,
16389
+ type: "button",
16390
+ onClick: () => onFilter(item.key),
16391
+ style: {
16392
+ display: "inline-flex",
16208
16393
  alignItems: "center",
16209
- gap: "8px",
16210
- padding: "6px 12px",
16394
+ gap: "5px",
16395
+ padding: "3px 10px",
16396
+ borderRadius: "12px",
16397
+ border: "none",
16398
+ background: isActive ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
16399
+ color: isActive ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary)",
16211
16400
  cursor: "pointer",
16212
- borderRadius: "4px",
16213
16401
  fontSize: "13px",
16214
- color: "var(--dsw-fg)",
16215
- borderLeft: `3px solid ${SEVERITY_COLORS[finding.severity]}`,
16216
- marginBottom: "2px"
16217
- },
16218
- onClick: () => openDetail(finding)
16402
+ fontWeight: isActive ? 500 : 400,
16403
+ whiteSpace: "nowrap"
16404
+ }
16219
16405
  },
16220
- import_react10.default.createElement("span", {
16406
+ item.color !== void 0 && import_react10.default.createElement("span", {
16221
16407
  style: {
16222
16408
  width: "8px",
16223
16409
  height: "8px",
16224
16410
  borderRadius: "50%",
16225
- background: SEVERITY_COLORS[finding.severity],
16411
+ background: item.color,
16226
16412
  flexShrink: 0
16227
16413
  }
16228
16414
  }),
16229
- import_react10.default.createElement("span", { style: { fontWeight: 500 } }, finding.title),
16230
- finding.affectedAsset !== void 0 && import_react10.default.createElement("span", {
16231
- style: { fontSize: "11px", color: "var(--dsw-fg-muted)", marginLeft: "auto" }
16232
- }, finding.affectedAsset)
16233
- )
16234
- );
16235
- return import_react10.default.createElement(ExpandableSection, {
16236
- key: severity,
16237
- title: `${SEVERITY_LABELS[severity]} \xB7 ${items.length}`,
16238
- defaultOpen: severity === "critical" || severity === "high",
16239
- children: findingRows
16240
- });
16241
- });
16242
- const modalContent = selectedFinding !== null ? import_react10.default.createElement(
16243
- import_dsh_client_ui_primitives3.Modal,
16415
+ import_react10.default.createElement("span", null, item.label),
16416
+ import_react10.default.createElement("span", {
16417
+ style: { fontWeight: 600, color: isActive ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)" }
16418
+ }, String(item.count))
16419
+ );
16420
+ })
16421
+ );
16422
+ }
16423
+ function FindingRow(props) {
16424
+ const { finding, isSelected, onClick } = props;
16425
+ const color = SEVERITY_COLORS[finding.severity];
16426
+ return import_react10.default.createElement(
16427
+ "div",
16244
16428
  {
16245
- open: true,
16246
- onClose: closeDetail,
16247
- title: selectedFinding.title,
16248
- contentClassName: "pentester-finding-detail"
16429
+ role: "button",
16430
+ tabIndex: 0,
16431
+ onClick,
16432
+ onKeyDown: (e) => {
16433
+ if (e.key === "Enter" || e.key === " ") {
16434
+ e.preventDefault();
16435
+ onClick();
16436
+ }
16437
+ },
16438
+ style: {
16439
+ display: "flex",
16440
+ flexDirection: "column",
16441
+ gap: "2px",
16442
+ padding: "8px 12px",
16443
+ cursor: "pointer",
16444
+ borderLeft: `3px solid ${isSelected ? color : "transparent"}`,
16445
+ background: isSelected ? "var(--dsw-alias-state-business-primary)" : "transparent",
16446
+ color: isSelected ? "var(--dsw-alias-state-business-primary-fg, #fff)" : "var(--dsw-alias-label-primary)",
16447
+ fontSize: "13px",
16448
+ lineHeight: "18px",
16449
+ borderBottom: "1px solid var(--dsw-alias-border-l2)",
16450
+ transition: "background 0.1s"
16451
+ },
16452
+ onMouseEnter: (e) => {
16453
+ if (!isSelected) e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)";
16454
+ },
16455
+ onMouseLeave: (e) => {
16456
+ if (!isSelected) e.currentTarget.style.background = "transparent";
16457
+ }
16249
16458
  },
16459
+ // Row 1: title + severity
16250
16460
  import_react10.default.createElement(
16251
16461
  "div",
16252
- { style: { fontSize: "13px", lineHeight: "1.6" } },
16253
- import_react10.default.createElement("div", {
16462
+ {
16254
16463
  style: {
16255
- display: "inline-block",
16256
- padding: "2px 10px",
16257
- borderRadius: "10px",
16258
- background: SEVERITY_COLORS[selectedFinding.severity],
16259
- color: "#fff",
16260
- fontSize: "12px",
16464
+ display: "flex",
16465
+ alignItems: "center",
16466
+ gap: "8px",
16467
+ minWidth: 0
16468
+ }
16469
+ },
16470
+ import_react10.default.createElement("span", {
16471
+ style: {
16472
+ width: "7px",
16473
+ height: "7px",
16474
+ borderRadius: "50%",
16475
+ background: color,
16476
+ flexShrink: 0
16477
+ }
16478
+ }),
16479
+ import_react10.default.createElement("span", {
16480
+ style: {
16481
+ overflow: "hidden",
16482
+ textOverflow: "ellipsis",
16483
+ whiteSpace: "nowrap",
16484
+ fontWeight: 500,
16485
+ flex: 1,
16486
+ minWidth: 0
16487
+ }
16488
+ }, finding.title),
16489
+ import_react10.default.createElement("span", {
16490
+ style: {
16491
+ fontSize: "11px",
16261
16492
  fontWeight: 600,
16262
- marginBottom: "12px"
16493
+ color: isSelected ? "rgba(255,255,255,0.75)" : color,
16494
+ flexShrink: 0
16263
16495
  }
16264
- }, SEVERITY_LABELS[selectedFinding.severity]),
16265
- selectedFinding.affectedAsset !== void 0 && import_react10.default.createElement("div", {
16266
- style: { color: "var(--dsw-fg-muted)", marginBottom: "4px" }
16267
- }, "Affected: ", import_react10.default.createElement("code", null, selectedFinding.affectedAsset)),
16268
- import_react10.default.createElement("div", {
16269
- style: { color: "var(--dsw-fg-muted)", marginBottom: "12px", fontSize: "12px" }
16270
- }, `Source: ${selectedFinding.sourceStage} \xB7 ${selectedFinding.sourceDelegationId}`),
16271
- detailLoading && import_react10.default.createElement("div", {
16272
- style: { color: "var(--dsw-fg-muted)", padding: "16px 0" }
16273
- }, "Loading..."),
16274
- detailContent !== null && import_react10.default.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text: detailContent })
16275
- )
16276
- ) : null;
16496
+ }, SEVERITY_LABELS[finding.severity].toUpperCase())
16497
+ ),
16498
+ // Row 2: affected asset (if exists)
16499
+ finding.affectedAsset !== void 0 && import_react10.default.createElement("div", {
16500
+ style: {
16501
+ fontSize: "12px",
16502
+ color: isSelected ? "rgba(255,255,255,0.6)" : "var(--dsw-alias-label-tertiary)",
16503
+ overflow: "hidden",
16504
+ textOverflow: "ellipsis",
16505
+ whiteSpace: "nowrap",
16506
+ marginLeft: "15px"
16507
+ }
16508
+ }, finding.affectedAsset),
16509
+ // Row 3: source stage + delegation
16510
+ import_react10.default.createElement("div", {
16511
+ style: {
16512
+ fontSize: "11px",
16513
+ color: isSelected ? "rgba(255,255,255,0.5)" : "var(--dsw-alias-label-tertiary)",
16514
+ marginLeft: "15px",
16515
+ overflow: "hidden",
16516
+ textOverflow: "ellipsis",
16517
+ whiteSpace: "nowrap"
16518
+ }
16519
+ }, `${finding.sourceStage} \xB7 ${finding.sourceDelegationId}`)
16520
+ );
16521
+ }
16522
+ function FindingDetail(props) {
16523
+ const { finding, content, loading, error: error51, onNavigate } = props;
16524
+ const color = SEVERITY_COLORS[finding.severity];
16277
16525
  return import_react10.default.createElement(
16278
16526
  "div",
16279
- { style: { maxWidth: "640px" } },
16280
- ...severitySections,
16281
- modalContent
16527
+ {
16528
+ style: {
16529
+ flex: 1,
16530
+ minHeight: 0,
16531
+ display: "flex",
16532
+ flexDirection: "column",
16533
+ overflow: "hidden"
16534
+ }
16535
+ },
16536
+ // ── Metadata header (fixed) ─────────────────────────────────────────
16537
+ import_react10.default.createElement(
16538
+ "div",
16539
+ {
16540
+ style: {
16541
+ flex: "none",
16542
+ padding: "12px 16px",
16543
+ borderBottom: "1px solid var(--dsw-alias-border-l2)"
16544
+ }
16545
+ },
16546
+ // Title + severity
16547
+ import_react10.default.createElement(
16548
+ "div",
16549
+ {
16550
+ style: {
16551
+ display: "flex",
16552
+ alignItems: "center",
16553
+ gap: "10px",
16554
+ marginBottom: "8px"
16555
+ }
16556
+ },
16557
+ import_react10.default.createElement("span", {
16558
+ style: {
16559
+ fontSize: "15px",
16560
+ fontWeight: 600,
16561
+ color: "var(--dsw-alias-label-primary)",
16562
+ overflow: "hidden",
16563
+ textOverflow: "ellipsis",
16564
+ whiteSpace: "nowrap",
16565
+ flex: 1,
16566
+ minWidth: 0
16567
+ }
16568
+ }, finding.title),
16569
+ import_react10.default.createElement("span", {
16570
+ style: {
16571
+ display: "inline-block",
16572
+ padding: "2px 8px",
16573
+ borderRadius: "10px",
16574
+ background: color,
16575
+ color: "#fff",
16576
+ fontSize: "11px",
16577
+ fontWeight: 600,
16578
+ flexShrink: 0
16579
+ }
16580
+ }, SEVERITY_LABELS[finding.severity].toUpperCase())
16581
+ ),
16582
+ // Affected asset
16583
+ finding.affectedAsset !== void 0 && import_react10.default.createElement(
16584
+ "div",
16585
+ {
16586
+ style: {
16587
+ fontSize: "13px",
16588
+ color: "var(--dsw-alias-label-secondary)",
16589
+ marginBottom: "6px",
16590
+ display: "flex",
16591
+ gap: "6px"
16592
+ }
16593
+ },
16594
+ import_react10.default.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", flexShrink: 0 } }, "Affected asset"),
16595
+ import_react10.default.createElement("code", {
16596
+ style: {
16597
+ fontSize: "12px",
16598
+ background: "var(--dsw-alias-bg-subtle)",
16599
+ padding: "1px 6px",
16600
+ borderRadius: "3px"
16601
+ }
16602
+ }, finding.affectedAsset)
16603
+ ),
16604
+ // Stage + Delegation metadata
16605
+ import_react10.default.createElement(
16606
+ "div",
16607
+ {
16608
+ style: {
16609
+ display: "flex",
16610
+ gap: "20px",
16611
+ fontSize: "12px",
16612
+ color: "var(--dsw-alias-label-tertiary)"
16613
+ }
16614
+ },
16615
+ import_react10.default.createElement(
16616
+ "div",
16617
+ null,
16618
+ import_react10.default.createElement("span", { style: { fontWeight: 500, color: "var(--dsw-alias-label-secondary)" } }, "Stage"),
16619
+ " ",
16620
+ import_react10.default.createElement("span", null, finding.sourceStage)
16621
+ ),
16622
+ import_react10.default.createElement(
16623
+ "div",
16624
+ null,
16625
+ import_react10.default.createElement("span", { style: { fontWeight: 500, color: "var(--dsw-alias-label-secondary)" } }, "Delegation"),
16626
+ " ",
16627
+ import_react10.default.createElement("span", null, finding.sourceDelegationId)
16628
+ )
16629
+ )
16630
+ ),
16631
+ // ── DetailBody (scrollable) ────────────────────────────────────────
16632
+ import_react10.default.createElement(
16633
+ "div",
16634
+ {
16635
+ style: {
16636
+ flex: 1,
16637
+ minHeight: 0,
16638
+ overflowY: "auto",
16639
+ overflowX: "auto",
16640
+ overscrollBehaviorY: "contain",
16641
+ scrollbarGutter: "stable",
16642
+ padding: "16px",
16643
+ fontSize: "13px",
16644
+ lineHeight: "1.6",
16645
+ color: "var(--dsw-alias-label-primary)"
16646
+ }
16647
+ },
16648
+ loading && import_react10.default.createElement("div", {
16649
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "16px 0" }
16650
+ }, "Loading finding..."),
16651
+ error51 && import_react10.default.createElement("div", {
16652
+ style: { color: "var(--dsw-status-error)", padding: "16px 0" }
16653
+ }, "Unable to load finding detail"),
16654
+ content !== null && import_react10.default.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text: content })
16655
+ ),
16656
+ // ── Footer actions (fixed) ──────────────────────────────────────────
16657
+ import_react10.default.createElement(
16658
+ "div",
16659
+ {
16660
+ style: {
16661
+ flex: "none",
16662
+ padding: "8px 16px",
16663
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
16664
+ display: "flex",
16665
+ gap: "8px"
16666
+ }
16667
+ },
16668
+ import_react10.default.createElement(ActionButton, {
16669
+ label: "View Stage",
16670
+ onClick: () => onNavigate("stages", { stage: finding.sourceStage })
16671
+ }),
16672
+ import_react10.default.createElement(ActionButton, {
16673
+ label: "View in Output",
16674
+ onClick: () => onNavigate("output", { output: `findings/${finding.detailFile}` })
16675
+ })
16676
+ )
16282
16677
  );
16283
16678
  }
16679
+ function ActionButton(props) {
16680
+ return import_react10.default.createElement("button", {
16681
+ type: "button",
16682
+ onClick: props.onClick,
16683
+ style: {
16684
+ padding: "4px 12px",
16685
+ border: "1px solid var(--dsw-alias-border-l2)",
16686
+ borderRadius: "4px",
16687
+ background: "transparent",
16688
+ color: "var(--dsw-alias-label-secondary)",
16689
+ cursor: "pointer",
16690
+ fontSize: "12px",
16691
+ lineHeight: "20px"
16692
+ },
16693
+ onMouseEnter: (e) => {
16694
+ e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)";
16695
+ },
16696
+ onMouseLeave: (e) => {
16697
+ e.currentTarget.style.background = "transparent";
16698
+ }
16699
+ }, props.label);
16700
+ }
16284
16701
 
16285
16702
  // src/ui/client/pentester/TraceView.tsx
16286
16703
  var import_react15 = __toESM(require("react"), 1);
@@ -17767,7 +18184,7 @@ function TargetRow(props) {
17767
18184
  );
17768
18185
  }
17769
18186
  function OutputView(props) {
17770
- const { snapshot, command, sessionId } = props;
18187
+ const { snapshot, command, sessionId, focusOutput } = props;
17771
18188
  const [filter, setFilter] = (0, import_react17.useState)("all");
17772
18189
  const [preview, setPreview] = (0, import_react17.useState)(null);
17773
18190
  const [previewContent, setPreviewContent] = (0, import_react17.useState)(null);
@@ -17849,6 +18266,16 @@ function OutputView(props) {
17849
18266
  setPreview(null);
17850
18267
  setPreviewContent(null);
17851
18268
  }, []);
18269
+ const focusAppliedRef = (0, import_react17.useRef)(void 0);
18270
+ (0, import_react17.useEffect)(() => {
18271
+ if (focusOutput === void 0 || focusOutput === focusAppliedRef.current) return;
18272
+ if (outputTree === null) return;
18273
+ focusAppliedRef.current = focusOutput;
18274
+ const entry = findEntryByPath(outputTree.children, focusOutput);
18275
+ if (entry !== void 0) {
18276
+ openPreview(entry, outputTree.targetId);
18277
+ }
18278
+ }, [focusOutput, outputTree, openPreview]);
17852
18279
  const initialLoading = outputTree === null && outputLoading;
17853
18280
  if (initialLoading) {
17854
18281
  return import_react17.default.createElement("div", {
@@ -17907,7 +18334,7 @@ function OutputView(props) {
17907
18334
  return import_react17.default.createElement(
17908
18335
  "div",
17909
18336
  {
17910
- style: { display: "flex", gap: "16px", minHeight: "400px", overflow: "visible" }
18337
+ style: { display: "flex", gap: "16px", flex: 1, height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden" }
17911
18338
  },
17912
18339
  // Left: file tree
17913
18340
  import_react17.default.createElement(
@@ -17915,16 +18342,24 @@ function OutputView(props) {
17915
18342
  {
17916
18343
  style: {
17917
18344
  flex: "0 0 320px",
17918
- overflow: "auto",
17919
- borderRight: "1px solid var(--dsw-alias-border-l2)",
17920
- paddingRight: "12px",
18345
+ minHeight: 0,
18346
+ overflow: "hidden",
17921
18347
  display: "flex",
17922
- flexDirection: "column"
18348
+ flexDirection: "column",
18349
+ borderRight: "1px solid var(--dsw-alias-border-l2)",
18350
+ paddingRight: "12px"
17923
18351
  }
17924
18352
  },
18353
+ // Filter tabs (fixed)
17925
18354
  import_react17.default.createElement(
17926
18355
  "div",
17927
- { style: { marginBottom: "12px" } },
18356
+ {
18357
+ style: {
18358
+ flex: "none",
18359
+ marginBottom: "12px",
18360
+ paddingTop: "2px"
18361
+ }
18362
+ },
17928
18363
  import_react17.default.createElement(PentesterTabBar, {
17929
18364
  items: OUTPUT_FILTER_TABS,
17930
18365
  active: filter,
@@ -17932,10 +18367,20 @@ function OutputView(props) {
17932
18367
  variant: "compact"
17933
18368
  })
17934
18369
  ),
17935
- // ── Tree: "Targets" section label + target rows at root depth ─────
18370
+ // ── TreeViewport (scrollable) ──────────────────────────────────
17936
18371
  import_react17.default.createElement(
17937
18372
  "div",
17938
- { style: { flex: 1 } },
18373
+ {
18374
+ style: {
18375
+ flex: 1,
18376
+ minHeight: 0,
18377
+ minWidth: 0,
18378
+ overflowY: "auto",
18379
+ overflowX: "hidden",
18380
+ overscrollBehaviorY: "contain",
18381
+ scrollbarGutter: "stable"
18382
+ }
18383
+ },
17939
18384
  import_react17.default.createElement("div", {
17940
18385
  style: {
17941
18386
  fontSize: "11px",
@@ -17969,14 +18414,13 @@ function OutputView(props) {
17969
18414
  style: {
17970
18415
  flex: 1,
17971
18416
  minWidth: 0,
18417
+ minHeight: 0,
17972
18418
  display: "flex",
17973
18419
  flexDirection: "column",
17974
18420
  overflow: "hidden",
17975
18421
  border: "1px solid var(--dsw-alias-border-l2)",
17976
18422
  borderRadius: "8px",
17977
- background: "var(--dsw-alias-bg-base)",
17978
- minHeight: "320px",
17979
- maxHeight: "clamp(320px, 65vh, 680px)"
18423
+ background: "var(--dsw-alias-bg-base)"
17980
18424
  }
17981
18425
  },
17982
18426
  preview === null ? import_react17.default.createElement("div", {
@@ -17991,6 +18435,7 @@ function OutputView(props) {
17991
18435
  }, "Select a file to preview") : import_react17.default.createElement(
17992
18436
  import_react17.default.Fragment,
17993
18437
  null,
18438
+ // PreviewHeader (fixed)
17994
18439
  import_react17.default.createElement(
17995
18440
  "div",
17996
18441
  {
@@ -18019,10 +18464,20 @@ function OutputView(props) {
18019
18464
  "aria-label": "Close preview"
18020
18465
  }, "\xD7")
18021
18466
  ),
18467
+ // PreviewBody (scrollable)
18022
18468
  import_react17.default.createElement(
18023
18469
  "div",
18024
18470
  {
18025
- style: { flex: 1, minHeight: 0, overflow: "auto", padding: "12px" }
18471
+ style: {
18472
+ flex: 1,
18473
+ minHeight: 0,
18474
+ minWidth: 0,
18475
+ overflowY: "auto",
18476
+ overflowX: "auto",
18477
+ overscrollBehaviorY: "contain",
18478
+ scrollbarGutter: "stable",
18479
+ padding: "12px"
18480
+ }
18026
18481
  },
18027
18482
  previewLoading && import_react17.default.createElement("div", {
18028
18483
  style: { color: "var(--dsw-alias-label-tertiary)", padding: "16px 0" }
@@ -18109,6 +18564,16 @@ function formatSize(bytes) {
18109
18564
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
18110
18565
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
18111
18566
  }
18567
+ function findEntryByPath(entries, targetPath) {
18568
+ for (const entry of entries) {
18569
+ if (entry.path === targetPath) return entry;
18570
+ if (entry.kind === "directory" && entry.children !== void 0) {
18571
+ const found = findEntryByPath(entry.children, targetPath);
18572
+ if (found !== void 0) return found;
18573
+ }
18574
+ }
18575
+ return void 0;
18576
+ }
18112
18577
 
18113
18578
  // src/ui/client/pentester-view.tsx
18114
18579
  var CORE_REFRESH_MS = 1e4;
@@ -18411,12 +18876,14 @@ Client: ${mismatch.client}`,
18411
18876
  {
18412
18877
  "data-testid": "pentester-view",
18413
18878
  "data-pentester-view": "",
18879
+ "data-conversation-composer-overlay": "",
18414
18880
  style: {
18415
18881
  display: "flex",
18416
18882
  flexDirection: "column",
18417
18883
  width: "100%",
18418
- minHeight: "100%",
18419
- overflow: "visible"
18884
+ height: "100%",
18885
+ minHeight: 0,
18886
+ overflow: "hidden"
18420
18887
  }
18421
18888
  },
18422
18889
  // ── Layer 1: TabsBar (sticky) ────────────────────────────────────────
@@ -18426,8 +18893,6 @@ Client: ${mismatch.client}`,
18426
18893
  "data-testid": "pentester-tabs-bar",
18427
18894
  style: {
18428
18895
  flex: "none",
18429
- position: "sticky",
18430
- top: 0,
18431
18896
  zIndex: 29,
18432
18897
  background: "var(--dsw-alias-bg-base)",
18433
18898
  borderBottom: "1px solid var(--dsw-alias-border-l2)",
@@ -18482,15 +18947,18 @@ Client: ${mismatch.client}`,
18482
18947
  }
18483
18948
  }, isRetrying ? "Retrying\u2026" : "Retry")
18484
18949
  ),
18485
- // ── Body: flex 1, no scroll ──────────────────────────────────────────
18950
+ // ── Body: bounded flex viewport ────────────────────────────────────
18486
18951
  import_react18.default.createElement(
18487
18952
  "div",
18488
18953
  {
18489
18954
  style: {
18490
18955
  flex: 1,
18491
18956
  minHeight: 0,
18492
- overflow: "visible",
18493
- padding: "16px"
18957
+ overflow: "hidden",
18958
+ display: "flex",
18959
+ flexDirection: "column",
18960
+ padding: "16px",
18961
+ boxSizing: "border-box"
18494
18962
  }
18495
18963
  },
18496
18964
  // Initial loading: shell is already visible, show skeleton
@@ -18597,23 +19065,40 @@ Client: ${mismatch.client}`,
18597
19065
  }
18598
19066
  }, "Refresh")
18599
19067
  ),
18600
- // Bound content
19068
+ // Bound content — wrapped in a bounded viewport
18601
19069
  dataState.kind === "bound" && import_react18.default.createElement(
18602
- import_react18.default.Fragment,
18603
- null,
18604
- tab === "overview" && import_react18.default.createElement(OverviewView, {
18605
- snapshot,
18606
- liveActivity,
18607
- onNavigate: navigate,
18608
- onOpenSession: openSession
18609
- }),
18610
- tab === "stages" && import_react18.default.createElement(StagesView, {
18611
- snapshot,
18612
- liveActivity,
18613
- onNavigate: navigate,
18614
- onOpenSession: openSession,
18615
- focusStage: navFocus.stage
18616
- }),
19070
+ "div",
19071
+ {
19072
+ style: {
19073
+ flex: 1,
19074
+ minHeight: 0,
19075
+ minWidth: 0,
19076
+ overflow: "hidden",
19077
+ display: "flex",
19078
+ flexDirection: "column"
19079
+ }
19080
+ },
19081
+ tab === "overview" && import_react18.default.createElement(
19082
+ ScrollablePage,
19083
+ null,
19084
+ import_react18.default.createElement(OverviewView, {
19085
+ snapshot,
19086
+ liveActivity,
19087
+ onNavigate: navigate,
19088
+ onOpenSession: openSession
19089
+ })
19090
+ ),
19091
+ tab === "stages" && import_react18.default.createElement(
19092
+ ScrollablePage,
19093
+ null,
19094
+ import_react18.default.createElement(StagesView, {
19095
+ snapshot,
19096
+ liveActivity,
19097
+ onNavigate: navigate,
19098
+ onOpenSession: openSession,
19099
+ focusStage: navFocus.stage
19100
+ })
19101
+ ),
18617
19102
  tab === "findings" && import_react18.default.createElement(FindingsView, {
18618
19103
  snapshot,
18619
19104
  command: pentesterCommand,
@@ -18621,13 +19106,17 @@ Client: ${mismatch.client}`,
18621
19106
  onNavigate: navigate,
18622
19107
  focusFinding: navFocus.finding
18623
19108
  }),
18624
- tab === "trace" && import_react18.default.createElement(TraceView, {
18625
- snapshot,
18626
- liveActivity,
18627
- onNavigate: navigate,
18628
- onOpenSession: openSession,
18629
- activeTargetId
18630
- }),
19109
+ tab === "trace" && import_react18.default.createElement(
19110
+ ScrollablePage,
19111
+ null,
19112
+ import_react18.default.createElement(TraceView, {
19113
+ snapshot,
19114
+ liveActivity,
19115
+ onNavigate: navigate,
19116
+ onOpenSession: openSession,
19117
+ activeTargetId
19118
+ })
19119
+ ),
18631
19120
  tab === "output" && import_react18.default.createElement(OutputView, {
18632
19121
  snapshot,
18633
19122
  command: pentesterCommand,
@@ -18638,6 +19127,17 @@ Client: ${mismatch.client}`,
18638
19127
  )
18639
19128
  );
18640
19129
  }
19130
+ function ScrollablePage(props) {
19131
+ return import_react18.default.createElement("div", {
19132
+ style: {
19133
+ flex: 1,
19134
+ minHeight: 0,
19135
+ overflowY: "auto",
19136
+ overflowX: "hidden",
19137
+ overscrollBehavior: "contain"
19138
+ }
19139
+ }, props.children);
19140
+ }
18641
19141
 
18642
19142
  // src/ui/client/pentester/locales.ts
18643
19143
  var NS = "dsh-pentester-view";
@@ -18719,7 +19219,7 @@ var en = {
18719
19219
  };
18720
19220
 
18721
19221
  // src/build-id.ts
18722
- var BUILD_ID = "2.2.0+478786d";
19222
+ var BUILD_ID = "2.3.0+2a31301";
18723
19223
 
18724
19224
  // src/ui/client/plugin.tsx
18725
19225
  var SESSION_SWITCH_PERF = "[SESSION_SWITCH_PERF]";
@@ -18747,7 +19247,7 @@ function getRemoteMountError() {
18747
19247
  }
18748
19248
  var inject = ["slots", "remote", "sessions", "locale"];
18749
19249
  function apply(ctx) {
18750
- console.log(`[dsh-pentester] client build=${BUILD_ID}`);
19250
+ console.log(`[dsh-pentester/client] build=${BUILD_ID}`);
18751
19251
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "pentester: locale");
18752
19252
  ctx.effect(() => {
18753
19253
  let disposeMount;
@@ -18780,17 +19280,20 @@ function apply(ctx) {
18780
19280
  if (result.ok && result.value !== null && typeof result.value === "object" && "ok" in result.value) {
18781
19281
  const inner = result.value.value;
18782
19282
  if (inner?.ready === true && typeof inner.buildId === "string") {
19283
+ console.log(`[dsh-pentester/client] health host=${inner.buildId} client=${BUILD_ID}`);
18783
19284
  if (inner.buildId === BUILD_ID) {
19285
+ if (buildMismatch !== null) {
19286
+ console.log(`[dsh-pentester/client] build mismatch cleared (was host=${buildMismatch.host} client=${buildMismatch.client})`);
19287
+ }
18784
19288
  buildMismatch = null;
18785
- console.log(`[dsh-pentester] health check passed host=${inner.buildId} client=${BUILD_ID}`);
18786
19289
  } else {
18787
19290
  buildMismatch = { host: inner.buildId, client: BUILD_ID };
18788
- console.error(`[dsh-pentester] BUILD MISMATCH host=${inner.buildId} client=${BUILD_ID}`);
19291
+ console.error(`[dsh-pentester/client] build mismatch set host=${inner.buildId} client=${BUILD_ID}`);
18789
19292
  }
18790
19293
  }
18791
19294
  }
18792
19295
  } catch (error51) {
18793
- console.warn("[dsh-pentester] build health check unavailable", error51);
19296
+ console.warn("[dsh-pentester/client] build health check unavailable", error51);
18794
19297
  }
18795
19298
  })();
18796
19299
  }).catch((error51) => {