mixdog 0.9.85 → 0.9.87
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/README.md +7 -1
- package/package.json +4 -3
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/runtime/shared/atomic-file.mjs +25 -14
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +49 -9
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +286 -548
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +443 -0
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +112 -941
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +9 -10
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2569
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +0 -45
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +9857 -9159
- package/src/tui/engine/session-api-ext.mjs +47 -9
- package/src/tui/engine.mjs +7 -1
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -35,10 +35,8 @@ export function createSlashDispatch({
|
|
|
35
35
|
openThemePicker,
|
|
36
36
|
themeNotice,
|
|
37
37
|
openEffortPicker,
|
|
38
|
-
projectNameFromPath,
|
|
39
38
|
enterProject,
|
|
40
39
|
openProjectPicker,
|
|
41
|
-
openToolsPicker,
|
|
42
40
|
openMcpPicker,
|
|
43
41
|
openSkillsPicker,
|
|
44
42
|
openPluginsPicker,
|
|
@@ -231,19 +229,6 @@ export function createSlashDispatch({
|
|
|
231
229
|
.catch((e) => store.pushNotice(`Couldn’t update fast mode: ${e?.message || e}`, 'error'));
|
|
232
230
|
return true;
|
|
233
231
|
}
|
|
234
|
-
case 'cwd': {
|
|
235
|
-
const nextPath = arg.trim();
|
|
236
|
-
if (!nextPath) {
|
|
237
|
-
store.pushNotice(`Project path: ${state.cwd}`, 'info');
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
store.setCwd?.(nextPath, { message: `Project set: ${projectNameFromPath(nextPath)}` });
|
|
242
|
-
} catch (e) {
|
|
243
|
-
store.pushNotice(`project switch failed: ${e?.message || e}`, 'error');
|
|
244
|
-
}
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
232
|
case 'project': {
|
|
248
233
|
const target = arg.trim();
|
|
249
234
|
if (target) {
|
|
@@ -253,9 +238,6 @@ export function createSlashDispatch({
|
|
|
253
238
|
openProjectPicker();
|
|
254
239
|
return true;
|
|
255
240
|
}
|
|
256
|
-
case 'tools':
|
|
257
|
-
openToolsPicker(arg.trim());
|
|
258
|
-
return true;
|
|
259
241
|
case 'mcp':
|
|
260
242
|
openMcpPicker();
|
|
261
243
|
return true;
|
|
@@ -280,23 +262,6 @@ export function createSlashDispatch({
|
|
|
280
262
|
// palette, and a typed command answers instead of opening a picker.
|
|
281
263
|
store.pushNotice('Schedules and webhooks are managed in the Mixdog desktop app', 'info');
|
|
282
264
|
return true;
|
|
283
|
-
case 'auth':
|
|
284
|
-
store.pushNotice('/auth moved to /providers', 'info');
|
|
285
|
-
void openProviderSetupPicker();
|
|
286
|
-
return true;
|
|
287
|
-
case 'auth-forget': {
|
|
288
|
-
const providerId = arg.trim();
|
|
289
|
-
if (!providerId) {
|
|
290
|
-
store.pushNotice('usage: /auth-forget <provider>', 'warn');
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
store.forgetProviderAuth(providerId);
|
|
295
|
-
} catch (e) {
|
|
296
|
-
store.pushNotice(`auth-forget failed: ${e?.message || e}`, 'error');
|
|
297
|
-
}
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
265
|
case 'memory': {
|
|
301
266
|
if (!arg.trim()) {
|
|
302
267
|
openMemoryCorePicker({ returnTo: null });
|
|
@@ -306,16 +271,6 @@ export function createSlashDispatch({
|
|
|
306
271
|
.catch((e) => store.pushNotice(`memory failed: ${e?.message || e}`, 'error'));
|
|
307
272
|
return true;
|
|
308
273
|
}
|
|
309
|
-
case 'recall': {
|
|
310
|
-
const query = arg.trim();
|
|
311
|
-
if (!query) {
|
|
312
|
-
store.pushNotice('usage: /recall <query>', 'warn');
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
void store.recall?.(query)
|
|
316
|
-
.catch((e) => store.pushNotice(`recall failed: ${e?.message || e}`, 'error'));
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
274
|
case 'autoclear': {
|
|
320
275
|
const value = arg.trim().toLowerCase();
|
|
321
276
|
if (!value) {
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// /usage and /context panel builders, extracted from App.jsx. Follows the
|
|
2
|
+
// createRoutePickers factory pattern: called each render with the current
|
|
3
|
+
// store/state and panel setters; opening either surface closes every other
|
|
4
|
+
// prompt/picker first so exactly one bottom surface owns the screen.
|
|
5
|
+
export function createUsageContextPanels({
|
|
6
|
+
store,
|
|
7
|
+
state,
|
|
8
|
+
usageRequestRef,
|
|
9
|
+
setPicker,
|
|
10
|
+
setProviderPrompt,
|
|
11
|
+
setChannelPrompt,
|
|
12
|
+
setHookPrompt,
|
|
13
|
+
setSettingsPrompt,
|
|
14
|
+
setContextPanel,
|
|
15
|
+
setUsagePanel,
|
|
16
|
+
closeUsagePanel,
|
|
17
|
+
}) {
|
|
18
|
+
const openUsagePanel = (arg = '') => {
|
|
19
|
+
const refresh = /(?:^|\s)(?:refresh|--refresh|-r|true)(?:\s|$)/i.test(String(arg || ''));
|
|
20
|
+
const requestId = usageRequestRef.current + 1;
|
|
21
|
+
usageRequestRef.current = requestId;
|
|
22
|
+
setProviderPrompt(null);
|
|
23
|
+
setChannelPrompt(null);
|
|
24
|
+
setHookPrompt(null);
|
|
25
|
+
setSettingsPrompt(null);
|
|
26
|
+
setPicker(null);
|
|
27
|
+
setContextPanel(null);
|
|
28
|
+
setUsagePanel({
|
|
29
|
+
title: 'Provider Quotas',
|
|
30
|
+
subtitle: 'Statusline-style provider quota windows.',
|
|
31
|
+
checking: true,
|
|
32
|
+
refresh,
|
|
33
|
+
rows: [],
|
|
34
|
+
total: null,
|
|
35
|
+
});
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
if (usageRequestRef.current !== requestId) return;
|
|
38
|
+
void store.getUsageDashboard?.({
|
|
39
|
+
refresh,
|
|
40
|
+
onUpdate: (dashboard) => {
|
|
41
|
+
if (usageRequestRef.current !== requestId) return;
|
|
42
|
+
if (!dashboard) return;
|
|
43
|
+
setUsagePanel(dashboard);
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.then((dashboard) => {
|
|
47
|
+
if (usageRequestRef.current !== requestId) return;
|
|
48
|
+
if (!dashboard) {
|
|
49
|
+
closeUsagePanel();
|
|
50
|
+
store.pushNotice('usage dashboard unavailable', 'warn');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setUsagePanel(dashboard);
|
|
54
|
+
})
|
|
55
|
+
.catch((e) => {
|
|
56
|
+
if (usageRequestRef.current !== requestId) return;
|
|
57
|
+
closeUsagePanel();
|
|
58
|
+
store.pushNotice(`usage failed: ${e?.message || e}`, 'error');
|
|
59
|
+
});
|
|
60
|
+
}, 0);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const openContextPicker = () => {
|
|
64
|
+
const tools = store.toolsStatus?.() || {
|
|
65
|
+
activeCount: 0,
|
|
66
|
+
count: 0,
|
|
67
|
+
mcpToolCount: 0,
|
|
68
|
+
activeMcpToolCount: 0,
|
|
69
|
+
activeTools: [],
|
|
70
|
+
};
|
|
71
|
+
const mcp = store.mcpStatus?.() || { connectedCount: 0, configuredCount: 0, failedCount: 0 };
|
|
72
|
+
const skills = store.skillsStatus?.() || { count: 0 };
|
|
73
|
+
const plugins = store.pluginsStatus?.() || { count: 0 };
|
|
74
|
+
const context = store.contextStatus?.() || {};
|
|
75
|
+
const usage = context.usage || {};
|
|
76
|
+
const messages = context.messages || {};
|
|
77
|
+
const request = context.request || {};
|
|
78
|
+
const schemaBreakdown = request.toolSchemaBreakdown || {};
|
|
79
|
+
const schemaTokensFor = (buckets) => buckets.reduce(
|
|
80
|
+
(sum, bucket) => sum + Number(schemaBreakdown?.[bucket]?.tokens || 0),
|
|
81
|
+
0,
|
|
82
|
+
);
|
|
83
|
+
const builtInToolSchemaTokens = schemaTokensFor(['code', 'web', 'mutation', 'channels', 'setup', 'other']);
|
|
84
|
+
const mcpToolSchemaTokens = schemaTokensFor(['mcp']);
|
|
85
|
+
const compaction = context.compaction || {};
|
|
86
|
+
const windowTokens = Number(context.contextWindow || state.contextWindow || context.rawContextWindow || state.rawContextWindow || 0);
|
|
87
|
+
const rawWindowTokens = Number(context.rawContextWindow || state.rawContextWindow || windowTokens || 0);
|
|
88
|
+
// Compaction boundary/trigger are sourced from the runtime contextStatus
|
|
89
|
+
// (context.compaction). Fall back to the visible window for the boundary
|
|
90
|
+
// and to the boundary for the trigger so /context still renders on a
|
|
91
|
+
// fresh/resumed session before any compaction telemetry exists.
|
|
92
|
+
const compactBoundary = Number(compaction.boundaryTokens || windowTokens || 0);
|
|
93
|
+
const compactTrigger = Number(compaction.triggerTokens || compactBoundary || 0);
|
|
94
|
+
const usedTokens = Number(context.usedTokens || context.currentEstimatedTokens || usage.lastContextTokens || 0);
|
|
95
|
+
const freeTokens = windowTokens ? Math.max(0, windowTokens - usedTokens) : Number(context.freeTokens || 0);
|
|
96
|
+
const pct = (value, total = windowTokens) => {
|
|
97
|
+
const n = Number(value || 0);
|
|
98
|
+
const d = Number(total || 0);
|
|
99
|
+
if (!d) return 'N/A';
|
|
100
|
+
const p = Math.max(0, Math.min(100, (n / d) * 100));
|
|
101
|
+
return `${p > 0 && p < 1 ? p.toFixed(1) : Math.floor(p)}%`;
|
|
102
|
+
};
|
|
103
|
+
const fmt = (value) => {
|
|
104
|
+
const n = Number(value || 0);
|
|
105
|
+
if (!Number.isFinite(n) || n <= 0) return '0';
|
|
106
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`;
|
|
107
|
+
if (n >= 10_000) return `${Math.round(n / 1000)}k`;
|
|
108
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
109
|
+
return `${Math.round(n)}`;
|
|
110
|
+
};
|
|
111
|
+
const cachedRead = Number(usage.lastCachedReadTokens || 0);
|
|
112
|
+
const cacheWrite = Number(usage.lastCacheWriteTokens || 0);
|
|
113
|
+
const freshInput = Number(
|
|
114
|
+
usage.lastUncachedInputTokens != null
|
|
115
|
+
? usage.lastUncachedInputTokens
|
|
116
|
+
: Math.max(Number(usage.lastInputTokens || 0) - cachedRead - cacheWrite, 0)
|
|
117
|
+
);
|
|
118
|
+
const cacheDenom = Number(usage.lastContextTokens || 0) || (cachedRead + freshInput + cacheWrite);
|
|
119
|
+
const cacheHitRate = cacheDenom > 0
|
|
120
|
+
? `${((cachedRead / cacheDenom) * 100).toFixed(0)}%`
|
|
121
|
+
: 'N/A';
|
|
122
|
+
const cacheWriteLabel = cacheWrite > 0 ? ` · ${fmt(cacheWrite)} write` : '';
|
|
123
|
+
const contextSource = context.usedSource === 'last_api_request' ? 'last API request' : 'estimated';
|
|
124
|
+
const lastApiLabel = context.lastApiRequestStale ? 'last API request (pre-compact)' : 'last API request';
|
|
125
|
+
const compactElapsed = (value) => {
|
|
126
|
+
const n = Number(value || 0);
|
|
127
|
+
if (!Number.isFinite(n) || n <= 0) return '';
|
|
128
|
+
return `${Math.max(1, Math.ceil(n / 1000))}s`;
|
|
129
|
+
};
|
|
130
|
+
const compactRunning = compaction.inProgress === true || compaction.lastStage === 'compacting';
|
|
131
|
+
const autoClearFailed = compaction.lastStage === 'auto_clear_failed' || !!compaction.lastClearCompactError;
|
|
132
|
+
const autoClearStage = compaction.lastStage === 'auto_clear' || compaction.lastClearAt;
|
|
133
|
+
const compactDuration = compactElapsed(compaction.lastDurationMs);
|
|
134
|
+
const compactInterrupted = compaction.lastStage === 'interrupted';
|
|
135
|
+
const compactReactive = String(compaction.lastTrigger || '').toLowerCase() === 'reactive';
|
|
136
|
+
const compactState = compactRunning
|
|
137
|
+
? 'Compacting conversation'
|
|
138
|
+
: compactInterrupted
|
|
139
|
+
? 'Compact interrupted'
|
|
140
|
+
: autoClearFailed
|
|
141
|
+
? `auto-clear skipped${compaction.lastClearCompactError ? `: ${compaction.lastClearCompactError}` : ''}`
|
|
142
|
+
: autoClearStage
|
|
143
|
+
? 'Auto-clear complete'
|
|
144
|
+
: compaction.lastChanged
|
|
145
|
+
? (compactReactive ? 'Compact complete (overflow recovery)' : 'Compact complete')
|
|
146
|
+
: 'Compact checked';
|
|
147
|
+
const compactDescription = compactDuration
|
|
148
|
+
? `${compactState} · ${compactDuration}`
|
|
149
|
+
: compactState;
|
|
150
|
+
const contextRows = [
|
|
151
|
+
{
|
|
152
|
+
value: 'summary',
|
|
153
|
+
label: 'Context Usage',
|
|
154
|
+
description: `${fmt(usedTokens)}/${fmt(windowTokens)} (${pct(usedTokens)}) · ${fmt(freeTokens)} free · ${contextSource} · effective`,
|
|
155
|
+
_action: 'summary',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
value: 'compaction',
|
|
159
|
+
label: 'Compaction',
|
|
160
|
+
description: compactDescription,
|
|
161
|
+
_action: 'compaction',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
value: 'messages',
|
|
165
|
+
label: 'Messages',
|
|
166
|
+
description: `${fmt(messages.estimatedTokens)} tokens (${pct(messages.estimatedTokens)}) · ${messages.count || 0} messages`,
|
|
167
|
+
_action: 'messages',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
value: 'tools',
|
|
171
|
+
label: 'Tools',
|
|
172
|
+
description: `${fmt(builtInToolSchemaTokens)} schema tokens (${pct(builtInToolSchemaTokens)}) · ${tools.activeCount || 0}/${tools.count || 0} active`,
|
|
173
|
+
_action: 'tools',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: 'tool-io',
|
|
177
|
+
label: 'Tool calls/results',
|
|
178
|
+
description: `${messages.toolCallCount || 0} calls (${fmt(messages.toolCallTokens)}) · ${messages.toolResultCount || 0} results (${fmt(messages.toolResultTokens)})`,
|
|
179
|
+
_action: 'tool-io',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
value: 'request',
|
|
183
|
+
label: 'Request overhead',
|
|
184
|
+
description: `${fmt(request.requestOverheadTokens)} framing · ${fmt(request.reserveTokens)} reserve incl. tools`,
|
|
185
|
+
_action: 'request',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
value: 'last-api',
|
|
189
|
+
label: 'Last API usage',
|
|
190
|
+
description: `${fmt(usage.lastContextTokens)} context · ${fmt(freshInput)} uncached input · ${fmt(usage.lastOutputTokens)} output · ${lastApiLabel}`,
|
|
191
|
+
_action: 'last-api',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
value: 'cache',
|
|
195
|
+
label: 'Prompt cache',
|
|
196
|
+
description: `${cacheHitRate} hit · ${fmt(usage.lastCachedReadTokens)} read${cacheWriteLabel} · ${fmt(freshInput)} new (last request)`,
|
|
197
|
+
_action: 'cache',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
value: 'free',
|
|
201
|
+
label: 'Free space',
|
|
202
|
+
description: `${fmt(freeTokens)} tokens (${pct(freeTokens)}) · raw window ${fmt(rawWindowTokens)}`,
|
|
203
|
+
_action: 'free',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
value: 'extensions',
|
|
207
|
+
label: 'Skills/plugins',
|
|
208
|
+
description: `${skills.count || 0} skills · ${plugins.count || 0} plugins`,
|
|
209
|
+
_action: 'extensions',
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
setProviderPrompt(null);
|
|
213
|
+
setChannelPrompt(null);
|
|
214
|
+
setHookPrompt(null);
|
|
215
|
+
setSettingsPrompt(null);
|
|
216
|
+
setPicker(null);
|
|
217
|
+
setContextPanel({
|
|
218
|
+
kind: 'context',
|
|
219
|
+
title: 'Context Usage',
|
|
220
|
+
detail: {
|
|
221
|
+
type: 'context',
|
|
222
|
+
usage: {
|
|
223
|
+
usedTokens,
|
|
224
|
+
windowTokens,
|
|
225
|
+
freeTokens,
|
|
226
|
+
rawWindowTokens,
|
|
227
|
+
source: contextSource,
|
|
228
|
+
effective: true,
|
|
229
|
+
},
|
|
230
|
+
compaction: {
|
|
231
|
+
stage: compaction.lastStage || 'pending',
|
|
232
|
+
state: compactState,
|
|
233
|
+
triggerTokens: compactTrigger,
|
|
234
|
+
boundaryTokens: compactBoundary,
|
|
235
|
+
type: compaction.compactType || compaction.type || null,
|
|
236
|
+
bufferTokens: Number(compaction.bufferTokens || (compactBoundary && compactTrigger ? Math.max(0, compactBoundary - compactTrigger) : 0)) || null,
|
|
237
|
+
pressureTokens: Number(compaction.lastPressureTokens || compaction.currentEstimatedTokens || 0) || null,
|
|
238
|
+
lastChanged: compaction.lastChanged === true,
|
|
239
|
+
},
|
|
240
|
+
messages: {
|
|
241
|
+
tokens: messages.estimatedTokens,
|
|
242
|
+
count: messages.count,
|
|
243
|
+
semantic: messages.semantic,
|
|
244
|
+
},
|
|
245
|
+
tools: {
|
|
246
|
+
schemaTokens: builtInToolSchemaTokens,
|
|
247
|
+
active: tools.activeCount,
|
|
248
|
+
count: tools.count,
|
|
249
|
+
},
|
|
250
|
+
toolIo: {
|
|
251
|
+
calls: messages.toolCallCount,
|
|
252
|
+
results: messages.toolResultCount,
|
|
253
|
+
},
|
|
254
|
+
request: {
|
|
255
|
+
toolSchemaBreakdown: request.toolSchemaBreakdown,
|
|
256
|
+
overheadTokens: request.requestOverheadTokens,
|
|
257
|
+
reserveTokens: request.reserveTokens,
|
|
258
|
+
},
|
|
259
|
+
lastApi: {
|
|
260
|
+
contextTokens: usage.lastContextTokens,
|
|
261
|
+
inputTokens: freshInput,
|
|
262
|
+
rawInputTokens: usage.lastInputTokens,
|
|
263
|
+
outputTokens: usage.lastOutputTokens,
|
|
264
|
+
},
|
|
265
|
+
cache: {
|
|
266
|
+
hitRate: cacheHitRate,
|
|
267
|
+
readTokens: usage.lastCachedReadTokens,
|
|
268
|
+
writeTokens: cacheWrite,
|
|
269
|
+
},
|
|
270
|
+
extensions: {
|
|
271
|
+
skills: skills.count,
|
|
272
|
+
plugins: plugins.count,
|
|
273
|
+
},
|
|
274
|
+
mcp: {
|
|
275
|
+
connected: mcp.connectedCount,
|
|
276
|
+
configured: mcp.configuredCount,
|
|
277
|
+
failed: mcp.failedCount,
|
|
278
|
+
tools: tools.mcpToolCount,
|
|
279
|
+
activeTools: tools.activeMcpToolCount,
|
|
280
|
+
schemaTokens: mcpToolSchemaTokens,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
rows: contextRows,
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return { openUsagePanel, openContextPicker };
|
|
288
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Ctrl+C selection copy, extracted from App.jsx. Merges the three harvest
|
|
2
|
+
// sources — the current render's visible selection, the last fully-painted
|
|
3
|
+
// remembered text, and the cross-scroll stitch buffer (only when contiguous)
|
|
4
|
+
// — retries briefly while ink refreshes, and reports feedback via the prompt
|
|
5
|
+
// hint band.
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { copyToClipboard } from './clipboard.mjs';
|
|
8
|
+
|
|
9
|
+
export function useCopySelection({
|
|
10
|
+
store,
|
|
11
|
+
selectionTextRef,
|
|
12
|
+
getStitchedSelectionText,
|
|
13
|
+
showSelectionCopyHint,
|
|
14
|
+
}) {
|
|
15
|
+
const copySelection = useCallback((attempt = 0) => {
|
|
16
|
+
const renderText = store.getRenderSelectionText?.();
|
|
17
|
+
const remembered = selectionTextRef.current || '';
|
|
18
|
+
// A selection that has partially scrolled out of the viewport renders —
|
|
19
|
+
// and therefore harvests — only its visible rows. The remembered text
|
|
20
|
+
// (captured while the selection was last fully painted) is the fuller
|
|
21
|
+
// copy; prefer whichever is longer so scrolling never shrinks a copy.
|
|
22
|
+
let text = renderText == null
|
|
23
|
+
? remembered
|
|
24
|
+
: (remembered.length > renderText.length ? remembered : renderText);
|
|
25
|
+
// The stitch buffer accumulates rows harvested across every scroll position
|
|
26
|
+
// during a transcript drag, so it can reconstruct rows that scrolled out of
|
|
27
|
+
// view entirely (neither renderText nor the last-full-paint remembered text
|
|
28
|
+
// ever saw them). getStitchedSelectionText reports a `complete` flag:
|
|
29
|
+
// prefer the stitch ONLY when it contiguously covers the selection (no
|
|
30
|
+
// interior gap) AND adds rows. A gapped stitch silently drops a scrolled-off
|
|
31
|
+
// row, so preferring it purely on length yielded a mangled copy.
|
|
32
|
+
const stitched = getStitchedSelectionText?.() || { text: '', complete: false };
|
|
33
|
+
if (stitched.complete && stitched.text.length > text.length) text = stitched.text;
|
|
34
|
+
if ((!text || !text.trim()) && attempt < 4) {
|
|
35
|
+
setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!text || !text.trim()) {
|
|
39
|
+
// Retries exhausted with nothing to copy: never return silently — the
|
|
40
|
+
// user pressed Ctrl+C expecting feedback. Surface a hint (and still
|
|
41
|
+
// swallow the key, which the caller already did).
|
|
42
|
+
showSelectionCopyHint('nothing to copy · select text first', 'error');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
selectionTextRef.current = text;
|
|
46
|
+
copyToClipboard(text)
|
|
47
|
+
.then(() => {
|
|
48
|
+
const lines = text.split('\n').length;
|
|
49
|
+
const chars = text.length;
|
|
50
|
+
showSelectionCopyHint(`copied ${chars} char${chars === 1 ? '' : 's'}${lines > 1 ? ` · ${lines} lines` : ''}`, 'plain');
|
|
51
|
+
})
|
|
52
|
+
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
53
|
+
}, [store, selectionTextRef, showSelectionCopyHint, getStitchedSelectionText]);
|
|
54
|
+
|
|
55
|
+
return { copySelection };
|
|
56
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Global key input, extracted from App.jsx. Everything the app handles
|
|
2
|
+
// before PromptInput sees a key: tool-approval y/n, copy-first Ctrl+C with
|
|
3
|
+
// region-aware selection sources, Ctrl+O expand, shift-arrow grid-selection
|
|
4
|
+
// moves, panel Escapes, and PageUp/PageDown transcript paging.
|
|
5
|
+
import { useInput } from 'ink';
|
|
6
|
+
import { copyToClipboard } from './clipboard.mjs';
|
|
7
|
+
import { overlayBlocksGlobalTranscriptScroll } from './slash-commands.mjs';
|
|
8
|
+
export function useGlobalKeyInput({
|
|
9
|
+
store,
|
|
10
|
+
state,
|
|
11
|
+
toolApproval,
|
|
12
|
+
picker,
|
|
13
|
+
usagePanel,
|
|
14
|
+
contextPanel,
|
|
15
|
+
setContextPanel,
|
|
16
|
+
closeUsagePanel,
|
|
17
|
+
isRawModeSupported,
|
|
18
|
+
resizeState,
|
|
19
|
+
promptSelectionRef,
|
|
20
|
+
promptMouseSelectionRef,
|
|
21
|
+
dragRef,
|
|
22
|
+
scrollFocusRef,
|
|
23
|
+
gridSelectionActiveRef,
|
|
24
|
+
moveSelectionFocus,
|
|
25
|
+
copySelection,
|
|
26
|
+
showSelectionCopyHint,
|
|
27
|
+
toggleExpand,
|
|
28
|
+
scrollTranscriptRows,
|
|
29
|
+
resetTranscriptScroll,
|
|
30
|
+
applySelectionRect,
|
|
31
|
+
}) {
|
|
32
|
+
// (body moved verbatim)
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (toolApproval) {
|
|
35
|
+
const value = String(input || '').trim().toLowerCase();
|
|
36
|
+
if (key.escape || value === 'd' || value === 'n') {
|
|
37
|
+
store.resolveToolApproval?.(toolApproval.id, { approved: false, reason: 'denied by user' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (value === 'a' || value === 'y') {
|
|
41
|
+
store.resolveToolApproval?.(toolApproval.id, { approved: true, reason: 'approved by user' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (key.ctrl && (input === 'c' || input === 'C')) {
|
|
46
|
+
// Ctrl+C is copy-first. Native terminal selections can still forward the
|
|
47
|
+
// key event to us on Windows Terminal, so a missing app-owned selection
|
|
48
|
+
// must NOT cancel the active turn; use Esc to interrupt instead.
|
|
49
|
+
// Region-aware copy source: a prompt-box selection (its OWN engine) copies
|
|
50
|
+
// from promptSelectionRef; a transcript/status ink-grid selection copies
|
|
51
|
+
// from store.getRenderSelectionText via copySelection(). Only one region is
|
|
52
|
+
// ever active at a time (a press in one region clears the others), but when
|
|
53
|
+
// the last drag was in the prompt we prefer its selection explicitly.
|
|
54
|
+
const promptSelectionText = promptSelectionRef.current?.text;
|
|
55
|
+
const lastRegion = dragRef.current.region;
|
|
56
|
+
const inkRect = dragRef.current.rect;
|
|
57
|
+
const hasInkSelection = inkRect && !(inkRect.x1 === inkRect.x2 && inkRect.y1 === inkRect.y2);
|
|
58
|
+
if (promptSelectionText && (lastRegion === 'prompt' || !hasInkSelection)) {
|
|
59
|
+
copyToClipboard(promptSelectionText)
|
|
60
|
+
.then(() => showSelectionCopyHint(`copied ${promptSelectionText.length} char${promptSelectionText.length === 1 ? '' : 's'}`, 'plain'))
|
|
61
|
+
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (hasInkSelection) {
|
|
65
|
+
copySelection();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// No app-owned selection. On Windows Terminal the same Ctrl+C is also the
|
|
69
|
+
// native terminal's copy shortcut for a mouse selection we can't see — so
|
|
70
|
+
// rendering a hint here fights that copy and flashes a spurious message.
|
|
71
|
+
// Suppress the hint on win32 (interrupt routing is unchanged: Esc still
|
|
72
|
+
// interrupts). Other platforms keep the guidance.
|
|
73
|
+
if (process.platform !== 'win32') {
|
|
74
|
+
showSelectionCopyHint('select text to copy · Esc interrupts', 'plain');
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
79
|
+
toggleExpand();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const rawShiftUp = input === '\x1b[1;2A' || input === '\x1b[a' || input === '[1;2A';
|
|
83
|
+
const rawShiftDown = input === '\x1b[1;2B' || input === '\x1b[b' || input === '[1;2B';
|
|
84
|
+
const rawShiftRight = input === '\x1b[1;2C' || input === '\x1b[c' || input === '[1;2C';
|
|
85
|
+
const rawShiftLeft = input === '\x1b[1;2D' || input === '\x1b[d' || input === '[1;2D';
|
|
86
|
+
const rawCtrlShiftUp = input === '\x1b[1;6A' || input === '[1;6A';
|
|
87
|
+
const rawCtrlShiftDown = input === '\x1b[1;6B' || input === '[1;6B';
|
|
88
|
+
const rawCtrlShiftRight = input === '\x1b[1;6C' || input === '[1;6C';
|
|
89
|
+
const rawCtrlShiftLeft = input === '\x1b[1;6D' || input === '[1;6D';
|
|
90
|
+
const rawModifiedShiftArrow = rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
|
|
91
|
+
|| rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
|
|
92
|
+
if (
|
|
93
|
+
!picker
|
|
94
|
+
&& (
|
|
95
|
+
(key.shift && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end))
|
|
96
|
+
|| rawModifiedShiftArrow
|
|
97
|
+
)
|
|
98
|
+
) {
|
|
99
|
+
// Consume the chord whenever a transcript/status ink-grid selection is
|
|
100
|
+
// live — even if the focus clamps at an edge (moveSelectionFocus returns
|
|
101
|
+
// false there). PromptInput independently skips the same chord via the
|
|
102
|
+
// shared gridSelectionActiveRef predicate, so there is no double-handling.
|
|
103
|
+
// When no grid selection is live, fall through to PromptInput.
|
|
104
|
+
let move = null;
|
|
105
|
+
if (key.leftArrow || rawShiftLeft || rawCtrlShiftLeft) move = 'left';
|
|
106
|
+
else if (key.rightArrow || rawShiftRight || rawCtrlShiftRight) move = 'right';
|
|
107
|
+
else if (key.upArrow || rawShiftUp || rawCtrlShiftUp) move = 'up';
|
|
108
|
+
else if (key.downArrow || rawShiftDown || rawCtrlShiftDown) move = 'down';
|
|
109
|
+
else if (key.home) move = 'lineStart';
|
|
110
|
+
else if (key.end) move = 'lineEnd';
|
|
111
|
+
if (move && gridSelectionActiveRef.current()) {
|
|
112
|
+
moveSelectionFocus(move);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (key.escape && usagePanel && !picker) {
|
|
117
|
+
closeUsagePanel();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.escape && contextPanel && !picker) {
|
|
121
|
+
setContextPanel(null);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key.pageUp) {
|
|
125
|
+
if (overlayBlocksGlobalTranscriptScroll(scrollFocusRef.current)) return;
|
|
126
|
+
const pageRows = Math.max(3, Math.floor((resizeState.rows ?? 24) * 0.6));
|
|
127
|
+
scrollTranscriptRows(pageRows);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (key.pageDown) {
|
|
131
|
+
if (overlayBlocksGlobalTranscriptScroll(scrollFocusRef.current)) return;
|
|
132
|
+
const pageRows = Math.max(3, Math.floor((resizeState.rows ?? 24) * 0.6));
|
|
133
|
+
scrollTranscriptRows(-pageRows);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (key.ctrl && key.end) {
|
|
137
|
+
resetTranscriptScroll();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (key.escape && !picker) {
|
|
141
|
+
dragRef.current.active = false;
|
|
142
|
+
dragRef.current.region = null;
|
|
143
|
+
dragRef.current.anchorSpan = null;
|
|
144
|
+
// Clear whichever region's selection is active. PromptInput's own ESC also
|
|
145
|
+
// clears its selection when focused/enabled; this covers the disabled case
|
|
146
|
+
// and a status/transcript ink-grid selection in one press.
|
|
147
|
+
promptMouseSelectionRef.current?.clear?.();
|
|
148
|
+
applySelectionRect(null);
|
|
149
|
+
}
|
|
150
|
+
}, { isActive: isRawModeSupported });
|
|
151
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Pasted-attachment buffers, extracted from App.jsx. Large pasted images and
|
|
2
|
+
// texts fold into [Image #N] / [Pasted text #N +M lines] prompt tokens; the
|
|
3
|
+
// original payloads live here (ref + mirrored state) until submit expands
|
|
4
|
+
// them back. install* rehydrates a queued/restored draft's buffers, the
|
|
5
|
+
// snapshot clears drop exactly the entries a submit consumed, and register*
|
|
6
|
+
// mints the next token for a fresh paste.
|
|
7
|
+
import { useCallback, useRef, useState } from 'react';
|
|
8
|
+
import { formatImageRef, formatPastedTextRef } from '../paste-attachments.mjs';
|
|
9
|
+
|
|
10
|
+
export function usePastedBuffers() {
|
|
11
|
+
const [, setPastedImages] = useState({});
|
|
12
|
+
const pastedImagesRef = useRef({});
|
|
13
|
+
const nextPastedImageIdRef = useRef(1);
|
|
14
|
+
const [, setPastedTexts] = useState({});
|
|
15
|
+
const pastedTextsRef = useRef({});
|
|
16
|
+
const nextPastedTextIdRef = useRef(1);
|
|
17
|
+
|
|
18
|
+
const installPastedImages = useCallback((images, { merge = true } = {}) => {
|
|
19
|
+
if (!images || typeof images !== 'object' || Object.keys(images).length === 0) return;
|
|
20
|
+
const next = merge ? { ...pastedImagesRef.current, ...images } : { ...images };
|
|
21
|
+
pastedImagesRef.current = next;
|
|
22
|
+
const maxId = Object.keys(next)
|
|
23
|
+
.map((id) => Number(id) || 0)
|
|
24
|
+
.reduce((max, id) => Math.max(max, id), 0);
|
|
25
|
+
if (maxId >= nextPastedImageIdRef.current) nextPastedImageIdRef.current = maxId + 1;
|
|
26
|
+
setPastedImages(next);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const clearPastedImagesSnapshot = useCallback((snapshot = null) => {
|
|
30
|
+
if (!snapshot) {
|
|
31
|
+
if (Object.keys(pastedImagesRef.current || {}).length === 0) return;
|
|
32
|
+
pastedImagesRef.current = {};
|
|
33
|
+
setPastedImages({});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
37
|
+
const next = { ...pastedImagesRef.current };
|
|
38
|
+
let changed = false;
|
|
39
|
+
for (const [id, image] of Object.entries(snapshot)) {
|
|
40
|
+
if (next[id] === image) {
|
|
41
|
+
delete next[id];
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!changed) return;
|
|
46
|
+
pastedImagesRef.current = next;
|
|
47
|
+
setPastedImages(next);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const registerPastedImage = useCallback((image) => {
|
|
51
|
+
if (!image || image.type !== 'image' || !image.content) return '';
|
|
52
|
+
const id = nextPastedImageIdRef.current++;
|
|
53
|
+
const entry = { ...image, id };
|
|
54
|
+
pastedImagesRef.current = { ...pastedImagesRef.current, [id]: entry };
|
|
55
|
+
setPastedImages(pastedImagesRef.current);
|
|
56
|
+
return formatImageRef(id);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const installPastedTexts = useCallback((texts, { merge = true } = {}) => {
|
|
60
|
+
if (!texts || typeof texts !== 'object' || Object.keys(texts).length === 0) return;
|
|
61
|
+
const next = merge ? { ...pastedTextsRef.current, ...texts } : { ...texts };
|
|
62
|
+
pastedTextsRef.current = next;
|
|
63
|
+
const maxId = Object.keys(next)
|
|
64
|
+
.map((id) => Number(id) || 0)
|
|
65
|
+
.reduce((max, id) => Math.max(max, id), 0);
|
|
66
|
+
if (maxId >= nextPastedTextIdRef.current) nextPastedTextIdRef.current = maxId + 1;
|
|
67
|
+
setPastedTexts(next);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const clearPastedTextsSnapshot = useCallback((snapshot = null) => {
|
|
71
|
+
if (!snapshot) {
|
|
72
|
+
if (Object.keys(pastedTextsRef.current || {}).length === 0) return;
|
|
73
|
+
pastedTextsRef.current = {};
|
|
74
|
+
setPastedTexts({});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (typeof snapshot !== 'object' || Object.keys(snapshot).length === 0) return;
|
|
78
|
+
const next = { ...pastedTextsRef.current };
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const [id, text] of Object.entries(snapshot)) {
|
|
81
|
+
if (next[id] === text) {
|
|
82
|
+
delete next[id];
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!changed) return;
|
|
87
|
+
pastedTextsRef.current = next;
|
|
88
|
+
setPastedTexts(next);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const registerPastedText = useCallback((text) => {
|
|
92
|
+
const value = String(text ?? '');
|
|
93
|
+
if (!value) return '';
|
|
94
|
+
const id = nextPastedTextIdRef.current++;
|
|
95
|
+
const entry = { id, text: value };
|
|
96
|
+
pastedTextsRef.current = { ...pastedTextsRef.current, [id]: entry };
|
|
97
|
+
setPastedTexts(pastedTextsRef.current);
|
|
98
|
+
return formatPastedTextRef(id, value);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
pastedImagesRef,
|
|
103
|
+
nextPastedImageIdRef,
|
|
104
|
+
pastedTextsRef,
|
|
105
|
+
nextPastedTextIdRef,
|
|
106
|
+
setPastedImages,
|
|
107
|
+
setPastedTexts,
|
|
108
|
+
installPastedImages,
|
|
109
|
+
clearPastedImagesSnapshot,
|
|
110
|
+
registerPastedImage,
|
|
111
|
+
installPastedTexts,
|
|
112
|
+
clearPastedTextsSnapshot,
|
|
113
|
+
registerPastedText,
|
|
114
|
+
};
|
|
115
|
+
}
|