@fyeeme/pi-thinking-ui 1.0.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
- ### Added
3
+ ## [1.1.0] - 2026-08-08
4
+
5
+ ### Removed
6
+
7
+ - Internal monkeypatch of pi compiled modules (`internal-patch.ts`, `render.ts`) — thinking rendering no longer imports `dist/modes/interactive/...` internals; it now uses the provided `registerMarkdownTransformer` hook only.
8
+
9
+ ### Changed
10
+
11
+ - Thinking rendering rebuilt on `registerMarkdownTransformer` (new `markdown-render.ts`): `collapsed` → one blockquote line (role icon + summary), `summary` → bullet list of derived step summaries (capped, `… (+N more)` tail), `expanded` → passthrough. Reuses the pure-heuristic `parse.ts` engine.
12
+ - Streaming: the full heuristic derivation now runs only on finalize/restore/resize; during streaming a cheap first-line view is shown for `collapsed` (pi's transformer contract requires synchronous, inexpensive transformers).
13
+ - `state.ts` slimmed: removed patch bookkeeping and the now-unused active-thinking-state / message-scope-ownership subsystems.
14
+ - Mode switching re-renders via the existing `refreshThinkingUI` (`setHiddenThinkingLabel` toggle); mode resolution for rendering is process-global (transformer context carries no message/scope identity).
15
+
16
+ ## [1.0.1] - 2025-07-25
17
+
18
+ ### Changed
19
+ - Default thinking view mode is now `collapsed` instead of `summary`
20
+ - Step-core cache: completed step summaries are memoized by step text, avoiding re-summarization of unchanged steps during streaming (linearizes per-derive summarization cost)
21
+ - Inlined `blocksLengthFingerprint` at its single call site
22
+
23
+ ### Fixed
24
+ - Respect the host `hideThinkingBlock` setting: when thinking is hidden, the extension falls back to Pi's native hidden-label renderer instead of forcing the custom thinking UI
4
25
  - Three-mode thinking visualization: `collapsed`, `summary`, `expanded`
5
26
  - Deterministic step derivation from raw thinking text
6
27
  - Semantic role inference with icons and colors
package/index.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
3
  import { Key } from "@earendil-works/pi-tui";
4
- import { retainThinkingUIPatch } from "./internal-patch.ts";
5
4
  import { clearThinkingUIModePreference, readThinkingUIModePreference, writeThinkingUIModePreference } from "./persistence.ts";
6
5
  import { parseThinkingMode } from "./parse.ts";
7
- import { clearActiveThinkingState, clearThinkingMessageOwnership, getCurrentThinkingScopeKey, getThinkingUIMode, nextThinkingRefreshLabel, recordThinkingMessageScope, registerThinkingPatchRelease, resolveThinkingMessageScope, setActiveThinkingState, setCurrentThinkingScopeKey, setThinkingUIMode, takeThinkingPatchRelease } from "./state.ts";
6
+ import { streamingThinkingMarkdown, thinkingToMarkdown } from "./markdown-render.ts";
7
+ import {
8
+ getThinkingUIMode,
9
+ nextThinkingRefreshLabel,
10
+ setCurrentThinkingScopeKey,
11
+ setThinkingUIMode,
12
+ } from "./state.ts";
8
13
  import type { PersistedThinkingUIPreferenceScope, ThinkingUIMode } from "./types.ts";
9
14
 
10
15
  type ThinkingUICommandScope = "session" | PersistedThinkingUIPreferenceScope;
@@ -63,9 +68,17 @@ async function restoreMode(ctx: ExtensionContext): Promise<ThinkingUIMode> {
63
68
  if (projectMode) return projectMode;
64
69
 
65
70
  const globalMode = await readRestoredModePreference(ctx, "global");
66
- return globalMode ?? "summary";
71
+ return globalMode ?? "collapsed";
67
72
  }
68
73
 
74
+ /**
75
+ * Force every AssistantMessageComponent to re-run updateContent (and thus the
76
+ * markdown transformer) by toggling the hidden-thinking label through pi's
77
+ * ctx.ui proxy. `nextThinkingRefreshLabel` appends/toggles an invisible suffix
78
+ * so the label string changes on every call — without it, setHiddenThinkingLabel
79
+ * would no-op when the visible label is unchanged and the new mode would not
80
+ * take effect until the next natural re-render.
81
+ */
69
82
  function refreshThinkingUI(ctx: ExtensionContext): void {
70
83
  if (!ctx.hasUI) return;
71
84
  setCurrentThinkingScopeKey(ctx.cwd);
@@ -175,28 +188,25 @@ function reportPersistenceError(ctx: ExtensionContext, error: unknown): void {
175
188
  notifyUser(ctx, `Thinking UI persistence error: ${error instanceof Error ? error.message : String(error)}`, "warning");
176
189
  }
177
190
 
178
- function reportPatchError(ctx: ExtensionContext, error: unknown): void {
179
- notifyUser(ctx, `Thinking UI patch error: ${error instanceof Error ? error.message : String(error)}`, "warning");
180
- }
181
-
182
191
  export default function thinkingUIExtension(pi: ExtensionAPI): void {
183
- let sessionScopeKey = getCurrentThinkingScopeKey();
184
- const degradedSessionScopes = new Set<string>();
185
- const setSessionScopeKey = (scopeKey: string): string => {
186
- sessionScopeKey = scopeKey;
187
- setCurrentThinkingScopeKey(scopeKey);
188
- return sessionScopeKey;
189
- };
190
- const markSessionDegraded = (scopeKey: string, degraded: boolean): void => {
191
- if (degraded) {
192
- degradedSessionScopes.add(scopeKey);
193
- return;
194
- }
195
- degradedSessionScopes.delete(scopeKey);
196
- };
197
- const isSessionDegraded = (scopeKey: string): boolean => degradedSessionScopes.has(scopeKey);
198
- const degradedSessionMessage = (): string => "Thinking UI is using Pi's native thinking renderer for this session; live mode switching is disabled.";
199
- const futureCompatibleSessionMessage = (scope: PersistedThinkingUIPreferenceScope, action: "saved" | "cleared"): string => `${action === "saved" ? "Saved" : "Cleared"} ${scope} thinking view default for future compatible sessions; the current session is using Pi's native thinking renderer.`;
192
+ // Render thinking via the provided markdown-transformer hook (no monkeypatch).
193
+ // The native Markdown renderer draws whatever we return, styled as thinking
194
+ // text. Re-runs on streaming, finalize, restore, resize, and whenever
195
+ // refreshThinkingUI toggles the hidden-thinking label (mode switch).
196
+ //
197
+ // Scope caveat: MarkdownTransformContext carries no message/scope identity, so
198
+ // the transformer resolves mode from the process-global currentScopeKey. The
199
+ // per-scope mode map still serves the command/shortcut handlers (which have
200
+ // ctx.cwd); only rendering is process-global. Fine while pi is single-session
201
+ // interactive; a host-side scope id would be needed to fix (out of scope here).
202
+ pi.registerMarkdownTransformer((markdown, context) => {
203
+ if (context.messageType !== "assistant-thinking") return markdown;
204
+ const mode = getThinkingUIMode();
205
+ // Heavy heuristic derivation only on finalize/restore/resize; during
206
+ // streaming (every chunk) stay cheap per pi's transformer contract.
207
+ if (context.isStreaming) return streamingThinkingMarkdown(markdown, mode);
208
+ return thinkingToMarkdown(markdown, mode);
209
+ });
200
210
 
201
211
  pi.registerCommand("thinking-ui", {
202
212
  description: "Switch thinking view or set/clear project/global defaults",
@@ -208,7 +218,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
208
218
  return;
209
219
  }
210
220
 
211
- const degraded = isSessionDegraded(ctx.cwd);
212
221
  if (action.type === "clear") {
213
222
  try {
214
223
  await clearThinkingUIModePreference(action.scope, ctx.cwd);
@@ -217,11 +226,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
217
226
  return;
218
227
  }
219
228
 
220
- if (degraded) {
221
- notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "cleared"), "info");
222
- return;
223
- }
224
-
225
229
  refreshThinkingUI(ctx);
226
230
  notifyUser(ctx, `Cleared ${action.scope} thinking view default`, "info");
227
231
  return;
@@ -241,15 +245,6 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
241
245
  }
242
246
  }
243
247
 
244
- if (degraded) {
245
- if (action.scope === "session") {
246
- notifyUser(ctx, degradedSessionMessage(), "warning");
247
- return;
248
- }
249
- notifyUser(ctx, futureCompatibleSessionMessage(action.scope, "saved"), "info");
250
- return;
251
- }
252
-
253
248
  applyMode(pi, ctx, selectedMode, { announceScope: action.scope });
254
249
  },
255
250
  });
@@ -257,104 +252,21 @@ export default function thinkingUIExtension(pi: ExtensionAPI): void {
257
252
  pi.registerShortcut(Key.alt("t"), {
258
253
  description: "Cycle thinking view (collapsed, summary, expanded)",
259
254
  handler: async (ctx) => {
260
- if (isSessionDegraded(ctx.cwd)) {
261
- notifyUser(ctx, degradedSessionMessage(), "warning");
262
- return;
263
- }
264
255
  const nextMode = cycleMode(getThinkingUIMode(ctx.cwd));
265
256
  applyMode(pi, ctx, nextMode, { announceScope: "session" });
266
257
  },
267
258
  });
268
259
 
269
260
  pi.on("session_start", async (_event, ctx) => {
270
- const activeScopeKey = setSessionScopeKey(ctx.cwd);
271
- clearActiveThinkingState(undefined, activeScopeKey);
272
- try {
273
- registerThinkingPatchRelease(activeScopeKey, await retainThinkingUIPatch());
274
- markSessionDegraded(activeScopeKey, false);
275
- } catch (error) {
276
- markSessionDegraded(activeScopeKey, true);
277
- reportPatchError(ctx, error);
278
- notifyUser(ctx, degradedSessionMessage(), "warning");
279
- return;
280
- }
281
-
261
+ setCurrentThinkingScopeKey(ctx.cwd);
282
262
  const restoredMode = await restoreMode(ctx);
283
263
  applyMode(pi, ctx, restoredMode, { persistSession: false });
284
264
  });
285
265
 
286
- pi.on("message_start", async (event) => {
287
- if (event.message.role === "assistant") {
288
- recordThinkingMessageScope(event.message, sessionScopeKey);
289
- const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
290
- const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
291
- ? (event.message as { timestamp: number }).timestamp
292
- : undefined;
293
- clearActiveThinkingState(timestamp, ownerScopeKey);
294
- }
295
- });
296
-
297
- pi.on("message_update", async (event) => {
298
- if (event.message.role !== "assistant") return;
299
- recordThinkingMessageScope(event.message, sessionScopeKey);
300
- const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
301
- const assistantEvent = event.assistantMessageEvent;
302
- if (assistantEvent.type === "thinking_start" || assistantEvent.type === "thinking_delta") {
303
- setActiveThinkingState({
304
- active: true,
305
- messageTimestamp: event.message.timestamp,
306
- contentIndex: assistantEvent.contentIndex,
307
- }, ownerScopeKey);
308
- return;
309
- }
310
-
311
- if (
312
- assistantEvent.type === "thinking_end" ||
313
- assistantEvent.type === "text_start" ||
314
- assistantEvent.type === "text_delta" ||
315
- assistantEvent.type === "text_end" ||
316
- assistantEvent.type === "toolcall_start" ||
317
- assistantEvent.type === "toolcall_delta" ||
318
- assistantEvent.type === "toolcall_end"
319
- ) {
320
- clearActiveThinkingState(event.message.timestamp, ownerScopeKey);
321
- }
322
- });
323
-
324
- pi.on("message_end", async (event) => {
325
- if (event.message.role === "assistant") {
326
- recordThinkingMessageScope(event.message, sessionScopeKey);
327
- const ownerScopeKey = resolveThinkingMessageScope(event.message, sessionScopeKey);
328
- const timestamp = typeof (event.message as { timestamp?: unknown }).timestamp === "number"
329
- ? (event.message as { timestamp: number }).timestamp
330
- : undefined;
331
- clearActiveThinkingState(timestamp, ownerScopeKey);
332
- }
333
- });
334
-
335
- pi.on("agent_end", async () => {
336
- clearActiveThinkingState(undefined, sessionScopeKey);
337
- });
338
-
339
266
  pi.on("session_shutdown", async (_event, ctx) => {
340
- const activeScopeKey = setSessionScopeKey(ctx.cwd);
341
- clearActiveThinkingState(undefined, activeScopeKey);
342
- clearThinkingMessageOwnership(activeScopeKey);
343
- markSessionDegraded(activeScopeKey, false);
267
+ setCurrentThinkingScopeKey(ctx.cwd);
344
268
  if (ctx.hasUI) {
345
269
  ctx.ui.setStatus("thinking-ui", undefined);
346
270
  }
347
-
348
- const releasePatch = takeThinkingPatchRelease(activeScopeKey);
349
- if (!releasePatch) {
350
- return;
351
- }
352
-
353
- try {
354
- await releasePatch();
355
- } catch (error) {
356
- registerThinkingPatchRelease(activeScopeKey, releasePatch);
357
- reportPatchError(ctx, error);
358
- }
359
271
  });
360
272
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * thinking-ui markdown emitter.
3
+ *
4
+ * Converts raw thinking text into the markdown string the registered
5
+ * transformer returns, per the current ThinkingUIMode. Reuses the pure
6
+ * heuristic engine in parse.ts (summarize / split / role) — no LLM, no async.
7
+ *
8
+ * Trade-off vs the removed ThinkingUIComponent (ANSI tree): the native
9
+ * Markdown renderer draws whatever markdown we return, styled as thinking
10
+ * text (dim/italic). The bespoke connector tree (├─└─) and live activity
11
+ * pulse are gone; the summary text + per-step role icons survive.
12
+ */
13
+ import type { ThinkingUIMode } from "./types.ts";
14
+ import { deriveStepCore, splitThinkingIntoStepTexts } from "./parse.ts";
15
+
16
+ /** Cap the summary-mode list so a long thinking run stays readable. */
17
+ const SUMMARY_LIST_LIMIT = 8;
18
+ /** Max chars of the first line shown for collapsed mode while streaming. */
19
+ const STREAMING_LINE_MAX = 80;
20
+
21
+ /**
22
+ * LRU result cache for the finalize-path derivation. The transformer re-runs
23
+ * on every re-render where the cached width changed (e.g. terminal resize) —
24
+ * the thinking text is unchanged, so the result is fully reusable. Keyed by
25
+ * (mode, text) since thinkingToMarkdown output does not depend on width.
26
+ * Bounded to keep memory in check (each key holds a copy of the thinking text).
27
+ */
28
+ const RESULT_CACHE_LIMIT = 50;
29
+ const resultCache = new Map<string, string>();
30
+
31
+ /** One blockquote line: role icon + summary. Derived via the fused parse.ts core. */
32
+ function collapseLine(text: string): string {
33
+ const core = deriveStepCore(text);
34
+ return `> ${core.icon} ${core.summary}`;
35
+ }
36
+
37
+ /**
38
+ * Cheap view while the assistant message is still streaming, before the full
39
+ * heuristic derivation runs on finalize. collapsed → first line (keeps the
40
+ * "hide the bulk" intent without the O(n²) MMR cost); summary/expanded → pass
41
+ * through so the user can read the live stream. The transformer must stay
42
+ * "synchronous and inexpensive" (pi docs) and runs on every streaming chunk.
43
+ */
44
+ export function streamingThinkingMarkdown(markdown: string, mode: ThinkingUIMode): string {
45
+ if (mode !== "collapsed") return markdown;
46
+ const line = markdown.split("\n").map((l) => l.trim()).find(Boolean);
47
+ if (!line) return markdown;
48
+ return `> ${line.length > STREAMING_LINE_MAX ? `${line.slice(0, STREAMING_LINE_MAX - 1)}…` : line}`;
49
+ }
50
+
51
+ /**
52
+ * Transform a run of thinking markdown for the given mode (finalize/restore).
53
+ *
54
+ * - expanded: pass through verbatim (native renderer handles full text).
55
+ * - collapsed: one blockquote line — role icon + the single best summary.
56
+ * - summary: a markdown bullet list of derived step summaries (icon + summary),
57
+ * capped at SUMMARY_LIST_LIMIT with a `… (+N more)` tail.
58
+ *
59
+ * Result is memoized per (mode, text) — the expensive MMR derivation is not
60
+ * re-run for unchanged text across re-renders (resize, restore, …).
61
+ */
62
+ export function thinkingToMarkdown(markdown: string, mode: ThinkingUIMode): string {
63
+ const cacheKey = `${mode}\u0000${markdown}`;
64
+ const cached = resultCache.get(cacheKey);
65
+ if (cached !== undefined) {
66
+ // LRU refresh: move to the newest slot.
67
+ resultCache.delete(cacheKey);
68
+ resultCache.set(cacheKey, cached);
69
+ return cached;
70
+ }
71
+
72
+ const result = computeThinkingToMarkdown(markdown, mode);
73
+
74
+ resultCache.set(cacheKey, result);
75
+ if (resultCache.size > RESULT_CACHE_LIMIT) {
76
+ const oldestKey = resultCache.keys().next().value;
77
+ if (oldestKey !== undefined) {
78
+ resultCache.delete(oldestKey);
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+
84
+ function computeThinkingToMarkdown(markdown: string, mode: ThinkingUIMode): string {
85
+ if (mode === "expanded") return markdown;
86
+
87
+ const text = markdown.trim();
88
+ if (!text) return markdown;
89
+
90
+ if (mode === "collapsed") return collapseLine(text);
91
+
92
+ const steps = splitThinkingIntoStepTexts(text);
93
+ if (steps.length === 0) return collapseLine(text);
94
+
95
+ const visible = steps.slice(0, SUMMARY_LIST_LIMIT);
96
+ const lines = visible.map((step) => {
97
+ const core = deriveStepCore(step);
98
+ return `- ${core.icon} ${core.summary}`;
99
+ });
100
+ if (steps.length > visible.length) {
101
+ lines.push(`- … (+${steps.length - visible.length} more)`);
102
+ }
103
+ return lines.join("\n");
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-thinking-ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Faithful, terminal-native three-mode thinking visualization for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,10 +28,9 @@
28
28
  ],
29
29
  "files": [
30
30
  "index.ts",
31
- "internal-patch.ts",
31
+ "markdown-render.ts",
32
32
  "parse.ts",
33
33
  "persistence.ts",
34
- "render.ts",
35
34
  "state.ts",
36
35
  "types.ts",
37
36
  "README.md",
@@ -48,14 +47,14 @@
48
47
  "typecheck": "tsc"
49
48
  },
50
49
  "peerDependencies": {
51
- "@earendil-works/pi-coding-agent": ">=0.77.0",
52
- "@earendil-works/pi-ai": ">=0.77.0",
53
- "@earendil-works/pi-tui": ">=0.77.0"
50
+ "@earendil-works/pi-coding-agent": ">=0.84.1",
51
+ "@earendil-works/pi-ai": ">=0.84.1",
52
+ "@earendil-works/pi-tui": ">=0.84.1"
54
53
  },
55
54
  "devDependencies": {
56
- "@earendil-works/pi-coding-agent": "0.77.0",
57
- "@earendil-works/pi-ai": "0.77.0",
58
- "@earendil-works/pi-tui": "0.77.0",
55
+ "@earendil-works/pi-coding-agent": "0.84.1",
56
+ "@earendil-works/pi-ai": "0.84.1",
57
+ "@earendil-works/pi-tui": "0.84.1",
59
58
  "@types/node": "22.19.19",
60
59
  "typescript": "5.9.3"
61
60
  }
package/parse.ts CHANGED
@@ -1086,7 +1086,38 @@ export function iconForThinkingRole(role: ThinkingSemanticRole): string {
1086
1086
  }
1087
1087
  }
1088
1088
 
1089
- export function deriveThinkingUI(blocks: ThinkingSourceBlock[]): DerivedThinkingStep[] {
1089
+ export interface DerivedStepCore {
1090
+ summary: string;
1091
+ baselineSummary: string;
1092
+ challengerSummary: string;
1093
+ summaryEvents: ThinkingSummaryEvent[];
1094
+ collapsedPriority: number;
1095
+ hasExplicitFailure: boolean;
1096
+ hasExplicitSuccess: boolean;
1097
+ role: ThinkingSemanticRole;
1098
+ icon: string;
1099
+ }
1100
+
1101
+ export function deriveStepCore(stepText: string): DerivedStepCore {
1102
+ const summaryDetails = summarizeThinkingTextDetailed(stepText);
1103
+ const role = inferThinkingRole(`${summaryDetails.summary}\n${stepText}`);
1104
+ return {
1105
+ summary: summaryDetails.summary,
1106
+ baselineSummary: summaryDetails.baselineSummary,
1107
+ challengerSummary: summaryDetails.challengerSummary,
1108
+ summaryEvents: summaryDetails.events,
1109
+ collapsedPriority: summaryDetails.collapsedPriority,
1110
+ hasExplicitFailure: summaryDetails.hasExplicitFailure,
1111
+ hasExplicitSuccess: summaryDetails.hasExplicitSuccess,
1112
+ role,
1113
+ icon: iconForThinkingRole(role),
1114
+ };
1115
+ }
1116
+
1117
+ export function deriveThinkingUI(
1118
+ blocks: ThinkingSourceBlock[],
1119
+ resolveCore: (stepText: string) => DerivedStepCore = deriveStepCore,
1120
+ ): DerivedThinkingStep[] {
1090
1121
  const steps: DerivedThinkingStep[] = [];
1091
1122
  blocks.forEach((block, blockIndex) => {
1092
1123
  if (block.redacted && !block.text.trim()) {
@@ -1112,23 +1143,22 @@ export function deriveThinkingUI(blocks: ThinkingSourceBlock[]): DerivedThinking
1112
1143
 
1113
1144
  const stepTexts = splitThinkingIntoStepTexts(block.text);
1114
1145
  stepTexts.forEach((stepText, stepIndex) => {
1115
- const summaryDetails = summarizeThinkingTextDetailed(stepText);
1116
- const role = inferThinkingRole(`${summaryDetails.summary}\n${stepText}`);
1146
+ const core = resolveCore(stepText);
1117
1147
  steps.push({
1118
1148
  id: `${block.contentIndex}-${stepIndex}`,
1119
1149
  contentIndex: block.contentIndex,
1120
1150
  blockIndex,
1121
1151
  stepIndex,
1122
- summary: summaryDetails.summary,
1152
+ summary: core.summary,
1123
1153
  body: stepText.trim(),
1124
- role,
1125
- icon: iconForThinkingRole(role),
1126
- baselineSummary: summaryDetails.baselineSummary,
1127
- challengerSummary: summaryDetails.challengerSummary,
1128
- summaryEvents: summaryDetails.events,
1129
- collapsedPriority: summaryDetails.collapsedPriority,
1130
- hasExplicitFailure: summaryDetails.hasExplicitFailure,
1131
- hasExplicitSuccess: summaryDetails.hasExplicitSuccess,
1154
+ role: core.role,
1155
+ icon: core.icon,
1156
+ baselineSummary: core.baselineSummary,
1157
+ challengerSummary: core.challengerSummary,
1158
+ summaryEvents: core.summaryEvents,
1159
+ collapsedPriority: core.collapsedPriority,
1160
+ hasExplicitFailure: core.hasExplicitFailure,
1161
+ hasExplicitSuccess: core.hasExplicitSuccess,
1132
1162
  });
1133
1163
  });
1134
1164
  });