@ferris1225/pi-subagents 0.32.2 → 1.0.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/src/fixloop.ts CHANGED
@@ -87,7 +87,7 @@ export function chainKeyFragments(result: SingleResult): string[] {
87
87
  }
88
88
 
89
89
  /**
90
- * Compact one-line outcome for a finished chain run, shown in the widget so
90
+ * Compact one-line outcome for a finished chain run, retained so
91
91
  * each round reads as what it did: a reviewer reports its verdict plus the
92
92
  * key fragments of what it found ("fail · src/index.ts · render()"), a worker
93
93
  * the fragments of what it changed. Failed runs and runs with nothing
package/src/format.ts CHANGED
@@ -103,7 +103,8 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
103
103
  (result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
104
104
  ].filter((value): value is string => Boolean(value));
105
105
  const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
106
- const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
106
+ const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
107
+ const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
107
108
  if (result.isolation === "worktree") {
108
109
  const isolation =
109
110
  result.integrationStatus === "integrated"
package/src/index.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * The heavy lifting lives in focused modules:
6
6
  * - dispatch.ts — the `subagent` tool (spawn, auto-fix chain, vision model)
7
7
  * - tools.ts — subagent_control / subagent_wait / status / stop
8
- * - widget.ts session_start widget + one-time feature announcements
9
- * - runtime.ts — shared per-session state
8
+ * - announcements.ts session-start recovery and feature notices
9
+ * - runtime.ts — shared per-session state
10
10
  *
11
11
  * Also registers the `/subagents-setup` command and a `before_agent_start` hook
12
12
  * that injects a delegation directive into the parent system prompt so the main
@@ -19,16 +19,15 @@
19
19
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
20
  import { Text } from "@earendil-works/pi-tui";
21
21
  import { discoverAgents } from "./agents.ts";
22
+ import { registerAnnouncements } from "./announcements.ts";
22
23
  import { getConfigPath, loadConfig } from "./config.ts";
23
24
  import { registerSubagentTool } from "./dispatch.ts";
24
25
  import { matchRunIds } from "./format.ts";
25
- import { registerInspectorCommand } from "./inspector-panel.ts";
26
26
  import { buildDelegationDirective } from "./prompt.ts";
27
27
  import { createRuntime } from "./runtime.ts";
28
28
  import { runSetup } from "./setup.ts";
29
29
  import { currentSubagentDepth } from "./spawn.ts";
30
30
  import { registerLookupTools } from "./tools.ts";
31
- import { registerWidget } from "./widget.ts";
32
31
 
33
32
  export { matchRunIds };
34
33
 
@@ -64,7 +63,6 @@ export default function (pi: ExtensionAPI): void {
64
63
 
65
64
  registerSubagentTool(pi, runtime);
66
65
  registerLookupTools(pi, runtime);
67
- registerInspectorCommand(pi, runtime);
68
66
 
69
67
  pi.registerCommand("subagents-setup", {
70
68
  description: "Configure pi-subagents: enabled agents, primary/backup model pools, and runtime settings",
@@ -73,9 +71,7 @@ export default function (pi: ExtensionAPI): void {
73
71
  },
74
72
  });
75
73
 
76
- // Persistent widget above the editor showing live sub-agent status, plus
77
- // one-time feature announcements after updates.
78
- registerWidget(pi, runtime);
74
+ registerAnnouncements(pi, runtime);
79
75
 
80
76
  // Proactive dispatch: inject the delegation directive into the parent system prompt.
81
77
  pi.on("before_agent_start", async (event, ctx) => {
package/src/models.ts CHANGED
@@ -72,18 +72,25 @@ export function currentModelRef(ctx: Pick<ModelContext, "model">): string | unde
72
72
  }
73
73
 
74
74
  /**
75
- * Models usable by this main window. Scoped models replace the full available
76
- * registry, matching pi's built-in model picker semantics.
75
+ * Current authenticated registry models narrowed by the session scope. Scope
76
+ * entries are a session snapshot, so they act only as a whitelist; the live
77
+ * registry remains the source of truth for availability and model metadata.
77
78
  */
78
- export function availableModelRefs(ctx: ModelContext): string[] {
79
+ export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
80
+ const models = ctx.modelRegistry.getAvailable();
79
81
  // scopedModels was added after the declared Pi 0.80.6 minimum. Treat a
80
- // missing field exactly like an empty scope and use the registry fallback.
82
+ // missing field exactly like an empty scope and use the full live registry.
81
83
  const scopedModels = ctx.scopedModels ?? [];
82
- const scoped = scopedModels.length > 0 ? scopedModels.map((entry) => entry.model) : undefined;
83
- const models = scoped ?? ctx.modelRegistry.getAvailable();
84
- const refs = [...new Set(models.map(modelRef))];
84
+ if (scopedModels.length === 0) return models;
85
+ const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
86
+ return models.filter((model) => scopedRefs.has(modelRef(model)));
87
+ }
88
+
89
+ /** Model refs usable by setup, with an available current main model first. */
90
+ export function availableModelRefs(ctx: ModelContext): string[] {
91
+ const refs = [...new Set(availableModelsInScope(ctx).map(modelRef))];
85
92
  const currentRef = currentModelRef(ctx);
86
- if (!currentRef) return refs;
93
+ if (!currentRef || !refs.includes(currentRef)) return refs;
87
94
  return [currentRef, ...refs.filter((ref) => ref !== currentRef)];
88
95
  }
89
96
 
@@ -118,7 +125,9 @@ function modelCapabilities(model: ModelListEntry): string {
118
125
  return capabilities.join(" + ");
119
126
  }
120
127
 
121
- /** Build the single searchable model list shared by primary/backup/vision picks. */
128
+ /** Build the single searchable model list shared by primary/backup/vision picks.
129
+ * Only refs Pi currently reports as available are shown, which means providers
130
+ * without a configured API key/OAuth session never flood the setup picker. */
122
131
  export function buildModelPickerItems(options: {
123
132
  models: readonly ModelListEntry[];
124
133
  availableRefs: readonly string[];
@@ -132,21 +141,16 @@ export function buildModelPickerItems(options: {
132
141
  const byRef = new Map<string, ModelListEntry>();
133
142
  for (const model of options.models) {
134
143
  const ref = modelRef(model);
135
- if (!byRef.has(ref)) byRef.set(ref, model);
144
+ if (available.has(ref) && !byRef.has(ref)) byRef.set(ref, model);
136
145
  }
137
146
 
138
147
  const refs = [...byRef.keys()]
139
- .filter((ref) =>
140
- options.slot !== "vision" ||
141
- byRef.get(ref)?.input.includes("image") === true ||
142
- ref === configuredRef,
143
- )
148
+ .filter((ref) => options.slot !== "vision" || byRef.get(ref)?.input.includes("image") === true)
144
149
  .sort((left, right) => {
145
150
  const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
146
151
  const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
147
152
  return leftRank - rightRank || left.localeCompare(right);
148
153
  });
149
- if (configuredRef && !byRef.has(configuredRef)) refs.unshift(configuredRef);
150
154
 
151
155
  const dynamic = options.slot === "backup"
152
156
  ? {
@@ -164,31 +168,15 @@ export function buildModelPickerItems(options: {
164
168
 
165
169
  const items: ModelPickerItem[] = [dynamic];
166
170
  for (const ref of refs) {
167
- const model = byRef.get(ref);
168
- const tags = [available.has(ref) ? "available" : "unavailable"];
171
+ const model = byRef.get(ref)!;
172
+ const tags = ["available"];
169
173
  if (ref === configuredRef) tags.push("configured");
170
174
  if (ref === mainRef) tags.push("current main");
171
- if (!model) {
172
- const compatibility = options.slot === "vision"
173
- ? "incompatible with vision (capability unknown)"
174
- : undefined;
175
- items.push({
176
- value: ref,
177
- label: ref,
178
- description: [...tags, compatibility, "saved model reference"].filter(Boolean).join(" · "),
179
- ...(options.slot === "vision" ? { disabled: true } : {}),
180
- });
181
- continue;
182
- }
183
175
  const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
184
- const compatibility = options.slot === "vision" && !model.input.includes("image")
185
- ? "incompatible with vision"
186
- : undefined;
187
176
  items.push({
188
177
  value: ref,
189
178
  label: ref,
190
- description: [name, modelCapabilities(model), compatibility, ...tags].filter(Boolean).join(" · "),
191
- ...(compatibility ? { disabled: true } : {}),
179
+ description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
192
180
  });
193
181
  }
194
182
  return items;
package/src/monitor.ts CHANGED
@@ -2,18 +2,17 @@
2
2
  * Sub-agent monitor: a module-level singleton store that tracks subagent runs
3
3
  * for the current turn.
4
4
  *
5
- * The store notifies subscribers on every mutation so the persistent widget
6
- * above the editor can re-render. Each run carries timing information
7
- * (started/ended) plus a concise activity string describing what the run is
8
- * doing right now ("thinking", "read src/index.ts", ...). Runs are removed
9
- * as soon as they finish: the tool result is the durable record in the main
10
- * conversation, so a stale "done" row must not linger in the widget.
5
+ * The store notifies wait/status consumers on every mutation. Each run carries
6
+ * timing information plus a concise activity string ("thinking",
7
+ * "read src/index.ts", ...). Runs are removed after publication; tool results
8
+ * and the finished-run registry are the durable user-facing records.
11
9
  */
12
10
 
13
11
  import { stripVTControlCharacters } from "node:util";
14
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
15
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
13
+ import { visibleWidth } from "@earendil-works/pi-tui";
16
14
  import type { UsageStats } from "./spawn.ts";
15
+ import { redactSensitiveText } from "./trajectory.ts";
17
16
  import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
18
17
 
19
18
  // ---------------------------------------------------------------------------
@@ -26,20 +25,6 @@ export function isRunActiveStatus(status: RunStatus): boolean {
26
25
  return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
27
26
  }
28
27
 
29
- /** Soft state-awareness signals, complementary to the hard idle-kill: a run may
30
- * be alive (stdout streaming) yet "stuck thinking" (no tool running for a while),
31
- * or simply taking a long time. Both are surfaced as widget annotations so the
32
- * user can tell a healthy busy run from one that needs a nudge. */
33
- export type ActivityState = "needs_attention" | "active_long_running";
34
-
35
- /** A run with no tool running and no activity for this long is "needs attention"
36
- * (the model may be stuck between turns). Below the idle-kill threshold so the
37
- * soft signal always fires before the hard kill. */
38
- export const NEEDS_ATTENTION_AFTER_MS = 60_000;
39
- /** A run whose total elapsed time exceeds this is "long-running": still active
40
- * but worth flagging so the user can decide whether to wait or steer. */
41
- export const ACTIVE_LONG_RUNNING_AFTER_MS = 240_000;
42
-
43
28
  export interface RunView {
44
29
  id: number;
45
30
  agent: string;
@@ -77,15 +62,14 @@ export interface RunView {
77
62
  groupId?: string;
78
63
  /** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
79
64
  relationLabel?: string;
80
- /** Free-form note shown in the widget next to the status label (e.g. "auto-fix chain running"). */
65
+ /** Free-form orchestration note (e.g. "auto-fix chain running"). */
81
66
  annotation?: string;
82
- /** One-line outcome summary of a finished chain run, shown in the widget so
83
- * each auto-fix round reads as what it did: a reviewer reports its verdict
67
+ /** One-line outcome summary of a finished chain run: a reviewer reports its verdict
84
68
  * plus key fragments of what it found ("fail · src/index.ts · render()"), a
85
69
  * worker the fragments of what it changed. Unset for non-chain runs. */
86
70
  summary?: string;
87
- /** True when a finished run is intentionally kept in the widget (e.g. an
88
- * auto-fix chain parent whose chain is still running). beginTurn preserves
71
+ /** True when a finished run remains in monitor state (e.g. an auto-fix
72
+ * parent whose chain is still running). beginTurn preserves
89
73
  * retained runs so they are not swept between turns. */
90
74
  retained?: boolean;
91
75
  }
@@ -107,7 +91,7 @@ const TASK_SUMMARY_ELLIPSIS = "…";
107
91
  /** Columns reserved at the END of a truncated summary so the distinguishing
108
92
  * keywords (paths, symbols, ...) survive; the head gets the rest. */
109
93
  const TASK_SUMMARY_TAIL_MAX = 28;
110
- /** Tail share of a non-default maxWidth (narrow widgets keep a usable tail). */
94
+ /** Tail share of a non-default maxWidth (narrow summaries keep a usable tail). */
111
95
  const TASK_SUMMARY_TAIL_SHARE = 0.35;
112
96
  const TASK_SUMMARY_TAIL_MIN = 8;
113
97
  const TASK_SUMMARY_KEY_SEP = " · ";
@@ -305,63 +289,18 @@ export function formatElapsed(run: RunView, now: number = Date.now()): string {
305
289
  return formatDuration(end - run.startedAt);
306
290
  }
307
291
 
308
- /** Strip the provider prefix from a "provider/model-id" reference for compact
309
- * widget display ("anthropic/claude-sonnet-4" → "claude-sonnet-4"). A bare id is
310
- * left unchanged. */
311
- export function compactModelRef(model: string | undefined): string {
312
- if (!model) return "";
313
- const slash = model.lastIndexOf("/");
314
- return slash >= 0 ? model.slice(slash + 1) : model;
315
- }
316
-
317
- /** Human-readable label for a soft activity-state annotation. */
318
- export function activityStateLabel(state: ActivityState): string {
319
- return state === "needs_attention" ? "idle" : "long-running";
320
- }
321
-
322
- /** Derive the soft activity state of a run at render time: needs_attention
323
- * (no tool running, idle past the threshold) takes priority over
324
- * active_long_running (total elapsed past its threshold). Both are suppressed
325
- * for non-running runs. */
326
- export function deriveActivityState(run: RunView, now: number = Date.now()): ActivityState | undefined {
327
- if (run.status !== "running" && run.status !== "steering") return undefined;
328
- if (!run.currentTool) {
329
- const since = run.lastActivityAt ?? run.startedAt ?? now;
330
- if (now - since >= NEEDS_ATTENTION_AFTER_MS) return "needs_attention";
331
- }
332
- if (run.startedAt !== undefined && now - run.startedAt >= ACTIVE_LONG_RUNNING_AFTER_MS) {
333
- return "active_long_running";
334
- }
335
- return undefined;
336
- }
337
-
338
- /** Left/right split a widget line so the right side (status, elapsed) is always
339
- * visible and the left side (title) clips on overflow instead of pushing it off.
340
- * `left`/`right` may carry ANSI styling; widths are measured display-column-wise. */
341
- export function rightAlign(left: string, right: string, width: number): string {
342
- const rightWidth = visibleWidth(right);
343
- const leftMax = Math.max(0, width - rightWidth - 1);
344
- const leftClipped = truncateToWidth(left, leftMax);
345
- const gap = Math.max(1, width - visibleWidth(leftClipped) - rightWidth);
346
- return truncateToWidth(`${leftClipped}${" ".repeat(gap)}${right}`, width);
347
- }
348
-
349
- /** Concatenate `left` and `right` (no center padding) and clip the combined line
350
- * to `width`. Unlike {@link rightAlign}, the right side trails the left side
351
- * instead of being pinned to the far edge, so a short header leaves no gap in
352
- * the middle. `right` should carry its own leading separator (e.g. ` \u00b7 `);
353
- * pass an empty string when there is nothing to append. Styled strings are
354
- * measured by display width. */
355
- export function compactLine(left: string, right: string, width: number): string {
356
- return truncateToWidth(`${left}${right}`, width);
357
- }
358
-
359
292
  /** Max length of the argument target inside a formatted activity line. */
360
293
  export const ACTIVITY_TARGET_MAX = 60;
361
294
 
295
+ /** Monitor activity is returned to the parent model and rendered in the terminal,
296
+ * so treat every live string as untrusted before it reaches store state. */
297
+ function sanitizeActivityText(value: string): string {
298
+ return redactSensitiveText(value).replace(/\s+/g, " ").trim();
299
+ }
300
+
362
301
  function shortTarget(value: unknown): string {
363
302
  if (typeof value !== "string") return "";
364
- const oneLine = value.replace(/\s+/g, " ").trim();
303
+ const oneLine = sanitizeActivityText(value);
365
304
  // Slice by code point so emoji / CJK-ext never leave a lone surrogate.
366
305
  const chars = [...oneLine];
367
306
  return chars.length > ACTIVITY_TARGET_MAX ? `${chars.slice(0, ACTIVITY_TARGET_MAX - 1).join("")}…` : oneLine;
@@ -410,7 +349,8 @@ export function formatToolActivity(toolName: string, args: unknown): string {
410
349
  default:
411
350
  target = pick("path", "command", "query", "pattern", "url", "file", "task");
412
351
  }
413
- return target ? `${toolName} ${target}` : toolName;
352
+ const safeToolName = sanitizeActivityText(toolName) || "tool";
353
+ return target ? `${safeToolName} ${target}` : safeToolName;
414
354
  }
415
355
 
416
356
  // ---------------------------------------------------------------------------
@@ -491,7 +431,7 @@ export class MonitorStore {
491
431
  setActivity(id: number, text: string): void {
492
432
  const run = this.find(id);
493
433
  if (!run) return;
494
- run.activity = text;
434
+ run.activity = sanitizeActivityText(text) || undefined;
495
435
  run.lastActivityAt = Date.now();
496
436
  this.notify();
497
437
  }
@@ -502,9 +442,10 @@ export class MonitorStore {
502
442
  recordToolStart(id: number, toolName: string, activity: string): void {
503
443
  const run = this.find(id);
504
444
  if (!run) return;
445
+ const safeToolName = sanitizeActivityText(toolName) || "tool";
505
446
  run.toolCount = (run.toolCount ?? 0) + 1;
506
- run.currentTool = toolName;
507
- run.activity = activity;
447
+ run.currentTool = safeToolName;
448
+ run.activity = sanitizeActivityText(activity) || safeToolName;
508
449
  run.lastActivityAt = Date.now();
509
450
  this.notify();
510
451
  }
@@ -516,11 +457,11 @@ export class MonitorStore {
516
457
  if (!run) return;
517
458
  run.currentTool = undefined;
518
459
  run.lastActivityAt = Date.now();
519
- if (isError) run.activity = `✗ ${toolName} failed`;
460
+ if (isError) run.activity = `✗ ${sanitizeActivityText(toolName) || "tool"} failed`;
520
461
  this.notify();
521
462
  }
522
463
 
523
- /** Set a widget note on the run (e.g. that its auto-fix chain is still running). */
464
+ /** Set an orchestration note on the run (e.g. auto-fix chain running). */
524
465
  setAnnotation(id: number, text: string): void {
525
466
  const run = this.find(id);
526
467
  if (!run) return;
@@ -536,7 +477,7 @@ export class MonitorStore {
536
477
  this.notify();
537
478
  }
538
479
 
539
- /** Mark a run as retained (kept in the widget despite being finished). */
480
+ /** Keep a finished chain step in status state until its group settles. */
540
481
  setRetained(id: number, retained: boolean): void {
541
482
  const run = this.find(id);
542
483
  if (!run) return;
@@ -624,7 +565,7 @@ export class MonitorStore {
624
565
  this.notify();
625
566
  }
626
567
 
627
- /** Remove a run (finished runs leave the widget). Returns the removed run. */
568
+ /** Remove a run after publication. Returns the removed run. */
628
569
  removeRun(id: number): RunView | undefined {
629
570
  const index = this.runs.findIndex((r) => r.id === id);
630
571
  if (index === -1) return undefined;
@@ -698,7 +639,7 @@ export function statusIcon(status: RunStatus, theme: Theme): string {
698
639
  }
699
640
  }
700
641
 
701
- /** User-facing status label shown in the widget. */
642
+ /** User-facing status label used by tool/status rendering. */
702
643
  export function statusLabel(status: RunStatus): string {
703
644
  switch (status) {
704
645
  case "queued":
package/src/rpc-run.ts CHANGED
@@ -96,15 +96,6 @@ export type SubagentLiveEvent =
96
96
  | { kind: "thinking" }
97
97
  | { kind: "text" };
98
98
 
99
- /** Carries the actual streamed payload (dropped by the plain text/thinking
100
- * live events). Emitted in addition to the aliveness events so monitor
101
- * observers stay unchanged while the inspector can apply its rolling budget
102
- * without silently losing part of a transport delta. */
103
- export interface SubagentRecordEvent {
104
- kind: "thinking" | "text";
105
- delta: string;
106
- }
107
-
108
99
  export type RpcControlPhase =
109
100
  | "queued"
110
101
  | "starting"
@@ -413,15 +404,13 @@ export interface RunRpcAttemptOptions {
413
404
  prompt: string;
414
405
  signal?: AbortSignal;
415
406
  onLive?: (event: SubagentLiveEvent) => void;
416
- /** Receives the raw streamed deltas; observer errors are swallowed. */
417
- onRecord?: (event: SubagentRecordEvent) => void;
418
407
  env?: NodeJS.ProcessEnv;
419
408
  control?: RpcRunControl;
420
409
  }
421
410
 
422
411
  /** Run one persistent RPC child until a stable `agent_settled` or control action. */
423
412
  export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise<RpcSingleResult> {
424
- const { agent, agentName, task, thinkingLevel, idleTimeoutMs, signal, onLive, onRecord, control } = options;
413
+ const { agent, agentName, task, thinkingLevel, idleTimeoutMs, signal, onLive, control } = options;
425
414
  const args: string[] = ["--mode", "rpc", "--exclude-tools", "subagent,subagent_control"];
426
415
  if (options.sessionDir && options.sessionId) {
427
416
  args.push("--session-dir", options.sessionDir);
@@ -506,14 +495,6 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
506
495
  }
507
496
  };
508
497
 
509
- const emitRecord = (event: SubagentRecordEvent): void => {
510
- try {
511
- onRecord?.(event);
512
- } catch {
513
- /* record observers must never break protocol handling */
514
- }
515
- };
516
-
517
498
  const setAttemptPhase = (phase: RpcControlPhase): void => {
518
499
  if (attemptToken !== undefined) control?.updateAttemptPhase(attemptToken, phase);
519
500
  switch (phase) {
@@ -806,12 +787,6 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
806
787
  const type = event.assistantMessageEvent?.type;
807
788
  if (type === "thinking_delta" || type === "text_delta") {
808
789
  emit({ kind: type === "thinking_delta" ? "thinking" : "text" });
809
- const delta = event.assistantMessageEvent?.delta;
810
- if (typeof delta === "string" && delta) {
811
- // TranscriptBuffer owns the complete rolling-budget decision. Truncating
812
- // one RPC delta here would silently lose text without setting its flag.
813
- emitRecord({ kind: type === "thinking_delta" ? "thinking" : "text", delta });
814
- }
815
790
  }
816
791
  }
817
792