phewsh 0.15.3 → 0.15.4

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/README.md CHANGED
@@ -84,6 +84,17 @@ Inside the shell:
84
84
  /help All commands
85
85
  ```
86
86
 
87
+ The prompt includes a compact status rail with the current folder, route,
88
+ loaded `.intent/` files, and approximate context size. Multi-line pastes and
89
+ single-line pastes of 300+ characters collapse after submission:
90
+
91
+ ```text
92
+ [pasted 1,284 chars · 12 lines · Ctrl+O to expand]
93
+ ```
94
+
95
+ Press `Ctrl+O` to inspect the last submitted paste and `Esc` to clear input or
96
+ cancel an in-flight provider turn.
97
+
87
98
  ## What it does
88
99
 
89
100
  Creates three structured artifacts in `.intent/`:
@@ -22,6 +22,14 @@ const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES }
22
22
  const { recordSessionEvent } = require('../lib/receipts-data');
23
23
  const configFile = require('../lib/config-file');
24
24
  const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
25
+ const {
26
+ echoedRows,
27
+ estimateTokens,
28
+ formatPasteSummary,
29
+ formatTokenCount,
30
+ relativeFolder,
31
+ shouldCollapsePaste,
32
+ } = require('../lib/session-display');
25
33
  const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib/projects-index');
26
34
 
27
35
  // Brand palette shortcuts
@@ -631,6 +639,39 @@ async function main() {
631
639
  prompt: ` ${teal('phewsh')} ${sage('>')} `,
632
640
  historySize: 100,
633
641
  });
642
+ const promptText = ` phewsh > `;
643
+ let lastPaste = null;
644
+
645
+ function currentContextTokens() {
646
+ const conversation = messages.map(message => message.content).join('\n');
647
+ return estimateTokens(`${systemPrompt}\n${conversation}`);
648
+ }
649
+
650
+ function renderStatusRail() {
651
+ if (!process.stdout.isTTY) return;
652
+ const folder = relativeFolder(process.cwd(), os.homedir());
653
+ const contextLabel = intentFiles.length > 0
654
+ ? `${intentFiles.length} intent file${intentFiles.length === 1 ? '' : 's'}`
655
+ : 'no project context';
656
+ const routeName = routeLabel(route, config);
657
+ const tokens = formatTokenCount(currentContextTokens());
658
+ console.log(` ${slate(folder)} ${slate('·')} ${cream(routeName)} ${slate('·')} ${sage(contextLabel)} ${slate('·')} ${sage(`~${tokens} ctx tokens`)}`);
659
+ console.log(` ${slate('/help commands · /use route · /context memory · Ctrl+O last paste · Esc cancel')}`);
660
+ }
661
+
662
+ const readlinePrompt = rl.prompt.bind(rl);
663
+ rl.prompt = function promptWithStatusRail(preserveCursor) {
664
+ renderStatusRail();
665
+ return readlinePrompt(preserveCursor);
666
+ };
667
+
668
+ function collapsePastedEcho(lines, input) {
669
+ if (!process.stdout.isTTY || !shouldCollapsePaste(lines, input)) return;
670
+ const rows = echoedRows(lines, promptText, process.stdout.columns || 80);
671
+ for (let i = 0; i < rows; i++) process.stdout.write('\x1b[1A\x1b[2K\r');
672
+ lastPaste = input;
673
+ console.log(` ${peach(formatPasteSummary(input, lines.length))}`);
674
+ }
634
675
 
635
676
  // Live input coloring — like Claude Code: text stays normal, and only a
636
677
  // RECOGNIZED leading /command (or @harness) token turns teal (peach for @)
@@ -674,8 +715,20 @@ async function main() {
674
715
  }
675
716
 
676
717
  if (process.stdin.isTTY) {
677
- process.stdin.on('keypress', (str, key) => {
718
+ process.stdin.prependListener('keypress', (str, key) => {
678
719
  try {
720
+ if (key?.ctrl && key.name === 'o' && lastPaste) {
721
+ setImmediate(() => {
722
+ rl.line = '';
723
+ rl.cursor = 0;
724
+ process.stdout.write('\x1b[2K\r');
725
+ console.log(` ${b(cream('Last paste'))} ${slate(`(${lastPaste.length.toLocaleString('en-US')} chars)`)}`);
726
+ console.log(lastPaste.split('\n').map(line => ` ${line}`).join('\n'));
727
+ console.log('');
728
+ rl.prompt();
729
+ });
730
+ return;
731
+ }
679
732
  // ESC: cancel an in-flight turn, or clear the input line.
680
733
  if (key && key.name === 'escape') {
681
734
  if (turnInFlight) {
@@ -1827,6 +1880,7 @@ async function main() {
1827
1880
  }
1828
1881
 
1829
1882
  const lineDispatcher = createLineDispatcher(handleInput, {
1883
+ onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
1830
1884
  onNoop: () => rl.prompt(),
1831
1885
  onError: (err) => {
1832
1886
  console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
@@ -0,0 +1,54 @@
1
+ const path = require('path');
2
+
3
+ const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
4
+
5
+ function visibleLength(value) {
6
+ return String(value || '').replace(ANSI_RE, '').length;
7
+ }
8
+
9
+ function estimateTokens(value) {
10
+ return Math.max(0, Math.ceil(String(value || '').length / 4));
11
+ }
12
+
13
+ function formatTokenCount(tokens) {
14
+ if (tokens < 1000) return String(tokens);
15
+ return `${(tokens / 1000).toFixed(tokens < 10000 ? 1 : 0)}k`;
16
+ }
17
+
18
+ function shouldCollapsePaste(lines, input, threshold = 300) {
19
+ return lines.length > 1 || input.length >= threshold;
20
+ }
21
+
22
+ function formatPasteSummary(input, lineCount) {
23
+ const chars = input.length.toLocaleString('en-US');
24
+ const lines = lineCount > 1 ? ` · ${lineCount} lines` : '';
25
+ return `[pasted ${chars} chars${lines} · Ctrl+O to expand]`;
26
+ }
27
+
28
+ function echoedRows(lines, prompt, columns = 80) {
29
+ const width = Math.max(20, columns || 80);
30
+ return lines.reduce((total, line, index) => {
31
+ const prefix = index === 0 ? visibleLength(prompt) : 0;
32
+ return total + Math.max(1, Math.ceil((prefix + visibleLength(line)) / width));
33
+ }, 0);
34
+ }
35
+
36
+ function relativeFolder(cwd, home) {
37
+ if (!cwd) return '';
38
+ const relative = home ? path.relative(home, cwd) : cwd;
39
+ if (home && relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
40
+ return `~/${relative}`;
41
+ }
42
+ if (home && relative === '') return '~';
43
+ return cwd;
44
+ }
45
+
46
+ module.exports = {
47
+ echoedRows,
48
+ estimateTokens,
49
+ formatPasteSummary,
50
+ formatTokenCount,
51
+ relativeFolder,
52
+ shouldCollapsePaste,
53
+ visibleLength,
54
+ };
@@ -1,6 +1,7 @@
1
1
  function createLineDispatcher(handleInput, {
2
2
  onError = (err) => { throw err; },
3
3
  onNoop = () => {},
4
+ onBatch = () => {},
4
5
  schedule = setImmediate,
5
6
  } = {}) {
6
7
  let pendingLines = [];
@@ -16,6 +17,7 @@ function createLineDispatcher(handleInput, {
16
17
  onNoop();
17
18
  return;
18
19
  }
20
+ onBatch({ input, lines });
19
21
  chain = chain.then(() => handleInput(input)).catch(onError);
20
22
  }
21
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"
@@ -35,6 +35,9 @@
35
35
  "engines": {
36
36
  "node": ">=18.0.0"
37
37
  },
38
+ "scripts": {
39
+ "test": "node --test test/*.test.js"
40
+ },
38
41
  "dependencies": {
39
42
  "@modelcontextprotocol/sdk": "^1.0.0"
40
43
  }