magus-cli 7.6.0 → 7.7.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.
@@ -2,7 +2,12 @@ import React, { useEffect, useCallback, useMemo } from "react";
2
2
  import { ScreenLayout } from "../components/layout/index.js";
3
3
  import { useScreenKeys } from "../input/KeyRouter.js";
4
4
  import { moveDirection } from "../keymap.js";
5
- import { useApp, useModal, useNavigation } from "../state/AppContext.js";
5
+ import {
6
+ useApp,
7
+ useModal,
8
+ useNavigation,
9
+ useNotify,
10
+ } from "../state/AppContext.js";
6
11
  import { useDimensions } from "../state/DimensionsContext.js";
7
12
 
8
13
  import {
@@ -30,6 +35,7 @@ export function McpScreen() {
30
35
  const { state, dispatch } = useApp();
31
36
  const { mcp } = state;
32
37
  const modal = useModal();
38
+ const notify = useNotify();
33
39
  const { navigateToScreen } = useNavigation();
34
40
  const dimensions = useDimensions();
35
41
 
@@ -69,9 +75,12 @@ export function McpScreen() {
69
75
  }
70
76
  }, [dispatch, state.projectPath]);
71
77
 
78
+ // Load data on mount or when dataRefreshVersion changes — a profile
79
+ // activating changes what this screen shows.
80
+ // biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-run the fetch even though the body never reads it
72
81
  useEffect(() => {
73
82
  fetchData();
74
- }, [fetchData]);
83
+ }, [fetchData, state.dataRefreshVersion]);
75
84
 
76
85
  const allListItems = useMemo((): McpListItem[] => {
77
86
  if (mcp.servers.status !== "success") return [];
@@ -138,11 +147,7 @@ export function McpScreen() {
138
147
  try {
139
148
  await removeMcpServer(server.name, state.projectPath);
140
149
  modal.hideModal();
141
- await modal.message(
142
- "Removed",
143
- `${server.name} has been removed.`,
144
- "success",
145
- );
150
+ // The row turns uninstalled; nothing else to say.
146
151
  fetchData();
147
152
  } catch (error) {
148
153
  modal.hideModal();
@@ -216,11 +221,10 @@ export function McpScreen() {
216
221
  try {
217
222
  await addMcpServer(server.name, config, state.projectPath);
218
223
  modal.hideModal();
219
- await modal.message(
220
- "Installed",
221
- `${server.name} has been configured.\n\nRestart Claude Code to activate.`,
222
- "success",
223
- );
224
+ // The row shows it installed; the restart it needs does not show,
225
+ // and it is something the user has to do — so a warning, which also
226
+ // stays long enough to be read.
227
+ notify(`Added ${server.name} — restart Claude Code to use it`, "warning");
224
228
  fetchData();
225
229
  } catch (error) {
226
230
  modal.hideModal();
@@ -91,7 +91,12 @@ import {
91
91
  renderPluginDetail,
92
92
  renderPluginRow,
93
93
  } from "../renderers/pluginRenderers.js";
94
- import { useApp, useModal, useProgress } from "../state/AppContext.js";
94
+ import {
95
+ useApp,
96
+ useModal,
97
+ useNotify,
98
+ useProgress,
99
+ } from "../state/AppContext.js";
95
100
  import { useDimensions } from "../state/DimensionsContext.js";
96
101
  import { asyncValue } from "../state/types.js";
97
102
  import { theme } from "../theme.js";
@@ -121,6 +126,7 @@ export function PluginsScreen() {
121
126
  const { plugins: pluginsState } = state;
122
127
  const modal = useModal();
123
128
  const progress = useProgress();
129
+ const notify = useNotify();
124
130
  const dimensions = useDimensions();
125
131
 
126
132
  const isSearchActive =
@@ -558,21 +564,15 @@ export function PluginsScreen() {
558
564
  clearContentDriftCache();
559
565
  progress.hide();
560
566
 
561
- let message =
562
- "Cache refreshed.\n\n" +
563
- "To update marketplaces from GitHub, run in Claude Code:\n" +
564
- " /plugin marketplace update\n\n" +
565
- "Then refresh magus again with 'r'.";
566
-
567
- if (result.repair.length > 0) {
568
- const totalRepaired = result.repair.reduce(
569
- (sum, r) => sum + r.repaired.length,
570
- 0,
571
- );
572
- message += `\n\nAuto-repaired ${totalRepaired} plugin(s) with missing agents/commands.`;
573
- }
574
-
575
- await modal.message("Refreshed", message, "success");
567
+ const totalRepaired = result.repair.reduce(
568
+ (sum, r) => sum + r.repaired.length,
569
+ 0,
570
+ );
571
+ notify(
572
+ totalRepaired > 0
573
+ ? `Refreshed — repaired ${totalRepaired} plugin(s) with missing files`
574
+ : "Refreshed",
575
+ );
576
576
  fetchData();
577
577
  } catch (error) {
578
578
  progress.hide();
@@ -697,10 +697,9 @@ export function PluginsScreen() {
697
697
  );
698
698
 
699
699
  if (!wantToConfigure) {
700
- await modal.message(
701
- "Skipped Configuration",
702
- `You can configure these variables later in the Settings tab (press ${tabKey("settings")}).`,
703
- "info",
700
+ notify(
701
+ `${serverNames.join(", ")} not configured — set the variables in the Settings tab (press ${tabKey("settings")})`,
702
+ "warning",
704
703
  );
705
704
  return true;
706
705
  }
@@ -725,10 +724,9 @@ export function PluginsScreen() {
725
724
  );
726
725
 
727
726
  if (value === null) {
728
- await modal.message(
729
- "Configuration Incomplete",
730
- `Skipped remaining configuration.\nYou can configure these later in the Settings tab (press ${tabKey("settings")}).`,
731
- "info",
727
+ notify(
728
+ `${serverNames.join(", ")} partly configured — set the rest in the Settings tab (press ${tabKey("settings")})`,
729
+ "warning",
732
730
  );
733
731
  return true;
734
732
  }
@@ -807,11 +805,7 @@ export function PluginsScreen() {
807
805
  "error",
808
806
  );
809
807
  } else if (result.installed.length > 0) {
810
- await modal.message(
811
- "Dependencies Installed",
812
- `Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`,
813
- "success",
814
- );
808
+ notify(`Installed ${result.installed.join(", ")}`);
815
809
  }
816
810
  } catch (error) {
817
811
  console.error("Error installing plugin deps:", error);
@@ -1047,13 +1041,8 @@ export function PluginsScreen() {
1047
1041
  // Ran, exited 0, moved nothing — and the row will not have moved
1048
1042
  // either. Saying nothing here is what made "press U, press U,
1049
1043
  // forever": the CLI names the cause, so the screen does too.
1050
- if (result.unchanged.length > 0) {
1051
- await modal.message(
1052
- "No change",
1053
- reasons.lines(result.unchanged),
1054
- "info",
1055
- );
1056
- }
1044
+ const unchanged = reasons.notice(result.unchanged);
1045
+ if (unchanged) notify(unchanged, "warning");
1057
1046
  fetchData();
1058
1047
  } catch (error) {
1059
1048
  modal.hideModal();
@@ -1121,13 +1110,10 @@ export function PluginsScreen() {
1121
1110
  "error",
1122
1111
  );
1123
1112
  }
1124
- if (result.unchanged.length > 0) {
1125
- await modal.message(
1126
- "No change",
1127
- reasons.lines(result.unchanged),
1128
- "info",
1129
- );
1130
- }
1113
+ // One line however many were left where they were: a line each
1114
+ // filled the banner and pushed out everything else it had to say.
1115
+ const unchanged = reasons.notice(result.unchanged);
1116
+ if (unchanged) notify(unchanged, "warning");
1131
1117
  fetchData();
1132
1118
  } catch (error) {
1133
1119
  modal.hideModal();
@@ -1278,10 +1264,8 @@ export function PluginsScreen() {
1278
1264
  try {
1279
1265
  await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
1280
1266
  modal.hideModal();
1281
- await modal.message(
1282
- "Saved",
1283
- `Profile "${name.trim()}" saved.\nPress ${tabKey("profiles")} to manage profiles.`,
1284
- "success",
1267
+ notify(
1268
+ `Saved profile "${name.trim()}" at ${scope === "user" ? "USER" : "PROJECT"} scope`,
1285
1269
  );
1286
1270
  } catch (error) {
1287
1271
  modal.hideModal();
@@ -1379,18 +1363,13 @@ export function PluginsScreen() {
1379
1363
  width: dimensions.terminalWidth,
1380
1364
  now: nowTick,
1381
1365
  });
1382
- // Without a resolved profile, exact pins are unknown here and every row is
1383
- // judged against "latest". Saying so is what keeps a row that disagrees with
1384
- // `magus update <profile>` from reading as a contradiction.
1366
+ // Without a resolved profile every row is judged against "latest". That is
1367
+ // not said here: the startup question already tells the user there is no
1368
+ // active profile, and "profile not resolved" was mechanism, not state.
1385
1369
  const notice = isReloading ? (
1386
1370
  <text fg={theme.colors.muted}>⟳ Refreshing catalogs from GitHub…</text>
1387
1371
  ) : (
1388
- (catalogNotice ??
1389
- (pins === null ? (
1390
- <text fg={theme.colors.muted}>
1391
- profile not resolved — states judged against latest
1392
- </text>
1393
- ) : null))
1372
+ catalogNotice
1394
1373
  );
1395
1374
 
1396
1375
  return (
@@ -2,7 +2,7 @@ import React, { useEffect, useCallback } from "react";
2
2
  import { ScreenLayout } from "../components/layout/index.js";
3
3
  import { useScreenKeys } from "../input/KeyRouter.js";
4
4
  import { moveDirection } from "../keymap.js";
5
- import { useApp, useModal } from "../state/AppContext.js";
5
+ import { useApp, useModal, useNotify } from "../state/AppContext.js";
6
6
  import { useDimensions } from "../state/DimensionsContext.js";
7
7
 
8
8
  import {
@@ -15,7 +15,6 @@ import {
15
15
  writeSettings,
16
16
  } from "../../services/claude-settings.js";
17
17
  import { reconcileActiveProfile } from "../../services/profile-absorb.js";
18
- import { activationNotes } from "../../services/profile-activate.js";
19
18
  import { switchToProfile } from "../../services/profile-state.js";
20
19
  import {
21
20
  applyUserProfile,
@@ -32,7 +31,7 @@ import {
32
31
  writeClipboard,
33
32
  } from "../../utils/clipboard.js";
34
33
  import { ScrollableList } from "../components/ScrollableList.js";
35
- import { describeOutcome } from "../hooks/useProfileStartupModal.js";
34
+ import { activationNotices } from "../hooks/useProfileStartupModal.js";
36
35
  import {
37
36
  type ProfileListItem,
38
37
  buildProfileListItems,
@@ -45,6 +44,7 @@ export function ProfilesScreen() {
45
44
  const { state, dispatch } = useApp();
46
45
  const { profiles: profilesState } = state;
47
46
  const modal = useModal();
47
+ const notify = useNotify();
48
48
  const dimensions = useDimensions();
49
49
 
50
50
  const fetchData = useCallback(async () => {
@@ -165,14 +165,10 @@ export function ProfilesScreen() {
165
165
  const recorded = await reconcileActiveProfile(state.projectPath);
166
166
  modal.hideModal();
167
167
  dispatch({ type: "DATA_REFRESH_COMPLETE" });
168
- await modal.message(
169
- "Applied",
170
- [
171
- `Profile "${profile.name}" merged into project settings.`,
172
- ...recorded.lines,
173
- ].join("\n"),
174
- "success",
175
- );
168
+ notify(`Merged "${profile.name}" into this project`);
169
+ // What was recorded is routine; a refusal to record it is not, and
170
+ // the user has to fix it before the merge reaches the profile.
171
+ for (const problem of recorded.problems) notify(problem, "warning");
176
172
  } catch (error) {
177
173
  modal.hideModal();
178
174
  await modal.message(
@@ -215,8 +211,10 @@ export function ProfilesScreen() {
215
211
  { onDisplace: () => {} },
216
212
  );
217
213
  modal.hideModal();
218
- dispatch({ type: "DATA_REFRESH_COMPLETE" });
219
214
  if (result.problems.length > 0 || !result.activation) {
215
+ // Nothing activated, but what was recorded on the way may have
216
+ // changed the list.
217
+ dispatch({ type: "DATA_REFRESH_COMPLETE" });
220
218
  await modal.message(
221
219
  "Could not switch",
222
220
  [...result.lines, ...result.problems].join("\n"),
@@ -224,20 +222,16 @@ export function ProfilesScreen() {
224
222
  );
225
223
  return;
226
224
  }
227
- const outcome = describeOutcome(
228
- result.activation.profileId,
229
- result.activation.displaced,
230
- state.projectPath,
231
- false,
232
- activationNotes(result.activation),
233
- );
234
- await modal.message(
235
- "Profile active",
236
- result.lines.length > 0
237
- ? `${result.lines.join("\n")}\n\n${outcome}`
238
- : outcome,
239
- "success",
240
- );
225
+ // The same signal as the startup question: every screen reloads,
226
+ // and the app re-runs its gitignore check — the profile switched
227
+ // to may generate files git still tracks, and that question is how
228
+ // the user hears of them (activationNotices leaves them out).
229
+ dispatch({ type: "PROFILE_ACTIVATED" });
230
+ // `result.lines` (edits saved into the profile left behind) are not
231
+ // repeated: the confirmation above already said that happens.
232
+ for (const line of activationNotices(result.activation)) {
233
+ notify(line.text, line.tone);
234
+ }
241
235
  } catch (error) {
242
236
  modal.hideModal();
243
237
  await modal.message(
@@ -266,11 +260,7 @@ export function ProfilesScreen() {
266
260
  await applyUserProfile(selectedProfile);
267
261
  modal.hideModal();
268
262
  dispatch({ type: "DATA_REFRESH_COMPLETE" });
269
- await modal.message(
270
- "Applied",
271
- `Profile "${selectedProfile.name}" applied to ~/.claude/settings.json.`,
272
- "success",
273
- );
263
+ notify(`Applied "${selectedProfile.name}" at USER scope`);
274
264
  } catch (error) {
275
265
  modal.hideModal();
276
266
  await modal.message(
@@ -301,12 +291,8 @@ export function ProfilesScreen() {
301
291
  state.projectPath,
302
292
  );
303
293
  modal.hideModal();
294
+ // The row shows the new name; nothing else to say.
304
295
  await fetchData();
305
- await modal.message(
306
- "Renamed",
307
- `Profile renamed to "${newName.trim()}".`,
308
- "success",
309
- );
310
296
  } catch (error) {
311
297
  modal.hideModal();
312
298
  await modal.message("Error", `Failed to rename: ${error}`, "error");
@@ -336,8 +322,8 @@ export function ProfilesScreen() {
336
322
  Math.min(profilesState.selectedIndex, allItems.length - 2),
337
323
  );
338
324
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
325
+ // The row is gone from the list; nothing else to say.
339
326
  await fetchData();
340
- await modal.message("Deleted", "Profile deleted.", "success");
341
327
  } catch (error) {
342
328
  modal.hideModal();
343
329
  await modal.message("Error", `Failed to delete: ${error}`, "error");
@@ -358,10 +344,8 @@ export function ProfilesScreen() {
358
344
  modal.hideModal();
359
345
  try {
360
346
  await writeClipboard(json);
361
- await modal.message(
362
- "Copied",
363
- "Profile JSON copied to clipboard.\nShare it with teammates to import.",
364
- "success",
347
+ notify(
348
+ `Copied "${selectedProfile.name}" — share it for teammates to import`,
365
349
  );
366
350
  } catch (err) {
367
351
  if (err instanceof ClipboardUnavailableError) {
@@ -409,18 +393,10 @@ export function ProfilesScreen() {
409
393
 
410
394
  modal.loading("Importing...");
411
395
  try {
412
- const id = await importProfileFromJson(
413
- json,
414
- targetScope,
415
- state.projectPath,
416
- );
396
+ await importProfileFromJson(json, targetScope, state.projectPath);
417
397
  modal.hideModal();
398
+ // The imported profile appears in the list; nothing else to say.
418
399
  await fetchData();
419
- await modal.message(
420
- "Imported",
421
- `Profile imported successfully (id: ${id}).`,
422
- "success",
423
- );
424
400
  } catch (error) {
425
401
  modal.hideModal();
426
402
  await modal.message("Error", `Failed to import: ${error}`, "error");
@@ -1,10 +1,17 @@
1
1
  import type React from "react";
2
- import { type ReactNode, createContext, useContext, useReducer } from "react";
3
- import { appReducer, initialState } from "./reducer.js";
2
+ import {
3
+ type ReactNode,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useReducer,
8
+ } from "react";
9
+ import { NOTICE_DURATION_MS, appReducer, initialState } from "./reducer.js";
4
10
  import type {
5
11
  AppAction,
6
12
  AppState,
7
13
  ModalState,
14
+ NoticeTone,
8
15
  ProgressState,
9
16
  Route,
10
17
  Screen,
@@ -177,7 +184,7 @@ export function useModal() {
177
184
  message: (
178
185
  title: string,
179
186
  message: string,
180
- variant: "info" | "success" | "error" = "info",
187
+ variant: "info" | "error" = "info",
181
188
  ): Promise<void> => {
182
189
  return new Promise((resolve) => {
183
190
  dispatch({
@@ -213,6 +220,42 @@ export function useModal() {
213
220
  };
214
221
  }
215
222
 
223
+ // ============================================================================
224
+ // Hook: useNotify
225
+ // ============================================================================
226
+
227
+ export { NOTICE_DURATION_MS };
228
+
229
+ let nextNoticeId = 0;
230
+
231
+ /**
232
+ * `notify(text, tone)` puts one line in the app's notice banner (rendered by
233
+ * `NoticeBanner` in ui/App.tsx) and returns at once. It is how a screen or hook
234
+ * reports the result of an action: it never opens a modal, never takes a key,
235
+ * and the line clears itself after {@link NOTICE_DURATION_MS}. Time spent
236
+ * under a modal does not count (see PAUSE_NOTICES in the reducer), so a line
237
+ * posted while a question is on screen is still there once it is answered.
238
+ *
239
+ * Stable across renders, so it is safe in a `useCallback` dependency list.
240
+ */
241
+ export function useNotify(): (text: string, tone?: NoticeTone) => void {
242
+ const { dispatch } = useApp();
243
+ return useCallback(
244
+ (text: string, tone: NoticeTone = "success") => {
245
+ dispatch({
246
+ type: "NOTIFY",
247
+ notice: {
248
+ id: nextNoticeId++,
249
+ text,
250
+ tone,
251
+ expiresAt: Date.now() + NOTICE_DURATION_MS[tone],
252
+ },
253
+ });
254
+ },
255
+ [dispatch],
256
+ );
257
+ }
258
+
216
259
  // ============================================================================
217
260
  // Hook: useProgress
218
261
  // ============================================================================
@@ -1,6 +1,38 @@
1
- import type { AppAction, AppState } from "./types.js";
1
+ import type { AppAction, AppState, Notice, NoticeTone } from "./types.js";
2
2
  import { asyncValue } from "./types.js";
3
3
 
4
+ /** How many notice lines the banner shows at once. */
5
+ export const MAX_NOTICES = 4;
6
+
7
+ /**
8
+ * How long a notice stays. A warning may ask the user to do something, so it
9
+ * gets long enough to read and act on; a success only confirms what happened.
10
+ *
11
+ * Here rather than beside `useNotify`, because RESUME_NOTICES caps a held line
12
+ * at its full duration and the reducer cannot import from the context module
13
+ * that imports it.
14
+ */
15
+ export const NOTICE_DURATION_MS: Record<NoticeTone, number> = {
16
+ success: 4000,
17
+ warning: 10000,
18
+ };
19
+
20
+ /**
21
+ * Add `notice` and keep at most {@link MAX_NOTICES}, dropping successes first.
22
+ *
23
+ * A success only confirms what the screen already shows; a warning may be the
24
+ * one thing telling the user to act. So a burst of lines evicts the oldest
25
+ * success, and the oldest warning goes only when nothing but warnings is left.
26
+ */
27
+ function withNotice(notices: readonly Notice[], notice: Notice): Notice[] {
28
+ const next = [...notices, notice];
29
+ while (next.length > MAX_NOTICES) {
30
+ const success = next.findIndex((n) => n.tone === "success");
31
+ next.splice(success === -1 ? 0 : success, 1);
32
+ }
33
+ return next;
34
+ }
35
+
4
36
  export const initialState: AppState = {
5
37
  // Navigation - start on plugins screen
6
38
  currentRoute: { screen: "plugins" },
@@ -10,12 +42,17 @@ export const initialState: AppState = {
10
42
 
11
43
  // Global UI state
12
44
  modal: null,
45
+ notices: [],
46
+ noticesPausedAt: null,
13
47
  progress: null,
14
48
  isSearching: false,
15
49
 
16
50
  // Refresh counter for triggering data refetch
17
51
  dataRefreshVersion: 0,
18
52
 
53
+ // 0 is the check every open runs; each bump asks for another
54
+ gitignoreCheckRequest: 0,
55
+
19
56
  // Screen-specific state
20
57
  plugins: {
21
58
  scope: "project",
@@ -777,6 +814,57 @@ export function appReducer(state: AppState, action: AppAction): AppState {
777
814
  case "HIDE_MODAL":
778
815
  return { ...state, modal: null };
779
816
 
817
+ // =========================================================================
818
+ // Notices
819
+ // =========================================================================
820
+ // The same text again replaces the old line rather than stacking a copy,
821
+ // and only a few are kept: the banner takes rows from the screen, and a
822
+ // burst must not push the content off the terminal. Which few is
823
+ // `withNotice`'s call — warnings outlast successes.
824
+ case "NOTIFY": {
825
+ const rest = state.notices.filter((n) => n.text !== action.notice.text);
826
+ return { ...state, notices: withNotice(rest, action.notice) };
827
+ }
828
+
829
+ case "EXPIRE_NOTICES": {
830
+ // Held behind a modal: nothing runs down, so nothing expires.
831
+ if (state.noticesPausedAt !== null) return state;
832
+ const live = state.notices.filter((n) => n.expiresAt > action.now);
833
+ // Same state when nothing expired, so the timer effect is not re-run.
834
+ return live.length === state.notices.length
835
+ ? state
836
+ : { ...state, notices: live };
837
+ }
838
+
839
+ // A modal opened. Only the first pause counts: modal to modal (a confirm
840
+ // followed by its loading modal) is one uninterrupted hold.
841
+ case "PAUSE_NOTICES":
842
+ return state.noticesPausedAt !== null
843
+ ? state
844
+ : { ...state, noticesPausedAt: action.now };
845
+
846
+ // The modal slot emptied: every line gets back the time it spent held.
847
+ // A line posted DURING the hold would be over-credited by the time
848
+ // between the pause and its posting, so it is capped at its full
849
+ // duration from now — which is exactly what it would have had, had it
850
+ // been posted the moment the modal closed.
851
+ case "RESUME_NOTICES": {
852
+ const pausedAt = state.noticesPausedAt;
853
+ if (pausedAt === null) return state;
854
+ const held = action.now - pausedAt;
855
+ return {
856
+ ...state,
857
+ noticesPausedAt: null,
858
+ notices: state.notices.map((n) => ({
859
+ ...n,
860
+ expiresAt: Math.min(
861
+ n.expiresAt + held,
862
+ action.now + NOTICE_DURATION_MS[n.tone],
863
+ ),
864
+ })),
865
+ };
866
+ }
867
+
780
868
  // =========================================================================
781
869
  // Progress
782
870
  // =========================================================================
@@ -801,6 +889,17 @@ export function appReducer(state: AppState, action: AppAction): AppState {
801
889
  case "DATA_REFRESH_COMPLETE":
802
890
  return { ...state, dataRefreshVersion: state.dataRefreshVersion + 1 };
803
891
 
892
+ // One action for both, so no activation path can do one and forget the
893
+ // other: the screens reload what the profile turned on, and the gitignore
894
+ // question is asked again, because a first activation is what turns a
895
+ // committed settings file into a generated one git should not track.
896
+ case "PROFILE_ACTIVATED":
897
+ return {
898
+ ...state,
899
+ dataRefreshVersion: state.dataRefreshVersion + 1,
900
+ gitignoreCheckRequest: state.gitignoreCheckRequest + 1,
901
+ };
902
+
804
903
  default:
805
904
  return state;
806
905
  }