@gajae-code/coding-agent 0.10.1 → 0.10.2

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.
@@ -79,6 +79,7 @@ import { HistorySearchComponent } from "../components/history-search";
79
79
  import { JobsOverlayComponent } from "../components/jobs-overlay";
80
80
  import { ModelSelectorComponent } from "../components/model-selector";
81
81
  import { OAuthSelectorComponent } from "../components/oauth-selector";
82
+ import { isPetAvailable } from "../components/pet-capability";
82
83
  import { PetSelectorComponent } from "../components/pet-selector";
83
84
  import { PluginSelectorComponent } from "../components/plugin-selector";
84
85
  import {
@@ -492,6 +493,7 @@ export class SelectorController {
492
493
  availableThemes,
493
494
  availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
494
495
  cwd: getProjectDir(),
496
+ petAvailable: isPetAvailable(),
495
497
  },
496
498
  {
497
499
  onChange: (id, value) => this.handleSettingChange(id, value),
@@ -514,6 +516,7 @@ export class SelectorController {
514
516
  onPetPreview: mode => {
515
517
  this.ctx.previewPetMode(mode as PetMode);
516
518
  },
519
+ onPetCommit: mode => this.ctx.commitPetPreviewMode(mode as PetMode),
517
520
  onStatusLinePreview: previewSettings => {
518
521
  // Update status line with preview settings
519
522
  this.ctx.statusLine.updateSettings({
@@ -591,6 +594,7 @@ export class SelectorController {
591
594
  showPetSelector(): void {
592
595
  const stored = settings.get("pet.mode");
593
596
  const initial: PetMode = isPetMode(stored) ? stored : "off";
597
+ const available = isPetAvailable();
594
598
  this.showSelector(done => {
595
599
  // Live-preview via previewMode (no editor re-mount, so the overlay stays);
596
600
  // Enter commits + persists, Esc restores the initial skin.
@@ -607,6 +611,7 @@ export class SelectorController {
607
611
  mode => {
608
612
  this.ctx.previewPetMode(mode);
609
613
  },
614
+ available,
610
615
  );
611
616
  return { component: selector, focus: selector.getSelectList() };
612
617
  });
@@ -762,12 +767,6 @@ export class SelectorController {
762
767
  });
763
768
  break;
764
769
  }
765
- case "pet.mode":
766
- // The settings submenu already persisted the value; apply it to the live
767
- // widget via previewMode (the settings overlay is still open, so a full
768
- // re-mount would tear it down — restoreComposer re-mounts on close).
769
- this.ctx.previewPetMode(value as PetMode);
770
- break;
771
770
  case "symbolPreset": {
772
771
  setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
773
772
  this.ctx.statusLine.invalidate();
@@ -985,21 +984,14 @@ export class SelectorController {
985
984
  assignments,
986
985
  });
987
986
  if (!materializedProfile) {
988
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
989
- const nextOverrides = { ...overrides };
990
- let writesOverrides = false;
991
987
  for (const targetRole of targetRoles) {
992
988
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
993
989
  if (target.settingsPath === "modelRoles") {
994
990
  this.ctx.settings.setModelRole(targetRole, value);
995
991
  } else {
996
- nextOverrides[targetRole] = value;
997
- writesOverrides = true;
992
+ this.ctx.settings.setAgentModelOverride(targetRole, value);
998
993
  }
999
994
  }
1000
- if (writesOverrides) {
1001
- this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
1002
- }
1003
995
  }
1004
996
  modelSelector.refreshRoleAssignments({
1005
997
  currentModel: this.ctx.session.model,
@@ -1069,10 +1061,7 @@ export class SelectorController {
1069
1061
  if (target.settingsPath === "modelRoles") {
1070
1062
  this.ctx.settings.setModelRole(role, value);
1071
1063
  } else {
1072
- this.ctx.settings.set("task.agentModelOverrides", {
1073
- ...this.ctx.settings.get("task.agentModelOverrides"),
1074
- [role]: value,
1075
- });
1064
+ this.ctx.settings.setAgentModelOverride(role, value);
1076
1065
  }
1077
1066
  }
1078
1067
  modelSelector.refreshRoleAssignments({
@@ -87,6 +87,12 @@ import type { HookEditorComponent } from "./components/hook-editor";
87
87
  import type { HookInputComponent } from "./components/hook-input";
88
88
  import type { HookSelectorComponent } from "./components/hook-selector";
89
89
  import { IrcSplitViewComponent } from "./components/irc-sidebar";
90
+ import {
91
+ getPetUnavailableWarning,
92
+ isPetAvailable,
93
+ isPetCapabilityProbePending,
94
+ warnWhenPetCapabilitySettled,
95
+ } from "./components/pet-capability";
90
96
  import { StatusLineComponent } from "./components/status-line";
91
97
  import type { ToolExecutionHandle } from "./components/tool-execution";
92
98
  import {
@@ -389,6 +395,8 @@ export class InteractiveMode implements InteractiveModeContext {
389
395
  #cleanupUnsubscribe?: () => void;
390
396
  #subprocessTeardownUnsubscribe?: () => void;
391
397
  #petProtocolUnsubscribe?: () => void;
398
+ /** Cancels a startup pet-unavailable warning still awaiting probe settlement. */
399
+ #petUnavailableWarningDisposer?: () => void;
392
400
  readonly #version: string;
393
401
  readonly #changelogMarkdown: string | undefined;
394
402
  #planModePreviousTools: string[] | undefined;
@@ -617,8 +625,10 @@ export class InteractiveMode implements InteractiveModeContext {
617
625
  this.ui.addChild(this.hookWidgetContainerBelow);
618
626
  this.ui.setBottomPinnedComponent(this.statusLine);
619
627
  this.ui.setFocus(this.editor);
628
+ this.petWidget?.dispose();
620
629
  this.petWidget = this.#createPetWidget(this.editor);
621
- this.petWidget.setMode(settings.get("pet.mode"));
630
+ const configuredPetMode = settings.get("pet.mode");
631
+ this.petWidget.setMode(configuredPetMode);
622
632
  // The async sixel capability probe can enable graphics after the saved
623
633
  // pet mode was applied and dropped (no protocol yet at startup).
624
634
  // Re-apply the configured mode when capability arrives so the pet
@@ -631,6 +641,19 @@ export class InteractiveMode implements InteractiveModeContext {
631
641
  this.petWidget.setMode(saved);
632
642
  }
633
643
  });
644
+ if (configuredPetMode !== "off" && !isPetAvailable()) {
645
+ // The async Sixel capability probe (started by TUI.start()) may still
646
+ // enable graphics; warn only once the capability question is settled
647
+ // so a supported terminal is never told it is incompatible.
648
+ this.#petUnavailableWarningDisposer?.();
649
+ this.#petUnavailableWarningDisposer = warnWhenPetCapabilitySettled({
650
+ probePending: isPetCapabilityProbePending(),
651
+ onUnavailable: () => {
652
+ this.showStatus(theme.fg("warning", getPetUnavailableWarning()), { dim: false });
653
+ this.ui.requestRender();
654
+ },
655
+ });
656
+ }
634
657
 
635
658
  this.#inputController.setupKeyHandlers();
636
659
  this.#inputController.setupEditorSubmitHandler();
@@ -1078,10 +1101,26 @@ export class InteractiveMode implements InteractiveModeContext {
1078
1101
  this.editor.setTopBorder(undefined);
1079
1102
  }
1080
1103
 
1081
- setPetMode(mode: PetMode): void {
1082
- this.petWidget?.setMode(mode);
1104
+ /**
1105
+ * Single result-returning pet commit policy shared by every entry path
1106
+ * (`/pet`, the pet selector, and the Settings submenu). Capability is
1107
+ * rechecked immediately before mutation, and the preference persists only
1108
+ * after the commit is accepted.
1109
+ */
1110
+ #commitPetMode(mode: PetMode, apply: (mode: PetMode) => void): boolean {
1111
+ if (mode !== "off" && !isPetAvailable()) {
1112
+ this.showStatus(theme.fg("warning", getPetUnavailableWarning()), { dim: false });
1113
+ this.ui.requestRender();
1114
+ return false;
1115
+ }
1116
+ apply(mode);
1083
1117
  settings.set("pet.mode", mode);
1084
1118
  this.ui.requestRender();
1119
+ return true;
1120
+ }
1121
+
1122
+ setPetMode(mode: PetMode): boolean {
1123
+ return this.#commitPetMode(mode, next => this.petWidget?.setMode(next));
1085
1124
  }
1086
1125
 
1087
1126
  previewPetMode(mode: PetMode): void {
@@ -1089,6 +1128,10 @@ export class InteractiveMode implements InteractiveModeContext {
1089
1128
  this.ui.requestRender();
1090
1129
  }
1091
1130
 
1131
+ commitPetPreviewMode(mode: PetMode): boolean {
1132
+ return this.#commitPetMode(mode, next => this.petWidget?.commitPreviewMode(next));
1133
+ }
1134
+
1092
1135
  restoreComposer(): void {
1093
1136
  if (this.petWidget) {
1094
1137
  this.petWidget.remountComposer();
@@ -2090,6 +2133,8 @@ export class InteractiveMode implements InteractiveModeContext {
2090
2133
  stop(): void {
2091
2134
  this.#petProtocolUnsubscribe?.();
2092
2135
  this.#petProtocolUnsubscribe = undefined;
2136
+ this.#petUnavailableWarningDisposer?.();
2137
+ this.#petUnavailableWarningDisposer = undefined;
2093
2138
  this.petWidget?.dispose();
2094
2139
  this.petWidget = undefined;
2095
2140
  if (this.loadingAnimation) {
@@ -2223,7 +2268,7 @@ export class InteractiveMode implements InteractiveModeContext {
2223
2268
  nextEditor.setText(previousText);
2224
2269
  previousEditor.dispose();
2225
2270
 
2226
- const petMode = this.petWidget?.mode ?? settings.get("pet.mode");
2271
+ const petMode = settings.get("pet.mode");
2227
2272
  this.petWidget?.dispose();
2228
2273
 
2229
2274
  this.editorContainer.clear();
@@ -176,9 +176,19 @@ export interface InteractiveModeContext {
176
176
  setWorkingMessage(message?: string): void;
177
177
  applyPendingWorkingMessage(): void;
178
178
  ensureLoadingAnimation(): void;
179
- setPetMode(mode: PetMode): void;
179
+ /**
180
+ * Commit a pet mode through the shared result-returning policy: capability
181
+ * is rechecked immediately before mutation and the preference persists only
182
+ * on acceptance. Returns whether the commit was accepted.
183
+ */
184
+ setPetMode(mode: PetMode): boolean;
180
185
  /** Live-preview a pet skin during a selector without persisting. */
181
186
  previewPetMode(mode: PetMode): void;
187
+ /**
188
+ * Commit a settings-overlay pet change without re-mounting the composer.
189
+ * Same shared commit policy and result semantics as `setPetMode`.
190
+ */
191
+ commitPetPreviewMode(mode: PetMode): boolean;
182
192
  /** Re-mount the composer (pet-aware) after an overlay/selector closes. */
183
193
  restoreComposer(): void;
184
194
  startPendingSubmission(input: {
@@ -7024,12 +7024,16 @@ export class AgentSession {
7024
7024
  ThinkingLevel.Off;
7025
7025
  await this.waitForIdle();
7026
7026
  const previousDefaultModelRole = this.settings.getGlobal("modelRoles")?.default;
7027
+ await this.sessionManager.flush();
7028
+ const previousSessionState = this.sessionManager.captureState();
7029
+ const previousModel = this.model;
7030
+ const previousThinkingLevel = this.thinkingLevel;
7031
+ const previousActiveRetryFallback = this.#activeRetryFallback ? { ...this.#activeRetryFallback } : undefined;
7027
7032
  await this.settings.setGlobalModelRoleAndFlush(
7028
7033
  "default",
7029
7034
  formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel),
7030
7035
  );
7031
7036
  try {
7032
- const previousThinkingLevel = this.thinkingLevel;
7033
7037
  await this.setModelTemporary(model, effectiveLevel);
7034
7038
  if (this.thinkingLevel === previousThinkingLevel) {
7035
7039
  this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
@@ -7041,6 +7045,42 @@ export class AgentSession {
7041
7045
  error: String(rollbackError),
7042
7046
  });
7043
7047
  });
7048
+ let restoredLiveState = false;
7049
+ try {
7050
+ const failedEditMode = this.#resolveActiveEditMode();
7051
+ const currentModel = this.model;
7052
+ const hasCompatibleModelTransport =
7053
+ currentModel !== undefined &&
7054
+ previousModel !== undefined &&
7055
+ currentModel.provider === previousModel.provider &&
7056
+ currentModel.id === previousModel.id &&
7057
+ currentModel.api === previousModel.api;
7058
+ if (hasCompatibleModelTransport) {
7059
+ this.agent.setModel(previousModel);
7060
+ this.#syncAppendOnlyContext(previousModel);
7061
+ } else {
7062
+ this.#setModelWithProviderSessionReset(previousModel);
7063
+ }
7064
+ this.#thinkingLevel = previousThinkingLevel;
7065
+ this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
7066
+ await this.#syncEditToolModeAfterModelChange(failedEditMode);
7067
+ restoredLiveState = true;
7068
+ } catch (rollbackError) {
7069
+ logger.warn("Failed to restore live default model selection after live apply failure", {
7070
+ error: String(rollbackError),
7071
+ });
7072
+ }
7073
+ if (restoredLiveState) {
7074
+ this.#activeRetryFallback = previousActiveRetryFallback;
7075
+ }
7076
+ try {
7077
+ this.sessionManager.restoreState(previousSessionState);
7078
+ await this.sessionManager.rewriteEntries();
7079
+ } catch (rollbackError) {
7080
+ logger.warn("Failed to restore session state after live default model selection failure", {
7081
+ error: String(rollbackError),
7082
+ });
7083
+ }
7044
7084
  throw error;
7045
7085
  }
7046
7086
  return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
@@ -8409,7 +8449,7 @@ export class AgentSession {
8409
8449
  return candidate;
8410
8450
  }
8411
8451
 
8412
- #setModelWithProviderSessionReset(model: Model): void {
8452
+ #setModelWithProviderSessionReset(model: Model | undefined): void {
8413
8453
  const currentModel = this.model;
8414
8454
  if (currentModel) {
8415
8455
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
@@ -8449,15 +8489,15 @@ export class AgentSession {
8449
8489
  }
8450
8490
  }
8451
8491
 
8452
- #closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model): void {
8492
+ #closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model | undefined): void {
8453
8493
  const providerKeys = new Set<string>();
8454
- if (currentModel.api === "openai-codex-responses" || nextModel.api === "openai-codex-responses") {
8494
+ if (currentModel.api === "openai-codex-responses" || nextModel?.api === "openai-codex-responses") {
8455
8495
  providerKeys.add("openai-codex-responses");
8456
8496
  }
8457
8497
  if (currentModel.api === "openai-responses") {
8458
8498
  providerKeys.add(`openai-responses:${currentModel.provider}`);
8459
8499
  }
8460
- if (nextModel.api === "openai-responses") {
8500
+ if (nextModel?.api === "openai-responses") {
8461
8501
  providerKeys.add(`openai-responses:${nextModel.provider}`);
8462
8502
  }
8463
8503
 
@@ -10480,8 +10520,9 @@ export class AgentSession {
10480
10520
  // Accept the ordered pair as one volatile batch before committing its
10481
10521
  // roster claim, notifying either UI, or resolving the sender delivery.
10482
10522
  args.signal?.throwIfAborted();
10483
- this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
10523
+ this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord], { deferFlush: true });
10484
10524
  commitRosterClaim?.();
10525
+ this.#flushOrSchedulePendingBackgroundExchanges();
10485
10526
  announceIncoming();
10486
10527
  this.#emitIrcObservation(replyRecord);
10487
10528
  this.#forwardIrcRelayToMain({
@@ -10839,8 +10880,12 @@ export class AgentSession {
10839
10880
  return messages;
10840
10881
  }
10841
10882
 
10842
- #queueBackgroundExchangeInjection(messages: CustomMessage[]): void {
10883
+ #queueBackgroundExchangeInjection(messages: CustomMessage[], options?: { deferFlush?: boolean }): void {
10843
10884
  this.#pendingBackgroundExchanges.push(messages);
10885
+ if (!options?.deferFlush) this.#flushOrSchedulePendingBackgroundExchanges();
10886
+ }
10887
+
10888
+ #flushOrSchedulePendingBackgroundExchanges(): void {
10844
10889
  if (!this.isStreaming) {
10845
10890
  this.#flushPendingBackgroundExchanges();
10846
10891
  return;
@@ -3335,6 +3335,11 @@ export class SessionManager {
3335
3335
 
3336
3336
  restoreState(snapshot: SessionManagerStateSnapshot): void {
3337
3337
  const restoredFileEntries = [...snapshot.materializedFileEntries];
3338
+ const retainsPersistWriter =
3339
+ this.#persistWriter?.isOpen() === true &&
3340
+ this.#sessionId === snapshot.sessionId &&
3341
+ this.#sessionFile === snapshot.sessionFile &&
3342
+ this.#persistWriterPath === snapshot.sessionFile;
3338
3343
  this.#sessionId = snapshot.sessionId;
3339
3344
  this.#sessionName = snapshot.sessionName;
3340
3345
  this.#titleSource = snapshot.titleSource;
@@ -3342,8 +3347,10 @@ export class SessionManager {
3342
3347
  this.#flushed = snapshot.flushed;
3343
3348
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
3344
3349
  this.#fileEntries = restoredFileEntries;
3345
- this.#persistWriter = undefined;
3346
- this.#persistWriterPath = undefined;
3350
+ if (!retainsPersistWriter) {
3351
+ this.#persistWriter = undefined;
3352
+ this.#persistWriterPath = undefined;
3353
+ }
3347
3354
  this.#persistChain = Promise.resolve();
3348
3355
  this.#persistError = undefined;
3349
3356
  this.#persistErrorReported = false;
@@ -3,15 +3,7 @@ import * as path from "node:path";
3
3
  import { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
- import {
7
- isPetMode,
8
- isUnderTerminalMultiplexer,
9
- PET_MODE_IDS,
10
- PET_SKIN_IDS,
11
- PET_SKINS,
12
- Spacer,
13
- Text,
14
- } from "@gajae-code/tui";
6
+ import { PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
15
7
  import { setProjectDir } from "@gajae-code/utils";
16
8
  import { jobElapsedMs } from "../async";
17
9
  import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
@@ -30,7 +22,6 @@ import {
30
22
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
31
23
  import { resolveMemoryBackend } from "../memory-backend";
32
24
  import { DynamicBorder } from "../modes/components/dynamic-border";
33
- import { GajaePetWidget } from "../modes/components/gajae-pet-widget";
34
25
  import { theme } from "../modes/theme/theme";
35
26
  import type { InteractiveModeContext } from "../modes/types";
36
27
  import {
@@ -73,6 +64,22 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
73
64
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
74
65
  export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
75
66
 
67
+ const PET_COMMAND_OPTIONS: ReadonlyArray<{ name: string; mode: PetMode; description: string }> = [
68
+ { name: "off", mode: "off", description: "Hide the pet" },
69
+ { name: "RedGajae", mode: "red", description: PET_SKINS.red.description },
70
+ { name: "BlueGajae", mode: "blue", description: PET_SKINS.blue.description },
71
+ ];
72
+ const PET_COMMAND_HINT = `[${PET_COMMAND_OPTIONS.map(option => option.name).join("|")}]`;
73
+ /**
74
+ * Deprecated inputs kept accepted for compatibility (`/pet on|red|blue`).
75
+ * Display, completion, and inline hints stay canonical (`PET_COMMAND_OPTIONS`).
76
+ */
77
+ const PET_COMMAND_DEPRECATED_INPUTS: Readonly<Record<string, PetMode>> = {
78
+ on: "red",
79
+ red: "red",
80
+ blue: "blue",
81
+ };
82
+
76
83
  type GjcModelBatchAssignmentTargetId = "all-role-agents" | "all-targets";
77
84
  type ParsedModelCommandArgs =
78
85
  | { kind: "summary" }
@@ -570,34 +577,29 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
570
577
  {
571
578
  name: "pet",
572
579
  description: "Gajae pet living beside the composer",
573
- subcommands: [
574
- { name: "off", description: "Hide the pet" },
575
- ...PET_SKIN_IDS.map(id => ({ name: id, description: PET_SKINS[id].description })),
576
- ],
577
- inlineHint: `[${PET_MODE_IDS.join("|")}]`,
580
+ subcommands: PET_COMMAND_OPTIONS.map(option => ({ name: option.name, description: option.description })),
581
+ inlineHint: PET_COMMAND_HINT,
578
582
  allowArgs: true,
579
583
  handleTui: (command, runtime) => {
580
584
  const ctx = runtime.ctx;
581
585
  const raw = command.args?.trim().toLowerCase() ?? "";
582
- const arg = raw === "on" ? "red" : raw;
583
- if (!arg) {
586
+ const arg =
587
+ PET_COMMAND_OPTIONS.find(option => option.name.toLowerCase() === raw)?.mode ??
588
+ PET_COMMAND_DEPRECATED_INPUTS[raw];
589
+ if (!raw) {
584
590
  ctx.showPetSelector();
585
591
  ctx.editor.setText("");
586
592
  return;
587
593
  }
588
- if (arg !== "off" && !GajaePetWidget.pixelProtocol()) {
589
- ctx.showStatus(
590
- isUnderTerminalMultiplexer()
591
- ? "Gajae pet: graphics are suppressed inside tmux/screen/zellij — escapes are not forwarded end-to-end. Run gjc outside the multiplexer in a sixel/kitty-graphics terminal, or set PI_FORCE_IMAGE_PROTOCOL=sixel when your multiplexer+client chain renders sixel."
592
- : "Gajae pet needs a sixel/kitty-graphics terminal (Windows Terminal 1.22+, kitty, Ghostty, WezTerm)",
593
- { dim: true },
594
- );
595
- } else if (isPetMode(arg)) {
596
- ctx.setPetMode(arg);
597
- const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
598
- ctx.showStatus(name);
594
+ if (arg) {
595
+ // The shared commit policy rechecks capability, persists only on
596
+ // acceptance, and surfaces the actionable warning on rejection.
597
+ if (ctx.setPetMode(arg)) {
598
+ const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
599
+ ctx.showStatus(name);
600
+ }
599
601
  } else {
600
- ctx.showStatus(`Usage: /pet [${PET_MODE_IDS.join("|")}]`, { dim: true });
602
+ ctx.showStatus(`Usage: /pet ${PET_COMMAND_HINT}`, { dim: true });
601
603
  }
602
604
  ctx.editor.setText("");
603
605
  },