phewsh 0.15.5 → 0.15.7

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 +113 -13
  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,75 @@ 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 chars = text.length.toLocaleString('en-US');
778
+ const tag = lineCount > 1
779
+ ? `[paste #${pasteCounter}: ${chars} chars, ${lineCount} lines]`
780
+ : `[paste #${pasteCounter}: ${chars} chars]`;
781
+ pendingPastes.set(tag, text);
782
+ lastPaste = text;
783
+ rl.write(tag);
784
+ } else {
785
+ rl.write(text);
786
+ }
787
+ return;
788
+ }
789
+ pasteChunks.push(str !== undefined && str !== null ? String(str) : (key && key.sequence) || '');
790
+ return;
791
+ }
792
+
721
793
  if (key?.ctrl && key.name === 'o' && lastPaste) {
722
794
  setImmediate(() => {
723
795
  rl.line = '';
@@ -730,6 +802,17 @@ async function main() {
730
802
  });
731
803
  return;
732
804
  }
805
+ // shift+tab cycles the session mode — open → build → research → decide → review
806
+ if (key && key.name === 'tab' && key.shift) {
807
+ const ids = [null, ...Object.values(INTENT_MODES).map(m => m.id)];
808
+ const next = ids[(ids.indexOf(sessionMode) + 1) % ids.length];
809
+ sessionMode = next;
810
+ const label = next ? Object.values(INTENT_MODES).find(m => m.id === next).label : 'Open';
811
+ process.stdout.write('\x1b[2K\r');
812
+ console.log(` ${teal('⏵')} ${sage('mode:')} ${cream(label.toLowerCase())}${next ? slate(' — ' + 'shapes how routes respond') : slate(' — no slant')}`);
813
+ rl.prompt();
814
+ return;
815
+ }
733
816
  // ESC: cancel an in-flight turn, or clear the input line.
734
817
  if (key && key.name === 'escape') {
735
818
  if (turnInFlight) {
@@ -744,17 +827,30 @@ async function main() {
744
827
  }
745
828
  return;
746
829
  }
747
- // Re-render so token coloring tracks edits (and un-colors when it
748
- // stops matching a known command).
749
- const cur = rl.line || '';
750
- if (cur[0] === '/' || cur[0] === '@') rl._refreshLine();
830
+ // Re-render so token coloring tracks edits including the keystroke
831
+ // where the token stops matching and must un-color. Deferred one tick:
832
+ // this is a prependListener, so readline hasn't appended the just-typed
833
+ // char yet; without the defer, rl.line is stale by one and /model only
834
+ // ever evaluates as /mode.
835
+ setImmediate(() => {
836
+ try {
837
+ const cur = rl.line || '';
838
+ const special = cur[0] === '/' || cur[0] === '@';
839
+ if (special || wasSpecialInput) rl._refreshLine();
840
+ wasSpecialInput = special;
841
+ } catch { /* never break input */ }
842
+ });
751
843
  } catch { /* never break input */ }
752
- });
844
+ };
845
+ process.stdin.prependListener('keypress', phewshKeypress);
846
+ pasteMode(true);
847
+ process.on('exit', () => pasteMode(false)); // never leave the terminal in paste mode
753
848
  }
754
849
 
755
850
  rl.prompt();
756
851
 
757
852
  async function handleInput(input) {
853
+ input = expandPastes(input);
758
854
 
759
855
  // A bare number right after a route failure picks the fallback
760
856
  if (awaitingFallback) {
@@ -1452,10 +1548,12 @@ async function main() {
1452
1548
  console.log('');
1453
1549
  console.log(` ${teal('●')} ${sage('Handing you to the guided update')} ${slate('— exit to come back to phewsh')}`);
1454
1550
  console.log('');
1551
+ pasteMode(false);
1455
1552
  rl.pause();
1456
1553
  const { spawnSync } = require('child_process');
1457
1554
  spawnSync(process.execPath, [path.join(__dirname, '..', 'bin', 'phewsh.js'), 'clarify'], { stdio: 'inherit' });
1458
1555
  rl.resume();
1556
+ pasteMode(true);
1459
1557
  console.log('');
1460
1558
  console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('/intent view to see the result — agents pick it up automatically')}`);
1461
1559
  console.log('');
@@ -1764,10 +1862,12 @@ async function main() {
1764
1862
  console.log(` ${slate('your .intent/ context rides along via CLAUDE.md')}`);
1765
1863
  }
1766
1864
  console.log('');
1865
+ pasteMode(false);
1767
1866
  rl.pause();
1768
1867
  const { spawnSync } = require('child_process');
1769
1868
  const res = spawnSync(h.bin, [], { stdio: 'inherit' });
1770
1869
  rl.resume();
1870
+ pasteMode(true);
1771
1871
  recordSessionEvent(target, projectName, 'task_complete', {
1772
1872
  taskId: decisionId, success: res.status === 0, summary: `interactive ${h.label} session`,
1773
1873
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.5",
3
+ "version": "0.15.7",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"