phewsh 0.15.4 → 0.15.6
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/commands/session.js +104 -12
- package/lib/harnesses.js +12 -1
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -511,6 +511,7 @@ async function main() {
|
|
|
511
511
|
turnInFlight = true;
|
|
512
512
|
const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input), { model: harnessModel });
|
|
513
513
|
turnInFlight = false;
|
|
514
|
+
userCancelled = false; // a SIGTERM the harness rode out must not mislabel the next turn
|
|
514
515
|
messages.push({ role: 'user', content: input });
|
|
515
516
|
messages.push({ role: 'assistant', content: (output || '').trim() });
|
|
516
517
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
@@ -647,16 +648,22 @@ async function main() {
|
|
|
647
648
|
return estimateTokens(`${systemPrompt}\n${conversation}`);
|
|
648
649
|
}
|
|
649
650
|
|
|
651
|
+
// One quiet line, Claude Code-bar style: route + model · context gauge ·
|
|
652
|
+
// mode. Hints live in /help — the rail is glanceable state, not a manual.
|
|
650
653
|
function renderStatusRail() {
|
|
651
654
|
if (!process.stdout.isTTY) return;
|
|
652
655
|
const folder = relativeFolder(process.cwd(), os.homedir());
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const tokens =
|
|
658
|
-
|
|
659
|
-
|
|
656
|
+
const routeName = route?.type === 'harness' ? HARNESSES[route.id].label : routeLabel(route, config);
|
|
657
|
+
const model = route?.type === 'harness'
|
|
658
|
+
? (harnessModel || 'default model')
|
|
659
|
+
: modelName(currentModel);
|
|
660
|
+
const tokens = currentContextTokens();
|
|
661
|
+
const pct = Math.min(99, Math.round((tokens / 200000) * 100));
|
|
662
|
+
const bar = '█'.repeat(Math.max(1, Math.round(pct / 10))) + '░'.repeat(10 - Math.max(1, Math.round(pct / 10)));
|
|
663
|
+
const modeLabel = sessionMode
|
|
664
|
+
? Object.values(INTENT_MODES).find(m => m.id === sessionMode)?.label.toLowerCase()
|
|
665
|
+
: 'open';
|
|
666
|
+
console.log(` ${slate(folder)} ${slate('│')} ${cream(routeName)} ${slate(model)} ${slate('│')} ${slate(bar)} ${slate(pct + '%')} ${slate('│')} ${sage('⏵ ' + modeLabel)} ${slate('(shift+tab)')}`);
|
|
660
667
|
}
|
|
661
668
|
|
|
662
669
|
const readlinePrompt = rl.prompt.bind(rl);
|
|
@@ -714,9 +721,72 @@ async function main() {
|
|
|
714
721
|
};
|
|
715
722
|
}
|
|
716
723
|
|
|
724
|
+
// ── Bracketed paste: like Claude Code, a paste lands in the input line as
|
|
725
|
+
// a collapsed placeholder and NEVER auto-submits — Enter sends it. The
|
|
726
|
+
// terminal marks paste boundaries (\x1b[200~ … \x1b[201~); Node's keypress
|
|
727
|
+
// decoder surfaces them as paste-start/paste-end. While pasting we detach
|
|
728
|
+
// readline so embedded newlines can't fire 'line' events.
|
|
729
|
+
const PASTE_ON = '\x1b[?2004h';
|
|
730
|
+
const PASTE_OFF = '\x1b[?2004l';
|
|
731
|
+
let pasting = false;
|
|
732
|
+
let pasteChunks = [];
|
|
733
|
+
let detachedListeners = null;
|
|
734
|
+
let pasteCounter = 0;
|
|
735
|
+
const pendingPastes = new Map();
|
|
736
|
+
|
|
737
|
+
function pasteMode(on) {
|
|
738
|
+
if (process.stdout.isTTY) process.stdout.write(on ? PASTE_ON : PASTE_OFF);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Substitute placeholders back to the real pasted text at submit time.
|
|
742
|
+
function expandPastes(input) {
|
|
743
|
+
let out = input;
|
|
744
|
+
for (const [tag, text] of pendingPastes) {
|
|
745
|
+
if (out.includes(tag)) {
|
|
746
|
+
out = out.split(tag).join(text);
|
|
747
|
+
pendingPastes.delete(tag);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let wasSpecialInput = false; // recolor must also fire when the token STOPS matching
|
|
754
|
+
|
|
717
755
|
if (process.stdin.isTTY) {
|
|
718
|
-
|
|
756
|
+
const phewshKeypress = (str, key) => {
|
|
719
757
|
try {
|
|
758
|
+
// Paste interception comes first — everything inside the paste is data.
|
|
759
|
+
if (key && key.name === 'paste-start') {
|
|
760
|
+
pasting = true;
|
|
761
|
+
pasteChunks = [];
|
|
762
|
+
detachedListeners = process.stdin.listeners('keypress').filter(l => l !== phewshKeypress);
|
|
763
|
+
for (const l of detachedListeners) process.stdin.removeListener('keypress', l);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (pasting) {
|
|
767
|
+
if (key && key.name === 'paste-end') {
|
|
768
|
+
pasting = false;
|
|
769
|
+
for (const l of detachedListeners || []) process.stdin.on('keypress', l);
|
|
770
|
+
detachedListeners = null;
|
|
771
|
+
const text = pasteChunks.join('');
|
|
772
|
+
pasteChunks = [];
|
|
773
|
+
if (!text) return;
|
|
774
|
+
const lineCount = text.split('\n').length;
|
|
775
|
+
if (lineCount > 1 || text.length > 200) {
|
|
776
|
+
pasteCounter++;
|
|
777
|
+
const tag = `[paste #${pasteCounter}: ${text.length.toLocaleString('en-US')} chars, ${lineCount} lines]`;
|
|
778
|
+
pendingPastes.set(tag, text);
|
|
779
|
+
lastPaste = text;
|
|
780
|
+
rl.write(tag);
|
|
781
|
+
} else {
|
|
782
|
+
rl.write(text);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
pasteChunks.push(str !== undefined && str !== null ? String(str) : (key && key.sequence) || '');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
720
790
|
if (key?.ctrl && key.name === 'o' && lastPaste) {
|
|
721
791
|
setImmediate(() => {
|
|
722
792
|
rl.line = '';
|
|
@@ -729,10 +799,22 @@ async function main() {
|
|
|
729
799
|
});
|
|
730
800
|
return;
|
|
731
801
|
}
|
|
802
|
+
// shift+tab cycles the session mode — open → build → research → decide → review
|
|
803
|
+
if (key && key.name === 'tab' && key.shift) {
|
|
804
|
+
const ids = [null, ...Object.values(INTENT_MODES).map(m => m.id)];
|
|
805
|
+
const next = ids[(ids.indexOf(sessionMode) + 1) % ids.length];
|
|
806
|
+
sessionMode = next;
|
|
807
|
+
const label = next ? Object.values(INTENT_MODES).find(m => m.id === next).label : 'Open';
|
|
808
|
+
process.stdout.write('\x1b[2K\r');
|
|
809
|
+
console.log(` ${teal('⏵')} ${sage('mode:')} ${cream(label.toLowerCase())}${next ? slate(' — ' + 'shapes how routes respond') : slate(' — no slant')}`);
|
|
810
|
+
rl.prompt();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
732
813
|
// ESC: cancel an in-flight turn, or clear the input line.
|
|
733
814
|
if (key && key.name === 'escape') {
|
|
734
815
|
if (turnInFlight) {
|
|
735
816
|
userCancelled = true;
|
|
817
|
+
process.stdout.write('\n \x1b[38;5;247mcancelling…\x1b[0m\n');
|
|
736
818
|
if (turnAbort) turnAbort.abort();
|
|
737
819
|
cancelActive();
|
|
738
820
|
} else if (rl.line) {
|
|
@@ -742,17 +824,23 @@ async function main() {
|
|
|
742
824
|
}
|
|
743
825
|
return;
|
|
744
826
|
}
|
|
745
|
-
// Re-render so token coloring tracks edits
|
|
746
|
-
// stops matching
|
|
827
|
+
// Re-render so token coloring tracks edits — including the keystroke
|
|
828
|
+
// where the token stops matching and must un-color.
|
|
747
829
|
const cur = rl.line || '';
|
|
748
|
-
|
|
830
|
+
const special = cur[0] === '/' || cur[0] === '@';
|
|
831
|
+
if (special || wasSpecialInput) rl._refreshLine();
|
|
832
|
+
wasSpecialInput = special;
|
|
749
833
|
} catch { /* never break input */ }
|
|
750
|
-
}
|
|
834
|
+
};
|
|
835
|
+
process.stdin.prependListener('keypress', phewshKeypress);
|
|
836
|
+
pasteMode(true);
|
|
837
|
+
process.on('exit', () => pasteMode(false)); // never leave the terminal in paste mode
|
|
751
838
|
}
|
|
752
839
|
|
|
753
840
|
rl.prompt();
|
|
754
841
|
|
|
755
842
|
async function handleInput(input) {
|
|
843
|
+
input = expandPastes(input);
|
|
756
844
|
|
|
757
845
|
// A bare number right after a route failure picks the fallback
|
|
758
846
|
if (awaitingFallback) {
|
|
@@ -1450,10 +1538,12 @@ async function main() {
|
|
|
1450
1538
|
console.log('');
|
|
1451
1539
|
console.log(` ${teal('●')} ${sage('Handing you to the guided update')} ${slate('— exit to come back to phewsh')}`);
|
|
1452
1540
|
console.log('');
|
|
1541
|
+
pasteMode(false);
|
|
1453
1542
|
rl.pause();
|
|
1454
1543
|
const { spawnSync } = require('child_process');
|
|
1455
1544
|
spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'clarify'], { stdio: 'inherit' });
|
|
1456
1545
|
rl.resume();
|
|
1546
|
+
pasteMode(true);
|
|
1457
1547
|
console.log('');
|
|
1458
1548
|
console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('/intent view to see the result — agents pick it up automatically')}`);
|
|
1459
1549
|
console.log('');
|
|
@@ -1762,10 +1852,12 @@ async function main() {
|
|
|
1762
1852
|
console.log(` ${slate('your .intent/ context rides along via CLAUDE.md')}`);
|
|
1763
1853
|
}
|
|
1764
1854
|
console.log('');
|
|
1855
|
+
pasteMode(false);
|
|
1765
1856
|
rl.pause();
|
|
1766
1857
|
const { spawnSync } = require('child_process');
|
|
1767
1858
|
const res = spawnSync(h.bin, [], { stdio: 'inherit' });
|
|
1768
1859
|
rl.resume();
|
|
1860
|
+
pasteMode(true);
|
|
1769
1861
|
recordSessionEvent(target, projectName, 'task_complete', {
|
|
1770
1862
|
taskId: decisionId, success: res.status === 0, summary: `interactive ${h.label} session`,
|
|
1771
1863
|
});
|
package/lib/harnesses.js
CHANGED
|
@@ -42,7 +42,17 @@ const ACTIVE_CHILDREN = new Set();
|
|
|
42
42
|
function cancelActive() {
|
|
43
43
|
let n = 0;
|
|
44
44
|
for (const c of ACTIVE_CHILDREN) {
|
|
45
|
-
try {
|
|
45
|
+
try {
|
|
46
|
+
c._phewshCancelled = true; // close handler rejects even if exit is 0
|
|
47
|
+
c.kill('SIGTERM');
|
|
48
|
+
// Some harnesses (codex) ride out SIGTERM and finish anyway —
|
|
49
|
+
// escalate so esc means esc.
|
|
50
|
+
const t = setTimeout(() => {
|
|
51
|
+
try { if (c.exitCode === null) c.kill('SIGKILL'); } catch { /* gone */ }
|
|
52
|
+
}, 1200);
|
|
53
|
+
if (t.unref) t.unref();
|
|
54
|
+
n++;
|
|
55
|
+
} catch { /* already gone */ }
|
|
46
56
|
}
|
|
47
57
|
return n;
|
|
48
58
|
}
|
|
@@ -93,6 +103,7 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
93
103
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
94
104
|
child.on('close', (code) => {
|
|
95
105
|
if (!opts.quiet) process.stdout.write('\n');
|
|
106
|
+
if (child._phewshCancelled) return reject(new Error(`${h.label} cancelled`));
|
|
96
107
|
if (code === 0) resolve(stdout);
|
|
97
108
|
else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
|
|
98
109
|
});
|