u-foo 2.4.2 → 2.4.4
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/package.json +1 -1
- package/src/ui/ink/ChatApp.js +43 -25
- package/src/ui/ink/MultilineInput.js +10 -2
- package/src/ui/ink/UcodeApp.js +35 -4
- package/src/ui/ink/chatReducer.js +81 -7
package/package.json
CHANGED
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -407,11 +407,11 @@ function classifyChatLogLine(text = "") {
|
|
|
407
407
|
const speaker = dotMatch[1].trim();
|
|
408
408
|
const lower = speaker.toLowerCase();
|
|
409
409
|
const kind = lower === "ufoo" ? "assistant" : "agent";
|
|
410
|
-
return { kind, marker: kind === "assistant" ? "◆" : "
|
|
410
|
+
return { kind, marker: kind === "assistant" ? "◆" : "•", speaker, body: dotMatch[2] || " " };
|
|
411
411
|
}
|
|
412
412
|
const colonMatch = clean.match(/^([A-Za-z0-9_.:@/-]{1,42}):\s+(.*)$/);
|
|
413
413
|
if (colonMatch) {
|
|
414
|
-
return { kind: "agent", marker: "
|
|
414
|
+
return { kind: "agent", marker: "•", speaker: colonMatch[1], body: colonMatch[2] || " " };
|
|
415
415
|
}
|
|
416
416
|
if (/^(CHAT|UCODE)\s+·/i.test(trimmed)) {
|
|
417
417
|
return { kind: "meta", marker: "·", speaker: "", body: clean };
|
|
@@ -419,6 +419,16 @@ function classifyChatLogLine(text = "") {
|
|
|
419
419
|
return { kind: "plain", marker: "│", speaker: "", body: clean };
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
function buildChatLogLineModel(text = "") {
|
|
423
|
+
const row = classifyChatLogLine(text);
|
|
424
|
+
const hasSpeaker = Boolean(row.speaker);
|
|
425
|
+
return {
|
|
426
|
+
...row,
|
|
427
|
+
markerText: hasSpeaker ? `${row.marker || " "} ` : `${row.marker || " "} `,
|
|
428
|
+
bodyText: row.body || " ",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
422
432
|
function createInkStreamState({
|
|
423
433
|
dispatch,
|
|
424
434
|
appendHistory,
|
|
@@ -1010,8 +1020,14 @@ function inferStatusType(text = "", requestedType = "") {
|
|
|
1010
1020
|
const type = String(requestedType || "").trim().toLowerCase();
|
|
1011
1021
|
if (type === "done" || type === "success" || type === "error" || type === "idle") return type;
|
|
1012
1022
|
const clean = stripBlessedTags(String(text || "")).trim();
|
|
1013
|
-
if (/^[
|
|
1014
|
-
if (
|
|
1023
|
+
if (/^[✗!]/.test(clean) || /\b(error|failed|failure|offline)\b/i.test(clean) || /失败|错误/.test(clean)) return "error";
|
|
1024
|
+
if (
|
|
1025
|
+
/^[✓✔]/.test(clean) ||
|
|
1026
|
+
/^(done|closed|complete|completed|finished|success|succeeded|ready)\b/i.test(clean) ||
|
|
1027
|
+
/\b(processed|reconnected|switched|saved)\b/i.test(clean) ||
|
|
1028
|
+
/\bdone\s*$/i.test(clean) ||
|
|
1029
|
+
/完成|成功|已处理|已保存|已切换|已连接/.test(clean)
|
|
1030
|
+
) return "done";
|
|
1015
1031
|
return type || "typing";
|
|
1016
1032
|
}
|
|
1017
1033
|
|
|
@@ -1123,12 +1139,13 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1123
1139
|
dispatch({ type: "status/idle" });
|
|
1124
1140
|
return;
|
|
1125
1141
|
}
|
|
1142
|
+
const type = inferStatusType(clean, options.type || "typing");
|
|
1126
1143
|
dispatch({
|
|
1127
1144
|
type: "status/set",
|
|
1128
1145
|
payload: {
|
|
1129
1146
|
message: clean,
|
|
1130
|
-
type
|
|
1131
|
-
showTimer: options.showTimer === true,
|
|
1147
|
+
type,
|
|
1148
|
+
showTimer: options.showTimer === true && isAnimatedStatusType(type),
|
|
1132
1149
|
startedAt: options.startedAt || Date.now(),
|
|
1133
1150
|
},
|
|
1134
1151
|
});
|
|
@@ -1312,8 +1329,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1312
1329
|
|
|
1313
1330
|
useEffect(() => {
|
|
1314
1331
|
if (!stdout) return undefined;
|
|
1315
|
-
const update = () =>
|
|
1316
|
-
|
|
1332
|
+
const update = () => {
|
|
1333
|
+
const next = { cols: stdout.columns || 0, rows: stdout.rows || 0 };
|
|
1334
|
+
setSize((prev) => (prev.cols === next.cols && prev.rows === next.rows ? prev : next));
|
|
1335
|
+
};
|
|
1317
1336
|
update();
|
|
1318
1337
|
stdout.on("resize", update);
|
|
1319
1338
|
return () => stdout.off("resize", update);
|
|
@@ -1881,7 +1900,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1881
1900
|
useEffect(() => {
|
|
1882
1901
|
const internalStatus = state.viewingAgentId ? internalStatusLabel(internalAgentView.status) : "ready";
|
|
1883
1902
|
const internalActive = internalStatus !== "ready";
|
|
1884
|
-
const
|
|
1903
|
+
const statusType = inferStatusType(state.status.message, state.status.type);
|
|
1904
|
+
const statusAnimated = state.status.message && isAnimatedStatusType(statusType);
|
|
1885
1905
|
if ((!statusAnimated) && !internalActive) return undefined;
|
|
1886
1906
|
const timer = setInterval(() => setSpinnerTick((t) => t + 1), 100);
|
|
1887
1907
|
return () => clearInterval(timer);
|
|
@@ -1922,13 +1942,10 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
1922
1942
|
dispatch({ type: "log/append", text: `Error: ${err && err.message ? err.message : err}` });
|
|
1923
1943
|
}
|
|
1924
1944
|
if (statusText) {
|
|
1925
|
-
|
|
1926
|
-
type: "status/set",
|
|
1927
|
-
payload: { message: statusText, type: "typing", showTimer: false, startedAt: Date.now() },
|
|
1928
|
-
});
|
|
1945
|
+
setStatusText(statusText, { type: "typing", showTimer: false });
|
|
1929
1946
|
}
|
|
1930
1947
|
if (restart) restartDaemonBestEffort();
|
|
1931
|
-
}, [restartDaemonBestEffort]);
|
|
1948
|
+
}, [restartDaemonBestEffort, setStatusText]);
|
|
1932
1949
|
|
|
1933
1950
|
const clearUfooAgentIdentity = useCallback(() => {
|
|
1934
1951
|
try {
|
|
@@ -3241,7 +3258,7 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3241
3258
|
}
|
|
3242
3259
|
|
|
3243
3260
|
const renderChatLogLine = (item) => {
|
|
3244
|
-
const row =
|
|
3261
|
+
const row = buildChatLogLineModel((item && item.text) || "");
|
|
3245
3262
|
const key = item && item.id ? item.id : `log-${row.body}`;
|
|
3246
3263
|
if (row.kind === "spacer") {
|
|
3247
3264
|
return h(Text, { key, color: "gray" }, " ");
|
|
@@ -3268,16 +3285,16 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
3268
3285
|
);
|
|
3269
3286
|
}
|
|
3270
3287
|
return h(Box, { key, width: "100%", marginBottom: 1 },
|
|
3271
|
-
h(
|
|
3272
|
-
|
|
3288
|
+
h(Text, { color: colors.marker, bold: row.kind === "error" }, row.markerText),
|
|
3289
|
+
h(Text, { color: colors.body, wrap: "wrap" },
|
|
3290
|
+
row.speaker
|
|
3291
|
+
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3292
|
+
: null,
|
|
3293
|
+
row.speaker
|
|
3294
|
+
? h(Text, { color: "gray" }, " · ")
|
|
3295
|
+
: null,
|
|
3296
|
+
row.bodyText,
|
|
3273
3297
|
),
|
|
3274
|
-
row.speaker
|
|
3275
|
-
? h(Text, { color: colors.speaker, bold: colors.bold }, row.speaker)
|
|
3276
|
-
: null,
|
|
3277
|
-
row.speaker
|
|
3278
|
-
? h(Text, { color: "gray" }, " · ")
|
|
3279
|
-
: null,
|
|
3280
|
-
h(Text, { color: colors.body, wrap: "wrap" }, row.body || " "),
|
|
3281
3298
|
);
|
|
3282
3299
|
};
|
|
3283
3300
|
|
|
@@ -3501,7 +3518,7 @@ function buildDashHints(state, targetAgentLabel) {
|
|
|
3501
3518
|
function computeStatusText(status, spinnerTick) {
|
|
3502
3519
|
const message = String((status && status.message) || "");
|
|
3503
3520
|
if (!message) return "CHAT · Ready";
|
|
3504
|
-
const type =
|
|
3521
|
+
const type = inferStatusType(message, status && status.type);
|
|
3505
3522
|
if (type === "done" || type === "success") {
|
|
3506
3523
|
const clean = stripBlessedTags(message).trim();
|
|
3507
3524
|
return /^[✓✔]/.test(clean) ? clean : `✓ ${clean}`;
|
|
@@ -3656,6 +3673,7 @@ module.exports = {
|
|
|
3656
3673
|
buildPromptIpcRequest,
|
|
3657
3674
|
chatHistoryOptionsForScope,
|
|
3658
3675
|
classifyChatLogLine,
|
|
3676
|
+
buildChatLogLineModel,
|
|
3659
3677
|
createInkMultiWindowToggle,
|
|
3660
3678
|
resolveActiveAgentId,
|
|
3661
3679
|
resolveInjectSockPathForAgent,
|
|
@@ -482,11 +482,19 @@ function createMultilineInput({ React, ink }) {
|
|
|
482
482
|
return undefined;
|
|
483
483
|
}
|
|
484
484
|
patchStdoutForIME(out);
|
|
485
|
+
const targetRowsUp = __imeCursor.lastFrameHadNewline
|
|
486
|
+
? rowsBelowCursor
|
|
487
|
+
: Math.max(0, rowsBelowCursor - 1);
|
|
488
|
+
const alreadyParked = __imeCursor.active === true
|
|
489
|
+
&& __imeCursor.parkRowsUp === rowsBelowCursor
|
|
490
|
+
&& __imeCursor.parkCol === cursorTermCol
|
|
491
|
+
&& __imeCursor.movedUpRows === targetRowsUp;
|
|
485
492
|
// Publish the desired park target so the stdout monkey-patch can
|
|
486
493
|
// re-park after every throttled ink frame write.
|
|
487
494
|
__imeCursor.active = true;
|
|
488
495
|
__imeCursor.parkRowsUp = rowsBelowCursor;
|
|
489
496
|
__imeCursor.parkCol = cursorTermCol;
|
|
497
|
+
if (alreadyParked) return undefined;
|
|
490
498
|
// Park immediately — covers cases where ink has nothing to render
|
|
491
499
|
// (output unchanged) and won't fire a frame write at all, and keeps
|
|
492
500
|
// the caret visible between frames. Combine hide + restore + park +
|
|
@@ -496,7 +504,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
496
504
|
// CRITICAL: the move-up amount must match the anchor that movedUpRows
|
|
497
505
|
// was measured against. If the last frame ended without '\n' (the
|
|
498
506
|
// full-screen path), the anchor is one row higher than the log-update
|
|
499
|
-
// case, so we use
|
|
507
|
+
// case, so we use targetRowsUp rather than parkRowsUp directly.
|
|
500
508
|
// Otherwise restoring down by movedUpRows then moving up parkRowsUp
|
|
501
509
|
// overshoots by one and leaves the hardware cursor one row above the
|
|
502
510
|
// inverse caret — the residual "ghost cursor" symptom.
|
|
@@ -505,7 +513,7 @@ function createMultilineInput({ React, ink }) {
|
|
|
505
513
|
combined += `\x1b[${__imeCursor.movedUpRows}B`;
|
|
506
514
|
__imeCursor.movedUpRows = 0;
|
|
507
515
|
}
|
|
508
|
-
combined += applyParkSequence(
|
|
516
|
+
combined += applyParkSequence(targetRowsUp);
|
|
509
517
|
out.write(combined);
|
|
510
518
|
return undefined;
|
|
511
519
|
});
|
package/src/ui/ink/UcodeApp.js
CHANGED
|
@@ -643,8 +643,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
643
643
|
|
|
644
644
|
useEffect(() => {
|
|
645
645
|
if (!stdout) return undefined;
|
|
646
|
-
const update = () =>
|
|
647
|
-
|
|
646
|
+
const update = () => {
|
|
647
|
+
const next = { cols: stdout.columns || 0, rows: stdout.rows || 0 };
|
|
648
|
+
setSize((prev) => (prev.cols === next.cols && prev.rows === next.rows ? prev : next));
|
|
649
|
+
};
|
|
648
650
|
update();
|
|
649
651
|
stdout.on("resize", update);
|
|
650
652
|
return () => stdout.off("resize", update);
|
|
@@ -652,7 +654,11 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
|
|
|
652
654
|
|
|
653
655
|
// Drive the spinner + elapsed-timer redraws while a task is in flight.
|
|
654
656
|
useEffect(() => {
|
|
655
|
-
|
|
657
|
+
const statusType = inferStatusType(status.message, status.type);
|
|
658
|
+
if (!status.message || statusType === "none" || statusType === "idle" ||
|
|
659
|
+
statusType === "done" || statusType === "success" || statusType === "error") {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
656
662
|
const timer = setInterval(() => {
|
|
657
663
|
setSpinnerTick((t) => t + 1);
|
|
658
664
|
if (status.showTimer) setNowTick((t) => t + 1);
|
|
@@ -792,6 +798,22 @@ function runUcodeInkTui(props = {}) {
|
|
|
792
798
|
|
|
793
799
|
module.exports = { runUcodeInkTui, createUcodeApp, computeStatusText };
|
|
794
800
|
|
|
801
|
+
function inferStatusType(text = "", requestedType = "") {
|
|
802
|
+
const type = String(requestedType || "").trim().toLowerCase();
|
|
803
|
+
if (type === "done" || type === "success" || type === "error" || type === "idle" || type === "none") {
|
|
804
|
+
return type;
|
|
805
|
+
}
|
|
806
|
+
const clean = String(text || "").trim();
|
|
807
|
+
if (/^[✗!]/.test(clean) || /\b(error|failed|failure)\b/i.test(clean) || /失败|错误/.test(clean)) return "error";
|
|
808
|
+
if (
|
|
809
|
+
/^[✓✔]/.test(clean) ||
|
|
810
|
+
/^(done|complete|completed|finished|success|succeeded|ready)\b/i.test(clean) ||
|
|
811
|
+
/\bdone\s*$/i.test(clean) ||
|
|
812
|
+
/完成|成功/.test(clean)
|
|
813
|
+
) return "done";
|
|
814
|
+
return type || "thinking";
|
|
815
|
+
}
|
|
816
|
+
|
|
795
817
|
/**
|
|
796
818
|
* Pure status-line text builder used by the React component (and unit
|
|
797
819
|
* tests). Returns "UCODE · Ready" while idle and a spinner+message+timer
|
|
@@ -802,7 +824,16 @@ function computeStatusText(status, spinnerTick, backgroundSuffix = "") {
|
|
|
802
824
|
const message = String((status && status.message) || "");
|
|
803
825
|
const suffix = String(backgroundSuffix || "");
|
|
804
826
|
if (!message) return `UCODE · Ready${suffix}`;
|
|
805
|
-
const type =
|
|
827
|
+
const type = inferStatusType(message, status && status.type);
|
|
828
|
+
if (type === "done" || type === "success") {
|
|
829
|
+
const clean = message.trim();
|
|
830
|
+
return `${/^[✓✔]/.test(clean) ? clean : `✓ ${clean}`}${suffix}`;
|
|
831
|
+
}
|
|
832
|
+
if (type === "error") {
|
|
833
|
+
const clean = message.trim();
|
|
834
|
+
return `${/^[✗!]/.test(clean) ? clean : `✗ ${clean}`}${suffix}`;
|
|
835
|
+
}
|
|
836
|
+
if (type === "idle" || type === "none") return `${message.trim() || "UCODE · Ready"}${suffix}`;
|
|
806
837
|
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
807
838
|
const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
808
839
|
const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
|
|
@@ -48,6 +48,53 @@ function projectRootOf(row = {}) {
|
|
|
48
48
|
return String((row && (row.root || row.project_root || row.projectRoot)) || "");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function stableJson(value) {
|
|
52
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
53
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
54
|
+
const keys = Object.keys(value).sort();
|
|
55
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shallowArrayEqual(left = [], right = []) {
|
|
59
|
+
if (left === right) return true;
|
|
60
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
61
|
+
if (left.length !== right.length) return false;
|
|
62
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
63
|
+
if (left[i] !== right[i]) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function listPayloadEqual(left = [], right = []) {
|
|
69
|
+
if (left === right) return true;
|
|
70
|
+
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
71
|
+
if (left.length !== right.length) return false;
|
|
72
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
73
|
+
if (stableJson(left[i]) !== stableJson(right[i])) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mapPayloadEqual(left, right) {
|
|
79
|
+
if (left === right) return true;
|
|
80
|
+
if (!(left instanceof Map) || !(right instanceof Map)) return false;
|
|
81
|
+
if (left.size !== right.size) return false;
|
|
82
|
+
for (const [key, value] of left.entries()) {
|
|
83
|
+
if (!right.has(key)) return false;
|
|
84
|
+
if (stableJson(value) !== stableJson(right.get(key))) return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statusPayloadEqual(left, right) {
|
|
90
|
+
const normalize = (value = {}) => {
|
|
91
|
+
const normalized = { ...value };
|
|
92
|
+
if (normalized.showTimer !== true) normalized.startedAt = 0;
|
|
93
|
+
return normalized;
|
|
94
|
+
};
|
|
95
|
+
return stableJson(normalize(left)) === stableJson(normalize(right));
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
function createInitialState({ banner = [], globalMode = false, globalScope = "controller", settings = {} } = {}) {
|
|
52
99
|
const initialLaunchMode = settings.launchMode || "auto";
|
|
53
100
|
const initialAgentProvider = settings.agentProvider || "codex-cli";
|
|
@@ -174,6 +221,14 @@ function reducer(state, action) {
|
|
|
174
221
|
} else if (nextIdx >= ids.length) {
|
|
175
222
|
nextIdx = ids.length - 1;
|
|
176
223
|
}
|
|
224
|
+
if (
|
|
225
|
+
shallowArrayEqual(state.agents, ids) &&
|
|
226
|
+
mapPayloadEqual(state.activeAgentMeta, meta) &&
|
|
227
|
+
state.selectedAgentIndex === nextIdx &&
|
|
228
|
+
state.agentSelectionMode === nextMode
|
|
229
|
+
) {
|
|
230
|
+
return state;
|
|
231
|
+
}
|
|
177
232
|
return {
|
|
178
233
|
...state,
|
|
179
234
|
agents: ids,
|
|
@@ -216,12 +271,22 @@ function reducer(state, action) {
|
|
|
216
271
|
const selectedIndex = selectedRoot
|
|
217
272
|
? list.findIndex((row) => projectRootOf(row) === selectedRoot)
|
|
218
273
|
: -1;
|
|
274
|
+
const nextActiveRoot = action.activeProjectRoot || state.activeProjectRoot;
|
|
275
|
+
if (
|
|
276
|
+
listPayloadEqual(state.projects, list) &&
|
|
277
|
+
state.selectedProjectRoot === (selectedIndex >= 0 ? selectedRoot : "") &&
|
|
278
|
+
state.selectedProjectIndex === selectedIndex &&
|
|
279
|
+
state.activeProjectRoot === nextActiveRoot &&
|
|
280
|
+
state.emptyProjectsDownArmed === (list.length === 0 ? state.emptyProjectsDownArmed : false)
|
|
281
|
+
) {
|
|
282
|
+
return state;
|
|
283
|
+
}
|
|
219
284
|
return {
|
|
220
285
|
...state,
|
|
221
286
|
projects: list,
|
|
222
287
|
selectedProjectRoot: selectedIndex >= 0 ? selectedRoot : "",
|
|
223
288
|
selectedProjectIndex: selectedIndex,
|
|
224
|
-
activeProjectRoot:
|
|
289
|
+
activeProjectRoot: nextActiveRoot,
|
|
225
290
|
emptyProjectsDownArmed: list.length === 0 ? state.emptyProjectsDownArmed : false,
|
|
226
291
|
};
|
|
227
292
|
}
|
|
@@ -240,8 +305,11 @@ function reducer(state, action) {
|
|
|
240
305
|
return { ...state, projectListWindowStart: Math.max(0, action.windowStart | 0) };
|
|
241
306
|
case "scope/set":
|
|
242
307
|
return { ...state, globalScope: action.scope === "project" ? "project" : "controller" };
|
|
243
|
-
case "status/set":
|
|
244
|
-
|
|
308
|
+
case "status/set": {
|
|
309
|
+
const nextStatus = { ...state.status, ...action.payload };
|
|
310
|
+
if (statusPayloadEqual(state.status, nextStatus)) return state;
|
|
311
|
+
return { ...state, status: nextStatus };
|
|
312
|
+
}
|
|
245
313
|
case "status/idle":
|
|
246
314
|
return { ...state, status: { message: "", type: "thinking", showTimer: false, startedAt: 0 } };
|
|
247
315
|
case "history/push": {
|
|
@@ -311,10 +379,16 @@ function reducer(state, action) {
|
|
|
311
379
|
return { ...state, selectedProviderIndex: Math.max(0, action.index | 0) };
|
|
312
380
|
case "cronIndex/set":
|
|
313
381
|
return { ...state, selectedCronIndex: Math.max(-1, action.index | 0) };
|
|
314
|
-
case "cron/set":
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return { ...state,
|
|
382
|
+
case "cron/set": {
|
|
383
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
384
|
+
if (listPayloadEqual(state.cronTasks, list)) return state;
|
|
385
|
+
return { ...state, cronTasks: list };
|
|
386
|
+
}
|
|
387
|
+
case "loop/set": {
|
|
388
|
+
const summary = action.summary && typeof action.summary === "object" ? action.summary : null;
|
|
389
|
+
if (stableJson(state.loopSummary) === stableJson(summary)) return state;
|
|
390
|
+
return { ...state, loopSummary: summary };
|
|
391
|
+
}
|
|
318
392
|
case "stream/begin":
|
|
319
393
|
return {
|
|
320
394
|
...state,
|