koneck 2.127.1 → 2.128.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/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -2
- package/dist/browser.js.map +1 -1
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.d.ts.map +1 -1
- package/dist/clipboard.js +133 -0
- package/dist/clipboard.js.map +1 -1
- package/dist/config-store.d.ts +3 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +7 -2
- package/dist/config-store.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +30 -12
- package/dist/engine.js.map +1 -1
- package/dist/frame-pace.d.ts +17 -0
- package/dist/frame-pace.d.ts.map +1 -1
- package/dist/frame-pace.js +21 -0
- package/dist/frame-pace.js.map +1 -1
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +1 -2
- package/dist/handoff.js.map +1 -1
- package/dist/image-draft.d.ts +9 -0
- package/dist/image-draft.d.ts.map +1 -0
- package/dist/image-draft.js +39 -0
- package/dist/image-draft.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +184 -25
- package/dist/ink-chat.js.map +1 -1
- package/dist/ledger.d.ts +3 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +21 -6
- package/dist/ledger.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/ink-chat.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Box, Static, Text, render, useApp, useInput, useStdin } from 'ink';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { resolveAgentConcurrency, toolsUnsupportedNotice } from './engine-facts.js';
|
|
6
6
|
import { withSynchronizedFrames, probeSynchronizedOutput } from './frame-sync.js';
|
|
7
|
-
import { pacedInterval, startingInterval, floorFor } from './frame-pace.js';
|
|
7
|
+
import { pacedInterval, startingInterval, floorFor, shouldDrawFrame } from './frame-pace.js';
|
|
8
8
|
import { estimateCost, formatCost, rateText } from './pricing.js';
|
|
9
9
|
import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
|
|
10
10
|
import { loadMemory } from './memory.js';
|
|
@@ -13,7 +13,8 @@ import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, removeUserProvi
|
|
|
13
13
|
import { normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveModelChoice, withModel } from './endpoints.js';
|
|
14
14
|
import { loadProjectConfig, loadGlobalConfig, saveGlobalConfig, withoutStoredApiKey } from './config.js';
|
|
15
15
|
import { rememberKey, forgetAllKeys } from './credentials.js';
|
|
16
|
-
import {
|
|
16
|
+
import { ImageDraft, pastedImagePath } from './image-draft.js';
|
|
17
|
+
import { readClipboard, readClipboardImage, startSelectionCopy, cleanPastedSecret } from './clipboard.js';
|
|
17
18
|
import { paceFrom } from './pace.js';
|
|
18
19
|
import { policyFrom, FAILOVER_OFF } from './failover.js';
|
|
19
20
|
import { listCheckpoints, revertCheckpoint, previewCheckpoint, snapshotTree, filesTouchedBy, createWorkspaceCheckpoint, checkpointScope } from './checkpoint.js';
|
|
@@ -500,7 +501,7 @@ const COMMANDS = [
|
|
|
500
501
|
{ cmd: '/queue', desc: 'Inspect or clear prompts waiting behind the current task' },
|
|
501
502
|
{ cmd: '/new', desc: 'New session (alias of /clear)' },
|
|
502
503
|
{ cmd: '/reset', desc: 'Reset session (alias of /clear)' },
|
|
503
|
-
{ cmd: '/mode', desc: 'Set mode: auto,
|
|
504
|
+
{ cmd: '/mode', desc: 'Set mode: auto, edits, careful or plan' },
|
|
504
505
|
{ cmd: '/effort', desc: 'Set reasoning effort: low, medium, high, max (picker)' },
|
|
505
506
|
{ cmd: '/config', desc: 'View or set persistent config' },
|
|
506
507
|
{ cmd: '/usage', desc: 'Usage, context, model status and limits (tabbed pane)' },
|
|
@@ -1019,6 +1020,11 @@ export function inkApprovalDecision(requireApproval, mode, tool, argsJson, rules
|
|
|
1019
1020
|
// silently erase. It remains stronger than this session's convenience posture.
|
|
1020
1021
|
if (policyRequiresApproval)
|
|
1021
1022
|
return 'ask';
|
|
1023
|
+
// Auto is the explicit promise to carry a task through. A globally saved approval preference
|
|
1024
|
+
// must not quietly turn it into careful mode; changing into Auto is the newer, more specific
|
|
1025
|
+
// choice. Workspace policies above still win, and plan mode was already denied by `decide`.
|
|
1026
|
+
if (modeSpec(mode).name === 'auto')
|
|
1027
|
+
return 'allow';
|
|
1022
1028
|
if (!requireApproval)
|
|
1023
1029
|
return 'allow';
|
|
1024
1030
|
if (toolRisk(tool) === 'read')
|
|
@@ -1095,7 +1101,7 @@ Slash commands:
|
|
|
1095
1101
|
/queue [clear|drop n] Inspect, clear, or drop one queued prompt
|
|
1096
1102
|
/model [name] Show or switch model
|
|
1097
1103
|
/provider [name] Show or switch provider
|
|
1098
|
-
/mode [auto|plan
|
|
1104
|
+
/mode [auto|edits|careful|plan] Show or set mode
|
|
1099
1105
|
/effort [low|med|high|max] Set reasoning effort
|
|
1100
1106
|
/config [key] [value] View or set persistent config
|
|
1101
1107
|
/aside <question> Ask a side question without interrupting the running task
|
|
@@ -1486,6 +1492,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
1486
1492
|
diffBaseRef.current.set(rel, before);
|
|
1487
1493
|
}
|
|
1488
1494
|
liveToolRef.current = { kind: 'tool', name, detail: toolDetail(name, args), startedAt: Date.now() };
|
|
1495
|
+
setRepaint(v => v + 1);
|
|
1489
1496
|
}
|
|
1490
1497
|
function finishTool(name, ok, ms, detail) {
|
|
1491
1498
|
if (!ok)
|
|
@@ -1715,6 +1722,19 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
1715
1722
|
const [showDiffPanel, setShowDiffPanel] = useState(false);
|
|
1716
1723
|
const diffPanelTouchedRef = useRef(false);
|
|
1717
1724
|
const [draft, setDraft] = useState('');
|
|
1725
|
+
const imageDraftRef = useRef(new ImageDraft());
|
|
1726
|
+
const [copyNotice, setCopyNotice] = useState('');
|
|
1727
|
+
useEffect(() => {
|
|
1728
|
+
if (!process.stdin.isTTY || cfg.autoCopy === false)
|
|
1729
|
+
return;
|
|
1730
|
+
return startSelectionCopy(count => setCopyNotice(`Copied ${count} chars to clipboard`));
|
|
1731
|
+
}, [cfg.autoCopy]);
|
|
1732
|
+
useEffect(() => {
|
|
1733
|
+
if (!copyNotice)
|
|
1734
|
+
return;
|
|
1735
|
+
const timer = setTimeout(() => setCopyNotice(''), 3000);
|
|
1736
|
+
return () => clearTimeout(timer);
|
|
1737
|
+
}, [copyNotice]);
|
|
1718
1738
|
// Recall is deliberately session-local. It is for quick retries and refinements, not another
|
|
1719
1739
|
// persistence channel for prompts (which may contain sensitive project context).
|
|
1720
1740
|
const draftHistoryRef = useRef([]);
|
|
@@ -1804,6 +1824,17 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
1804
1824
|
* So: the wheel is the thing you opt into, not the thing you lose.
|
|
1805
1825
|
*/
|
|
1806
1826
|
const [mouse, setMouse] = useState(cfg.mouse === true);
|
|
1827
|
+
/*
|
|
1828
|
+
* Whether the working animation runs at all.
|
|
1829
|
+
*
|
|
1830
|
+
* On, because a turn that produces nothing for two minutes needs something saying it is alive.
|
|
1831
|
+
* Off is here because on a terminal that does not composite a redraw the animation is the only
|
|
1832
|
+
* thing repainting when nobody is typing, and somebody who would rather have a still screen than
|
|
1833
|
+
* a moving one should be able to say so once and be done — "i fucken hate the flickering with all
|
|
1834
|
+
* my heart". Nothing is lost but the movement: the elapsed time and the status bar still say what
|
|
1835
|
+
* is happening, they simply say it without flashing.
|
|
1836
|
+
*/
|
|
1837
|
+
const [animate, setAnimate] = useState(cfg.animate !== false);
|
|
1807
1838
|
useEffect(() => {
|
|
1808
1839
|
if (!mouse || !altScreen || !process.stdout.isTTY)
|
|
1809
1840
|
return;
|
|
@@ -2241,6 +2272,8 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2241
2272
|
const syncedFramesRef = useRef(synchronizedFrames);
|
|
2242
2273
|
/** Whether the diff panel is showing, so the transcript does not draw the same diff again. */
|
|
2243
2274
|
const diffPanelOpenRef = useRef(false);
|
|
2275
|
+
/** When a key was last pressed. The animation stands down while somebody is typing. */
|
|
2276
|
+
const lastKeyAtRef = useRef(0);
|
|
2244
2277
|
const wordTimer = useRef(0);
|
|
2245
2278
|
// Register on the session bus so other running KONECK instances can reach this one.
|
|
2246
2279
|
useEffect(() => {
|
|
@@ -2320,25 +2353,45 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2320
2353
|
* how a Pi ends up lagging while the same build feels instant on a laptop. See frame-pace.ts.
|
|
2321
2354
|
*/
|
|
2322
2355
|
const floor = floorFor(syncedFramesRef.current);
|
|
2323
|
-
let interval = startingInterval(syncedFramesRef.current);
|
|
2356
|
+
let interval = animate ? startingInterval(syncedFramesRef.current) : 2_000;
|
|
2324
2357
|
let asked = Date.now();
|
|
2325
2358
|
const tick = () => {
|
|
2326
2359
|
const now = Date.now();
|
|
2327
|
-
interval = pacedInterval(interval, now - asked, floor);
|
|
2360
|
+
interval = animate ? pacedInterval(interval, now - asked, floor) : 2_000;
|
|
2361
|
+
/*
|
|
2362
|
+
* Nothing is repainted while somebody is typing.
|
|
2363
|
+
*
|
|
2364
|
+
* Every frame erases the live region and writes it again — the activity lines, the composer
|
|
2365
|
+
* and the status bar together. Where the terminal does not composite that, each one is a
|
|
2366
|
+
* visible flash, and it lands on top of the repaint the keystroke itself already caused. So
|
|
2367
|
+
* typing into the box while a turn ran flickered continuously: "when i type in the message
|
|
2368
|
+
* box, it's flickering. Please fix it, i fucken hate the flickering."
|
|
2369
|
+
*
|
|
2370
|
+
* The animation exists to show that work is still happening when nothing else does. Somebody
|
|
2371
|
+
* with their hands on the keys does not need telling, and the status bar says "processing"
|
|
2372
|
+
* without moving. So the clock keeps running and the frame is simply not drawn: the spinner
|
|
2373
|
+
* picks up from where the elapsed time has reached the moment they stop, having lost nothing
|
|
2374
|
+
* but the frames nobody wanted.
|
|
2375
|
+
*/
|
|
2376
|
+
if (!shouldDrawFrame(now, lastKeyAtRef.current)) {
|
|
2377
|
+
asked = Date.now();
|
|
2378
|
+
timer = setTimeout(tick, interval);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2328
2381
|
// Elapsed is read from the clock rather than counted in ticks, so a frame the event loop
|
|
2329
2382
|
// delivers late moves the animation on by what actually passed instead of falling behind.
|
|
2330
2383
|
const elapsed = now - busyStart.current;
|
|
2331
|
-
const { spin, shimmer } = animFrames(elapsed, SPINNER.length);
|
|
2384
|
+
const { spin, shimmer } = animate ? animFrames(elapsed, SPINNER.length) : { spin: 0, shimmer: 0 };
|
|
2332
2385
|
setAnim({ spin, shimmer, elapsed });
|
|
2333
2386
|
wordTimer.current += 1;
|
|
2334
|
-
if (wordTimer.current % wordEvery === 0)
|
|
2387
|
+
if (animate && wordTimer.current % wordEvery === 0)
|
|
2335
2388
|
setSpinWord(w => w + 1);
|
|
2336
2389
|
asked = Date.now();
|
|
2337
2390
|
timer = setTimeout(tick, interval);
|
|
2338
2391
|
};
|
|
2339
2392
|
timer = setTimeout(tick, interval);
|
|
2340
2393
|
return () => clearTimeout(timer);
|
|
2341
|
-
}, [busy]);
|
|
2394
|
+
}, [busy, animate]);
|
|
2342
2395
|
// A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
|
|
2343
2396
|
// inside the turn's own exit paths, so every way a turn can end — success, failure, cancel —
|
|
2344
2397
|
// drains the queue the same way.
|
|
@@ -2723,6 +2776,12 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
2723
2776
|
case 'mouse':
|
|
2724
2777
|
setMouse(value === true);
|
|
2725
2778
|
break;
|
|
2779
|
+
case 'animate':
|
|
2780
|
+
setAnimate(value !== false);
|
|
2781
|
+
break;
|
|
2782
|
+
case 'autoCopy':
|
|
2783
|
+
setCfg(c => ({ ...c, autoCopy: value !== false }));
|
|
2784
|
+
break;
|
|
2726
2785
|
case 'provider': {
|
|
2727
2786
|
const name = typeof value === 'string' && value.trim() !== '' ? value.trim() : cfg.provider;
|
|
2728
2787
|
const def = PROVIDERS[name.toLowerCase()];
|
|
@@ -3250,7 +3309,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3250
3309
|
const choice = arg.trim().toLowerCase();
|
|
3251
3310
|
if (!choice) {
|
|
3252
3311
|
addSystem(`**Approval posture**\n\n` +
|
|
3253
|
-
`Session approval: ${
|
|
3312
|
+
`Session approval: ${mode === 'auto'
|
|
3313
|
+
? 'Auto mode runs ordinary work without asking; workspace policy can still require approval'
|
|
3314
|
+
: cfg.requireApproval
|
|
3315
|
+
? 'on — KONECK asks before writes and non-read-only commands'
|
|
3316
|
+
: 'off — KONECK follows the selected mode and workspace policy'}\n` +
|
|
3254
3317
|
`Mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
|
|
3255
3318
|
'`/permissions on` keeps you in control of every consequential action.\n' +
|
|
3256
3319
|
'`/permissions off` removes the extra session gate; workspace policy and plan mode still win.');
|
|
@@ -3269,9 +3332,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3269
3332
|
// and the rebuilt session makes the new posture durable for every later turn.
|
|
3270
3333
|
if (!busy)
|
|
3271
3334
|
await resetSession(merged);
|
|
3272
|
-
addSystem(
|
|
3273
|
-
?
|
|
3274
|
-
:
|
|
3335
|
+
addSystem(mode === 'auto'
|
|
3336
|
+
? `Approval ${next ? 'on' : 'off'} saved. Auto mode still runs ordinary work without asking; workspace policy remains enforceable.`
|
|
3337
|
+
: next
|
|
3338
|
+
? 'Approval on. KONECK will ask in this interface before writes and consequential commands. Read-only inspection stays uninterrupted.'
|
|
3339
|
+
: 'Approval off. KONECK follows the selected mode and workspace policy; plan mode and policy blocks still cannot be bypassed.');
|
|
3275
3340
|
return;
|
|
3276
3341
|
}
|
|
3277
3342
|
case '/worktree': {
|
|
@@ -3622,6 +3687,39 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3622
3687
|
'PageUp and PageDown still scroll.');
|
|
3623
3688
|
return;
|
|
3624
3689
|
}
|
|
3690
|
+
/*
|
|
3691
|
+
* The working animation, on or off.
|
|
3692
|
+
*
|
|
3693
|
+
* Off leaves a still screen: on a terminal that does not composite a redraw, the animation is
|
|
3694
|
+
* the only thing repainting while nobody types, and a still screen is what some people want.
|
|
3695
|
+
*/
|
|
3696
|
+
case '/animation':
|
|
3697
|
+
case '/animate': {
|
|
3698
|
+
const want = arg.trim().toLowerCase();
|
|
3699
|
+
if (want !== '' && want !== 'on' && want !== 'off') {
|
|
3700
|
+
addSystem('Usage: /animation on, /animation off, or /animation to see which it is.');
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
if (want === '') {
|
|
3704
|
+
addSystem(`The working animation is **${animate ? 'on' : 'off'}**.\n\n`
|
|
3705
|
+
+ (animate
|
|
3706
|
+
? 'It repaints a few times a second while a turn runs. On a terminal that does not '
|
|
3707
|
+
+ 'composite a redraw that is visible as flicker. `/animation off` stops it — the '
|
|
3708
|
+
+ 'elapsed time and the status bar still say what is happening.'
|
|
3709
|
+
: 'The spinner is off. Activity and elapsed time refresh every two seconds while you are not typing. `/animation on` brings the spinner back.'));
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const next = want === 'on';
|
|
3713
|
+
setAnimate(next);
|
|
3714
|
+
const saved = { ...(await loadKoneckConfig()), animate: next };
|
|
3715
|
+
await saveKoneckConfig(saved);
|
|
3716
|
+
setStored(saved);
|
|
3717
|
+
addSystem(next
|
|
3718
|
+
? 'Working animation **on**.'
|
|
3719
|
+
: 'Working animation **off**. The status bar still shows '
|
|
3720
|
+
+ 'progress and elapsed time every two seconds, pausing while you type.');
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3625
3723
|
case '/redraw': {
|
|
3626
3724
|
redrawScreen();
|
|
3627
3725
|
return;
|
|
@@ -3675,8 +3773,11 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3675
3773
|
case '/auto':
|
|
3676
3774
|
case '/plan':
|
|
3677
3775
|
case '/edits':
|
|
3776
|
+
case '/edit':
|
|
3777
|
+
case '/code':
|
|
3678
3778
|
case '/careful': {
|
|
3679
|
-
const
|
|
3779
|
+
const requested = cmd === '/mode' ? arg.trim() : cmd.slice(1);
|
|
3780
|
+
const target = requested === 'edit' || requested === 'code' ? 'edits' : requested;
|
|
3680
3781
|
const names = MODE_SPECS.map(m => m.name).join(', ');
|
|
3681
3782
|
if (!target) {
|
|
3682
3783
|
addSystem(`Current mode: ${modeSpec(mode).label} — ${modeSpec(mode).blurb}\n\n` +
|
|
@@ -3847,6 +3948,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
3847
3948
|
}
|
|
3848
3949
|
return;
|
|
3849
3950
|
}
|
|
3951
|
+
const { readAll } = await import('./ledger.js');
|
|
3952
|
+
const notes = await readAll(cfg.cwd);
|
|
3953
|
+
// Older releases shortened an entire shell pipeline to its first two words. That turned
|
|
3954
|
+
// `cd project && slow-test` into a bogus claim that changing directory was slow. Keep the
|
|
3955
|
+
// old line on disk for auditability, but do not present it as knowledge or send it back to
|
|
3956
|
+
// a model.
|
|
3957
|
+
const usableNotes = notes.filter(n => !(n.kind === 'timing'
|
|
3958
|
+
&& n.commands?.some(command => /^cd\s+[^;&|]+$/.test(command))));
|
|
3959
|
+
if (usableNotes.length) {
|
|
3960
|
+
addSystem('Saved project notes (.koneck/ledger.jsonl):\n'
|
|
3961
|
+
+ usableNotes.slice(-20).map(n => '• ' + n.text).join('\n'));
|
|
3962
|
+
}
|
|
3963
|
+
if (usableNotes.length !== notes.length) {
|
|
3964
|
+
addSystem(`${notes.length - usableNotes.length} old timing note${notes.length - usableNotes.length === 1 ? '' : 's'} ignored: it measured only a directory change, not the command that followed it.`);
|
|
3965
|
+
}
|
|
3850
3966
|
const mem = await loadMemory(cfg.cwd).catch(() => null);
|
|
3851
3967
|
addSystem(mem?.trim()
|
|
3852
3968
|
? mem + '\n\n(/memory clear removes these notes)'
|
|
@@ -4268,6 +4384,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
4268
4384
|
const n = Number(val);
|
|
4269
4385
|
setConfigMaxWidth(Number.isFinite(n) && n >= 40 ? n : undefined);
|
|
4270
4386
|
}
|
|
4387
|
+
if (key === 'autoCopy')
|
|
4388
|
+
setCfg(c => ({ ...c, autoCopy: ['true', '1', 'yes'].includes(val.toLowerCase()) }));
|
|
4389
|
+
if (key === 'animate')
|
|
4390
|
+
setAnimate(['true', '1', 'yes'].includes(val.toLowerCase()));
|
|
4271
4391
|
if (key === 'mouse') {
|
|
4272
4392
|
setMouse(['true', '1', 'yes'].includes(val.toLowerCase()));
|
|
4273
4393
|
}
|
|
@@ -4598,6 +4718,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
4598
4718
|
setKeyPrompt(p => (p ? { ...p, value: p.value + clean.replace(/\s+$/, '') } : p));
|
|
4599
4719
|
return;
|
|
4600
4720
|
}
|
|
4721
|
+
const imagePath = pastedImagePath(clean, cfg.cwd);
|
|
4722
|
+
if (imagePath) {
|
|
4723
|
+
const token = imageDraftRef.current.add(imagePath);
|
|
4724
|
+
setDraft(d => d.slice(0, caret) + token + d.slice(caret));
|
|
4725
|
+
setCaret(c => c + token.length);
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4601
4728
|
if (!clean.includes('\n') && clean.length <= 200) {
|
|
4602
4729
|
setDraft(d => d.slice(0, caret) + clean + d.slice(caret));
|
|
4603
4730
|
setCaret(c => c + clean.length);
|
|
@@ -4610,6 +4737,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
4610
4737
|
setCaret(c => c + token.length);
|
|
4611
4738
|
}
|
|
4612
4739
|
useInput((input, key) => {
|
|
4740
|
+
/*
|
|
4741
|
+
* When a key was last pressed, so the animation can get out of the way.
|
|
4742
|
+
*
|
|
4743
|
+
* Set before anything else, on every key including the ones swallowed below: what matters is
|
|
4744
|
+
* that somebody is at the keyboard, not what they typed. See the animation ticker.
|
|
4745
|
+
*/
|
|
4746
|
+
lastKeyAtRef.current = Date.now();
|
|
4613
4747
|
// ── Mouse reports are not typing ────────────────────────────────────────
|
|
4614
4748
|
//
|
|
4615
4749
|
// Ink hands any escape sequence it does not recognise to the application as `input`, and a
|
|
@@ -5033,6 +5167,21 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5033
5167
|
});
|
|
5034
5168
|
return;
|
|
5035
5169
|
}
|
|
5170
|
+
if (key.ctrl && input.toLowerCase() === 'v') {
|
|
5171
|
+
void (async () => {
|
|
5172
|
+
const image = await readClipboardImage();
|
|
5173
|
+
if (image.path) {
|
|
5174
|
+
filePaste(image.path);
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
5177
|
+
const text = await readClipboard();
|
|
5178
|
+
if (text.ok && text.text)
|
|
5179
|
+
filePaste(text.text);
|
|
5180
|
+
else
|
|
5181
|
+
addSystem(image.reason || text.reason || 'The clipboard is empty.');
|
|
5182
|
+
})();
|
|
5183
|
+
return;
|
|
5184
|
+
}
|
|
5036
5185
|
// Only submission waits for the agent. Editing stays live throughout, so a thought had
|
|
5037
5186
|
// while it works can be typed as it arrives rather than held until the turn ends — and
|
|
5038
5187
|
// cancelling with esc then hands the keyboard back to a composer that still has it.
|
|
@@ -5061,7 +5210,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5061
5210
|
}
|
|
5062
5211
|
addSystem(`${cmd} changes the session, so it waits until this turn finishes.`);
|
|
5063
5212
|
}
|
|
5064
|
-
setQueued(q => [...q, expandPastes(text, pastesRef.current)]);
|
|
5213
|
+
setQueued(q => [...q, imageDraftRef.current.expand(expandPastes(text, pastesRef.current))]);
|
|
5065
5214
|
setDraft('');
|
|
5066
5215
|
setCaret(0);
|
|
5067
5216
|
pastesRef.current = [];
|
|
@@ -5157,6 +5306,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5157
5306
|
const text = input.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
|
|
5158
5307
|
if (text === '')
|
|
5159
5308
|
return;
|
|
5309
|
+
if (text.length > 1 && pastedImagePath(text, cfg.cwd)) {
|
|
5310
|
+
filePaste(text);
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5160
5313
|
const next = draft.slice(0, caret) + text + draft.slice(caret);
|
|
5161
5314
|
const nextCaret = caret + text.length;
|
|
5162
5315
|
// Once the recalled line is edited it is a new draft, not a history cursor position.
|
|
@@ -5201,7 +5354,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5201
5354
|
const shown = raw.trim();
|
|
5202
5355
|
if (!shown)
|
|
5203
5356
|
return;
|
|
5204
|
-
const task = expandPastes(shown, pastesRef.current).trim();
|
|
5357
|
+
const task = imageDraftRef.current.expand(expandPastes(shown, pastesRef.current)).trim();
|
|
5205
5358
|
if (!task)
|
|
5206
5359
|
return;
|
|
5207
5360
|
const generated = generatedTaskRef.current?.task === task ? generatedTaskRef.current : null;
|
|
@@ -5231,7 +5384,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5231
5384
|
'written into the session log.');
|
|
5232
5385
|
}
|
|
5233
5386
|
const mentions = findMentions(task, pathKind);
|
|
5234
|
-
const forDisplay = taskTranscriptLabel(pasteForDisplay(mentions.length ? shortenMentions(task, mentions) : task), generated?.label);
|
|
5387
|
+
const forDisplay = taskTranscriptLabel(shown.includes('[Image #') ? shown : pasteForDisplay(mentions.length ? shortenMentions(task, mentions) : task), generated?.label);
|
|
5235
5388
|
addRow({ role: 'user', text: forDisplay });
|
|
5236
5389
|
if (isSlashCommand(task)) {
|
|
5237
5390
|
const spaceIdx = task.indexOf(' ');
|
|
@@ -5422,11 +5575,14 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5422
5575
|
* Only on the alternate screen: in inline mode the transcript is committed to the terminal's own
|
|
5423
5576
|
* scrollback full-width, and there is no frame to put a second column in.
|
|
5424
5577
|
*/
|
|
5578
|
+
const activityPanelOpen = busy && uiWidth >= 100 && termRows >= 24
|
|
5579
|
+
&& (diffPanelTouchedRef.current ? showDiffPanel : cfg.diffPanel !== false);
|
|
5425
5580
|
const diffPanelOpen = showDiffPanel && altScreen && liveDiffs.length > 0;
|
|
5581
|
+
const sidePanelOpen = activityPanelOpen || diffPanelOpen;
|
|
5426
5582
|
// Read by renderStep, which is defined above this line, so it goes through a ref rather than
|
|
5427
5583
|
// depending on declaration order.
|
|
5428
5584
|
diffPanelOpenRef.current = diffPanelOpen;
|
|
5429
|
-
const diffPanelWidth =
|
|
5585
|
+
const diffPanelWidth = sidePanelOpen
|
|
5430
5586
|
? Math.max(34, Math.min(Math.floor(uiWidth * 0.46), 96))
|
|
5431
5587
|
: 0;
|
|
5432
5588
|
const contentWidth = uiWidth - diffPanelWidth;
|
|
@@ -5549,7 +5705,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5549
5705
|
// Both halves matter. Clipping the bottom would hide what just happened; anchoring
|
|
5550
5706
|
// always to the bottom is what put the banner there.
|
|
5551
5707
|
_jsx(Box, { flexGrow: 0, flexShrink: 1, flexDirection: "column", justifyContent: "flex-end", overflowY: "hidden", children: visibleRows.map((row, i) => (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: renderRow(row, i) }, i))) })), altScreen && scrollBack > 0 && (_jsx(Text, { color: AMBER, children: `${G.caret} scrolled back ${scrollBack} rows — PageDown, or just type, to follow again` })), showAnalytics && (_jsxs(Box, { borderStyle: "round", borderColor: AMBER, paddingX: 1, marginBottom: 1, flexDirection: "column", width: barWidth, children: [_jsx(Text, { color: AMBER, bold: true, children: "\u25C6 Session Analytics" }), _jsxs(Text, { color: MUTED, children: ["Turns : ", _jsx(Text, { color: INK, children: stats.turns })] }), _jsxs(Text, { color: MUTED, children: ["Prompt tok : ", _jsx(Text, { color: INK, children: stats.promptTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Output tok : ", _jsx(Text, { color: INK, children: stats.completionTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Total tok : ", _jsx(Text, { color: INK, children: stats.totalTokens.toLocaleString() })] }), _jsxs(Text, { color: MUTED, children: ["Model : ", _jsx(Text, { color: INK, children: modelShort }), " Provider: ", _jsx(Text, { color: INK, children: cfg.provider })] }), _jsx(Text, { color: DIM, children: "Tab to close" })] })), _jsx(Static, { items: altScreen ? [] : rows, children: (row, index) => renderRow(row, index) }), _jsx(Box, { flexDirection: "column", flexShrink: 0, children: busy && (agentState === 'processing' || agentState === 'syncing') && (_jsxs(Box, { flexDirection: "column", children: [!ask && sayRef.current.trim() !== '' && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: CYAN, bold: true, children: replyStartedRef.current ? ' ' : G.brand }), _jsx(Box, { flexDirection: "column", children: tailLines(sayRef.current, liveReplyLines(termRows), replyWidth - 3)
|
|
5552
|
-
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), queued.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: AMBER, children: "\u25AA" }), _jsxs(Text, { color: MUTED, children: [queued.length, " queued, will run when this finishes \u00B7 /aside to ask without waiting"] })] }), queued.map((q, i) => (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: fitCells(q.split('\n')[0] ?? '', Math.max(20, barWidth - 6), true) }) }, i)))] })), agents.length > 0 && (() => {
|
|
5708
|
+
.split('\n').map((line, i) => _jsx(Text, { color: INK, children: line }, i)) })] })), liveToolRef.current && renderStep(liveToolRef.current, 1), queued.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: AMBER, children: "\u25AA" }), _jsxs(Text, { color: MUTED, children: [queued.length, " queued, will run when this finishes \u00B7 /aside to ask without waiting"] })] }), queued.map((q, i) => (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: fitCells(q.split('\n')[0] ?? '', Math.max(20, barWidth - 6), true) }) }, i)))] })), !activityPanelOpen && agents.length > 0 && (() => {
|
|
5553
5709
|
const done = agents.filter(a => a.status !== 'running').length;
|
|
5554
5710
|
const failed = agents.filter(a => a.status === 'failed').length;
|
|
5555
5711
|
const tokens = agents.reduce((n, a) => n + a.tokens, 0);
|
|
@@ -5566,7 +5722,7 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5566
5722
|
const task = a.task.length > taskWidth ? a.task.slice(0, taskWidth - 3) + '...' : a.task;
|
|
5567
5723
|
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { color: DIM, children: String(a.index + 1).padStart(2) }), _jsx(Text, { color: a.status === 'running' ? INK : MUTED, children: fitCells(task, taskWidth) }), _jsx(Text, { color: DIM, children: humanTokens(a.tokens).padStart(6) }), _jsx(Text, { color: DIM, children: fmtElapsed(took).padStart(6) })] }, a.index));
|
|
5568
5724
|
})] }));
|
|
5569
|
-
})(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [SPINNER[spinFrame], " "] }), _jsx(Shimmer, { text: workWord({
|
|
5725
|
+
})(), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: GREEN, children: [animate ? SPINNER[spinFrame] : '●', " "] }), _jsx(Shimmer, { text: workWord({
|
|
5570
5726
|
elapsedMs, tools: turnToolsRef.current,
|
|
5571
5727
|
tokens: liveTokens, recovered: turnRecoveredRef.current,
|
|
5572
5728
|
}, spinWord)[0], frame: shimmerFrame, base: MUTED }), _jsx(Text, { color: MUTED, children: "\u2026 " }), _jsxs(Text, { color: DIM, children: ["(", fmtElapsed(elapsedMs), " | ", liveTokens > 0
|
|
@@ -5574,9 +5730,10 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5574
5730
|
// A gateway routing to several backends sends content-free frames while it
|
|
5575
5731
|
// finds one. Saying "waiting for the first token" through that reads as a
|
|
5576
5732
|
// hang, when in fact the request was accepted and is being worked on.
|
|
5577
|
-
:
|
|
5578
|
-
:
|
|
5579
|
-
:
|
|
5733
|
+
: liveToolRef.current ? 'running ' + liveToolRef.current.name
|
|
5734
|
+
: providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
|
|
5735
|
+
: elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
|
|
5736
|
+
: 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
|
|
5580
5737
|
? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
|
|
5581
5738
|
: '', ")"] })] }) }), showThinking && !ask && thinkingTextRef.current.trim() !== '' && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: thinkingTail(thinkingTextRef.current, THINKING_LINES, barWidth - 6).map((line, i) => (_jsx(Text, { color: DIM, italic: true, children: line }, i))) })), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: DIM, children: "esc to cancel and keep your input \u00B7 ctrl+c to exit" }) })] })) }), usagePane && (() => {
|
|
5582
5739
|
const width = Math.min(barWidth, usagePane === 'settings' ? 96 : 66);
|
|
@@ -5896,11 +6053,13 @@ export function App({ config: initialConfig, clearFrame, synchronizedFrames = fa
|
|
|
5896
6053
|
: '*type the number, or arrows and enter · esc refuses*',
|
|
5897
6054
|
];
|
|
5898
6055
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 46), color: AMBER, title: `Permission — ${modeSpec(mode).label} mode`, children: lines }) }));
|
|
5899
|
-
})()] }),
|
|
6056
|
+
})()] }), sidePanelOpen && (_jsxs(Box, { flexDirection: "column", width: diffPanelWidth, flexShrink: 0, children: [activityPanelOpen && (_jsxs(Box, { borderStyle: "round", borderColor: CYAN, flexDirection: "column", paddingX: 1, height: Math.min(18, Math.max(8, termRows - 7)), overflowY: "hidden", children: [_jsxs(Text, { color: CYAN, bold: true, children: ["Activity \u00B7 ", fmtElapsed(elapsedMs)] }), _jsx(Text, { color: AMBER, wrap: "truncate", children: ask ? 'Waiting for your approval' : liveToolRef.current
|
|
6057
|
+
? 'Running ' + liveToolRef.current.name : thinkingRef.current.chars > 0
|
|
6058
|
+
? 'Waiting for model · reasoning received' : 'Waiting for model' }), liveToolRef.current && _jsx(Text, { color: INK, wrap: "truncate", children: liveToolRef.current.detail }), liveToolRef.current && _jsxs(Text, { color: DIM, children: ["Running for ", fmtElapsed(Date.now() - liveToolRef.current.startedAt)] }), _jsx(Text, { color: MUTED, wrap: "truncate", children: liveToolRef.current?.output || 'Status updates every 2s; esc to interrupt' }), plan && _jsx(Text, { color: MUTED, wrap: "truncate", children: plan.steps.find(s => s.state !== 'done')?.text || 'Plan complete' }), agents.length > 0 && _jsxs(Text, { color: CYAN, bold: true, children: ["Agents \u00B7 ", agents.filter(a => a.status !== 'running').length, "/", agents.length, " finished"] }), agents.map(a => _jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsxs(Text, { color: a.status === 'failed' ? CRIMSON : a.status === 'done' ? GREEN : AMBER, wrap: "truncate", children: [a.index + 1, ". ", a.status === 'running' ? (a.activity === 'Queued' ? 'queued' : 'working') : a.status, " \u00B7 ", a.activity, " \u00B7 ", a.task] }) }, a.index))] })), diffPanelOpen && _jsx(DiffPanel, { files: liveDiffs, width: diffPanelWidth, height: Math.max(4, frameRows - (activityPanelOpen ? 24 : 6)) })] }))] }), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? DIM : CYAN, paddingX: 1, width: uiWidth - 5, flexShrink: 0, children: [_jsxs(Text, { color: CYAN, bold: true, children: [G.caret, " "] }), _jsx(Text, { color: INK, children: draft.slice(0, caret) }), busy
|
|
5900
6059
|
? _jsx(Text, { color: INK, children: draft.slice(caret) })
|
|
5901
6060
|
: caret < draft.length
|
|
5902
6061
|
? _jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: CYAN, color: "#10222A", children: draft[caret] }), _jsx(Text, { color: INK, children: draft.slice(caret + 1) })] })
|
|
5903
|
-
: _jsx(Text, { color: CYAN, children: "\u2588" })] }), _jsx(Box, { marginTop: 1, paddingX: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [repaint % 2 === 1 ? '\u200b' : '', _jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" }), pasteCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: AMBER, children: [pasteCount, " paste", pasteCount === 1 ? '' : 's', " held, sent in full"] })] }))] }) })] }));
|
|
6062
|
+
: _jsx(Text, { color: CYAN, children: "\u2588" })] }), copyNotice && _jsx(Text, { color: CYAN, children: copyNotice }), _jsx(Box, { marginTop: 1, paddingX: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", backgroundColor: "#24343B", children: [repaint % 2 === 1 ? '\u200b' : '', _jsx(Text, { color: MUTED, children: "TAB " }), _jsx(Text, { color: INK, children: "Analytics" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: CYAN, children: "/HELP" }), _jsx(Text, { color: INK, children: " Commands" }), _jsx(Text, { color: MUTED, children: " / SHIFT+TAB " }), _jsx(Text, { color: INK, children: "Mode" }), _jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: statusColor, children: statusLabel.toLowerCase() }), plan && planSummary(plan) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / PLAN: " }), _jsx(Text, { color: CYAN, children: planSummary(plan) })] })), _jsx(Text, { color: MUTED, children: " / MODE: " }), _jsx(Text, { color: mode === 'auto' ? INK : AMBER, children: modeSpec(mode).label }), _jsx(Text, { color: MUTED, children: " / EFFORT: " }), _jsx(Text, { color: effort === 'medium' ? INK : AMBER, children: effort }), mouse && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsx(Text, { color: AMBER, children: "WHEEL: on \u2014 shift to select \u00B7 /mouse off" })] })), _jsx(Text, { color: MUTED, children: " / MODEL: " }), _jsx(Text, { color: INK, children: modelShort }), _jsx(Text, { color: MUTED, children: " / v" }), _jsx(Text, { color: updatePane?.behind ? AMBER : INK, children: VERSION }), updatePane?.behind && _jsx(Text, { color: AMBER, children: " \u2191" }), pasteCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: MUTED, children: " / " }), _jsxs(Text, { color: AMBER, children: [pasteCount, " paste", pasteCount === 1 ? '' : 's', " held, sent in full"] })] }))] }) })] }));
|
|
5904
6063
|
}
|
|
5905
6064
|
export async function runInkChatMode(config) {
|
|
5906
6065
|
// Nothing is written before the first frame. Filling the screen to push the prompt to the foot
|