koneck 2.26.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +28 -8
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -1
- package/dist/engine.js.map +1 -1
- package/dist/ink-chat.d.ts +3 -3
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +134 -25
- package/dist/ink-chat.js.map +1 -1
- package/dist/quality-gate.d.ts +5 -1
- package/dist/quality-gate.d.ts.map +1 -1
- package/dist/quality-gate.js +8 -6
- package/dist/quality-gate.js.map +1 -1
- package/dist/session.d.ts +10 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +12 -1
- package/dist/session.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +60 -26
- package/dist/web/api.js.map +1 -1
- package/dist/web/changes.d.ts +55 -0
- package/dist/web/changes.d.ts.map +1 -0
- package/dist/web/changes.js +215 -0
- package/dist/web/changes.js.map +1 -0
- package/dist/web/history.d.ts +61 -0
- package/dist/web/history.d.ts.map +1 -0
- package/dist/web/history.js +197 -0
- package/dist/web/history.js.map +1 -0
- package/dist/web/mark.d.ts +15 -0
- package/dist/web/mark.d.ts.map +1 -0
- package/dist/web/mark.js +15 -0
- package/dist/web/mark.js.map +1 -0
- package/dist/web/sessions.d.ts +8 -0
- package/dist/web/sessions.d.ts.map +1 -1
- package/dist/web/sessions.js +9 -0
- package/dist/web/sessions.js.map +1 -1
- package/dist/web/ui-client.d.ts +14 -0
- package/dist/web/ui-client.d.ts.map +1 -0
- package/dist/web/ui-client.js +0 -0
- package/dist/web/ui-client.js.map +1 -0
- package/dist/web/ui-css.d.ts +15 -0
- package/dist/web/ui-css.d.ts.map +1 -0
- package/dist/web/ui-css.js +284 -0
- package/dist/web/ui-css.js.map +1 -0
- package/dist/web/ui.d.ts +6 -9
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +72 -542
- package/dist/web/ui.js.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -41,6 +41,10 @@ import { describeActivity } from './activity.js';
|
|
|
41
41
|
import { redactSecrets, containsSecret } from './redact.js';
|
|
42
42
|
import { renderTrajectory } from './trajectory.js';
|
|
43
43
|
import { describeSandbox, resolveSandbox } from './sandbox.js';
|
|
44
|
+
import { doctorChecks, doctorReport } from './doctor.js';
|
|
45
|
+
import { runQualityGate } from './quality-gate.js';
|
|
46
|
+
import { listAuditEvents } from './audit.js';
|
|
47
|
+
import { initializePolicy, loadPolicy, POLICY_FILE } from './policy.js';
|
|
44
48
|
const CYAN = '#54D7E7';
|
|
45
49
|
const GREEN = '#9BCB8F';
|
|
46
50
|
const CRIMSON = '#E8878D';
|
|
@@ -506,6 +510,7 @@ const COMMANDS = [
|
|
|
506
510
|
{ cmd: '/model', desc: 'Show or set the model by name' },
|
|
507
511
|
{ cmd: '/provider', desc: 'Show or switch provider' },
|
|
508
512
|
{ cmd: '/status', desc: 'Provider, model, mode, tokens, cost, workspace' },
|
|
513
|
+
{ cmd: '/doctor', desc: 'Check the active provider, workspace, and developer tools' },
|
|
509
514
|
{ cmd: '/revert', desc: 'Undo file edits KONECK made' },
|
|
510
515
|
{ cmd: '/sessions', desc: 'List running KONECK sessions and their state' },
|
|
511
516
|
{ cmd: '/send', desc: 'Hand work to another session: /send <repo> <instruction>' },
|
|
@@ -532,6 +537,9 @@ const COMMANDS = [
|
|
|
532
537
|
{ cmd: '/fork', desc: 'Copy this session and continue on the copy' },
|
|
533
538
|
{ cmd: '/archive', desc: 'Keep a session but hide it from /resume' },
|
|
534
539
|
{ cmd: '/diff', desc: 'Show uncommitted git changes' },
|
|
540
|
+
{ cmd: '/quality', desc: 'Run the detected lint and test checks' },
|
|
541
|
+
{ cmd: '/audit', desc: 'Show recent redacted tool activity' },
|
|
542
|
+
{ cmd: '/policy', desc: 'Show or initialize workspace safety rules' },
|
|
535
543
|
{ cmd: '/verify', desc: 'Run the project test command' },
|
|
536
544
|
{ cmd: '/rewind', desc: 'Drop the last exchange from history' },
|
|
537
545
|
{ cmd: '/copy', desc: 'Copy the last response to the clipboard' },
|
|
@@ -580,19 +588,20 @@ const MAX_DIFF_FILES = 4;
|
|
|
580
588
|
const USAGE_TABS = ['settings', 'session', 'context', 'models', 'limits'];
|
|
581
589
|
const MIN_REPLY_WIDTH = 24; // below this a table cannot render legibly anyway
|
|
582
590
|
const PICKER_MIN_ROWS = 6;
|
|
583
|
-
const PICKER_MAX_ROWS =
|
|
584
|
-
const PROVIDER_PICKER_MAX_ROWS = 16;
|
|
591
|
+
const PICKER_MAX_ROWS = 16;
|
|
585
592
|
/**
|
|
586
593
|
* Number of choices to keep visible in a palette.
|
|
587
594
|
*
|
|
588
|
-
*
|
|
589
|
-
* and
|
|
590
|
-
*
|
|
595
|
+
* A picker should feel like a real menu, not a letterbox. Sixteen rows keeps commands, models,
|
|
596
|
+
* files, sessions, and providers equally easy to scan; on short terminals it yields space to
|
|
597
|
+
* the composer and the rest remains available through search and paging.
|
|
591
598
|
*/
|
|
592
599
|
export function pickerRowLimit(kind, itemCount, terminalRows) {
|
|
593
600
|
const terminalBudget = Math.max(PICKER_MIN_ROWS, terminalRows - 8);
|
|
594
|
-
|
|
595
|
-
|
|
601
|
+
// Keep kind in the signature: it documents that this is the common policy for *every* picker
|
|
602
|
+
// and lets a future picker opt into a different policy without duplicating this calculation.
|
|
603
|
+
void kind;
|
|
604
|
+
return Math.min(itemCount, PICKER_MAX_ROWS, terminalBudget);
|
|
596
605
|
}
|
|
597
606
|
/**
|
|
598
607
|
* Filters picker entries. Prefix matches rank above substring matches so typing "mo" offers
|
|
@@ -754,6 +763,7 @@ Slash commands:
|
|
|
754
763
|
/endpoint http://192.168.1.50:11434/v1
|
|
755
764
|
/key Enter an API key for the current provider
|
|
756
765
|
/status Provider, model, mode, tokens, cost, CWD
|
|
766
|
+
/doctor Diagnose the active provider, workspace, and developer tools
|
|
757
767
|
/clear /reset /new Clear conversation history
|
|
758
768
|
/model [name] Show or switch model
|
|
759
769
|
/provider [name] Show or switch provider
|
|
@@ -766,6 +776,9 @@ Slash commands:
|
|
|
766
776
|
/cost Show session cost
|
|
767
777
|
/memory Show .koneck/MEMORY.md
|
|
768
778
|
/diff Show uncommitted git changes
|
|
779
|
+
/quality Run the detected lint and test checks
|
|
780
|
+
/audit [count] Show recent redacted tool activity
|
|
781
|
+
/policy [init] Show or create workspace safety rules
|
|
769
782
|
/verify Run project test command
|
|
770
783
|
/agents How parallel sub-agents are configured
|
|
771
784
|
/subtask <task> Run a task in an isolated sub-agent
|
|
@@ -810,8 +823,14 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
810
823
|
const streamedRef = useRef('');
|
|
811
824
|
/** When the live region last had something new to show, used to pick the repaint rate. */
|
|
812
825
|
const lastActivityRef = useRef(Date.now());
|
|
813
|
-
/**
|
|
814
|
-
|
|
826
|
+
/**
|
|
827
|
+
* Frames received before any content — proof the provider has the request and is working.
|
|
828
|
+
*
|
|
829
|
+
* Gateways can emit hundreds of these while routing. This must not be React state: doing so
|
|
830
|
+
* repainted the complete terminal for each network frame, which is the primary source of the
|
|
831
|
+
* visible flashing during a response. The already-throttled live refresh reads this ref.
|
|
832
|
+
*/
|
|
833
|
+
const providerFramesRef = useRef(0);
|
|
815
834
|
/**
|
|
816
835
|
* Reasoning characters this turn, and when the last one arrived.
|
|
817
836
|
*
|
|
@@ -1040,7 +1059,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1040
1059
|
if (liveToolRef.current)
|
|
1041
1060
|
liveToolRef.current.output = line;
|
|
1042
1061
|
},
|
|
1043
|
-
onStreamActivity: (frames) =>
|
|
1062
|
+
onStreamActivity: (frames) => { providerFramesRef.current = frames; },
|
|
1044
1063
|
onIncomplete: (reason) => { incompleteRef.current = reason; },
|
|
1045
1064
|
onToolsUnsupported: (model) => {
|
|
1046
1065
|
// Said once per session, not once per turn — it is a property of the model, not an event.
|
|
@@ -1496,16 +1515,23 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1496
1515
|
}
|
|
1497
1516
|
busyStart.current = Date.now();
|
|
1498
1517
|
wordTimer.current = 0;
|
|
1499
|
-
// The frame rate is the flicker rate. Every tick makes Ink
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1518
|
+
// The frame rate is the flicker rate. Every tick makes Ink update the live region, so network
|
|
1519
|
+
// activity must be sampled for a human reader rather than mirrored frame-for-frame. A third
|
|
1520
|
+
// of a second keeps streamed prose feeling live; an idle tool only needs a once-a-second
|
|
1521
|
+
// heartbeat. This is particularly important in terminals that repaint an ANSI line update as
|
|
1522
|
+
// a flash.
|
|
1504
1523
|
let timer;
|
|
1505
1524
|
const tick = () => {
|
|
1506
1525
|
const elapsed = Date.now() - busyStart.current;
|
|
1507
1526
|
const delay = currentDelay();
|
|
1508
|
-
|
|
1527
|
+
// Do not animate several cells for every content refresh. The spinner moves once a second
|
|
1528
|
+
// and the shimmer once every two seconds; the intervening refreshes are solely to reveal
|
|
1529
|
+
// new streamed text.
|
|
1530
|
+
setAnim({
|
|
1531
|
+
spin: Math.floor(elapsed / 1_000) % SPINNER.length,
|
|
1532
|
+
shimmer: Math.floor(elapsed / 2_000),
|
|
1533
|
+
elapsed,
|
|
1534
|
+
});
|
|
1509
1535
|
wordTimer.current += 1;
|
|
1510
1536
|
// Roughly every 60 seconds, whatever the current rate. Twenty-five felt churned on a long
|
|
1511
1537
|
// run — the word is there to be read and enjoyed, not to reassure by moving.
|
|
@@ -1516,13 +1542,9 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1516
1542
|
};
|
|
1517
1543
|
const currentDelay = () => {
|
|
1518
1544
|
const idleFor = Date.now() - lastActivityRef.current;
|
|
1519
|
-
|
|
1520
|
-
// like the app has seized. 500ms was too far — at two frames a second the spinner visibly
|
|
1521
|
-
// steps and reads as lag. These are the slowest rates at which the animation still reads
|
|
1522
|
-
// as motion, which is the property that matters more than the byte count.
|
|
1523
|
-
return idleFor < 2_000 ? 120 : idleFor > 15_000 ? 220 : 170;
|
|
1545
|
+
return idleFor < 2_000 ? 350 : 1_000;
|
|
1524
1546
|
};
|
|
1525
|
-
timer = setTimeout(tick,
|
|
1547
|
+
timer = setTimeout(tick, 350);
|
|
1526
1548
|
return () => clearTimeout(timer);
|
|
1527
1549
|
}, [busy]);
|
|
1528
1550
|
// A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
|
|
@@ -1618,7 +1640,9 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1618
1640
|
id, task, provider: cfg.provider, model: cfg.model,
|
|
1619
1641
|
cwd: cfg.cwd, startedAt: startedAtRef.current,
|
|
1620
1642
|
turns: live.turns, totalTokens: live.totalTokens,
|
|
1621
|
-
|
|
1643
|
+
// Timings persisted alongside the conversation: the messages say what was said, only this
|
|
1644
|
+
// says where the time went.
|
|
1645
|
+
}, s?.messages ?? [], s?.trajectory);
|
|
1622
1646
|
}
|
|
1623
1647
|
/**
|
|
1624
1648
|
* Compresses this session's state into something worth sending. One small model call, capped
|
|
@@ -2119,6 +2143,22 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2119
2143
|
`Cost : ${costStr}`);
|
|
2120
2144
|
return;
|
|
2121
2145
|
}
|
|
2146
|
+
case '/doctor': {
|
|
2147
|
+
// A session can change provider, endpoint, or workspace after startup. Running the same
|
|
2148
|
+
// checks here, against the *active* provider, means a diagnosis describes this session
|
|
2149
|
+
// rather than a stale global default.
|
|
2150
|
+
addSystem(`**KONECK Doctor** — checking ${cfg.provider} and this workspace…`);
|
|
2151
|
+
try {
|
|
2152
|
+
const checks = await doctorChecks(cfg.cwd, cfg.provider);
|
|
2153
|
+
setRows(prev => prev.slice(0, -1));
|
|
2154
|
+
addSystem(`**KONECK Doctor**\n\n${doctorReport(checks)}`);
|
|
2155
|
+
}
|
|
2156
|
+
catch (e) {
|
|
2157
|
+
setRows(prev => prev.slice(0, -1));
|
|
2158
|
+
addSystem(`Doctor could not finish: ${e instanceof Error ? e.message : String(e)}`);
|
|
2159
|
+
}
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2122
2162
|
case '/clear':
|
|
2123
2163
|
case '/reset':
|
|
2124
2164
|
case '/new': {
|
|
@@ -2381,6 +2421,75 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2381
2421
|
}
|
|
2382
2422
|
return;
|
|
2383
2423
|
}
|
|
2424
|
+
case '/quality': {
|
|
2425
|
+
// The engine uses this exact gate after a coding turn. Exposing it in chat gives the user
|
|
2426
|
+
// the same independent proof on demand, without the legacy TUI writing over Ink's frame.
|
|
2427
|
+
addSystem('**Quality gate** — running detected lint and test checks…');
|
|
2428
|
+
try {
|
|
2429
|
+
const result = await runQualityGate(cfg.cwd, { report: false });
|
|
2430
|
+
setRows(prev => prev.slice(0, -1));
|
|
2431
|
+
if (result.checks.length === 0) {
|
|
2432
|
+
addSystem('**Quality gate**\n\nNo lint or test command was detected for this workspace.');
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const lines = result.checks.flatMap(check => {
|
|
2436
|
+
const state = check.passed ? 'PASS' : 'FAIL';
|
|
2437
|
+
const detail = check.output?.trim();
|
|
2438
|
+
const tail = !check.passed && detail
|
|
2439
|
+
? `\n\n\`\`\`\n${detail.slice(-2_000)}\n\`\`\``
|
|
2440
|
+
: '';
|
|
2441
|
+
return [`**${state}** ${check.label} — \`${check.command}\` (${fmtElapsed(check.durationMs)})${tail}`];
|
|
2442
|
+
});
|
|
2443
|
+
addSystem(`**Quality gate ${result.passed ? 'passed' : 'needs attention'}**\n\n` +
|
|
2444
|
+
lines.join('\n\n'));
|
|
2445
|
+
}
|
|
2446
|
+
catch (e) {
|
|
2447
|
+
setRows(prev => prev.slice(0, -1));
|
|
2448
|
+
addSystem(`Quality gate could not finish: ${e instanceof Error ? e.message : String(e)}`);
|
|
2449
|
+
}
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
case '/audit': {
|
|
2453
|
+
const requested = Number.parseInt(arg, 10);
|
|
2454
|
+
const limit = Number.isFinite(requested) ? Math.max(1, Math.min(requested, 100)) : 20;
|
|
2455
|
+
const events = await listAuditEvents(cfg.cwd, limit);
|
|
2456
|
+
if (events.length === 0) {
|
|
2457
|
+
addSystem('No recorded tool activity yet. KONECK records redacted actions in `.koneck/audit/`.');
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
const lines = events.map(event => {
|
|
2461
|
+
const when = new Date(event.timestamp).toLocaleString();
|
|
2462
|
+
const state = event.outcome === 'success' ? 'OK' : 'FAIL';
|
|
2463
|
+
const action = Object.entries(event.action)
|
|
2464
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
2465
|
+
.join(' · ');
|
|
2466
|
+
return `${state.padEnd(4)} ${when} ${event.tool} ${action} (${fmtElapsed(event.durationMs)})`;
|
|
2467
|
+
});
|
|
2468
|
+
addSystem(`**Recent tool activity** — ${events.length} event${events.length === 1 ? '' : 's'}\n\n${lines.join('\n')}`);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
case '/policy': {
|
|
2472
|
+
if (arg.trim().toLowerCase() === 'init') {
|
|
2473
|
+
try {
|
|
2474
|
+
addSystem(await initializePolicy(cfg.cwd));
|
|
2475
|
+
}
|
|
2476
|
+
catch (e) {
|
|
2477
|
+
addSystem(`Could not create ${POLICY_FILE}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2478
|
+
}
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const policy = await loadPolicy(cfg.cwd);
|
|
2482
|
+
if (!policy) {
|
|
2483
|
+
addSystem(`No workspace policy is active. Run \`/policy init\` to create \`${POLICY_FILE}\`.`);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
addSystem(`**KONECK policy** — \`${POLICY_FILE}\`\n\n` +
|
|
2487
|
+
`Protected paths : ${policy.protectedPaths?.length ?? 0}\n` +
|
|
2488
|
+
`Write scopes : ${policy.allowedWritePaths?.length ? policy.allowedWritePaths.join(', ') : 'unrestricted'}\n` +
|
|
2489
|
+
`Blocked commands: ${policy.blockedCommands?.length ?? 0}\n` +
|
|
2490
|
+
`Approval rules : ${policy.requireApprovalFor?.join(', ') || 'none'}`);
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2384
2493
|
case '/verify': {
|
|
2385
2494
|
const { detectProject } = await import('./project.js');
|
|
2386
2495
|
const project = await detectProject(cfg.cwd);
|
|
@@ -3403,7 +3512,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3403
3512
|
thinkingTextRef.current = '';
|
|
3404
3513
|
incompleteRef.current = null;
|
|
3405
3514
|
lastActivityRef.current = Date.now();
|
|
3406
|
-
|
|
3515
|
+
providerFramesRef.current = 0;
|
|
3407
3516
|
liveToolRef.current = null;
|
|
3408
3517
|
charsRef.current = 0;
|
|
3409
3518
|
realTokRef.current = 0;
|
|
@@ -3685,7 +3794,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3685
3794
|
// A gateway routing to several backends sends content-free frames while it
|
|
3686
3795
|
// finds one. Saying "waiting for the first token" through that reads as a
|
|
3687
3796
|
// hang, when in fact the request was accepted and is being worked on.
|
|
3688
|
-
:
|
|
3797
|
+
: providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
|
|
3689
3798
|
: elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
|
|
3690
3799
|
: 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
|
|
3691
3800
|
? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
|