@gajae-code/coding-agent 0.10.0 → 0.10.1
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/CHANGELOG.md +13 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +17 -5
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/package.json +7 -7
- package/src/cli.ts +1 -0
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +294 -66
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/main.ts +3 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/status-line/segments.ts +13 -6
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +16 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/session/agent-session.ts +287 -85
- package/src/session/context-estimation.ts +91 -0
- package/src/slash-commands/builtin-registry.ts +12 -2
- package/src/slash-commands/helpers/context-report.ts +26 -4
package/src/main.ts
CHANGED
|
@@ -1398,12 +1398,10 @@ export async function runRootCommand(
|
|
|
1398
1398
|
if ($env.PI_TIMING) {
|
|
1399
1399
|
logger.printTimings();
|
|
1400
1400
|
}
|
|
1401
|
+
stopThemeWatcher();
|
|
1401
1402
|
if (!deps.suppressProcessExit) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
await postmortem.quit(0);
|
|
1405
|
-
} else {
|
|
1406
|
-
stopThemeWatcher();
|
|
1403
|
+
const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
1404
|
+
await postmortem.quit(exitCode);
|
|
1407
1405
|
}
|
|
1408
1406
|
}
|
|
1409
1407
|
}
|
|
@@ -2217,14 +2217,14 @@ export class AcpAgent implements Agent {
|
|
|
2217
2217
|
const sessionId = record.session.sessionId;
|
|
2218
2218
|
|
|
2219
2219
|
const contextUsage = record.session.getContextUsage();
|
|
2220
|
-
if (contextUsage) {
|
|
2220
|
+
if (contextUsage?.tokens != null) {
|
|
2221
2221
|
const usageStats = record.session.sessionManager.getUsageStatistics();
|
|
2222
2222
|
await this.#connection.sessionUpdate({
|
|
2223
2223
|
sessionId,
|
|
2224
2224
|
update: {
|
|
2225
2225
|
sessionUpdate: "usage_update",
|
|
2226
2226
|
size: contextUsage.contextWindow,
|
|
2227
|
-
used: contextUsage.tokens
|
|
2227
|
+
used: contextUsage.tokens,
|
|
2228
2228
|
cost: usageStats.cost > 0 ? { amount: usageStats.cost, currency: "USD" } : undefined,
|
|
2229
2229
|
},
|
|
2230
2230
|
});
|
|
@@ -139,8 +139,8 @@ export class FooterComponent implements Component {
|
|
|
139
139
|
// After compaction, tokens are unknown until the next LLM response.
|
|
140
140
|
const contextUsage = this.session.getContextUsage();
|
|
141
141
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
142
|
-
const contextPercentValue = contextUsage?.percent
|
|
143
|
-
const contextPercent =
|
|
142
|
+
const contextPercentValue = contextUsage?.percent;
|
|
143
|
+
const contextPercent = typeof contextPercentValue === "number" ? contextPercentValue.toFixed(1) : "?";
|
|
144
144
|
|
|
145
145
|
// Replace home directory with ~
|
|
146
146
|
let pwd = shortenPath(getProjectDir());
|
|
@@ -188,7 +188,7 @@ export class FooterComponent implements Component {
|
|
|
188
188
|
contextPercent === "?"
|
|
189
189
|
? `?/${formatNumber(contextWindow)}${autoIndicator}`
|
|
190
190
|
: `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
|
|
191
|
-
if (
|
|
191
|
+
if (typeof contextPercentValue === "number") {
|
|
192
192
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
193
193
|
contextPercentStr =
|
|
194
194
|
color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
|
|
@@ -114,9 +114,13 @@ const modelSegment: StatusLineSegment = {
|
|
|
114
114
|
if (opts.showContextPercent !== false && ctx.contextPctSegmentActive !== true) {
|
|
115
115
|
const pct = ctx.contextPercent;
|
|
116
116
|
const window = ctx.contextWindow;
|
|
117
|
-
if (window > 0
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if (window > 0) {
|
|
118
|
+
if (typeof pct === "number" && Number.isFinite(pct)) {
|
|
119
|
+
const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
|
|
120
|
+
content += `${theme.sep.dot}${theme.fg(color, `${pct.toFixed(1)}%`)}`;
|
|
121
|
+
} else {
|
|
122
|
+
content += `${theme.sep.dot}${theme.fg("statusLineContext", "?")}`;
|
|
123
|
+
}
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -382,11 +386,14 @@ const contextPctSegment: StatusLineSegment = {
|
|
|
382
386
|
render(ctx) {
|
|
383
387
|
const pct = ctx.contextPercent;
|
|
384
388
|
const window = ctx.contextWindow;
|
|
389
|
+
const knownPct = typeof pct === "number" && Number.isFinite(pct) ? pct : undefined;
|
|
385
390
|
|
|
386
391
|
const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
|
|
387
|
-
const text = `${
|
|
388
|
-
|
|
389
|
-
|
|
392
|
+
const text = `${knownPct === undefined ? "?" : `${knownPct.toFixed(1)}%`}/${formatNumber(window)}${autoIcon}`;
|
|
393
|
+
const color =
|
|
394
|
+
knownPct === undefined
|
|
395
|
+
? "statusLineContext"
|
|
396
|
+
: getContextUsageThemeColor(getContextUsageLevel(knownPct, window));
|
|
390
397
|
const content = withIcon(theme.icon.context, theme.fg(color, text));
|
|
391
398
|
|
|
392
399
|
return { content, visible: true };
|
|
@@ -39,7 +39,7 @@ export interface SegmentContext {
|
|
|
39
39
|
cost: number;
|
|
40
40
|
tokensPerSecond: number | null;
|
|
41
41
|
};
|
|
42
|
-
contextPercent: number;
|
|
42
|
+
contextPercent: number | null;
|
|
43
43
|
contextWindow: number;
|
|
44
44
|
/**
|
|
45
45
|
* True when a standalone `context_pct` segment is also part of the active
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
2
|
+
|
|
4
3
|
import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
5
4
|
import { formatCount, getProjectDir } from "@gajae-code/utils";
|
|
6
5
|
import { $ } from "bun";
|
|
@@ -13,7 +12,6 @@ import * as git from "../../utils/git";
|
|
|
13
12
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
|
|
14
13
|
import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
|
|
15
14
|
import { sanitizeStatusText } from "../shared";
|
|
16
|
-
import { computeNonMessageTokens } from "../utils/context-usage";
|
|
17
15
|
import { renderSkillHudBar } from "./skill-hud/render";
|
|
18
16
|
import {
|
|
19
17
|
canReuseCachedPr,
|
|
@@ -70,104 +68,6 @@ interface CollectedStatusSegments {
|
|
|
70
68
|
rightCapWidth: number;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
-
// Per-message token cache
|
|
75
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
|
|
79
|
-
* `estimateMessageTokensHeuristic` result. Keyed by message identity (the object itself);
|
|
80
|
-
* a cheap content fingerprint detects in-place mutations (post-hoc error
|
|
81
|
-
* attachment, retry-truncated branch rebuild, etc.) and forces recompute.
|
|
82
|
-
*
|
|
83
|
-
* Cache lives on the message — multiple `StatusLineComponent` instances
|
|
84
|
-
* share it for free, and entries collect with the message itself when the
|
|
85
|
-
* conversation is replaced or compacted.
|
|
86
|
-
*/
|
|
87
|
-
const kTokenCache = Symbol("statusLine.tokenCache");
|
|
88
|
-
interface TaggedMessage {
|
|
89
|
-
[kTokenCache]?: { fingerprint: string; tokens: number };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Cheap structural fingerprint mirroring `estimateMessageTokensHeuristic`'s content walk.
|
|
94
|
-
* O(blocks) — only reads string `.length` and primitives, never copies or
|
|
95
|
-
* serializes content. Any in-place mutation that alters total tokenized
|
|
96
|
-
* content also alters one of the byte-length sums or block counts captured
|
|
97
|
-
* here, forcing the cached heuristic token value to be recomputed.
|
|
98
|
-
*/
|
|
99
|
-
function messageFingerprint(msg: AgentMessage): string {
|
|
100
|
-
const role = (msg as { role?: string }).role ?? "";
|
|
101
|
-
const ts = (msg as { timestamp?: number }).timestamp ?? 0;
|
|
102
|
-
let textLen = 0;
|
|
103
|
-
let blocks = 0;
|
|
104
|
-
let images = 0;
|
|
105
|
-
if (role === "bashExecution") {
|
|
106
|
-
const b = msg as { command?: unknown; output?: unknown };
|
|
107
|
-
if (typeof b.command === "string") textLen += b.command.length;
|
|
108
|
-
if (typeof b.output === "string") textLen += b.output.length;
|
|
109
|
-
} else if (role === "user") {
|
|
110
|
-
const content = (msg as { content?: unknown }).content;
|
|
111
|
-
if (typeof content === "string") {
|
|
112
|
-
textLen += content.length;
|
|
113
|
-
} else if (Array.isArray(content)) {
|
|
114
|
-
blocks = content.length;
|
|
115
|
-
for (const block of content) {
|
|
116
|
-
if (block?.type === "text" && typeof block.text === "string") textLen += block.text.length;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} else if (role === "assistant") {
|
|
120
|
-
const content = (msg as { content?: unknown }).content;
|
|
121
|
-
if (Array.isArray(content)) {
|
|
122
|
-
blocks = content.length;
|
|
123
|
-
for (const block of content) {
|
|
124
|
-
if (!block || typeof block !== "object") continue;
|
|
125
|
-
const b = block as { type?: string; text?: string; thinking?: string; name?: string; arguments?: unknown };
|
|
126
|
-
if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
|
|
127
|
-
else if (b.type === "thinking" && typeof b.thinking === "string") textLen += b.thinking.length;
|
|
128
|
-
else if (b.type === "toolCall") {
|
|
129
|
-
if (typeof b.name === "string") textLen += b.name.length;
|
|
130
|
-
// Argument bytes vary; a length proxy is enough to detect in-place edits.
|
|
131
|
-
textLen += b.arguments === undefined ? 0 : JSON.stringify(b.arguments).length;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} else if (role === "toolResult" || role === "hookMessage") {
|
|
136
|
-
const content = (msg as { content?: unknown }).content;
|
|
137
|
-
if (typeof content === "string") {
|
|
138
|
-
textLen += content.length;
|
|
139
|
-
} else if (Array.isArray(content)) {
|
|
140
|
-
blocks = content.length;
|
|
141
|
-
for (const block of content) {
|
|
142
|
-
if (!block || typeof block !== "object") continue;
|
|
143
|
-
const b = block as { type?: string; text?: string };
|
|
144
|
-
if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
|
|
145
|
-
else if (b.type === "image") images++;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} else if (role === "branchSummary" || role === "compactionSummary") {
|
|
149
|
-
const s = (msg as { summary?: unknown }).summary;
|
|
150
|
-
if (typeof s === "string") textLen += s.length;
|
|
151
|
-
}
|
|
152
|
-
return `${role}:${ts}:${textLen}:${blocks}:${images}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Token count for a single message, using the per-message sidecar cache.
|
|
157
|
-
* The caller MUST skip caching for the last message during streaming —
|
|
158
|
-
* it may still be growing and its tokens belong recomputed each refresh.
|
|
159
|
-
*/
|
|
160
|
-
function tokensForMessage(msg: AgentMessage): number {
|
|
161
|
-
const fp = messageFingerprint(msg);
|
|
162
|
-
const tagged = msg as TaggedMessage;
|
|
163
|
-
const cached = tagged[kTokenCache];
|
|
164
|
-
if (cached && cached.fingerprint === fp) return cached.tokens;
|
|
165
|
-
const tokens = estimateMessageTokensHeuristic(msg);
|
|
166
|
-
tagged[kTokenCache] = { fingerprint: fp, tokens };
|
|
167
|
-
return tokens;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
171
71
|
// StatusLineComponent
|
|
172
72
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
73
|
|
|
@@ -217,20 +117,6 @@ export class StatusLineComponent implements Component {
|
|
|
217
117
|
#cachedUsage: SegmentContext["usage"] = null;
|
|
218
118
|
#usageFetchedAt = 0;
|
|
219
119
|
#usageInFlight = false;
|
|
220
|
-
// Context breakdown — incremental cache. Replaces the previous 2-second
|
|
221
|
-
// TTL design (which re-walked every message on each refresh and produced
|
|
222
|
-
// ~1.1 s sync freezes on 2,000+ message sessions because `updateEditorTopBorder`
|
|
223
|
-
// is called on every agent event in event-controller). The new scheme
|
|
224
|
-
// caches by message-object identity (a Symbol-keyed sidecar on each
|
|
225
|
-
// message) plus a cheap content fingerprint, so in-place mutations of
|
|
226
|
-
// an existing message (post-hoc error attachment, retry-truncated
|
|
227
|
-
// branch rebuild, replaceMessages with the same length) are detected
|
|
228
|
-
// and recomputed.
|
|
229
|
-
// Cached non-message total (system prompt + tools + skills). Invalidated
|
|
230
|
-
// when the inputs-identity fingerprint changes (model swap, skill toggle,
|
|
231
|
-
// tool registration).
|
|
232
|
-
#nonMessageTokensCache: number | undefined;
|
|
233
|
-
#nonMessageInputsKey: string | undefined;
|
|
234
120
|
|
|
235
121
|
constructor(
|
|
236
122
|
private readonly session: AgentSession,
|
|
@@ -618,57 +504,6 @@ export class StatusLineComponent implements Component {
|
|
|
618
504
|
return windows.length > 0 ? { windows } : null;
|
|
619
505
|
}
|
|
620
506
|
|
|
621
|
-
/**
|
|
622
|
-
* Compute the (cached) used-tokens / context-window totals for the
|
|
623
|
-
* status-line context% segment. Exposed (non-private) so unit tests can
|
|
624
|
-
* verify the incremental-cache invariants; not part of any external
|
|
625
|
-
* API.
|
|
626
|
-
*/
|
|
627
|
-
getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
|
|
628
|
-
const messages = this.session.messages ?? [];
|
|
629
|
-
const contextWindow = this.session.model?.contextWindow ?? 0;
|
|
630
|
-
|
|
631
|
-
// 1) Non-message tokens (system prompt + tools + skills). Refresh only
|
|
632
|
-
// when the inputs identity fingerprint changes — usually never
|
|
633
|
-
// during a streaming turn. ~10-30 ms when it does refresh.
|
|
634
|
-
const inputsKey = this.#computeNonMessageInputsKey();
|
|
635
|
-
if (this.#nonMessageTokensCache === undefined || this.#nonMessageInputsKey !== inputsKey) {
|
|
636
|
-
this.#nonMessageTokensCache = computeNonMessageTokens(this.session);
|
|
637
|
-
this.#nonMessageInputsKey = inputsKey;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// 2) Message tokens — incremental. The sidecar cache lives on the
|
|
641
|
-
// message object itself (Symbol-keyed), keyed by identity and
|
|
642
|
-
// validated by a cheap content fingerprint. Mutations that
|
|
643
|
-
// replace messages (replaceMessages, branch rebuild, compaction)
|
|
644
|
-
// yield fresh objects → cache miss → recompute. In-place
|
|
645
|
-
// mutations on the same object are caught by fingerprint
|
|
646
|
-
// mismatch. The LAST message is always recomputed because it
|
|
647
|
-
// may still be growing during streaming.
|
|
648
|
-
let messagesTokens = 0;
|
|
649
|
-
const lastIdx = messages.length - 1;
|
|
650
|
-
for (let i = 0; i < messages.length; i++) {
|
|
651
|
-
messagesTokens += i === lastIdx ? estimateMessageTokensHeuristic(messages[i]) : tokensForMessage(messages[i]);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const usedTokens = this.#nonMessageTokensCache + messagesTokens;
|
|
655
|
-
return { usedTokens, contextWindow };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Build an identity fingerprint for the non-message inputs (system prompt,
|
|
660
|
-
* tools, skills). When this changes, the non-message token cache must be
|
|
661
|
-
* recomputed. Cheap: just lengths + first-string-length. Doesn't need to
|
|
662
|
-
* be cryptographically unique — only stable for the same inputs.
|
|
663
|
-
*/
|
|
664
|
-
#computeNonMessageInputsKey(): string {
|
|
665
|
-
const sp = this.session.systemPrompt ?? [];
|
|
666
|
-
const tools = this.session.agent?.state?.tools ?? [];
|
|
667
|
-
const skills = this.session.skills ?? [];
|
|
668
|
-
const modelId = this.session.model?.id ?? "";
|
|
669
|
-
return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
507
|
#buildSegmentContext(
|
|
673
508
|
width: number,
|
|
674
509
|
effectiveSettings: Required<
|
|
@@ -695,11 +530,9 @@ export class StatusLineComponent implements Component {
|
|
|
695
530
|
tokensPerSecond: this.#getTokensPerSecond(),
|
|
696
531
|
};
|
|
697
532
|
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
const contextWindow = breakdown.contextWindow || state.model?.contextWindow || 0;
|
|
702
|
-
const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
533
|
+
const contextUsage = this.session.getContextUsage?.();
|
|
534
|
+
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
535
|
+
const contextPercent = contextUsage?.percent ?? null;
|
|
703
536
|
// Suppress the inline model percentage when a standalone context_pct
|
|
704
537
|
// segment is also rendered, so the value is not shown twice.
|
|
705
538
|
const contextPctSegmentActive =
|
|
@@ -578,7 +578,7 @@ export class ExtensionUiController {
|
|
|
578
578
|
shutdown: () => {
|
|
579
579
|
// Signal shutdown request
|
|
580
580
|
},
|
|
581
|
-
getSystemPrompt: () => this.ctx.session.systemPrompt,
|
|
581
|
+
getSystemPrompt: () => [...this.ctx.session.systemPrompt],
|
|
582
582
|
});
|
|
583
583
|
} catch (err) {
|
|
584
584
|
this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getRenderCacheRetainedBytes,
|
|
22
22
|
Loader,
|
|
23
23
|
Markdown,
|
|
24
|
+
onImageProtocolChanged,
|
|
24
25
|
ProcessTerminal,
|
|
25
26
|
Spacer,
|
|
26
27
|
Text,
|
|
@@ -387,6 +388,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
387
388
|
#baseReservedSlashCommandNames: Set<string> = new Set();
|
|
388
389
|
#cleanupUnsubscribe?: () => void;
|
|
389
390
|
#subprocessTeardownUnsubscribe?: () => void;
|
|
391
|
+
#petProtocolUnsubscribe?: () => void;
|
|
390
392
|
readonly #version: string;
|
|
391
393
|
readonly #changelogMarkdown: string | undefined;
|
|
392
394
|
#planModePreviousTools: string[] | undefined;
|
|
@@ -617,6 +619,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
617
619
|
this.ui.setFocus(this.editor);
|
|
618
620
|
this.petWidget = this.#createPetWidget(this.editor);
|
|
619
621
|
this.petWidget.setMode(settings.get("pet.mode"));
|
|
622
|
+
// The async sixel capability probe can enable graphics after the saved
|
|
623
|
+
// pet mode was applied and dropped (no protocol yet at startup).
|
|
624
|
+
// Re-apply the configured mode when capability arrives so the pet
|
|
625
|
+
// appears without the user re-running /pet.
|
|
626
|
+
this.#petProtocolUnsubscribe?.();
|
|
627
|
+
this.#petProtocolUnsubscribe = onImageProtocolChanged(protocol => {
|
|
628
|
+
if (!protocol) return;
|
|
629
|
+
const saved = settings.get("pet.mode");
|
|
630
|
+
if (saved !== "off" && this.petWidget && this.petWidget.mode === "off") {
|
|
631
|
+
this.petWidget.setMode(saved);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
620
634
|
|
|
621
635
|
this.#inputController.setupKeyHandlers();
|
|
622
636
|
this.#inputController.setupEditorSubmitHandler();
|
|
@@ -2074,6 +2088,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2074
2088
|
}
|
|
2075
2089
|
|
|
2076
2090
|
stop(): void {
|
|
2091
|
+
this.#petProtocolUnsubscribe?.();
|
|
2092
|
+
this.#petProtocolUnsubscribe = undefined;
|
|
2077
2093
|
this.petWidget?.dispose();
|
|
2078
2094
|
this.petWidget = undefined;
|
|
2079
2095
|
if (this.loadingAnimation) {
|