koneck 2.27.0 → 2.29.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 +13 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +43 -2
- package/dist/doctor.js.map +1 -1
- package/dist/ink-chat.d.ts +11 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +280 -24
- package/dist/ink-chat.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +99 -22
- package/dist/web/api.js.map +1 -1
- package/dist/web/events.d.ts +15 -0
- package/dist/web/events.d.ts.map +1 -1
- package/dist/web/events.js.map +1 -1
- 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/search.d.ts +57 -0
- package/dist/web/search.d.ts.map +1 -0
- package/dist/web/search.js +185 -0
- package/dist/web/search.js.map +1 -0
- package/dist/web/sessions.d.ts +61 -2
- package/dist/web/sessions.d.ts.map +1 -1
- package/dist/web/sessions.js +94 -4
- package/dist/web/sessions.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +0 -0
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +67 -0
- package/dist/web/ui-css.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +41 -0
- package/dist/web/ui.js.map +1 -1
- package/dist/workspace-trust.d.ts +10 -0
- package/dist/workspace-trust.d.ts.map +1 -1
- package/dist/workspace-trust.js +23 -0
- package/dist/workspace-trust.js.map +1 -1
- package/package.json +1 -1
package/dist/ink-chat.js
CHANGED
|
@@ -45,6 +45,7 @@ import { doctorChecks, doctorReport } from './doctor.js';
|
|
|
45
45
|
import { runQualityGate } from './quality-gate.js';
|
|
46
46
|
import { listAuditEvents } from './audit.js';
|
|
47
47
|
import { initializePolicy, loadPolicy, POLICY_FILE } from './policy.js';
|
|
48
|
+
import { isWorkspaceTrusted, trustWorkspace, untrustWorkspace } from './workspace-trust.js';
|
|
48
49
|
const CYAN = '#54D7E7';
|
|
49
50
|
const GREEN = '#9BCB8F';
|
|
50
51
|
const CRIMSON = '#E8878D';
|
|
@@ -130,6 +131,10 @@ function fitCells(text, width, ellipsis = false) {
|
|
|
130
131
|
const head = sliceToWidth(text, Math.max(0, width - 3));
|
|
131
132
|
return head.text + '.'.repeat(Math.min(3, width)) + ' '.repeat(Math.max(0, width - head.width - 3));
|
|
132
133
|
}
|
|
134
|
+
/** A picker label uses all of its allocated column, only ellipsising at the actual boundary. */
|
|
135
|
+
export function pickerLabel(text, width) {
|
|
136
|
+
return fitCells(text, width, true);
|
|
137
|
+
}
|
|
133
138
|
/**
|
|
134
139
|
* Wraps text while keeping the shape it was written in.
|
|
135
140
|
*
|
|
@@ -519,6 +524,9 @@ const COMMANDS = [
|
|
|
519
524
|
{ cmd: '/subtask', desc: 'Run a task in an isolated sub-agent' },
|
|
520
525
|
{ cmd: '/aside', desc: 'Ask a side question without interrupting the running task' },
|
|
521
526
|
{ cmd: '/clear', desc: 'Clear conversation history' },
|
|
527
|
+
{ cmd: '/cancel', desc: 'Stop the current task and keep your composer text' },
|
|
528
|
+
{ cmd: '/retry', desc: 'Run the last non-secret task again' },
|
|
529
|
+
{ cmd: '/queue', desc: 'Inspect or clear prompts waiting behind the current task' },
|
|
522
530
|
{ cmd: '/new', desc: 'New session (alias of /clear)' },
|
|
523
531
|
{ cmd: '/reset', desc: 'Reset session (alias of /clear)' },
|
|
524
532
|
{ cmd: '/mode', desc: 'Set mode: auto, plan or code' },
|
|
@@ -537,9 +545,11 @@ const COMMANDS = [
|
|
|
537
545
|
{ cmd: '/fork', desc: 'Copy this session and continue on the copy' },
|
|
538
546
|
{ cmd: '/archive', desc: 'Keep a session but hide it from /resume' },
|
|
539
547
|
{ cmd: '/diff', desc: 'Show uncommitted git changes' },
|
|
548
|
+
{ cmd: '/changes', desc: 'Summarize staged, unstaged, and untracked changes' },
|
|
540
549
|
{ cmd: '/quality', desc: 'Run the detected lint and test checks' },
|
|
541
550
|
{ cmd: '/audit', desc: 'Show recent redacted tool activity' },
|
|
542
551
|
{ cmd: '/policy', desc: 'Show or initialize workspace safety rules' },
|
|
552
|
+
{ cmd: '/trust', desc: 'Show, persist, or revoke this workspace trust grant' },
|
|
543
553
|
{ cmd: '/verify', desc: 'Run the project test command' },
|
|
544
554
|
{ cmd: '/rewind', desc: 'Drop the last exchange from history' },
|
|
545
555
|
{ cmd: '/copy', desc: 'Copy the last response to the clipboard' },
|
|
@@ -623,6 +633,61 @@ export function filterPickerItems(items, query) {
|
|
|
623
633
|
}
|
|
624
634
|
return [...starts, ...contains];
|
|
625
635
|
}
|
|
636
|
+
/** The smallest number of character edits that turns one command into another. */
|
|
637
|
+
function commandDistance(a, b) {
|
|
638
|
+
// A transposed neighbouring pair is the commonest command typo (`/modle`). Treat it as one
|
|
639
|
+
// edit before using the ordinary insertion/deletion/substitution matrix below.
|
|
640
|
+
if (a.length === b.length) {
|
|
641
|
+
const mismatches = [...a].map((char, index) => char === b[index] ? -1 : index).filter(index => index >= 0);
|
|
642
|
+
if (mismatches.length === 2 && mismatches[1] === mismatches[0] + 1
|
|
643
|
+
&& a[mismatches[0]] === b[mismatches[1]]
|
|
644
|
+
&& a[mismatches[1]] === b[mismatches[0]])
|
|
645
|
+
return 1;
|
|
646
|
+
}
|
|
647
|
+
const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
648
|
+
for (let i = 1; i <= a.length; i++) {
|
|
649
|
+
const current = [i];
|
|
650
|
+
for (let j = 1; j <= b.length; j++) {
|
|
651
|
+
current[j] = a[i - 1] === b[j - 1]
|
|
652
|
+
? previous[j - 1]
|
|
653
|
+
: 1 + Math.min(previous[j - 1], previous[j], current[j - 1]);
|
|
654
|
+
}
|
|
655
|
+
for (let j = 0; j <= b.length; j++)
|
|
656
|
+
previous[j] = current[j];
|
|
657
|
+
}
|
|
658
|
+
return previous[b.length];
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Turns a near-miss slash command into a direct recovery path.
|
|
662
|
+
*
|
|
663
|
+
* We deliberately do not guess at distant names: a wrong suggestion steals attention, while
|
|
664
|
+
* catching one or two transposed/omitted characters makes command use feel forgiving.
|
|
665
|
+
*/
|
|
666
|
+
export function commandSuggestion(command, commands = COMMANDS.map(item => item.cmd)) {
|
|
667
|
+
const wanted = command.trim().toLowerCase();
|
|
668
|
+
if (!wanted.startsWith('/'))
|
|
669
|
+
return undefined;
|
|
670
|
+
const maxDistance = wanted.length <= 5 ? 1 : wanted.length <= 9 ? 2 : 3;
|
|
671
|
+
let best;
|
|
672
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
673
|
+
for (const candidate of commands) {
|
|
674
|
+
const d = commandDistance(wanted, candidate.toLowerCase());
|
|
675
|
+
// At the same edit distance, preserve as much of what the user typed as possible. For
|
|
676
|
+
// `/modle`, `/model` is more likely than the shorter `/mode` even though both are two edits.
|
|
677
|
+
if (d < distance || (d === distance && (!best || Math.abs(candidate.length - wanted.length) < Math.abs(best.length - wanted.length)))) {
|
|
678
|
+
best = candidate;
|
|
679
|
+
distance = d;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return distance <= maxDistance ? best : undefined;
|
|
683
|
+
}
|
|
684
|
+
/** Keep a small, useful recall buffer without repeating the same submitted task. */
|
|
685
|
+
export function appendDraftHistory(history, draft, limit = 100) {
|
|
686
|
+
const entry = draft.trim();
|
|
687
|
+
if (entry === '' || history.at(-1) === entry)
|
|
688
|
+
return [...history];
|
|
689
|
+
return [...history, entry].slice(-Math.max(1, limit));
|
|
690
|
+
}
|
|
626
691
|
/**
|
|
627
692
|
* The choices offered for one waiting tool call.
|
|
628
693
|
*
|
|
@@ -716,8 +781,10 @@ export function pickerCountText(selected, shown, total) {
|
|
|
716
781
|
* its row, and the constant 36 that was here cut most of them in half.
|
|
717
782
|
*/
|
|
718
783
|
export function pickerLabelWidth(labels, descs, width) {
|
|
719
|
-
|
|
720
|
-
|
|
784
|
+
// Terminal cells, not JS code units: one wide glyph in a model name or session title used to
|
|
785
|
+
// make a supposedly exact row spill past the palette edge.
|
|
786
|
+
const longestDesc = Math.max(0, ...descs.map(displayWidth));
|
|
787
|
+
const longestLabel = Math.max(18, ...labels.map(displayWidth));
|
|
721
788
|
return Math.max(18, Math.min(longestLabel, width - longestDesc - 4));
|
|
722
789
|
}
|
|
723
790
|
/**
|
|
@@ -732,7 +799,7 @@ export function pickerLabelWidth(labels, descs, width) {
|
|
|
732
799
|
* a new one should work without being registered twice. The dangerous ones are few and named.
|
|
733
800
|
*/
|
|
734
801
|
const COMMANDS_THAT_MUST_WAIT = new Set([
|
|
735
|
-
'/clear', '/reset', '/new', // would wipe the history
|
|
802
|
+
'/clear', '/reset', '/new', '/retry', // would wipe the history or begin a new turn
|
|
736
803
|
'/model', '/provider', '/connect', '/key', '/endpoint', '/baseurl', // rebuild the session
|
|
737
804
|
'/resume', '/fork', // replace it
|
|
738
805
|
'/revert', // undo files the turn may still be writing
|
|
@@ -765,6 +832,9 @@ Slash commands:
|
|
|
765
832
|
/status Provider, model, mode, tokens, cost, CWD
|
|
766
833
|
/doctor Diagnose the active provider, workspace, and developer tools
|
|
767
834
|
/clear /reset /new Clear conversation history
|
|
835
|
+
/cancel Stop the current task; keeps the composer text
|
|
836
|
+
/retry Run the last non-secret task again
|
|
837
|
+
/queue [clear|drop n] Inspect, clear, or drop one queued prompt
|
|
768
838
|
/model [name] Show or switch model
|
|
769
839
|
/provider [name] Show or switch provider
|
|
770
840
|
/mode [auto|plan|code] Show or set mode
|
|
@@ -776,9 +846,11 @@ Slash commands:
|
|
|
776
846
|
/cost Show session cost
|
|
777
847
|
/memory Show .koneck/MEMORY.md
|
|
778
848
|
/diff Show uncommitted git changes
|
|
849
|
+
/changes Summarize staged, unstaged, and untracked changes
|
|
779
850
|
/quality Run the detected lint and test checks
|
|
780
851
|
/audit [count] Show recent redacted tool activity
|
|
781
852
|
/policy [init] Show or create workspace safety rules
|
|
853
|
+
/trust [always|forget] Show, persist, or revoke this workspace trust grant
|
|
782
854
|
/verify Run project test command
|
|
783
855
|
/agents How parallel sub-agents are configured
|
|
784
856
|
/subtask <task> Run a task in an isolated sub-agent
|
|
@@ -791,6 +863,7 @@ Slash commands:
|
|
|
791
863
|
Keyboard:
|
|
792
864
|
Tab Toggle analytics panel
|
|
793
865
|
Shift+Tab Cycle mode (auto → plan → code)
|
|
866
|
+
Up/Down Recall prior submitted prompts in the composer
|
|
794
867
|
Ctrl+C Cancel task / exit if idle
|
|
795
868
|
`.trim();
|
|
796
869
|
/**
|
|
@@ -1116,6 +1189,13 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1116
1189
|
}))[0];
|
|
1117
1190
|
const [rows, setRows] = useState([{ role: 'header' }]);
|
|
1118
1191
|
const [draft, setDraft] = useState('');
|
|
1192
|
+
// Recall is deliberately session-local. It is for quick retries and refinements, not another
|
|
1193
|
+
// persistence channel for prompts (which may contain sensitive project context).
|
|
1194
|
+
const draftHistoryRef = useRef([]);
|
|
1195
|
+
const historyIndexRef = useRef(null);
|
|
1196
|
+
const historyDraftRef = useRef('');
|
|
1197
|
+
/** Last agent task eligible for one-command recovery; never populated from secret-looking input. */
|
|
1198
|
+
const retryTaskRef = useRef(null);
|
|
1119
1199
|
/** Caret offset into `draft`. Kept beside it so every edit can place the caret deliberately. */
|
|
1120
1200
|
const [caret, setCaret] = useState(0);
|
|
1121
1201
|
const [busy, setBusy] = useState(false);
|
|
@@ -1571,6 +1651,52 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
1571
1651
|
useEffect(() => { void snapshotTree(cfg.cwd).then(t => { baseTreeRef.current = t; }); }, [cfg.cwd]);
|
|
1572
1652
|
const addRow = (row) => setRows(prev => [...prev, row]);
|
|
1573
1653
|
const addSystem = (text) => addRow({ role: 'system', text });
|
|
1654
|
+
/** Stops the in-flight turn without touching anything the developer is composing or queued. */
|
|
1655
|
+
function cancelCurrentTurn() {
|
|
1656
|
+
if (!busy || !abortRef.current)
|
|
1657
|
+
return false;
|
|
1658
|
+
abortRef.current.abort();
|
|
1659
|
+
// Anything still waiting on a permission answer is refused rather than left hanging: the
|
|
1660
|
+
// agents are being abandoned, and an unsettled promise keeps the pool alive after its work
|
|
1661
|
+
// has been cancelled.
|
|
1662
|
+
const stranded = askQueueRef.current.drain(false);
|
|
1663
|
+
if (stranded > 0) {
|
|
1664
|
+
setAsk(null);
|
|
1665
|
+
addSystem(`Cancelled with ${stranded} permission ${stranded === 1 ? 'request' : 'requests'} unanswered; refused.`);
|
|
1666
|
+
}
|
|
1667
|
+
return true;
|
|
1668
|
+
}
|
|
1669
|
+
/** Walk submitted composer entries without losing whatever was being typed now. */
|
|
1670
|
+
function recallDraft(direction) {
|
|
1671
|
+
const history = draftHistoryRef.current;
|
|
1672
|
+
if (history.length === 0)
|
|
1673
|
+
return;
|
|
1674
|
+
const current = historyIndexRef.current;
|
|
1675
|
+
if (direction === -1) {
|
|
1676
|
+
if (current === null)
|
|
1677
|
+
historyDraftRef.current = draft;
|
|
1678
|
+
const next = current === null ? history.length - 1 : Math.max(0, current - 1);
|
|
1679
|
+
historyIndexRef.current = next;
|
|
1680
|
+
const recalled = history[next];
|
|
1681
|
+
setDraft(recalled);
|
|
1682
|
+
setCaret(recalled.length);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (current === null)
|
|
1686
|
+
return;
|
|
1687
|
+
const next = current + 1;
|
|
1688
|
+
if (next >= history.length) {
|
|
1689
|
+
historyIndexRef.current = null;
|
|
1690
|
+
const restored = historyDraftRef.current;
|
|
1691
|
+
setDraft(restored);
|
|
1692
|
+
setCaret(restored.length);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
historyIndexRef.current = next;
|
|
1696
|
+
const recalled = history[next];
|
|
1697
|
+
setDraft(recalled);
|
|
1698
|
+
setCaret(recalled.length);
|
|
1699
|
+
}
|
|
1574
1700
|
async function getSession() {
|
|
1575
1701
|
return activeSession.p;
|
|
1576
1702
|
}
|
|
@@ -2149,7 +2275,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2149
2275
|
// rather than a stale global default.
|
|
2150
2276
|
addSystem(`**KONECK Doctor** — checking ${cfg.provider} and this workspace…`);
|
|
2151
2277
|
try {
|
|
2152
|
-
const checks = await doctorChecks(cfg.cwd, cfg.provider);
|
|
2278
|
+
const checks = await doctorChecks(cfg.cwd, cfg.provider, cfg.baseURL, cfg.apiKey);
|
|
2153
2279
|
setRows(prev => prev.slice(0, -1));
|
|
2154
2280
|
addSystem(`**KONECK Doctor**\n\n${doctorReport(checks)}`);
|
|
2155
2281
|
}
|
|
@@ -2175,6 +2301,54 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2175
2301
|
addSystem('Conversation cleared. The previous session is kept; /resume lists it.');
|
|
2176
2302
|
return;
|
|
2177
2303
|
}
|
|
2304
|
+
case '/cancel': {
|
|
2305
|
+
if (!cancelCurrentTurn())
|
|
2306
|
+
addSystem('No task is running.');
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
case '/retry': {
|
|
2310
|
+
const retry = retryTaskRef.current;
|
|
2311
|
+
if (!retry) {
|
|
2312
|
+
addSystem('Nothing safe to retry yet. Submit a non-secret task first.');
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
addSystem('Retrying the previous task…');
|
|
2316
|
+
// `/retry` itself is handled as a local command. Let its `finally` release the command
|
|
2317
|
+
// busy flag before starting the next agent turn, so the two cannot race each other.
|
|
2318
|
+
setTimeout(() => submitDraft(retry), 0);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
case '/queue': {
|
|
2322
|
+
const requested = arg.trim();
|
|
2323
|
+
const drop = /^drop\s+(\d+)$/i.exec(requested);
|
|
2324
|
+
if (drop) {
|
|
2325
|
+
const index = Number(drop[1]) - 1;
|
|
2326
|
+
if (!Number.isInteger(index) || index < 0 || index >= queued.length) {
|
|
2327
|
+
addSystem(`There is no queued prompt #${drop[1]}. Run \`/queue\` to inspect the queue.`);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const [removed] = queued.slice(index, index + 1);
|
|
2331
|
+
setQueued(items => items.filter((_, itemIndex) => itemIndex !== index));
|
|
2332
|
+
addSystem(`Dropped queued prompt #${index + 1}: ${removed.replace(/\s+/g, ' ').slice(0, 180)}`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
if (/^(clear|drop)$/i.test(requested)) {
|
|
2336
|
+
const count = queued.length;
|
|
2337
|
+
setQueued([]);
|
|
2338
|
+
addSystem(count === 0
|
|
2339
|
+
? 'There are no queued prompts to clear.'
|
|
2340
|
+
: `Cleared ${count} queued ${count === 1 ? 'prompt' : 'prompts'}. The current task keeps running.`);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
if (queued.length === 0) {
|
|
2344
|
+
addSystem('No prompts are queued. Type while a task runs and press enter to queue it.');
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
addSystem(`**Queued prompts** — ${queued.length}\n\n` +
|
|
2348
|
+
queued.map((prompt, index) => `${index + 1}. ${prompt.replace(/\s+/g, ' ').slice(0, 240)}`).join('\n') +
|
|
2349
|
+
'\n\n`/queue drop 2` removes one prompt. `/queue clear` drops all of them; the current task keeps running.');
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2178
2352
|
case '/model': {
|
|
2179
2353
|
if (!arg) {
|
|
2180
2354
|
const now = modelFor(cfg.provider);
|
|
@@ -2412,9 +2586,50 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2412
2586
|
}
|
|
2413
2587
|
case '/diff': {
|
|
2414
2588
|
try {
|
|
2415
|
-
const
|
|
2416
|
-
|
|
2417
|
-
|
|
2589
|
+
const [unstaged, staged] = await Promise.all([
|
|
2590
|
+
execa('git', ['diff', '--no-ext-diff'], { cwd: cfg.cwd, all: true, reject: false }),
|
|
2591
|
+
execa('git', ['diff', '--cached', '--no-ext-diff'], { cwd: cfg.cwd, all: true, reject: false }),
|
|
2592
|
+
]);
|
|
2593
|
+
if (unstaged.exitCode !== 0 || staged.exitCode !== 0) {
|
|
2594
|
+
addSystem('Not a git repository.');
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
const sections = [
|
|
2598
|
+
(staged.all ?? '').trim() ? `**Staged**\n\n${(staged.all ?? '').trim()}` : '',
|
|
2599
|
+
(unstaged.all ?? '').trim() ? `**Unstaged**\n\n${(unstaged.all ?? '').trim()}` : '',
|
|
2600
|
+
].filter(Boolean);
|
|
2601
|
+
addSystem(sections.length ? sections.join('\n\n') : 'No staged or unstaged changes. `/changes` also shows untracked files.');
|
|
2602
|
+
}
|
|
2603
|
+
catch {
|
|
2604
|
+
addSystem('Not a git repository.');
|
|
2605
|
+
}
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
case '/changes': {
|
|
2609
|
+
try {
|
|
2610
|
+
const [status, unstaged, staged] = await Promise.all([
|
|
2611
|
+
execa('git', ['status', '--short'], { cwd: cfg.cwd, all: true, reject: false }),
|
|
2612
|
+
execa('git', ['diff', '--stat', '--no-ext-diff'], { cwd: cfg.cwd, all: true, reject: false }),
|
|
2613
|
+
execa('git', ['diff', '--cached', '--stat', '--no-ext-diff'], { cwd: cfg.cwd, all: true, reject: false }),
|
|
2614
|
+
]);
|
|
2615
|
+
const entries = (status.all ?? '').trim();
|
|
2616
|
+
if (status.exitCode !== 0 || unstaged.exitCode !== 0 || staged.exitCode !== 0) {
|
|
2617
|
+
addSystem('Not a git repository.');
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!entries) {
|
|
2621
|
+
addSystem('Working tree is clean.');
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const lines = ['**Working tree changes**', '', '```', entries, '```'];
|
|
2625
|
+
const stagedStat = (staged.all ?? '').trim();
|
|
2626
|
+
const unstagedStat = (unstaged.all ?? '').trim();
|
|
2627
|
+
if (stagedStat)
|
|
2628
|
+
lines.push('', '**Staged**', '```', stagedStat, '```');
|
|
2629
|
+
if (unstagedStat)
|
|
2630
|
+
lines.push('', '**Unstaged**', '```', unstagedStat, '```');
|
|
2631
|
+
lines.push('', 'Use `/diff` to inspect patches, `/quality` to verify, or `/revert` to undo KONECK checkpoints.');
|
|
2632
|
+
addSystem(lines.join('\n'));
|
|
2418
2633
|
}
|
|
2419
2634
|
catch {
|
|
2420
2635
|
addSystem('Not a git repository.');
|
|
@@ -2490,6 +2705,28 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2490
2705
|
`Approval rules : ${policy.requireApprovalFor?.join(', ') || 'none'}`);
|
|
2491
2706
|
return;
|
|
2492
2707
|
}
|
|
2708
|
+
case '/trust': {
|
|
2709
|
+
const choice = arg.trim().toLowerCase();
|
|
2710
|
+
if (choice === 'always') {
|
|
2711
|
+
await trustWorkspace(cfg.cwd);
|
|
2712
|
+
addSystem(`This workspace is now trusted for future KONECK sessions:\n\`${cfg.cwd}\``);
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (choice === 'forget' || choice === 'revoke' || choice === 'remove') {
|
|
2716
|
+
const removed = await untrustWorkspace(cfg.cwd);
|
|
2717
|
+
addSystem(removed
|
|
2718
|
+
? `Removed the persistent trust grant for \`${cfg.cwd}\`. This open session remains active; KONECK will ask again next time.`
|
|
2719
|
+
: 'This workspace did not have a persistent trust grant.');
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const trusted = await isWorkspaceTrusted(cfg.cwd);
|
|
2723
|
+
addSystem(`**Workspace trust**\n\n` +
|
|
2724
|
+
`Path : \`${cfg.cwd}\`\n` +
|
|
2725
|
+
`Persistent : ${trusted ? 'yes' : 'no — this session may be a one-time grant'}\n\n` +
|
|
2726
|
+
'`/trust always` saves this grant for future sessions.\n' +
|
|
2727
|
+
'`/trust forget` removes a saved grant; this open session stays active.');
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2493
2730
|
case '/verify': {
|
|
2494
2731
|
const { detectProject } = await import('./project.js');
|
|
2495
2732
|
const project = await detectProject(cfg.cwd);
|
|
@@ -2912,7 +3149,12 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2912
3149
|
return;
|
|
2913
3150
|
}
|
|
2914
3151
|
default:
|
|
2915
|
-
|
|
3152
|
+
{
|
|
3153
|
+
const suggestion = commandSuggestion(cmd);
|
|
3154
|
+
addSystem(suggestion
|
|
3155
|
+
? `Unknown command: ${cmd}. Did you mean \`${suggestion}\`?`
|
|
3156
|
+
: `Unknown command: ${cmd}. Type /help for the full list.`);
|
|
3157
|
+
}
|
|
2916
3158
|
}
|
|
2917
3159
|
}
|
|
2918
3160
|
/**
|
|
@@ -2974,15 +3216,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
2974
3216
|
// Esc abandons the turn in flight and hands the keyboard straight back. The composer is
|
|
2975
3217
|
// deliberately left alone: cancelling should not cost whatever was typed while waiting.
|
|
2976
3218
|
if (key.escape && busy && !picker && !keyPrompt && !updatePane && !usagePane) {
|
|
2977
|
-
|
|
2978
|
-
// Anything still waiting on a permission answer is refused rather than left hanging: the
|
|
2979
|
-
// agents are being abandoned, and an unsettled promise keeps the pool alive after the work
|
|
2980
|
-
// it was doing has been given up on.
|
|
2981
|
-
const stranded = askQueueRef.current.drain(false);
|
|
2982
|
-
if (stranded > 0) {
|
|
2983
|
-
setAsk(null);
|
|
2984
|
-
addSystem(`Cancelled with ${stranded} permission ${stranded === 1 ? 'request' : 'requests'} unanswered; refused.`);
|
|
2985
|
-
}
|
|
3219
|
+
cancelCurrentTurn();
|
|
2986
3220
|
return;
|
|
2987
3221
|
}
|
|
2988
3222
|
if (key.ctrl && input === 'c') {
|
|
@@ -3358,6 +3592,14 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3358
3592
|
// The composer used to be append-and-backspace only, so fixing a typo near the start of a
|
|
3359
3593
|
// long prompt meant deleting everything after it. Everything below is the readline set a
|
|
3360
3594
|
// shell gives you, so an edit costs one keystroke rather than a retype.
|
|
3595
|
+
if (key.upArrow) {
|
|
3596
|
+
recallDraft(-1);
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
if (key.downArrow) {
|
|
3600
|
+
recallDraft(1);
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3361
3603
|
if (key.leftArrow) {
|
|
3362
3604
|
const step = 1 + takeBurst('left');
|
|
3363
3605
|
setCaret(c => (key.ctrl || key.meta ? wordStart(draft, c) : Math.max(0, c - step)));
|
|
@@ -3434,6 +3676,8 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3434
3676
|
return;
|
|
3435
3677
|
const next = draft.slice(0, caret) + text + draft.slice(caret);
|
|
3436
3678
|
const nextCaret = caret + text.length;
|
|
3679
|
+
// Once the recalled line is edited it is a new draft, not a history cursor position.
|
|
3680
|
+
historyIndexRef.current = null;
|
|
3437
3681
|
setDraft(next);
|
|
3438
3682
|
setCaret(nextCaret);
|
|
3439
3683
|
// A lone "@" at a word boundary offers workspace paths. Checking the draft as it will be,
|
|
@@ -3477,6 +3721,13 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3477
3721
|
const task = expandPastes(shown, pastesRef.current).trim();
|
|
3478
3722
|
if (!task)
|
|
3479
3723
|
return;
|
|
3724
|
+
// A recalled secret is just as dangerous as a saved one. Secret-looking input is sent only
|
|
3725
|
+
// when the user explicitly submits it, and never retained for Up-arrow recall.
|
|
3726
|
+
if (!containsSecret(task)) {
|
|
3727
|
+
draftHistoryRef.current = appendDraftHistory(draftHistoryRef.current, shown);
|
|
3728
|
+
}
|
|
3729
|
+
historyIndexRef.current = null;
|
|
3730
|
+
historyDraftRef.current = '';
|
|
3480
3731
|
setDraft('');
|
|
3481
3732
|
setCaret(0);
|
|
3482
3733
|
pastesRef.current = [];
|
|
@@ -3501,6 +3752,8 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3501
3752
|
void handleCommand(cmd, arg).finally(() => setBusy(false));
|
|
3502
3753
|
return;
|
|
3503
3754
|
}
|
|
3755
|
+
if (!containsSecret(task))
|
|
3756
|
+
retryTaskRef.current = task;
|
|
3504
3757
|
setBusy(true);
|
|
3505
3758
|
setAgentState('processing');
|
|
3506
3759
|
const started = Date.now();
|
|
@@ -3724,7 +3977,7 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
3724
3977
|
});
|
|
3725
3978
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 40), color: CYAN, title: `Plan ${done}/${steps.length}`, children: lines }) }, index));
|
|
3726
3979
|
})() : row.role === 'error' ? (() => {
|
|
3727
|
-
const lines = [row.text ?? '', '', 'Fix the above, then retry.
|
|
3980
|
+
const lines = [row.text ?? '', '', 'Fix the above, then `/retry` to rerun the task. `/status` shows the active provider and model.'];
|
|
3728
3981
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Panel, { width: panelWidth(lines, barWidth, 40), color: CRIMSON, title: "[!] ERROR", children: lines }) }, index));
|
|
3729
3982
|
})() : row.role === 'system' ? (() => {
|
|
3730
3983
|
const lines = (row.text ?? '').split('\n');
|
|
@@ -4013,13 +4266,16 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
4013
4266
|
// The header is a title on the left and a count on the right, on one line. Estimating the
|
|
4014
4267
|
// right-hand side at a constant made "Reference a file or directory" wrap onto two rows.
|
|
4015
4268
|
const countText = pickerCountText(pickIndex, list.length, picker.items.length);
|
|
4016
|
-
const headWidth = title
|
|
4017
|
-
|
|
4269
|
+
const headWidth = displayWidth(title) + displayWidth(countText) + 3;
|
|
4270
|
+
// Measure the filtered list, not just its current scroll window. A palette whose right
|
|
4271
|
+
// edge breathes as the highlight crosses a long description feels broken even when the
|
|
4272
|
+
// selection itself is correct.
|
|
4273
|
+
const rowWidth = Math.max(headWidth, ...list.map(it => 38 + (it.current ? 10 : 0) + displayWidth(it.desc) + displayWidth(it.group ?? '') + 3));
|
|
4018
4274
|
const boxWidth = Math.max(40, Math.min(barWidth, rowWidth + 4));
|
|
4019
4275
|
const width = boxWidth - 4;
|
|
4020
4276
|
// Room for the longest label the list actually holds, rather than a constant. A session
|
|
4021
4277
|
// title is what identifies the row, and 36 cells cut most of them in half.
|
|
4022
|
-
const labelWidth = pickerLabelWidth(
|
|
4278
|
+
const labelWidth = pickerLabelWidth(list.map(it => it.label), list.map(it => it.desc), width);
|
|
4023
4279
|
let lastGroup;
|
|
4024
4280
|
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 === ''
|
|
4025
4281
|
? _jsx(Text, { color: DIM, children: "type to search" })
|
|
@@ -4028,9 +4284,9 @@ function App({ config: initialConfig, clearFrame }) {
|
|
|
4028
4284
|
const selected = absolute === pickIndex;
|
|
4029
4285
|
const header = item.group && item.group !== lastGroup ? item.group : null;
|
|
4030
4286
|
lastGroup = item.group;
|
|
4031
|
-
const label = item.label
|
|
4032
|
-
return (
|
|
4033
|
-
|
|
4287
|
+
const label = pickerLabel(item.label, labelWidth);
|
|
4288
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${label}` +
|
|
4289
|
+
`${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
|
|
4034
4290
|
}), 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] }))] }));
|
|
4035
4291
|
})(), 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 && (() => {
|
|
4036
4292
|
let detail = '';
|