phewsh 0.15.5 → 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.
Files changed (2) hide show
  1. package/commands/session.js +102 -12
  2. package/package.json +1 -1
@@ -648,16 +648,22 @@ async function main() {
648
648
  return estimateTokens(`${systemPrompt}\n${conversation}`);
649
649
  }
650
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.
651
653
  function renderStatusRail() {
652
654
  if (!process.stdout.isTTY) return;
653
655
  const folder = relativeFolder(process.cwd(), os.homedir());
654
- const contextLabel = intentFiles.length > 0
655
- ? `${intentFiles.length} intent file${intentFiles.length === 1 ? '' : 's'}`
656
- : 'no project context';
657
- const routeName = routeLabel(route, config);
658
- const tokens = formatTokenCount(currentContextTokens());
659
- console.log(` ${slate(folder)} ${slate('·')} ${cream(routeName)} ${slate('·')} ${sage(contextLabel)} ${slate('·')} ${sage(`~${tokens} ctx tokens`)}`);
660
- console.log(` ${slate('/help commands · /use route · /context memory · Ctrl+O last paste · Esc cancel')}`);
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)')}`);
661
667
  }
662
668
 
663
669
  const readlinePrompt = rl.prompt.bind(rl);
@@ -715,9 +721,72 @@ async function main() {
715
721
  };
716
722
  }
717
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
+
718
755
  if (process.stdin.isTTY) {
719
- process.stdin.prependListener('keypress', (str, key) => {
756
+ const phewshKeypress = (str, key) => {
720
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
+
721
790
  if (key?.ctrl && key.name === 'o' && lastPaste) {
722
791
  setImmediate(() => {
723
792
  rl.line = '';
@@ -730,6 +799,17 @@ async function main() {
730
799
  });
731
800
  return;
732
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
+ }
733
813
  // ESC: cancel an in-flight turn, or clear the input line.
734
814
  if (key && key.name === 'escape') {
735
815
  if (turnInFlight) {
@@ -744,17 +824,23 @@ async function main() {
744
824
  }
745
825
  return;
746
826
  }
747
- // Re-render so token coloring tracks edits (and un-colors when it
748
- // stops matching a known command).
827
+ // Re-render so token coloring tracks edits including the keystroke
828
+ // where the token stops matching and must un-color.
749
829
  const cur = rl.line || '';
750
- if (cur[0] === '/' || cur[0] === '@') rl._refreshLine();
830
+ const special = cur[0] === '/' || cur[0] === '@';
831
+ if (special || wasSpecialInput) rl._refreshLine();
832
+ wasSpecialInput = special;
751
833
  } catch { /* never break input */ }
752
- });
834
+ };
835
+ process.stdin.prependListener('keypress', phewshKeypress);
836
+ pasteMode(true);
837
+ process.on('exit', () => pasteMode(false)); // never leave the terminal in paste mode
753
838
  }
754
839
 
755
840
  rl.prompt();
756
841
 
757
842
  async function handleInput(input) {
843
+ input = expandPastes(input);
758
844
 
759
845
  // A bare number right after a route failure picks the fallback
760
846
  if (awaitingFallback) {
@@ -1452,10 +1538,12 @@ async function main() {
1452
1538
  console.log('');
1453
1539
  console.log(` ${teal('●')} ${sage('Handing you to the guided update')} ${slate('— exit to come back to phewsh')}`);
1454
1540
  console.log('');
1541
+ pasteMode(false);
1455
1542
  rl.pause();
1456
1543
  const { spawnSync } = require('child_process');
1457
1544
  spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'clarify'], { stdio: 'inherit' });
1458
1545
  rl.resume();
1546
+ pasteMode(true);
1459
1547
  console.log('');
1460
1548
  console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('/intent view to see the result — agents pick it up automatically')}`);
1461
1549
  console.log('');
@@ -1764,10 +1852,12 @@ async function main() {
1764
1852
  console.log(` ${slate('your .intent/ context rides along via CLAUDE.md')}`);
1765
1853
  }
1766
1854
  console.log('');
1855
+ pasteMode(false);
1767
1856
  rl.pause();
1768
1857
  const { spawnSync } = require('child_process');
1769
1858
  const res = spawnSync(h.bin, [], { stdio: 'inherit' });
1770
1859
  rl.resume();
1860
+ pasteMode(true);
1771
1861
  recordSessionEvent(target, projectName, 'task_complete', {
1772
1862
  taskId: decisionId, success: res.status === 0, summary: `interactive ${h.label} session`,
1773
1863
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"