weir-harness 2.1.1 → 2.1.3

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.
@@ -63,7 +63,15 @@ window.__ModuleLoader__.load({
63
63
  if (el && el.style) Object.assign(el.style, styles);
64
64
  };
65
65
  const ring = {
66
- onFocus: (event) => paint(event, { outline: "2px solid var(--dsw-alias-focus-ring, currentColor)", outlineOffset: "1px" }),
66
+ onFocus: (event) => {
67
+ const el = event?.currentTarget;
68
+ const focused = event?.target ?? el;
69
+ const visible = typeof focused?.matches !== "function" || focused.matches(":focus-visible");
70
+ paint(event, visible
71
+ ? { outline: "2px solid var(--dsw-alias-focus-ring, currentColor)", outlineOffset: "1px" }
72
+ : { outline: "none", outlineOffset: "" });
73
+ },
74
+ onPointerDown: (event) => paint(event, { outline: "none", outlineOffset: "" }),
67
75
  onBlur: (event) => paint(event, { outline: "none", outlineOffset: "" }),
68
76
  };
69
77
  const hover = (over, out) => ({
@@ -115,7 +123,7 @@ window.__ModuleLoader__.load({
115
123
  borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.10))",
116
124
  flex: "none"
117
125
  },
118
- summary: { fontSize: "11px", lineHeight: "16px", color: MUTED, whiteSpace: "nowrap", flex: "none", marginLeft: "2px" },
126
+ summary: { fontSize: "11px", lineHeight: "16px", color: MUTED, whiteSpace: "nowrap", flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", marginLeft: "2px" },
119
127
  close: {
120
128
  width: "22px",
121
129
  height: "22px",
@@ -132,6 +140,54 @@ window.__ModuleLoader__.load({
132
140
  transition: TRANSITION
133
141
  }
134
142
  },
143
+ path: {
144
+ trigger: {
145
+ minWidth: "22px",
146
+ height: "22px",
147
+ display: "inline-flex",
148
+ alignItems: "center",
149
+ justifyContent: "center",
150
+ gap: "3px",
151
+ flex: "none",
152
+ padding: "0 4px",
153
+ cursor: "pointer",
154
+ background: "none",
155
+ border: "none",
156
+ borderRadius: "var(--dsw-radius-sm, 6px)",
157
+ color: MUTED,
158
+ transition: TRANSITION
159
+ },
160
+ count: {
161
+ fontSize: "9px",
162
+ lineHeight: "12px",
163
+ fontWeight: 600,
164
+ color: LABEL2,
165
+ background: HOVER_BG,
166
+ borderRadius: "var(--dsw-radius-xl, 999px)",
167
+ padding: "0 4px",
168
+ minWidth: "12px",
169
+ textAlign: "center",
170
+ flex: "none"
171
+ },
172
+ overlay: (position) => ({
173
+ position: "fixed",
174
+ zIndex: 1000,
175
+ top: position.top,
176
+ left: position.left,
177
+ width: position.width,
178
+ maxHeight: position.maxHeight,
179
+ boxSizing: "border-box",
180
+ overflowY: "auto",
181
+ overflowX: "hidden",
182
+ background: "var(--dsw-alias-bg-overlay, #fff)",
183
+ border: `1px solid ${BORDER}`,
184
+ borderRadius: "var(--dsw-radius-lg, 12px)",
185
+ boxShadow: "var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,.14))",
186
+ padding: "12px",
187
+ fontSize: "12px",
188
+ color: LABEL1
189
+ })
190
+ },
135
191
  tabs: {
136
192
  // Segmented control (host SegmentedControl recipe, containerless):
137
193
  // inactive segments carry the track fill, the active segment is a
@@ -446,6 +502,10 @@ window.__ModuleLoader__.load({
446
502
  // ---- 16x16 stroke icons (worktree-view style, stroke: currentColor) ----
447
503
  const ICON_PATHS = {
448
504
  search: [["circle", { cx: 7, cy: 7, r: 4.6 }], ["path", { d: "M10.6 10.6L14 14" }]],
505
+ folder: [
506
+ ["path", { d: "M2.2 4.2c0-.6.4-1 1-1h3l1.5 1.8h5.1c.6 0 1 .4 1 1v5.8c0 .6-.4 1-1 1H3.2c-.6 0-1-.4-1-1V4.2z" }],
507
+ ["path", { d: "M2.2 6h11.6" }]
508
+ ],
449
509
  x: [["path", { d: "M4 4l8 8M12 4l-8 8" }]],
450
510
  warn: [
451
511
  ["path", { d: "M8 2.6l6 10.4H2l6-10.4z" }],
@@ -518,6 +578,338 @@ window.__ModuleLoader__.load({
518
578
  }
519
579
  return react_jsx_runtime.jsx(impl, { ...props });
520
580
  }
581
+ /** Skills-only path management. All hooks live here so CapabilityManagerPanel
582
+ * keeps its pinned 7/1 hook structure in both shells. */
583
+ function SkillPathManager(props) {
584
+ const model = props.model;
585
+ const t = props.t;
586
+ const [registry, setRegistry] = react.useState(null);
587
+ const [input, setInput] = react.useState("");
588
+ const [reading, setReading] = react.useState(false);
589
+ const [busy, setBusy] = react.useState(null);
590
+ const [feedback, setFeedback] = react.useState(null);
591
+ const [pendingRemove, setPendingRemove] = react.useState(null);
592
+ const [returnFocusTo, setReturnFocusTo] = react.useState(null);
593
+ const [open, setOpen] = react.useState(false);
594
+ const [position, setPosition] = react.useState({ top: 12, left: 12, width: 360, maxHeight: 420 });
595
+ const readRequest = react.useRef(0);
596
+ const operationRequest = react.useRef(0);
597
+ const operationRunning = react.useRef(false);
598
+ const alive = react.useRef(true);
599
+ const triggerRef = react.useRef(null);
600
+ const overlayRef = react.useRef(null);
601
+ const inputRef = react.useRef(null);
602
+
603
+ const refreshPaths = async () => {
604
+ const request = ++readRequest.current;
605
+ setReading(true);
606
+ let response;
607
+ try {
608
+ response = typeof props.fetchSkillPaths === "function"
609
+ ? await props.fetchSkillPaths(props.sessionId)
610
+ : { status: "unavailable", saved: false, error: { code: "unavailable", message: "Skill scan path reads are unavailable on this client." } };
611
+ } catch (error) {
612
+ response = { status: "call-threw", saved: false, error: { code: "call-threw", message: error instanceof Error ? error.message : String(error) } };
613
+ }
614
+ if (!alive.current || request !== readRequest.current) return null;
615
+ setRegistry(current => model.skillPathsAfterRead(current, response));
616
+ setReading(false);
617
+ return response;
618
+ };
619
+
620
+ const closeManager = () => {
621
+ if (busy !== null) return;
622
+ setOpen(false);
623
+ triggerRef.current?.focus();
624
+ };
625
+ const openManager = () => {
626
+ if (open) return;
627
+ const viewportWidth = window.innerWidth;
628
+ const viewportHeight = window.innerHeight;
629
+ const rect = triggerRef.current?.getBoundingClientRect();
630
+ const width = Math.max(0, Math.min(360, viewportWidth - 24));
631
+ const left = Math.max(12, Math.min((rect?.right ?? viewportWidth - 12) - width, viewportWidth - width - 12));
632
+ const top = Math.max(12, Math.min((rect?.bottom ?? 12) + 8, Math.max(12, viewportHeight - 12)));
633
+ setPosition({ top, left, width, maxHeight: Math.max(0, viewportHeight - top - 12) });
634
+ setOpen(true);
635
+ void refreshPaths();
636
+ };
637
+
638
+ react.useEffect(() => {
639
+ alive.current = true;
640
+ setRegistry(null);
641
+ setInput("");
642
+ setOpen(false);
643
+ setFeedback(null);
644
+ setPendingRemove(null);
645
+ setReturnFocusTo(null);
646
+ setBusy(null);
647
+ operationRunning.current = false;
648
+ void refreshPaths();
649
+ return () => {
650
+ alive.current = false;
651
+ ++readRequest.current;
652
+ ++operationRequest.current;
653
+ operationRunning.current = false;
654
+ };
655
+ }, [props.sessionId]);
656
+
657
+ react.useEffect(() => {
658
+ if (!open) return undefined;
659
+ const document = window.document;
660
+ const onOutsidePointer = (event) => {
661
+ if (overlayRef.current?.contains(event.target) || triggerRef.current?.contains(event.target)) return;
662
+ closeManager();
663
+ };
664
+ const onViewportChange = () => { closeManager(); };
665
+ const onScroll = (event) => {
666
+ if (overlayRef.current?.contains(event.target)) return;
667
+ closeManager();
668
+ };
669
+ document.addEventListener("pointerdown", onOutsidePointer);
670
+ window.addEventListener("resize", onViewportChange);
671
+ window.addEventListener("scroll", onScroll, true);
672
+ return () => {
673
+ document.removeEventListener("pointerdown", onOutsidePointer);
674
+ window.removeEventListener("resize", onViewportChange);
675
+ window.removeEventListener("scroll", onScroll, true);
676
+ };
677
+ }, [open, busy]);
678
+
679
+ react.useEffect(() => {
680
+ if (open && busy === null && !reading && registry?.status === "ok") inputRef.current?.focus();
681
+ }, [open, busy, reading, registry?.status]);
682
+
683
+ const cancelRemove = (path) => {
684
+ if (busy !== null) return;
685
+ setPendingRemove(null);
686
+ setReturnFocusTo(path);
687
+ setFeedback(null);
688
+ };
689
+ const runMutation = async (kind, path) => {
690
+ if (operationRunning.current || busy !== null || reading || registry?.status !== "ok") return;
691
+ const verb = kind === "add" ? props.skillPathAdd : props.skillPathRemove;
692
+ if (typeof verb !== "function") {
693
+ setFeedback({ kind: "error", operation: kind, outcome: model.skillPathMutationOutcomeOf({ status: "unavailable", saved: false, error: { message: "Skill scan path mutations are unavailable on this client." } }) });
694
+ return;
695
+ }
696
+ const request = ++operationRequest.current;
697
+ operationRunning.current = true;
698
+ setBusy(kind);
699
+ setFeedback(null);
700
+ setReturnFocusTo(null);
701
+ let response;
702
+ try {
703
+ response = await verb(props.sessionId, { path, expectedRevision: registry.revision });
704
+ } catch (error) {
705
+ response = { status: "call-threw", saved: false, error: { code: "call-threw", message: error instanceof Error ? error.message : String(error) } };
706
+ }
707
+ if (!alive.current || request !== operationRequest.current) return;
708
+ const outcome = model.skillPathMutationOutcomeOf(response);
709
+ if (outcome.snapshot !== null) setRegistry(outcome.snapshot);
710
+ if (outcome.saved) {
711
+ if (kind === "add") setInput("");
712
+ else setPendingRemove(null);
713
+ }
714
+ setFeedback({ kind: outcome.saved ? (outcome.phase === "degraded" ? "warning" : "success") : "error", operation: kind, outcome });
715
+ try {
716
+ const refreshes = [];
717
+ if (outcome.refreshPaths) refreshes.push(refreshPaths());
718
+ if (outcome.refreshCapabilities && typeof props.refreshCapabilities === "function") refreshes.push(props.refreshCapabilities());
719
+ await Promise.all(refreshes);
720
+ } catch (error) {
721
+ if (alive.current && request === operationRequest.current) setFeedback(current => ({ ...current, refreshError: error instanceof Error ? error.message : String(error) }));
722
+ } finally {
723
+ if (alive.current && request === operationRequest.current) {
724
+ operationRunning.current = false;
725
+ setBusy(null);
726
+ }
727
+ }
728
+ };
729
+ const detail = (text) => {
730
+ const message = feedback?.outcome?.error?.message;
731
+ return typeof message === "string" && message !== "" && !text.includes(message) ? `${text} ${message}` : text;
732
+ };
733
+ let feedbackText = null;
734
+ if (feedback) {
735
+ const phase = feedback.outcome.phase;
736
+ if (phase === "saved") feedbackText = feedback.operation === "add"
737
+ ? t("capability.skills.paths.added", "Directory added. Newly discovered Skills remain unselected until you select and Apply them.")
738
+ : t("capability.skills.paths.removed", "Directory removed. Selection drafts, applied capabilities, and defaults were not rewritten.");
739
+ else if (phase === "degraded") feedbackText = detail(t("capability.skills.paths.degraded", "The path change was saved, but the host reported a post-commit problem. Fresh registry, Skill listing, and receipt reads were requested; no rollback is claimed."));
740
+ else if (phase === "validation-failed") feedbackText = detail(t("capability.skills.paths.validation", "The server rejected this directory. Enter an existing readable absolute directory or ~/directory; your input was kept and nothing was saved."));
741
+ else if (phase === "duplicate") feedbackText = detail(t("capability.skills.paths.duplicate", "This canonical directory is already registered. Current paths were re-read; your input was kept and no duplicate was saved."));
742
+ else if (phase === "not-found") feedbackText = detail(t("capability.skills.paths.notFound", "That canonical directory is no longer registered. Current paths were re-read; nothing was removed."));
743
+ else if (phase === "revision-conflict") feedbackText = detail(t("capability.skills.paths.conflict", "The path registry changed elsewhere. Current paths were re-read; your input was kept and was not merged. Review the registry before retrying."));
744
+ else if (phase === "indeterminate") feedbackText = detail(t("capability.skills.paths.indeterminate", "The operation outcome could not be confirmed. Fresh registry, Skill listing, and receipt reads were requested; your input was kept and no rollback is claimed."));
745
+ else if (phase === "unavailable") feedbackText = detail(t("capability.skills.paths.unavailable", "The Skill scan path registry is unavailable. No path change was saved."));
746
+ else feedbackText = detail(t("capability.skills.paths.failed", "The path operation failed. No saved change is claimed; your input was kept."));
747
+ if (feedback.refreshError) feedbackText += ` ${t("capability.skills.paths.refreshFailed", "A follow-up refresh failed: {message}").replace("{message}", feedback.refreshError)}`;
748
+ }
749
+ const unavailable = registry?.status !== "ok";
750
+ const controlsDisabled = busy !== null || reading || unavailable;
751
+ const liveText = busy === "add" ? t("capability.skills.paths.adding", "Adding…")
752
+ : busy === "remove" ? t("capability.skills.paths.removing", "Removing…")
753
+ : reading && registry === null ? t("capability.skills.paths.loading", "Loading scan paths…") : null;
754
+ const pathRow = (path) => {
755
+ const confirming = pendingRemove === path;
756
+ return react_jsx_runtime.jsx("div", {
757
+ style: { ...S.list.card, margin: "4px 0", padding: "6px 8px" },
758
+ "data-weir-skill-path": path,
759
+ children: confirming ? react_jsx_runtime.jsxs("div", {
760
+ role: "group",
761
+ "aria-label": t("capability.skills.paths.confirmTitle", "Remove this Skill scan path?"),
762
+ onKeyDown: (event) => {
763
+ if (event.key === "Escape" && busy === null) {
764
+ event.preventDefault();
765
+ event.stopPropagation();
766
+ cancelRemove(path);
767
+ }
768
+ },
769
+ children: [
770
+ react_jsx_runtime.jsx("div", { style: S.form.title, children: t("capability.skills.paths.confirmTitle", "Remove this Skill scan path?") }),
771
+ react_jsx_runtime.jsxs("div", { style: { fontSize: "11px", lineHeight: "16px", color: LABEL2, overflowWrap: "anywhere" }, children: [
772
+ t("capability.skills.paths.confirmBody", "Remove this canonical directory from the Profile? Selection drafts, applied capabilities, and defaults are not rewritten: "),
773
+ react_jsx_runtime.jsx("code", { style: { color: LABEL1, fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)" }, children: path })
774
+ ] }),
775
+ react_jsx_runtime.jsxs("div", { style: { ...S.form.actions, marginTop: "6px" }, children: [
776
+ react_jsx_runtime.jsx("button", {
777
+ type: "button", autoFocus: true, disabled: busy !== null,
778
+ style: busy !== null ? { ...S.form.primary, ...S.footer.disabled } : S.form.primary,
779
+ ...ring,
780
+ onClick: () => { void runMutation("remove", path); },
781
+ children: busy === "remove" ? t("capability.skills.paths.removing", "Removing…") : t("capability.skills.paths.confirmRemove", "Remove path")
782
+ }),
783
+ react_jsx_runtime.jsx("button", {
784
+ type: "button", disabled: busy !== null, style: S.form.ghost, ...ring,
785
+ onClick: () => cancelRemove(path),
786
+ children: t("capability.skills.paths.cancel", "Cancel")
787
+ })
788
+ ] })
789
+ ]
790
+ }) : react_jsx_runtime.jsxs("div", {
791
+ style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 },
792
+ children: [
793
+ react_jsx_runtime.jsx("code", {
794
+ title: path,
795
+ style: { flex: 1, minWidth: 0, color: LABEL1, fontSize: "11px", lineHeight: "16px", fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)", overflowWrap: "anywhere" },
796
+ children: path
797
+ }),
798
+ react_jsx_runtime.jsx("button", {
799
+ type: "button", autoFocus: returnFocusTo === path, disabled: controlsDisabled,
800
+ style: controlsDisabled ? { ...S.form.ghost, ...S.footer.disabled } : S.form.ghost,
801
+ ...ring,
802
+ "aria-label": t("capability.skills.paths.removeAria", "Remove scan path {path}").replace("{path}", path),
803
+ onClick: () => { setFeedback(null); setReturnFocusTo(null); setPendingRemove(path); },
804
+ children: t("capability.skills.paths.remove", "Remove")
805
+ })
806
+ ]
807
+ })
808
+ }, path);
809
+ };
810
+ return react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
811
+ children: [
812
+ react_jsx_runtime.jsxs("button", {
813
+ type: "button",
814
+ ref: triggerRef,
815
+ style: S.path.trigger,
816
+ ...ring,
817
+ ...hover(HOVER_BG, "none"),
818
+ onClick: openManager,
819
+ "aria-label": t("capability.skills.paths.manage", "Manage Skill scan paths"),
820
+ title: t("capability.skills.paths.manage", "Manage Skill scan paths"),
821
+ "aria-haspopup": "dialog",
822
+ "aria-expanded": open,
823
+ "data-weir-skill-path-trigger": "",
824
+ children: [
825
+ react_jsx_runtime.jsx(Icon, { name: "folder", size: 13 }),
826
+ registry?.status === "ok" ? react_jsx_runtime.jsx("span", {
827
+ style: S.path.count,
828
+ "aria-hidden": "true",
829
+ "data-weir-skill-path-count": "",
830
+ children: String(registry.paths.length)
831
+ }) : null
832
+ ]
833
+ }),
834
+ open ? react_jsx_runtime.jsxs("section", {
835
+ ref: overlayRef,
836
+ role: "dialog",
837
+ "aria-modal": false,
838
+ style: S.path.overlay(position),
839
+ "aria-label": t("capability.skills.paths.title", "Custom Skill scan paths"),
840
+ "aria-busy": busy !== null || reading,
841
+ "data-weir-skill-path-manager": "",
842
+ onKeyDown: (event) => {
843
+ if (event.key !== "Escape" || busy !== null) return;
844
+ event.preventDefault();
845
+ event.stopPropagation();
846
+ if (pendingRemove !== null) cancelRemove(pendingRemove);
847
+ else closeManager();
848
+ },
849
+ children: [
850
+ react_jsx_runtime.jsxs("div", { style: S.list.cardHead, children: [
851
+ react_jsx_runtime.jsx("span", { children: t("capability.skills.paths.title", "Custom Skill scan paths") }),
852
+ react_jsx_runtime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "4px" }, children: [
853
+ react_jsx_runtime.jsx("button", {
854
+ type: "button", onClick: () => { void refreshPaths(); }, disabled: busy !== null || reading,
855
+ style: busy !== null || reading ? { ...S.form.ghost, ...S.footer.disabled } : S.form.ghost,
856
+ ...ring,
857
+ children: t("capability.skills.paths.refresh", "Refresh paths")
858
+ }),
859
+ react_jsx_runtime.jsx("button", {
860
+ type: "button", onClick: closeManager, disabled: busy !== null,
861
+ style: busy !== null ? { ...S.header.close, ...S.footer.disabled } : S.header.close,
862
+ ...ring,
863
+ "aria-label": t("capability.close", "Close"),
864
+ title: t("capability.close", "Close"),
865
+ children: react_jsx_runtime.jsx(Icon, { name: "x", size: 12 })
866
+ })
867
+ ] })
868
+ ] }),
869
+ react_jsx_runtime.jsx("div", { style: { fontSize: "11px", lineHeight: "16px", color: MUTED, marginBottom: "6px" }, children: t("capability.skills.paths.description", "Profile-scoped directories shared by Weir sessions. Path changes never select Skills, Apply a draft, or rewrite defaults.") }),
870
+ react_jsx_runtime.jsxs("form", {
871
+ "aria-label": t("capability.skills.paths.addLabel", "Skill scan directory"),
872
+ onSubmit: (event) => { event.preventDefault(); void runMutation("add", input); },
873
+ children: [
874
+ react_jsx_runtime.jsx("label", {
875
+ style: { display: "flex", flexDirection: "column", gap: "3px", ...S.form.title },
876
+ children: [
877
+ t("capability.skills.paths.addLabel", "Skill scan directory"),
878
+ react_jsx_runtime.jsx("input", {
879
+ ref: inputRef,
880
+ value: input, onChange: (event) => setInput(event.target.value), disabled: controlsDisabled,
881
+ placeholder: t("capability.skills.paths.placeholder", "~/skills or /absolute/path"),
882
+ style: S.form.input, ...ring,
883
+ "aria-invalid": feedback?.kind === "error" && feedback.operation === "add" ? true : undefined
884
+ })
885
+ ]
886
+ }),
887
+ react_jsx_runtime.jsx("div", { style: { ...S.form.actions, marginTop: "6px" }, children: react_jsx_runtime.jsx("button", {
888
+ type: "submit", disabled: controlsDisabled,
889
+ style: controlsDisabled ? { ...S.form.primary, ...S.footer.disabled } : S.form.primary,
890
+ ...ring,
891
+ children: busy === "add" ? t("capability.skills.paths.adding", "Adding…") : t("capability.skills.paths.add", "Add path")
892
+ }) })
893
+ ]
894
+ }),
895
+ liveText ? react_jsx_runtime.jsx("div", { role: "status", "aria-live": "polite", style: { ...S.form.title, marginTop: "6px" }, children: liveText }) : null,
896
+ registry?.error ? react_jsx_runtime.jsx("div", { role: "alert", style: { ...S.form.error, marginTop: "6px" }, children: `${t("capability.skills.paths.readFailed", "The path registry could not be read. Existing rows, when present, are stale.")} ${registry.error.message}` }) : null,
897
+ feedbackText ? react_jsx_runtime.jsx("div", {
898
+ role: feedback.kind === "success" ? "status" : "alert",
899
+ "aria-live": feedback.kind === "success" ? "polite" : "assertive",
900
+ "data-weir-skill-path-feedback": feedback.kind,
901
+ style: { ...(feedback.kind === "success" ? S.form.title : S.form.error), marginTop: "6px", color: feedback.kind === "success" ? SUCCESS : feedback.kind === "warning" ? WARN_TEXT : DANGER },
902
+ children: feedbackText
903
+ }) : null,
904
+ registry?.status === "ok" && registry.paths.length === 0
905
+ ? react_jsx_runtime.jsx("div", { style: { ...S.form.title, marginTop: "6px" }, children: t("capability.skills.paths.empty", "No custom scan paths are registered.") })
906
+ : null,
907
+ ...(registry?.paths ?? []).map(pathRow)
908
+ ]
909
+ }) : null
910
+ ]
911
+ });
912
+ }
521
913
  function CapabilityManagerPanel(props) {
522
914
  const model = props.model;
523
915
  // Echo-guard like the Badge: an unregistered dictionary entry must
@@ -720,6 +1112,11 @@ window.__ModuleLoader__.load({
720
1112
  ];
721
1113
  const selectionDisabled = draft === null || commit?.phase === "submitting";
722
1114
  const addBlock = null;
1115
+ const pathManager = tab === "skills" ? react_jsx_runtime.jsx(SkillPathManager, {
1116
+ sessionId: props.sessionId, model, t,
1117
+ fetchSkillPaths: props.fetchSkillPaths, skillPathAdd: props.skillPathAdd, skillPathRemove: props.skillPathRemove,
1118
+ refreshCapabilities: refreshAll
1119
+ }) : null;
723
1120
  const skillRow = (skill, index) => {
724
1121
  const selected = skillKeys.has(model.skillEntryKeyOf(skill.identity ?? skill.name));
725
1122
  const selectedConflict = selectedKeys.has(model.skillEntryKeyOf(skill.identity));
@@ -836,6 +1233,7 @@ window.__ModuleLoader__.load({
836
1233
  react_jsx_runtime.jsx("input", { value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("capability.search", "Search"), style: S.search.input, ...ring })
837
1234
  ]
838
1235
  }),
1236
+ pathManager,
839
1237
  typeof props.onClose === "function"
840
1238
  ? react_jsx_runtime.jsx("button", {
841
1239
  type: "button",
@@ -884,6 +1282,73 @@ window.__ModuleLoader__.load({
884
1282
  ] }) : null
885
1283
  ] });
886
1284
  }
1285
+ // View-owned locale data face. The entry merges this with the model's
1286
+ // established capability dictionary when either shared shell arrives.
1287
+ const dictionaries = {
1288
+ en: {
1289
+ "capability.skills.paths.title": "Custom Skill scan paths",
1290
+ "capability.skills.paths.manage": "Manage Skill scan paths",
1291
+ "capability.skills.paths.description": "Profile-scoped directories shared by Weir sessions. Path changes never select Skills, Apply a draft, or rewrite defaults.",
1292
+ "capability.skills.paths.addLabel": "Skill scan directory",
1293
+ "capability.skills.paths.placeholder": "~/skills or /absolute/path",
1294
+ "capability.skills.paths.add": "Add path",
1295
+ "capability.skills.paths.adding": "Adding…",
1296
+ "capability.skills.paths.removing": "Removing…",
1297
+ "capability.skills.paths.loading": "Loading scan paths…",
1298
+ "capability.skills.paths.refresh": "Refresh paths",
1299
+ "capability.skills.paths.refreshFailed": "A follow-up refresh failed: {message}",
1300
+ "capability.skills.paths.readFailed": "The path registry could not be read. Existing rows, when present, are stale.",
1301
+ "capability.skills.paths.empty": "No custom scan paths are registered.",
1302
+ "capability.skills.paths.added": "Directory added. Newly discovered Skills remain unselected until you select and Apply them.",
1303
+ "capability.skills.paths.removed": "Directory removed. Selection drafts, applied capabilities, and defaults were not rewritten.",
1304
+ "capability.skills.paths.degraded": "The path change was saved, but the host reported a post-commit problem. Fresh registry, Skill listing, and receipt reads were requested; no rollback is claimed.",
1305
+ "capability.skills.paths.validation": "The server rejected this directory. Enter an existing readable absolute directory or ~/directory; your input was kept and nothing was saved.",
1306
+ "capability.skills.paths.duplicate": "This canonical directory is already registered. Current paths were re-read; your input was kept and no duplicate was saved.",
1307
+ "capability.skills.paths.notFound": "That canonical directory is no longer registered. Current paths were re-read; nothing was removed.",
1308
+ "capability.skills.paths.conflict": "The path registry changed elsewhere. Current paths were re-read; your input was kept and was not merged. Review the registry before retrying.",
1309
+ "capability.skills.paths.indeterminate": "The operation outcome could not be confirmed. Fresh registry, Skill listing, and receipt reads were requested; your input was kept and no rollback is claimed.",
1310
+ "capability.skills.paths.unavailable": "The Skill scan path registry is unavailable. No path change was saved.",
1311
+ "capability.skills.paths.failed": "The path operation failed. No saved change is claimed; your input was kept.",
1312
+ "capability.skills.paths.remove": "Remove",
1313
+ "capability.skills.paths.removeAria": "Remove scan path {path}",
1314
+ "capability.skills.paths.confirmTitle": "Remove this Skill scan path?",
1315
+ "capability.skills.paths.confirmBody": "Remove this canonical directory from the Profile? Selection drafts, applied capabilities, and defaults are not rewritten: ",
1316
+ "capability.skills.paths.confirmRemove": "Remove path",
1317
+ "capability.skills.paths.cancel": "Cancel"
1318
+ },
1319
+ zh: {
1320
+ "capability.skills.paths.title": "自定义技能扫描路径",
1321
+ "capability.skills.paths.manage": "管理技能扫描路径",
1322
+ "capability.skills.paths.description": "由 Weir 会话共享的 Profile 级目录。路径变更不会选择技能、应用草稿或改写默认值。",
1323
+ "capability.skills.paths.addLabel": "技能扫描目录",
1324
+ "capability.skills.paths.placeholder": "~/skills 或 /absolute/path",
1325
+ "capability.skills.paths.add": "添加路径",
1326
+ "capability.skills.paths.adding": "正在添加…",
1327
+ "capability.skills.paths.removing": "正在移除…",
1328
+ "capability.skills.paths.loading": "正在加载扫描路径…",
1329
+ "capability.skills.paths.refresh": "刷新路径",
1330
+ "capability.skills.paths.refreshFailed": "后续刷新失败:{message}",
1331
+ "capability.skills.paths.readFailed": "无法读取路径注册表;如有既有行,现已过期。",
1332
+ "capability.skills.paths.empty": "尚未登记自定义扫描路径。",
1333
+ "capability.skills.paths.added": "目录已添加。新发现的技能保持未选状态,需手动选择并应用后才会启用。",
1334
+ "capability.skills.paths.removed": "目录已移除。选择草稿、已应用能力和默认值均未被改写。",
1335
+ "capability.skills.paths.degraded": "路径变更已保存,但宿主报告了提交后检查问题。已请求重新读取注册表、技能清单和回执;不声称已回滚。",
1336
+ "capability.skills.paths.validation": "服务端拒绝了此目录。请输入存在且可读的绝对目录或 ~/directory;输入已保留,未保存任何变更。",
1337
+ "capability.skills.paths.duplicate": "此规范化目录已登记。已重新读取当前路径;输入已保留,未保存重复项。",
1338
+ "capability.skills.paths.notFound": "此规范化目录已不在注册表中。已重新读取当前路径;未移除任何项目。",
1339
+ "capability.skills.paths.conflict": "路径注册表已在别处变更。已重新读取当前路径;输入已保留且未合并。重试前请核对注册表。",
1340
+ "capability.skills.paths.indeterminate": "无法确认操作结果。已请求重新读取注册表、技能清单和回执;输入已保留,不声称已回滚。",
1341
+ "capability.skills.paths.unavailable": "技能扫描路径注册表不可用。未保存路径变更。",
1342
+ "capability.skills.paths.failed": "路径操作失败。不声称变更已保存;输入已保留。",
1343
+ "capability.skills.paths.remove": "移除",
1344
+ "capability.skills.paths.removeAria": "移除扫描路径 {path}",
1345
+ "capability.skills.paths.confirmTitle": "移除此技能扫描路径?",
1346
+ "capability.skills.paths.confirmBody": "从此 Profile 移除以下规范化目录?选择草稿、已应用能力和默认值均不会被改写:",
1347
+ "capability.skills.paths.confirmRemove": "移除路径",
1348
+ "capability.skills.paths.cancel": "取消"
1349
+ }
1350
+ };
1351
+ exports.dictionaries = dictionaries;
887
1352
  exports.CapabilityManagerPanel = CapabilityManagerPanel;
888
1353
  return module.exports;
889
1354
  }
@@ -83,6 +83,81 @@ window.__ModuleLoader__.load({
83
83
  return "unsupported";
84
84
  }
85
85
 
86
+ // ---- Profile-scoped Skill scan paths (add-custom-skill-scan-paths D5) ----
87
+ // This registry has its OWN revision/CAS. None of these helpers reads or
88
+ // writes the selection draft revision; receipt coordination remains solely
89
+ // draftAfterRefresh's job below.
90
+ const skillPathListOf = (value) => Array.isArray(value)
91
+ ? value.filter(path => typeof path === "string" && path !== "")
92
+ : null;
93
+ const skillPathRevisionOf = (value, fallback = 0) => Number.isSafeInteger(value) && value >= 0 ? value : fallback;
94
+ const skillPathErrorOf = (response, status) => {
95
+ const error = response?.error;
96
+ return {
97
+ code: typeof error?.code === "string" && error.code !== "" ? error.code : status,
98
+ message: typeof error?.message === "string" && error.message !== "" ? error.message : "The Skill scan path registry is unavailable."
99
+ };
100
+ };
101
+ /** Normalize a registry read. Absent successful lists mean an empty registry;
102
+ * failed re-reads retain last-good paths only as explicitly stale rows. */
103
+ function skillPathsAfterRead(current, response) {
104
+ const currentPaths = skillPathListOf(current?.paths) ?? [];
105
+ const currentRevision = skillPathRevisionOf(current?.revision);
106
+ if (response?.status === "ok") {
107
+ return {
108
+ status: "ok",
109
+ revision: skillPathRevisionOf(response.revision, currentRevision),
110
+ paths: skillPathListOf(response.paths) ?? [],
111
+ error: null,
112
+ stale: false
113
+ };
114
+ }
115
+ const status = typeof response?.status === "string" && response.status !== "" ? response.status : "unavailable";
116
+ return {
117
+ status,
118
+ revision: skillPathRevisionOf(response?.revision, currentRevision),
119
+ paths: skillPathListOf(response?.paths) ?? currentPaths,
120
+ error: skillPathErrorOf(response, status),
121
+ stale: current !== null && current !== undefined
122
+ };
123
+ }
124
+ /** Normalize every path mutation without ever inferring a save from status
125
+ * alone. Duplicate/not-found/conflict re-read only the registry; durable
126
+ * (including landed-but-degraded) outcomes re-read registry + capabilities. */
127
+ function skillPathMutationOutcomeOf(response) {
128
+ const status = typeof response?.status === "string" && response.status !== "" ? response.status : "unavailable";
129
+ const saved = response?.saved === true && ["saved", "removed", "invalidation-failed", "owner-changed"].includes(status);
130
+ const degraded = saved && (status === "invalidation-failed" || status === "owner-changed");
131
+ const uncertain = status === "indeterminate" || status === "call-threw" || status === "call-failed" || status.startsWith("gateway/");
132
+ const phase = saved ? (degraded ? "degraded" : "saved")
133
+ : status === "revision-conflict" ? "revision-conflict"
134
+ : status === "validation-failed" ? "validation-failed"
135
+ : status === "duplicate" ? "duplicate"
136
+ : status === "not-found" ? "not-found"
137
+ : uncertain ? "indeterminate"
138
+ : ["unavailable", "unreadable", "unsupported", "locked", "write-failed", "storage-failed"].includes(status) ? "unavailable" : "failed";
139
+ const current = response?.current && typeof response.current === "object" && !Array.isArray(response.current) ? response.current : null;
140
+ const rawPaths = response?.paths === undefined ? current?.paths : response.paths;
141
+ const paths = skillPathListOf(rawPaths);
142
+ const rawRevision = response?.revision === undefined ? current?.revision : response.revision;
143
+ const hasRevision = Number.isSafeInteger(rawRevision) && rawRevision >= 0;
144
+ const snapshot = paths !== null && hasRevision
145
+ ? { status: "ok", revision: rawRevision, paths, error: null, stale: false }
146
+ : null;
147
+ const refreshPaths = saved || phase === "revision-conflict" || phase === "duplicate" || phase === "not-found" || phase === "indeterminate";
148
+ return {
149
+ phase,
150
+ status,
151
+ saved,
152
+ revision: hasRevision ? rawRevision : null,
153
+ paths,
154
+ snapshot,
155
+ error: saved && !degraded ? null : skillPathErrorOf(response, status),
156
+ refreshPaths,
157
+ refreshCapabilities: saved || phase === "indeterminate"
158
+ };
159
+ }
160
+
86
161
  /**
87
162
  * Second-window convergence (12.5): an agent-preset/selected frame
88
163
  * refreshes THIS session's Badge only when the frame names the same
@@ -1347,6 +1422,8 @@ window.__ModuleLoader__.load({
1347
1422
  exports.skillRowOf = skillRowOf;
1348
1423
  exports.partitionManagerListing = partitionManagerListing;
1349
1424
  exports.managerConditionState = managerConditionState;
1425
+ exports.skillPathsAfterRead = skillPathsAfterRead;
1426
+ exports.skillPathMutationOutcomeOf = skillPathMutationOutcomeOf;
1350
1427
  exports.frameRefreshesSession = frameRefreshesSession;
1351
1428
  exports.draftFromReceipt = draftFromReceipt;
1352
1429
  exports.draftAfterRefresh = draftAfterRefresh;