@kenkaiiii/ggcoder 5.26.1 → 5.26.3
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/dist/app-sidecar.js +123 -53
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session-marker-anchors.test.js +181 -2
- package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
- package/dist/core/agent-session-queue.test.js +65 -0
- package/dist/core/agent-session-queue.test.js.map +1 -1
- package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
- package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
- package/dist/core/agent-session-slash-restore.test.js +121 -0
- package/dist/core/agent-session-slash-restore.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +37 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +126 -31
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +21 -0
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +16 -0
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +91 -0
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +7 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/run-claim.d.ts +37 -0
- package/dist/core/run-claim.d.ts.map +1 -0
- package/dist/core/run-claim.js +46 -0
- package/dist/core/run-claim.js.map +1 -0
- package/dist/core/run-claim.test.d.ts +2 -0
- package/dist/core/run-claim.test.d.ts.map +1 -0
- package/dist/core/run-claim.test.js +107 -0
- package/dist/core/run-claim.test.js.map +1 -0
- package/dist/core/session-history.d.ts +61 -4
- package/dist/core/session-history.d.ts.map +1 -1
- package/dist/core/session-history.js +144 -15
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-history.test.js +50 -1
- package/dist/core/session-history.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +30 -5
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +37 -9
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +14 -2
- package/dist/core/session-manager.test.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -24,6 +24,7 @@ import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
|
|
|
24
24
|
import { setStreamDiagnostic } from "@kenkaiiii/gg-agent";
|
|
25
25
|
import { AgentSession } from "./core/agent-session.js";
|
|
26
26
|
import { RunLifecycle } from "./core/run-lifecycle.js";
|
|
27
|
+
import { RunClaim } from "./core/run-claim.js";
|
|
27
28
|
import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
|
|
28
29
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
29
30
|
import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
|
|
@@ -33,7 +34,7 @@ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
|
33
34
|
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
34
35
|
import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
|
|
35
36
|
import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
|
|
36
|
-
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeKenTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
37
|
+
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeKenTurnsForHistory, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
37
38
|
import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
|
|
38
39
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
39
40
|
import { cleanupToolOutputs } from "./tools/overflow.js";
|
|
@@ -363,30 +364,6 @@ function detectHookKind(text) {
|
|
|
363
364
|
return "regrounding";
|
|
364
365
|
return null;
|
|
365
366
|
}
|
|
366
|
-
// Separator AgentSession.prompt() inserts between a command's prompt body and
|
|
367
|
-
// the user's trailing args. Must stay in sync with the expansion there.
|
|
368
|
-
const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
|
|
369
|
-
/**
|
|
370
|
-
* Reverse a prompt-template command's expansion. When a `/name` command runs,
|
|
371
|
-
* the agent persists the FULL expanded prompt body as the user message — so on
|
|
372
|
-
* resume the raw body would render instead of the short `/name` chip the user
|
|
373
|
-
* saw live. Given the candidate commands (built-in + custom) and a restored
|
|
374
|
-
* message body, recover the original `/name [args]` invocation. Returns null
|
|
375
|
-
* when the text isn't a known command body (an ordinary user message).
|
|
376
|
-
*/
|
|
377
|
-
function detectPromptCommand(text, candidates) {
|
|
378
|
-
for (const c of candidates) {
|
|
379
|
-
if (!c.prompt)
|
|
380
|
-
continue;
|
|
381
|
-
if (text === c.prompt)
|
|
382
|
-
return `/${c.name}`;
|
|
383
|
-
if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
|
|
384
|
-
const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
|
|
385
|
-
return args ? `/${c.name} ${args}` : `/${c.name}`;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
367
|
/** A short transport summary for display (URL for http/sse, command+args for stdio). */
|
|
391
368
|
function mcpRowSummary(config) {
|
|
392
369
|
if (config.url)
|
|
@@ -1532,6 +1509,10 @@ async function createSession(deps, opts) {
|
|
|
1532
1509
|
recordApprovedPlanMarkers(d.text);
|
|
1533
1510
|
});
|
|
1534
1511
|
session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
|
|
1512
|
+
// The agent consumed queued steering at a turn boundary. Re-broadcast as the
|
|
1513
|
+
// usual `queued` depth update so the webview drops the pending affordance the
|
|
1514
|
+
// moment the message lands in the loop, not at run_end.
|
|
1515
|
+
session.eventBus.on("queue_drained", (d) => broadcast("queued", { count: d.count, messages: session.listQueuedMessages() }));
|
|
1535
1516
|
session.eventBus.on("tool_call_start", (d) => {
|
|
1536
1517
|
toolCallNames.set(d.toolCallId, d.name);
|
|
1537
1518
|
broadcast("tool_call_start", d);
|
|
@@ -1577,6 +1558,10 @@ async function createSession(deps, opts) {
|
|
|
1577
1558
|
session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
|
|
1578
1559
|
session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
|
|
1579
1560
|
let running = false;
|
|
1561
|
+
// Closes the window between `/prompt` deciding to start a run and `runAgent`
|
|
1562
|
+
// flipping `running` — that stretch awaits, so Node yields inside it. See
|
|
1563
|
+
// RunClaim.
|
|
1564
|
+
const runClaim = new RunClaim();
|
|
1580
1565
|
const runLifecycle = new RunLifecycle((runState) => {
|
|
1581
1566
|
running = runState !== "idle";
|
|
1582
1567
|
if (runState === "cancelling")
|
|
@@ -1913,7 +1898,10 @@ async function createSession(deps, opts) {
|
|
|
1913
1898
|
// runAutopilotCycle), NOT from this shared finally — that keeps injected
|
|
1914
1899
|
// runs from recursively entering the same review loop.
|
|
1915
1900
|
broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
|
|
1916
|
-
broadcast("queued", {
|
|
1901
|
+
broadcast("queued", {
|
|
1902
|
+
count: session.getQueuedCount(),
|
|
1903
|
+
messages: session.listQueuedMessages(),
|
|
1904
|
+
});
|
|
1917
1905
|
broadcast("extras", footerExtras());
|
|
1918
1906
|
}
|
|
1919
1907
|
}
|
|
@@ -2109,12 +2097,14 @@ async function createSession(deps, opts) {
|
|
|
2109
2097
|
// falling back to the raw verdict text (e.g. ALL_CLEAR).
|
|
2110
2098
|
if (event.type === "autopilot_done") {
|
|
2111
2099
|
// Broadcast the SAME copySeed the persisted marker will produce on
|
|
2112
|
-
// resume, so the live all-clear wording matches the resumed one
|
|
2113
|
-
//
|
|
2100
|
+
// resume, so the live all-clear wording matches the resumed one.
|
|
2101
|
+
// Must use the PERSISTED count — that's what persistAutopilotMarker
|
|
2102
|
+
// anchors against, and it trails the in-memory list after a run
|
|
2103
|
+
// whose messages never made it to disk.
|
|
2114
2104
|
const seed = autopilotMarkerCopySeed({
|
|
2115
2105
|
version: 1,
|
|
2116
2106
|
phase: "done",
|
|
2117
|
-
afterMessageCount: session.
|
|
2107
|
+
afterMessageCount: session.getPersistedTranscriptCount(),
|
|
2118
2108
|
});
|
|
2119
2109
|
broadcast(event.type, { ...event.data, copySeed: seed });
|
|
2120
2110
|
void session.persistAutopilotMarker("done");
|
|
@@ -2159,7 +2149,10 @@ async function createSession(deps, opts) {
|
|
|
2159
2149
|
const next = session.takeNextQueuedMessage();
|
|
2160
2150
|
if (!next)
|
|
2161
2151
|
return;
|
|
2162
|
-
broadcast("queued", {
|
|
2152
|
+
broadcast("queued", {
|
|
2153
|
+
count: session.getQueuedCount(),
|
|
2154
|
+
messages: session.listQueuedMessages(),
|
|
2155
|
+
});
|
|
2163
2156
|
if (!next.text.trim() && next.attachments.length === 0)
|
|
2164
2157
|
continue;
|
|
2165
2158
|
// A queued message draining as a fresh turn supersedes any pending
|
|
@@ -2683,9 +2676,9 @@ async function createSession(deps, opts) {
|
|
|
2683
2676
|
// Autopilot verdict markers to interleave, same anchor scheme as Ken
|
|
2684
2677
|
// turns — each becomes a single assistant row the webview renders
|
|
2685
2678
|
// exactly like the live `autopilot` item (never a raw verdict string).
|
|
2686
|
-
//
|
|
2687
|
-
//
|
|
2688
|
-
//
|
|
2679
|
+
// Normalization pulls anchors left over from an unrebased compaction
|
|
2680
|
+
// back to where the marker was actually written, so stale all-clear
|
|
2681
|
+
// bubbles no longer bunch at the bottom of a reopened session.
|
|
2689
2682
|
const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
|
|
2690
2683
|
const autopilotByCount = new Map();
|
|
2691
2684
|
for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
|
|
@@ -2835,9 +2828,21 @@ async function createSession(deps, opts) {
|
|
|
2835
2828
|
const text = restored.text;
|
|
2836
2829
|
const hook = detectHookKind(text);
|
|
2837
2830
|
const compacted = !hook && text.startsWith("[Previous conversation summary]");
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
|
|
2831
|
+
const hint = userHintByCount.get(nonSystemCount);
|
|
2832
|
+
// The typed invocation persisted alongside the prompt is
|
|
2833
|
+
// authoritative. Reversing the expanded body only works while the
|
|
2834
|
+
// template is byte-identical, and templates drift (edited
|
|
2835
|
+
// `.gg/commands/*.md`, reworded built-ins, app-vs-CLI phrasing) —
|
|
2836
|
+
// after which the resumed session dumped the raw multi-KB body
|
|
2837
|
+
// instead of the `/name` chip. Older sessions have no hint, so the
|
|
2838
|
+
// body match stays as the fallback.
|
|
2839
|
+
const command = !hook && !compacted
|
|
2840
|
+
? resolveRestoredCommand(typeof hint?.command === "string" ? hint.command : null, text, commandCandidates)
|
|
2841
|
+
: null;
|
|
2842
|
+
// Autopilot injected this turn — live showed only the Ken-tinted
|
|
2843
|
+
// marker for it, never a user bubble. Emitting one here would print
|
|
2844
|
+
// the injected instruction a second time, unstyled.
|
|
2845
|
+
if (!restored.autopilotInjected && (text.trim() || restored.images.length > 0)) {
|
|
2841
2846
|
history.push({
|
|
2842
2847
|
role: "user",
|
|
2843
2848
|
text: command ?? text,
|
|
@@ -2965,7 +2970,12 @@ async function createSession(deps, opts) {
|
|
|
2965
2970
|
return;
|
|
2966
2971
|
}
|
|
2967
2972
|
if (method === "POST" && url === "/prompt") {
|
|
2968
|
-
|
|
2973
|
+
// Per-request: only the prompt that actually claimed the start may release
|
|
2974
|
+
// it. A bare release would let an early-returning request (bad JSON, or a
|
|
2975
|
+
// prompt that queued) clear a claim another request is still holding.
|
|
2976
|
+
let claimedStart = false;
|
|
2977
|
+
void readBody(req, res)
|
|
2978
|
+
.then(async (raw) => {
|
|
2969
2979
|
if (raw === null)
|
|
2970
2980
|
return;
|
|
2971
2981
|
let text;
|
|
@@ -2985,14 +2995,17 @@ async function createSession(deps, opts) {
|
|
|
2985
2995
|
json(res, 400, { error: "empty prompt" });
|
|
2986
2996
|
return;
|
|
2987
2997
|
}
|
|
2988
|
-
if (runLifecycle.running &&
|
|
2998
|
+
if (runLifecycle.running &&
|
|
2999
|
+
runLifecycle.isCancellationRequested(runLifecycle.generation)) {
|
|
2989
3000
|
json(res, 409, {
|
|
2990
3001
|
error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
|
|
2991
3002
|
runState: runLifecycle.state,
|
|
2992
3003
|
});
|
|
2993
3004
|
return;
|
|
2994
3005
|
}
|
|
2995
|
-
|
|
3006
|
+
// `runClaim` covers the gap before `runAgent` flips `running`: a
|
|
3007
|
+
// prompt arriving in that window must queue, not start a second run.
|
|
3008
|
+
if (running || runClaim.active || autopilotActive) {
|
|
2996
3009
|
// Queue prompts as mid-run steering (mirrors the CLI). Also queue while
|
|
2997
3010
|
// an autopilot cycle is active but between injected runs (build idle,
|
|
2998
3011
|
// Ken reviewing) so the message never starts a run that collides with
|
|
@@ -3001,20 +3014,47 @@ async function createSession(deps, opts) {
|
|
|
3001
3014
|
// path as a non-queued attachment prompt when it drains.
|
|
3002
3015
|
const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
|
|
3003
3016
|
const count = session.queueMessage(text, prepared);
|
|
3004
|
-
broadcast("queued", { count });
|
|
3017
|
+
broadcast("queued", { count, messages: session.listQueuedMessages() });
|
|
3005
3018
|
json(res, 202, { queued: true, count });
|
|
3006
3019
|
return;
|
|
3007
3020
|
}
|
|
3021
|
+
// Claim the run NOW, synchronously. Everything below this line may
|
|
3022
|
+
// yield, and `running` does not flip until runAgent begins.
|
|
3023
|
+
claimedStart = runClaim.claim();
|
|
3008
3024
|
json(res, 202, { accepted: true });
|
|
3025
|
+
// Gate inputs captured around the run: whether this turn is a workflow
|
|
3026
|
+
// slash command (attachment prompts skip slash expansion entirely), and
|
|
3027
|
+
// how many assistant messages the run actually adds. Computed even when
|
|
3028
|
+
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
3029
|
+
// gate reads the post-run value.
|
|
3030
|
+
const workflowCommand = attachments.length === 0 &&
|
|
3031
|
+
isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
3032
|
+
// Does this input actually expand into a persisted user message? Asked
|
|
3033
|
+
// of the session itself, because only it knows whether the command
|
|
3034
|
+
// resolves here (name/alias casing, custom `.gg/commands`, non-coder
|
|
3035
|
+
// agents that don't expand at all). A looser guess would anchor the
|
|
3036
|
+
// hint at +1 with no message to land on — decorating an unrelated
|
|
3037
|
+
// later bubble with the wrong `/name`.
|
|
3038
|
+
const expandsToTemplate = attachments.length === 0 && (await session.willExpandPromptTemplate(text));
|
|
3009
3039
|
// Webview display hint for this prompt's user bubble (kenSent shimmer
|
|
3010
|
-
// label / enhancer highlight segments
|
|
3011
|
-
// the user message the prompt below
|
|
3012
|
-
// skip this (their position in the run
|
|
3013
|
-
|
|
3040
|
+
// label / enhancer highlight segments / the `/name` a command was typed
|
|
3041
|
+
// as). Anchored +1 so it attaches to the user message the prompt below
|
|
3042
|
+
// is about to push. Queued prompts skip this (their position in the run
|
|
3043
|
+
// is unpredictable).
|
|
3044
|
+
//
|
|
3045
|
+
// Recording the invocation matters because the agent persists the
|
|
3046
|
+
// EXPANDED template as the user message. Resume used to recover
|
|
3047
|
+
// `/name` by matching that body against the current templates, which
|
|
3048
|
+
// silently fails the moment a template is edited or reworded — the
|
|
3049
|
+
// reopened session then rendered the raw multi-KB prompt instead of
|
|
3050
|
+
// the command chip.
|
|
3051
|
+
if (expandsToTemplate ||
|
|
3052
|
+
(meta && (meta.kenSent === true || Array.isArray(meta.enhancements)))) {
|
|
3014
3053
|
void session
|
|
3015
3054
|
.persistAppMarker("user_hint", {
|
|
3016
|
-
...(
|
|
3017
|
-
...(
|
|
3055
|
+
...(expandsToTemplate ? { command: text.trim() } : {}),
|
|
3056
|
+
...(meta?.kenSent === true ? { kenSent: true } : {}),
|
|
3057
|
+
...(Array.isArray(meta?.enhancements) ? { enhancements: meta.enhancements } : {}),
|
|
3018
3058
|
}, 1)
|
|
3019
3059
|
.catch(() => { });
|
|
3020
3060
|
}
|
|
@@ -3025,12 +3065,6 @@ async function createSession(deps, opts) {
|
|
|
3025
3065
|
// feedback, anything) supersedes the pending plan — the bump also
|
|
3026
3066
|
// invalidates any in-flight Ken plan review.
|
|
3027
3067
|
clearPendingPlan();
|
|
3028
|
-
// Gate inputs captured around the run: whether this turn is a workflow
|
|
3029
|
-
// slash command (attachment prompts skip slash expansion entirely), and
|
|
3030
|
-
// how many assistant messages the run actually adds. Computed even when
|
|
3031
|
-
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
3032
|
-
// gate reads the post-run value.
|
|
3033
|
-
const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
3034
3068
|
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
3035
3069
|
const messagesBefore = session.getMessages().length;
|
|
3036
3070
|
await runAgent(text, async () => {
|
|
@@ -3082,6 +3116,10 @@ async function createSession(deps, opts) {
|
|
|
3082
3116
|
// A prompt sent while Ken was reviewing (build idle) queued but had no
|
|
3083
3117
|
// run to steer into — run it now as a fresh turn so it never strands.
|
|
3084
3118
|
await runStrandedQueue();
|
|
3119
|
+
})
|
|
3120
|
+
.finally(() => {
|
|
3121
|
+
if (claimedStart)
|
|
3122
|
+
runClaim.release();
|
|
3085
3123
|
});
|
|
3086
3124
|
return;
|
|
3087
3125
|
}
|
|
@@ -3485,6 +3523,38 @@ async function createSession(deps, opts) {
|
|
|
3485
3523
|
});
|
|
3486
3524
|
return;
|
|
3487
3525
|
}
|
|
3526
|
+
// Pending queued steering, for the composer's cancel affordance.
|
|
3527
|
+
if (method === "GET" && url === "/queued") {
|
|
3528
|
+
json(res, 200, { queued: session.listQueuedMessages() });
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
// Cancel one pending queued message by id.
|
|
3532
|
+
if (method === "POST" && url === "/queued/cancel") {
|
|
3533
|
+
void readBody(req, res).then((raw) => {
|
|
3534
|
+
if (raw === null)
|
|
3535
|
+
return;
|
|
3536
|
+
let id;
|
|
3537
|
+
try {
|
|
3538
|
+
id = JSON.parse(raw).id ?? "";
|
|
3539
|
+
}
|
|
3540
|
+
catch {
|
|
3541
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (!id.trim()) {
|
|
3545
|
+
json(res, 400, { error: "missing queued message id" });
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
// `false` means it already drained into the run between render and
|
|
3549
|
+
// click. That is a race, not an error, so report it as a normal result
|
|
3550
|
+
// and let the client reconcile from the fresh list.
|
|
3551
|
+
const cancelled = session.cancelQueuedMessage(id);
|
|
3552
|
+
const queued = session.listQueuedMessages();
|
|
3553
|
+
broadcast("queued", { count: queued.length, messages: queued });
|
|
3554
|
+
json(res, 200, { cancelled, queued });
|
|
3555
|
+
});
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3488
3558
|
if (method === "POST" && url === "/kill") {
|
|
3489
3559
|
void readBody(req, res).then(async (raw) => {
|
|
3490
3560
|
if (raw === null)
|
|
@@ -3542,7 +3612,7 @@ async function createSession(deps, opts) {
|
|
|
3542
3612
|
const generation = runLifecycle.generation;
|
|
3543
3613
|
if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
|
|
3544
3614
|
pendingCancelDrain = { generation, text: session.drainQueue() };
|
|
3545
|
-
broadcast("queued", { count: 0 });
|
|
3615
|
+
broadcast("queued", { count: 0, messages: [] });
|
|
3546
3616
|
}
|
|
3547
3617
|
const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
|
|
3548
3618
|
const drained = pendingCancelDrain.text;
|