koneck 2.26.0 → 2.28.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 +27 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +70 -9
- 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 +12 -3
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +246 -30
- 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 +122 -27
- 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/files.d.ts +43 -0
- package/dist/web/files.d.ts.map +1 -0
- package/dist/web/files.js +131 -0
- package/dist/web/files.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/search.d.ts +45 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +147 -0
- package/dist/web/search.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 +329 -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 +100 -541
- package/dist/web/ui.js.map +1 -1
- package/dist/workspace-trust.d.ts +8 -0
- package/dist/workspace-trust.d.ts.map +1 -1
- package/dist/workspace-trust.js +11 -0
- package/dist/workspace-trust.js.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"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"}
|
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
|
|
@@ -614,6 +623,61 @@ export function filterPickerItems(items, query) {
|
|
|
614
623
|
}
|
|
615
624
|
return [...starts, ...contains];
|
|
616
625
|
}
|
|
626
|
+
/** The smallest number of character edits that turns one command into another. */
|
|
627
|
+
function commandDistance(a, b) {
|
|
628
|
+
// A transposed neighbouring pair is the commonest command typo (`/modle`). Treat it as one
|
|
629
|
+
// edit before using the ordinary insertion/deletion/substitution matrix below.
|
|
630
|
+
if (a.length === b.length) {
|
|
631
|
+
const mismatches = [...a].map((char, index) => char === b[index] ? -1 : index).filter(index => index >= 0);
|
|
632
|
+
if (mismatches.length === 2 && mismatches[1] === mismatches[0] + 1
|
|
633
|
+
&& a[mismatches[0]] === b[mismatches[1]]
|
|
634
|
+
&& a[mismatches[1]] === b[mismatches[0]])
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
638
|
+
for (let i = 1; i <= a.length; i++) {
|
|
639
|
+
const current = [i];
|
|
640
|
+
for (let j = 1; j <= b.length; j++) {
|
|
641
|
+
current[j] = a[i - 1] === b[j - 1]
|
|
642
|
+
? previous[j - 1]
|
|
643
|
+
: 1 + Math.min(previous[j - 1], previous[j], current[j - 1]);
|
|
644
|
+
}
|
|
645
|
+
for (let j = 0; j <= b.length; j++)
|
|
646
|
+
previous[j] = current[j];
|
|
647
|
+
}
|
|
648
|
+
return previous[b.length];
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Turns a near-miss slash command into a direct recovery path.
|
|
652
|
+
*
|
|
653
|
+
* We deliberately do not guess at distant names: a wrong suggestion steals attention, while
|
|
654
|
+
* catching one or two transposed/omitted characters makes command use feel forgiving.
|
|
655
|
+
*/
|
|
656
|
+
export function commandSuggestion(command, commands = COMMANDS.map(item => item.cmd)) {
|
|
657
|
+
const wanted = command.trim().toLowerCase();
|
|
658
|
+
if (!wanted.startsWith('/'))
|
|
659
|
+
return undefined;
|
|
660
|
+
const maxDistance = wanted.length <= 5 ? 1 : wanted.length <= 9 ? 2 : 3;
|
|
661
|
+
let best;
|
|
662
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
663
|
+
for (const candidate of commands) {
|
|
664
|
+
const d = commandDistance(wanted, candidate.toLowerCase());
|
|
665
|
+
// At the same edit distance, preserve as much of what the user typed as possible. For
|
|
666
|
+
// `/modle`, `/model` is more likely than the shorter `/mode` even though both are two edits.
|
|
667
|
+
if (d < distance || (d === distance && (!best || Math.abs(candidate.length - wanted.length) < Math.abs(best.length - wanted.length)))) {
|
|
668
|
+
best = candidate;
|
|
669
|
+
distance = d;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return distance <= maxDistance ? best : undefined;
|
|
673
|
+
}
|
|
674
|
+
/** Keep a small, useful recall buffer without repeating the same submitted task. */
|
|
675
|
+
export function appendDraftHistory(history, draft, limit = 100) {
|
|
676
|
+
const entry = draft.trim();
|
|
677
|
+
if (entry === '' || history.at(-1) === entry)
|
|
678
|
+
return [...history];
|
|
679
|
+
return [...history, entry].slice(-Math.max(1, limit));
|
|
680
|
+
}
|
|
617
681
|
/**
|
|
618
682
|
* The choices offered for one waiting tool call.
|
|
619
683
|
*
|
|
@@ -754,6 +818,7 @@ Slash commands:
|
|
|
754
818
|
/endpoint http://192.168.1.50:11434/v1
|
|
755
819
|
/key Enter an API key for the current provider
|
|
756
820
|
/status Provider, model, mode, tokens, cost, CWD
|
|
821
|
+
/doctor Diagnose the active provider, workspace, and developer tools
|
|
757
822
|
/clear /reset /new Clear conversation history
|
|
758
823
|
/model [name] Show or switch model
|
|
759
824
|
/provider [name] Show or switch provider
|
|
@@ -766,6 +831,9 @@ Slash commands:
|
|
|
766
831
|
/cost Show session cost
|
|
767
832
|
/memory Show .koneck/MEMORY.md
|
|
768
833
|
/diff Show uncommitted git changes
|
|
834
|
+
/quality Run the detected lint and test checks
|
|
835
|
+
/audit [count] Show recent redacted tool activity
|
|
836
|
+
/policy [init] Show or create workspace safety rules
|
|
769
837
|
/verify Run project test command
|
|
770
838
|
/agents How parallel sub-agents are configured
|
|
771
839
|
/subtask <task> Run a task in an isolated sub-agent
|
|
@@ -810,8 +878,14 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
810
878
|
const streamedRef = useRef('');
|
|
811
879
|
/** When the live region last had something new to show, used to pick the repaint rate. */
|
|
812
880
|
const lastActivityRef = useRef(Date.now());
|
|
813
|
-
/**
|
|
814
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Frames received before any content — proof the provider has the request and is working.
|
|
883
|
+
*
|
|
884
|
+
* Gateways can emit hundreds of these while routing. This must not be React state: doing so
|
|
885
|
+
* repainted the complete terminal for each network frame, which is the primary source of the
|
|
886
|
+
* visible flashing during a response. The already-throttled live refresh reads this ref.
|
|
887
|
+
*/
|
|
888
|
+
const providerFramesRef = useRef(0);
|
|
815
889
|
/**
|
|
816
890
|
* Reasoning characters this turn, and when the last one arrived.
|
|
817
891
|
*
|
|
@@ -1040,7 +1114,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1040
1114
|
if (liveToolRef.current)
|
|
1041
1115
|
liveToolRef.current.output = line;
|
|
1042
1116
|
},
|
|
1043
|
-
onStreamActivity: (frames) =>
|
|
1117
|
+
onStreamActivity: (frames) => { providerFramesRef.current = frames; },
|
|
1044
1118
|
onIncomplete: (reason) => { incompleteRef.current = reason; },
|
|
1045
1119
|
onToolsUnsupported: (model) => {
|
|
1046
1120
|
// Said once per session, not once per turn — it is a property of the model, not an event.
|
|
@@ -1097,6 +1171,11 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1097
1171
|
}))[0];
|
|
1098
1172
|
const [rows, setRows] = useState([{ role: 'header' }]);
|
|
1099
1173
|
const [draft, setDraft] = useState('');
|
|
1174
|
+
// Recall is deliberately session-local. It is for quick retries and refinements, not another
|
|
1175
|
+
// persistence channel for prompts (which may contain sensitive project context).
|
|
1176
|
+
const draftHistoryRef = useRef([]);
|
|
1177
|
+
const historyIndexRef = useRef(null);
|
|
1178
|
+
const historyDraftRef = useRef('');
|
|
1100
1179
|
/** Caret offset into `draft`. Kept beside it so every edit can place the caret deliberately. */
|
|
1101
1180
|
const [caret, setCaret] = useState(0);
|
|
1102
1181
|
const [busy, setBusy] = useState(false);
|
|
@@ -1496,16 +1575,23 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1496
1575
|
}
|
|
1497
1576
|
busyStart.current = Date.now();
|
|
1498
1577
|
wordTimer.current = 0;
|
|
1499
|
-
// The frame rate is the flicker rate. Every tick makes Ink
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1578
|
+
// The frame rate is the flicker rate. Every tick makes Ink update the live region, so network
|
|
1579
|
+
// activity must be sampled for a human reader rather than mirrored frame-for-frame. A third
|
|
1580
|
+
// of a second keeps streamed prose feeling live; an idle tool only needs a once-a-second
|
|
1581
|
+
// heartbeat. This is particularly important in terminals that repaint an ANSI line update as
|
|
1582
|
+
// a flash.
|
|
1504
1583
|
let timer;
|
|
1505
1584
|
const tick = () => {
|
|
1506
1585
|
const elapsed = Date.now() - busyStart.current;
|
|
1507
1586
|
const delay = currentDelay();
|
|
1508
|
-
|
|
1587
|
+
// Do not animate several cells for every content refresh. The spinner moves once a second
|
|
1588
|
+
// and the shimmer once every two seconds; the intervening refreshes are solely to reveal
|
|
1589
|
+
// new streamed text.
|
|
1590
|
+
setAnim({
|
|
1591
|
+
spin: Math.floor(elapsed / 1_000) % SPINNER.length,
|
|
1592
|
+
shimmer: Math.floor(elapsed / 2_000),
|
|
1593
|
+
elapsed,
|
|
1594
|
+
});
|
|
1509
1595
|
wordTimer.current += 1;
|
|
1510
1596
|
// Roughly every 60 seconds, whatever the current rate. Twenty-five felt churned on a long
|
|
1511
1597
|
// run — the word is there to be read and enjoyed, not to reassure by moving.
|
|
@@ -1516,13 +1602,9 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1516
1602
|
};
|
|
1517
1603
|
const currentDelay = () => {
|
|
1518
1604
|
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;
|
|
1605
|
+
return idleFor < 2_000 ? 350 : 1_000;
|
|
1524
1606
|
};
|
|
1525
|
-
timer = setTimeout(tick,
|
|
1607
|
+
timer = setTimeout(tick, 350);
|
|
1526
1608
|
return () => clearTimeout(timer);
|
|
1527
1609
|
}, [busy]);
|
|
1528
1610
|
// A queued prompt runs the moment the agent is free again. Keyed on `busy` rather than done
|
|
@@ -1549,6 +1631,37 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1549
1631
|
useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
|
|
1550
1632
|
const addRow = (row) => setRows(prev => [...prev, row]);
|
|
1551
1633
|
const addSystem = (text) => addRow({ role: 'system', text });
|
|
1634
|
+
/** Walk submitted composer entries without losing whatever was being typed now. */
|
|
1635
|
+
function recallDraft(direction) {
|
|
1636
|
+
const history = draftHistoryRef.current;
|
|
1637
|
+
if (history.length === 0)
|
|
1638
|
+
return;
|
|
1639
|
+
const current = historyIndexRef.current;
|
|
1640
|
+
if (direction === -1) {
|
|
1641
|
+
if (current === null)
|
|
1642
|
+
historyDraftRef.current = draft;
|
|
1643
|
+
const next = current === null ? history.length - 1 : Math.max(0, current - 1);
|
|
1644
|
+
historyIndexRef.current = next;
|
|
1645
|
+
const recalled = history[next];
|
|
1646
|
+
setDraft(recalled);
|
|
1647
|
+
setCaret(recalled.length);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
if (current === null)
|
|
1651
|
+
return;
|
|
1652
|
+
const next = current + 1;
|
|
1653
|
+
if (next >= history.length) {
|
|
1654
|
+
historyIndexRef.current = null;
|
|
1655
|
+
const restored = historyDraftRef.current;
|
|
1656
|
+
setDraft(restored);
|
|
1657
|
+
setCaret(restored.length);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
historyIndexRef.current = next;
|
|
1661
|
+
const recalled = history[next];
|
|
1662
|
+
setDraft(recalled);
|
|
1663
|
+
setCaret(recalled.length);
|
|
1664
|
+
}
|
|
1552
1665
|
async function getSession() {
|
|
1553
1666
|
return activeSession.p;
|
|
1554
1667
|
}
|
|
@@ -1618,7 +1731,9 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1618
1731
|
id, task, provider: cfg.provider, model: cfg.model,
|
|
1619
1732
|
cwd: cfg.cwd, startedAt: startedAtRef.current,
|
|
1620
1733
|
turns: live.turns, totalTokens: live.totalTokens,
|
|
1621
|
-
|
|
1734
|
+
// Timings persisted alongside the conversation: the messages say what was said, only this
|
|
1735
|
+
// says where the time went.
|
|
1736
|
+
}, s?.messages ?? [], s?.trajectory);
|
|
1622
1737
|
}
|
|
1623
1738
|
/**
|
|
1624
1739
|
* Compresses this session's state into something worth sending. One small model call, capped
|
|
@@ -2119,6 +2234,22 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2119
2234
|
`Cost : ${costStr}`);
|
|
2120
2235
|
return;
|
|
2121
2236
|
}
|
|
2237
|
+
case '/doctor': {
|
|
2238
|
+
// A session can change provider, endpoint, or workspace after startup. Running the same
|
|
2239
|
+
// checks here, against the *active* provider, means a diagnosis describes this session
|
|
2240
|
+
// rather than a stale global default.
|
|
2241
|
+
addSystem(`**KONECK Doctor** — checking ${cfg.provider} and this workspace…`);
|
|
2242
|
+
try {
|
|
2243
|
+
const checks = await doctorChecks(cfg.cwd, cfg.provider, cfg.baseURL, cfg.apiKey);
|
|
2244
|
+
setRows(prev => prev.slice(0, -1));
|
|
2245
|
+
addSystem(`**KONECK Doctor**\n\n${doctorReport(checks)}`);
|
|
2246
|
+
}
|
|
2247
|
+
catch (e) {
|
|
2248
|
+
setRows(prev => prev.slice(0, -1));
|
|
2249
|
+
addSystem(`Doctor could not finish: ${e instanceof Error ? e.message : String(e)}`);
|
|
2250
|
+
}
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2122
2253
|
case '/clear':
|
|
2123
2254
|
case '/reset':
|
|
2124
2255
|
case '/new': {
|
|
@@ -2381,6 +2512,75 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2381
2512
|
}
|
|
2382
2513
|
return;
|
|
2383
2514
|
}
|
|
2515
|
+
case '/quality': {
|
|
2516
|
+
// The engine uses this exact gate after a coding turn. Exposing it in chat gives the user
|
|
2517
|
+
// the same independent proof on demand, without the legacy TUI writing over Ink's frame.
|
|
2518
|
+
addSystem('**Quality gate** — running detected lint and test checks…');
|
|
2519
|
+
try {
|
|
2520
|
+
const result = await runQualityGate(cfg.cwd, { report: false });
|
|
2521
|
+
setRows(prev => prev.slice(0, -1));
|
|
2522
|
+
if (result.checks.length === 0) {
|
|
2523
|
+
addSystem('**Quality gate**\n\nNo lint or test command was detected for this workspace.');
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
const lines = result.checks.flatMap(check => {
|
|
2527
|
+
const state = check.passed ? 'PASS' : 'FAIL';
|
|
2528
|
+
const detail = check.output?.trim();
|
|
2529
|
+
const tail = !check.passed && detail
|
|
2530
|
+
? `\n\n\`\`\`\n${detail.slice(-2_000)}\n\`\`\``
|
|
2531
|
+
: '';
|
|
2532
|
+
return [`**${state}** ${check.label} — \`${check.command}\` (${fmtElapsed(check.durationMs)})${tail}`];
|
|
2533
|
+
});
|
|
2534
|
+
addSystem(`**Quality gate ${result.passed ? 'passed' : 'needs attention'}**\n\n` +
|
|
2535
|
+
lines.join('\n\n'));
|
|
2536
|
+
}
|
|
2537
|
+
catch (e) {
|
|
2538
|
+
setRows(prev => prev.slice(0, -1));
|
|
2539
|
+
addSystem(`Quality gate could not finish: ${e instanceof Error ? e.message : String(e)}`);
|
|
2540
|
+
}
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
case '/audit': {
|
|
2544
|
+
const requested = Number.parseInt(arg, 10);
|
|
2545
|
+
const limit = Number.isFinite(requested) ? Math.max(1, Math.min(requested, 100)) : 20;
|
|
2546
|
+
const events = await listAuditEvents(cfg.cwd, limit);
|
|
2547
|
+
if (events.length === 0) {
|
|
2548
|
+
addSystem('No recorded tool activity yet. KONECK records redacted actions in `.koneck/audit/`.');
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const lines = events.map(event => {
|
|
2552
|
+
const when = new Date(event.timestamp).toLocaleString();
|
|
2553
|
+
const state = event.outcome === 'success' ? 'OK' : 'FAIL';
|
|
2554
|
+
const action = Object.entries(event.action)
|
|
2555
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
2556
|
+
.join(' · ');
|
|
2557
|
+
return `${state.padEnd(4)} ${when} ${event.tool} ${action} (${fmtElapsed(event.durationMs)})`;
|
|
2558
|
+
});
|
|
2559
|
+
addSystem(`**Recent tool activity** — ${events.length} event${events.length === 1 ? '' : 's'}\n\n${lines.join('\n')}`);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
case '/policy': {
|
|
2563
|
+
if (arg.trim().toLowerCase() === 'init') {
|
|
2564
|
+
try {
|
|
2565
|
+
addSystem(await initializePolicy(cfg.cwd));
|
|
2566
|
+
}
|
|
2567
|
+
catch (e) {
|
|
2568
|
+
addSystem(`Could not create ${POLICY_FILE}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2569
|
+
}
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const policy = await loadPolicy(cfg.cwd);
|
|
2573
|
+
if (!policy) {
|
|
2574
|
+
addSystem(`No workspace policy is active. Run \`/policy init\` to create \`${POLICY_FILE}\`.`);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
addSystem(`**KONECK policy** — \`${POLICY_FILE}\`\n\n` +
|
|
2578
|
+
`Protected paths : ${policy.protectedPaths?.length ?? 0}\n` +
|
|
2579
|
+
`Write scopes : ${policy.allowedWritePaths?.length ? policy.allowedWritePaths.join(', ') : 'unrestricted'}\n` +
|
|
2580
|
+
`Blocked commands: ${policy.blockedCommands?.length ?? 0}\n` +
|
|
2581
|
+
`Approval rules : ${policy.requireApprovalFor?.join(', ') || 'none'}`);
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2384
2584
|
case '/verify': {
|
|
2385
2585
|
const { detectProject } = await import('./project.js');
|
|
2386
2586
|
const project = await detectProject(cfg.cwd);
|
|
@@ -2803,7 +3003,12 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2803
3003
|
return;
|
|
2804
3004
|
}
|
|
2805
3005
|
default:
|
|
2806
|
-
|
|
3006
|
+
{
|
|
3007
|
+
const suggestion = commandSuggestion(cmd);
|
|
3008
|
+
addSystem(suggestion
|
|
3009
|
+
? `Unknown command: ${cmd}. Did you mean \`${suggestion}\`?`
|
|
3010
|
+
: `Unknown command: ${cmd}. Type /help for the full list.`);
|
|
3011
|
+
}
|
|
2807
3012
|
}
|
|
2808
3013
|
}
|
|
2809
3014
|
/**
|
|
@@ -3249,6 +3454,14 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3249
3454
|
// The composer used to be append-and-backspace only, so fixing a typo near the start of a
|
|
3250
3455
|
// long prompt meant deleting everything after it. Everything below is the readline set a
|
|
3251
3456
|
// shell gives you, so an edit costs one keystroke rather than a retype.
|
|
3457
|
+
if (key.upArrow) {
|
|
3458
|
+
recallDraft(-1);
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
if (key.downArrow) {
|
|
3462
|
+
recallDraft(1);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3252
3465
|
if (key.leftArrow) {
|
|
3253
3466
|
const step = 1 + takeBurst('left');
|
|
3254
3467
|
setCaret(c => (key.ctrl || key.meta ? wordStart(draft, c) : Math.max(0, c - step)));
|
|
@@ -3403,7 +3616,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3403
3616
|
thinkingTextRef.current = '';
|
|
3404
3617
|
incompleteRef.current = null;
|
|
3405
3618
|
lastActivityRef.current = Date.now();
|
|
3406
|
-
|
|
3619
|
+
providerFramesRef.current = 0;
|
|
3407
3620
|
liveToolRef.current = null;
|
|
3408
3621
|
charsRef.current = 0;
|
|
3409
3622
|
realTokRef.current = 0;
|
|
@@ -3685,7 +3898,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3685
3898
|
// A gateway routing to several backends sends content-free frames while it
|
|
3686
3899
|
// finds one. Saying "waiting for the first token" through that reads as a
|
|
3687
3900
|
// hang, when in fact the request was accepted and is being worked on.
|
|
3688
|
-
:
|
|
3901
|
+
: providerFramesRef.current > 0 ? `${cfg.provider} is holding the line, no output yet`
|
|
3689
3902
|
: elapsedMs > 8_000 ? `no response from ${cfg.provider} yet`
|
|
3690
3903
|
: 'starting', thinkingRef.current.chars > 0 && Date.now() - thinkingRef.current.at < 2_000
|
|
3691
3904
|
? ` | thinking, ${fmtTokens(Math.round(thinkingRef.current.chars / 4))} reasoning tokens`
|
|
@@ -3905,12 +4118,15 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3905
4118
|
// right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
|
|
3906
4119
|
const countText = pickerCountText(pickIndex, list.length, picker.items.length);
|
|
3907
4120
|
const headWidth = title.length + countText.length + 3;
|
|
3908
|
-
|
|
4121
|
+
// Measure the filtered list, not just its current scroll window. A palette whose right
|
|
4122
|
+
// edge breathes as the highlight crosses a long description feels broken even when the
|
|
4123
|
+
// selection itself is correct.
|
|
4124
|
+
const rowWidth = Math.max(headWidth, ...list.map(it => 38 + (it.current ? 10 : 0) + it.desc.length + (it.group?.length ?? 0) + 3));
|
|
3909
4125
|
const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
|
|
3910
4126
|
const width = boxWidth - 4;
|
|
3911
4127
|
// Room for the longest label the list actually holds, rather than a constant. A session
|
|
3912
4128
|
// title is what identifies the row, and 36 cells cut most of them in half.
|
|
3913
|
-
const labelWidth = pickerLabelWidth(
|
|
4129
|
+
const labelWidth = pickerLabelWidth(list.map(it => it.label), list.map(it => it.desc), width);
|
|
3914
4130
|
let lastGroup;
|
|
3915
4131
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: boxWidth, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: CYAN, bold: true, children: title }), _jsx(Text, { color: DIM, children: countText })] }), _jsxs(Box, { children: [_jsxs(Text, { color: DIM, children: [G.search, " "] }), pickQuery === ''
|
|
3916
4132
|
? _jsx(Text, { color: DIM, children: "type to search" })
|
|
@@ -3920,8 +4136,8 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3920
4136
|
const header = item.group && item.group !== lastGroup ? item.group : null;
|
|
3921
4137
|
lastGroup = item.group;
|
|
3922
4138
|
const label = item.label.length > 34 ? item.label.slice(0, 31) + '...' : item.label;
|
|
3923
|
-
return (
|
|
3924
|
-
|
|
4139
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${fitCells(label, labelWidth)}` +
|
|
4140
|
+
`${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
|
|
3925
4141
|
}), list.length > pickerRows && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 pgup/pgdn jump \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
|
|
3926
4142
|
})(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter. Held in memory for this session only; esc to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] })), ask && (() => {
|
|
3927
4143
|
let detail = '';
|