@youdie006/prodex 0.36.1 → 0.36.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.
@@ -632,6 +632,46 @@ export function responseChoiceBlocksThisSend(input) {
632
632
  return false;
633
633
  return !input.newChat && input.project === undefined && input.projectNew === undefined;
634
634
  }
635
+ /**
636
+ * Why a requested Pro sub-mode did not make it onto a power-slider picker.
637
+ *
638
+ * The current picker is one five-step slider whose top step IS Pro, and it has
639
+ * no 기본/확장 anywhere - measured: `{ok:true, position:4, max:4, model:"GPT-5.6
640
+ * Sol", effort:"Pro"}`. The send takes that branch and returns, reading
641
+ * `effort ?? model`, so a sub-mode request was dropped on the way past. A run
642
+ * with `--model Pro --pro-mode 확장` finished clean and recorded no warning at
643
+ * all, having never applied 확장. The older picker refuses the same request
644
+ * out loud; silence on one machine and an error on another, for one command,
645
+ * is the half worth fixing. The send is still a correct Pro send, so this is a
646
+ * warning and not a refusal.
647
+ */
648
+ export function proModeNotAppliedWarning(proMode, sliderEffort) {
649
+ if (!proMode)
650
+ return undefined;
651
+ const step = sliderEffort ? `the slider's "${sliderEffort}" step` : "the slider's current step";
652
+ return (`pro_mode_not_applied: this ChatGPT picker is a power slider with no "Pro ${proMode}" step, ` +
653
+ `so --pro-mode ${proMode} was not applied and the send used ${step}. ` +
654
+ "Drop --pro-mode here, or clear a saved default with `prodex setup --clear-pro-mode`.");
655
+ }
656
+ /**
657
+ * Which of two flags the slider actually obeyed.
658
+ *
659
+ * On this picker one control carries both: "Pro" is the top EFFORT step, not a
660
+ * model. Naming a model AND an effort therefore asks for two positions of the
661
+ * same slider, and the send takes `effort ?? model` - so the model is dropped.
662
+ * Measured: `--model Pro --effort 중간` answered from gpt-5-6-thinking with an
663
+ * empty warnings list. Asking for Pro and quietly getting a cheaper model is
664
+ * the kind of thing a receipt exists to catch.
665
+ */
666
+ export function modelIgnoredForEffortWarning(model, effort) {
667
+ if (!model || !effort)
668
+ return undefined;
669
+ if (model.trim().toLowerCase() === effort.trim().toLowerCase())
670
+ return undefined;
671
+ return (`model_ignored: this ChatGPT picker sets the model and the effort with one slider, ` +
672
+ `so --model ${model} and --effort ${effort} name two positions of it. The send used ${effort}. ` +
673
+ `Pass only one of them to say which you meant.`);
674
+ }
635
675
  export function chatGptBusyBlocker(state) {
636
676
  // Waiting for a choice is not generating. Conflating them turned a click
637
677
  // away from ready into a full send budget spent on "still generating".
@@ -1614,6 +1654,14 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
1614
1654
  if (wanted) {
1615
1655
  await selectPowerStep(cdp, wanted);
1616
1656
  }
1657
+ // This branch cannot honour a sub-mode, and used to return without
1658
+ // saying so - the caller got a clean receipt for a 확장 it never got.
1659
+ const proModeWarning = proModeNotAppliedWarning(options.proMode, sliderState.effort ?? undefined);
1660
+ if (proModeWarning)
1661
+ selectionWarnings.push(proModeWarning);
1662
+ const modelWarning = modelIgnoredForEffortWarning(options.model, options.effort);
1663
+ if (modelWarning)
1664
+ selectionWarnings.push(modelWarning);
1617
1665
  await dispatchEscapeKey(cdp);
1618
1666
  return;
1619
1667
  }
@@ -3906,6 +3954,30 @@ export function transcriptMatchesSentPrompt(userText, sentPrompt) {
3906
3954
  // separates, U+E201 closes) and keeps the real sources in content_references.
3907
3955
  const CITATION_DELIMITER_PATTERN = /[\uE200-\uE206]/;
3908
3956
  const CITATION_MARKER_PATTERN = /\uE200[^\uE200-\uE206]*(?:[\uE202\uE204-\uE206][^\uE200-\uE206]*)*[\uE201\uE203]/g;
3957
+ /**
3958
+ * The words a citation marker was wrapping, if any.
3959
+ *
3960
+ * The markers come in two shapes. `<E200>cite<E202>turn0search0<E201>` is a
3961
+ * bare anchor - kind then reference id, nothing a reader would miss. But
3962
+ * `<E200>url<E202>Timeanddate.com — World Clock<E202>turn0search0<E201>` puts a
3963
+ * visible title in the middle, and that title is part of the sentence. Caught by
3964
+ * diffing a saved answer against its thread: a web-search reply ended at a
3965
+ * dangling "Source:" because the title had been deleted along with the marker.
3966
+ */
3967
+ export function citationMarkerText(marker) {
3968
+ // The first segment is the kind and the rest are a mix of reference tokens and,
3969
+ // for some kinds, prose. Sorting them by SHAPE rather than by kind keeps a
3970
+ // title from a kind not seen yet: `cite` can carry several ids
3971
+ // (`cite<E202>turn0search19<E202>turn0search5`), and dropping every
3972
+ // id-shaped segment leaves nothing there, which is right.
3973
+ const referenceToken = /^turn\d+[a-z]+\d+$/i;
3974
+ const segments = marker.replace(/^[\uE200-\uE206]|[\uE200-\uE206]$/g, "").split(/[\uE200-\uE206]/);
3975
+ return segments
3976
+ .slice(1)
3977
+ .filter((segment) => segment.length > 0 && !referenceToken.test(segment))
3978
+ .join(" ")
3979
+ .trim();
3980
+ }
3909
3981
  /**
3910
3982
  * Turn those markers into ordinary markdown links, so a saved answer keeps the
3911
3983
  * sources instead of the private-use noise (or, as in the rendered DOM, nothing
@@ -3936,11 +4008,13 @@ export function resolveTranscriptCitations(text, references = []) {
3936
4008
  };
3937
4009
  let resolved = text;
3938
4010
  for (const [marker, reference] of byMarker) {
3939
- resolved = resolved.split(marker).join(linksFor(reference));
4011
+ // No items to link is not permission to delete the sentence: fall back to
4012
+ // whatever the marker was wrapping.
4013
+ resolved = resolved.split(marker).join(linksFor(reference) || citationMarkerText(marker));
3940
4014
  }
3941
- // Anything still delimited had no reference to restore: strip it so private-use
3942
- // characters never reach a receipt.
3943
- return resolved.replace(CITATION_MARKER_PATTERN, "");
4015
+ // Anything still delimited had no reference to restore. The delimiters must not
4016
+ // reach a receipt, but the words between them still belong to the answer.
4017
+ return resolved.replace(CITATION_MARKER_PATTERN, (marker) => citationMarkerText(marker));
3944
4018
  }
3945
4019
  export function deepResearchReportExpression(conversationId) {
3946
4020
  return `(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.36.1",
3
+ "version": "0.36.3",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",