@things-factory/figure-ui 10.1.15 → 10.1.16

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.
@@ -147,32 +147,31 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
147
147
  detail: { figureId: this.figure?.id, source: this.folded }
148
148
  }));
149
149
  };
150
- this.acceptDockProposal = (event) => {
150
+ /*
151
+ `detail.staged` is the dock's answer to "did anything actually happen to my candidate". The
152
+ listener is synchronous, so the dock reads it right after dispatching and only then tells the
153
+ chat card to say so. It used to be called `accepted`, which claimed more than it knew — the
154
+ page had put the candidate somewhere, not the person had agreed to it.
155
+ */
156
+ this.receiveDockProposal = (event) => {
151
157
  const detail = event.detail;
152
158
  if (!detail)
153
159
  return;
154
- if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) {
155
- try {
156
- const parsedBase = typeof detail.baseSource === 'string' ? JSON.parse(detail.baseSource) : detail.baseSource;
157
- if (parsedBase?.type && this.folded?.type && parsedBase.type !== this.folded.type) {
158
- // type 불일치 시에만 차단하고 그 외 미세 차이는 수용
159
- }
160
- }
161
- catch {
162
- // ignore
163
- }
164
- }
165
- const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
160
+ const source = detail?.source ||
161
+ (detail?.proposal?.source
162
+ ? typeof detail.proposal.source === 'string'
163
+ ? JSON.parse(detail.proposal.source)
164
+ : detail.proposal.source
165
+ : undefined);
166
166
  if (source) {
167
- this.applySourceDirectly(source, detail.proposal);
168
- detail.accepted = true;
167
+ detail.staged = this.openProposalForReview(source, detail.proposal);
169
168
  }
170
169
  };
171
170
  this.onFigureAiStage = (event) => {
172
171
  const detail = event.detail;
173
172
  const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
174
173
  if (source) {
175
- this.applySourceDirectly(source, detail.proposal);
174
+ this.openProposalForReview(source, detail.proposal);
176
175
  }
177
176
  };
178
177
  this.onSaveRequest = () => {
@@ -727,36 +726,73 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
727
726
  }));
728
727
  }
729
728
  }
730
- applySourceDirectly(rawSource, proposal) {
729
+ /**
730
+ * A candidate from the dock opens the review lane. It does not become the draft.
731
+ *
732
+ * This used to write `board` and `parts` straight onto the working draft and then set
733
+ * `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
734
+ * erased the lane built to review it. There was no moment where the person said yes, and a
735
+ * draft has no version history to go back to, so the previous work was simply gone.
736
+ *
737
+ * `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
738
+ * and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
739
+ * `figure-ask` entrance already honoured it through `openAiProposal`; the dock entrance is now
740
+ * the same door. Applying is what the take button does, from the lane, per change.
741
+ *
742
+ * ## Why the overwrite was there — so it does not come back
743
+ *
744
+ * The dock opened the review lane originally. Before `c42a0b63df` this handler read:
745
+ *
746
+ * if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
747
+ * if (detail?.draftType && detail.draftType !== this.folded?.type) return
748
+ * if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
749
+ * this.receive(detail.proposal)
750
+ *
751
+ * Three guards, each a silent `return`, and the last compared a serialized source byte for byte.
752
+ * Any difference that changed nothing at all dropped the candidate with no message, so the dock
753
+ * would report a candidate and the screen would show none.
754
+ *
755
+ * The `ox-assistant-chat` migration fixed that by replacing the path rather than the guards, and
756
+ * the review lane went out with them — `applySourceDirectly` wrote straight to the draft, and
757
+ * cleared `proposalSession` on the way past. **Nobody decided that reviews were unwanted here.**
758
+ * It was the cost of loosening a guard by deleting what stood around it.
759
+ *
760
+ * So the guards are gone and stay gone, and the lane is back. If a candidate ever seems not to
761
+ * arrive, the thing to look for is a silent `return` on this path — not the lane.
762
+ *
763
+ * Returns whether the lane opened, so the dock can tell the chat whether to say it staged.
764
+ */
765
+ openProposalForReview(rawSource, proposal) {
731
766
  const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource;
732
767
  if (!source || !Array.isArray(source.parts))
733
- return;
734
- this.recordHistory();
768
+ return false;
735
769
  try {
736
- const { board, parts } = fromFigureSource(source);
737
- this.board = board;
738
- this.parts = parts;
739
- this.dirty = true;
740
- this.selected = parts.length ? 0 : -1;
741
- if (source.type) {
742
- this.figure = { ...(this.figure || {}), type: source.type };
743
- if (this.board) {
744
- this.board = { ...this.board, figureType: source.type };
745
- }
746
- }
747
- this.proposalSession = undefined;
748
- this.sceneRevision++;
770
+ /*
771
+ The dock sends a FigureProposal whose metrics sit at the top level; the staged-on-navigate
772
+ path carries the raw tool result, which nests them under score/cost. Read both rather than
773
+ making one of the two entrances reshape its payload.
774
+ */
775
+ this.proposalSession = proposalSessions.openAiProposal(this.folded, {
776
+ source: JSON.stringify(source),
777
+ grade: proposal?.grade ?? proposal?.score?.grade,
778
+ triangles: proposal?.triangles ?? proposal?.cost?.triangles,
779
+ groups: proposal?.groups ?? proposal?.cost?.groups,
780
+ attempts: proposal?.attempts,
781
+ quality: proposal?.quality
782
+ });
783
+ this.mode = 'edit';
749
784
  this.requestUpdate();
750
- document.dispatchEvent(new CustomEvent('notify', { detail: { message: `${source.type || 'Figure'} 모델이 3D 뷰어에 반영되었습니다.` } }));
785
+ return true;
751
786
  }
752
787
  catch (e) {
753
- console.error('[FigureModeller] Failed to apply AI proposal source:', e);
788
+ console.error('[FigureModeller] Failed to open the AI candidate for review:', e);
789
+ return false;
754
790
  }
755
791
  }
756
792
  connectedCallback() {
757
793
  super.connectedCallback();
758
794
  window.addEventListener('figure-ai-context-request', this.announceAiContext);
759
- window.addEventListener('figure-ai-review', this.acceptDockProposal);
795
+ window.addEventListener('figure-ai-review', this.receiveDockProposal);
760
796
  window.addEventListener('figure-ai-stage', this.onFigureAiStage);
761
797
  window.addEventListener('figure-save-request', this.onSaveRequest);
762
798
  window.addEventListener('keydown', this.handleKeyDown);
@@ -766,7 +802,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
766
802
  window.removeEventListener('figure-save-request', this.onSaveRequest);
767
803
  window.removeEventListener('figure-ai-stage', this.onFigureAiStage);
768
804
  window.removeEventListener('figure-ai-context-request', this.announceAiContext);
769
- window.removeEventListener('figure-ai-review', this.acceptDockProposal);
805
+ window.removeEventListener('figure-ai-review', this.receiveDockProposal);
770
806
  window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
771
807
  super.disconnectedCallback();
772
808
  }
@@ -1216,7 +1252,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1216
1252
  }
1217
1253
  const pending = consumePendingFigureProposal();
1218
1254
  if (pending?.source) {
1219
- this.applySourceDirectly(pending.source, pending.proposal);
1255
+ this.openProposalForReview(pending.source, pending.proposal);
1220
1256
  }
1221
1257
  return;
1222
1258
  }
@@ -1236,7 +1272,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1236
1272
  this.startNew();
1237
1273
  const pending = consumePendingFigureProposal();
1238
1274
  if (pending?.source) {
1239
- this.applySourceDirectly(pending.source, pending.proposal);
1275
+ this.openProposalForReview(pending.source, pending.proposal);
1240
1276
  }
1241
1277
  return;
1242
1278
  }
@@ -1244,7 +1280,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1244
1280
  this.startNew();
1245
1281
  const pending = consumePendingFigureProposal();
1246
1282
  if (pending?.source) {
1247
- this.applySourceDirectly(pending.source, pending.proposal);
1283
+ this.openProposalForReview(pending.source, pending.proposal);
1248
1284
  }
1249
1285
  return;
1250
1286
  }
@@ -1252,13 +1288,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1252
1288
  this.startNew();
1253
1289
  const pending = consumePendingFigureProposal();
1254
1290
  if (pending?.source) {
1255
- this.applySourceDirectly(pending.source, pending.proposal);
1291
+ this.openProposalForReview(pending.source, pending.proposal);
1256
1292
  }
1257
1293
  return;
1258
1294
  }
1259
1295
  const pending = consumePendingFigureProposal();
1260
1296
  if (pending?.source) {
1261
- this.applySourceDirectly(pending.source, pending.proposal);
1297
+ this.openProposalForReview(pending.source, pending.proposal);
1262
1298
  }
1263
1299
  }
1264
1300
  async load(id) {