phewsh 0.15.57 → 0.15.59

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/bin/phewsh.js CHANGED
@@ -18,7 +18,18 @@ if (command === 'shim-preflight') {
18
18
  // instead of the terminal's mid-word hard wrap. TTY-only, idempotent, and it
19
19
  // leaves process.stdout.write paths (spinner, streamed AI) alone. Installed
20
20
  // after the shim-preflight fast path so that stays instant and untouched.
21
- try { require('../lib/ui').installSmartWrap(); } catch { /* never block the CLI on cosmetics */ }
21
+ try {
22
+ const ui = require('../lib/ui');
23
+ // Restore a width the user pinned with `/width` in a past session, so the fix
24
+ // for a misreporting terminal sticks without re-typing it every launch.
25
+ try {
26
+ const os = require('os');
27
+ const path = require('path');
28
+ const cfg = require('../lib/config-file').loadConfig(path.join(os.homedir(), '.phewsh', 'config.json'));
29
+ if (cfg && cfg.displayWidth) ui.setWidth(cfg.displayWidth);
30
+ } catch { /* no saved width is fine */ }
31
+ ui.installSmartWrap();
32
+ } catch { /* never block the CLI on cosmetics */ }
22
33
 
23
34
  // ── ANSI helpers (no chalk dependency)
24
35
  const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
@@ -938,7 +938,7 @@ async function main() {
938
938
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
939
939
  'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
940
940
  'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
941
- 'pack', 'packs', 'remember', 'commands',
941
+ 'pack', 'packs', 'remember', 'commands', 'width',
942
942
  ]);
943
943
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
944
944
  let turnAbort = null; // AbortController while an API turn streams
@@ -1669,6 +1669,7 @@ async function main() {
1669
1669
  console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
1670
1670
  console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
1671
1671
  console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model — passed through, the provider validates')}`);
1672
+ console.log(` ${teal('/width')} ${slate('[n]')} ${sage('Fix mid-word wrapping if your terminal misreports its size')}`);
1672
1673
  console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
1673
1674
  console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
1674
1675
  console.log('');
@@ -1714,6 +1715,48 @@ async function main() {
1714
1715
  return;
1715
1716
  }
1716
1717
 
1718
+ // /width — fix mid-word breaks when the terminal misreports its size.
1719
+ // `/width` shows what phewsh thinks; `/width 80` pins it (saved, sticks
1720
+ // across sessions); `/width auto` clears the pin back to detection.
1721
+ if (cmd === 'width') {
1722
+ const arg = (cmdArg || '').trim().toLowerCase();
1723
+ if (!arg) {
1724
+ console.log('');
1725
+ console.log(` ${b(cream('Display width'))} ${slate('— used to wrap text at word boundaries')}`);
1726
+ console.log(` ${sage('phewsh is wrapping at')} ${cream(String(ui.rawWidth()))} ${sage('columns')} ${slate(config?.displayWidth ? '(pinned)' : '(auto-detected)')}`);
1727
+ console.log(` ${slate('If words still break mid-line, your terminal reports a wider size than it shows.')}`);
1728
+ console.log(` ${sage('Pin your real width:')} ${cream('/width 80')} ${slate('·')} ${sage('back to auto:')} ${cream('/width auto')}`);
1729
+ console.log('');
1730
+ rl.prompt();
1731
+ return;
1732
+ }
1733
+ if (arg === 'auto' || arg === 'off' || arg === 'reset') {
1734
+ ui.setWidth(null);
1735
+ config = config || {};
1736
+ delete config.displayWidth;
1737
+ try { configFile.saveConfig(CONFIG_PATH, config); } catch { /* best effort */ }
1738
+ console.log(` ${teal('●')} ${sage('Width back to auto-detect')} ${slate('(' + ui.rawWidth() + ' columns)')}`);
1739
+ console.log('');
1740
+ rl.prompt();
1741
+ return;
1742
+ }
1743
+ const set = ui.setWidth(arg);
1744
+ if (!set) {
1745
+ console.log(` ${ember('!')} ${sage('Give a number ≥ 20, e.g.')} ${cream('/width 80')} ${slate('· or')} ${cream('/width auto')}`);
1746
+ console.log('');
1747
+ rl.prompt();
1748
+ return;
1749
+ }
1750
+ config = config || {};
1751
+ config.displayWidth = set;
1752
+ try { configFile.saveConfig(CONFIG_PATH, config); } catch { /* best effort */ }
1753
+ console.log(` ${teal('●')} ${sage('Wrapping at')} ${cream(String(set))} ${sage('columns now')} ${slate('— saved, sticks across sessions')}`);
1754
+ console.log(` ${slate('This sentence is a quick check: if it wraps cleanly at a word and never cuts a word in half, you are set.')}`);
1755
+ console.log('');
1756
+ rl.prompt();
1757
+ return;
1758
+ }
1759
+
1717
1760
  if (cmd === 'context') {
1718
1761
  if (intentFiles.length > 0) {
1719
1762
  console.log('');
package/lib/md.js CHANGED
@@ -44,7 +44,7 @@ const ANSI = /\x1b\[[0-9;]*m/g;
44
44
  const vis = (s) => s.replace(ANSI, '').length;
45
45
 
46
46
  function softWrap(s, base = '', hang) {
47
- const width = process.stdout.columns;
47
+ const width = require('./ui').termWidth();
48
48
  if (!width || width < 24 || vis(s) <= width) return s;
49
49
  const lead = s.match(/^ */)[0].length;
50
50
  const indent = hang == null ? lead : hang;
package/lib/ui.js CHANGED
@@ -121,6 +121,36 @@ async function brandReveal(fast = false) {
121
121
  const SGR = /\x1b\[[0-9;]*m/g;
122
122
  const visW = (s) => s.replace(SGR, '').length;
123
123
 
124
+ // Best available terminal width, with a safety margin so content never sits
125
+ // flush against the edge (where terminals add their own mid-word wrap). The
126
+ // width is pinnable, the escape hatch for host terminals that misreport their
127
+ // size — some embedded/agent terminals (e.g. Grok Build) advertise a wider pty
128
+ // than the visible pane, so text breaks mid-word even with smart wrap on.
129
+ // Resolution order: live override (set by `/width`) → PHEWSH_WIDTH env →
130
+ // the tty's reported columns → COLUMNS env → 80. Two columns are held back as
131
+ // margin so an off-by-one in the host's report can't cause a mid-word break.
132
+ let _widthOverride = null;
133
+ const WIDTH_MARGIN = 2;
134
+
135
+ function setWidth(n) {
136
+ const v = parseInt(n, 10);
137
+ _widthOverride = (Number.isFinite(v) && v >= 20) ? v : null;
138
+ return _widthOverride;
139
+ }
140
+
141
+ // The raw width phewsh believes the terminal is, before margin — what `/width`
142
+ // reports back so the user can compare it to their visible pane.
143
+ function rawWidth() {
144
+ if (_widthOverride) return _widthOverride;
145
+ const pin = parseInt(process.env.PHEWSH_WIDTH, 10);
146
+ if (Number.isFinite(pin) && pin >= 20) return pin;
147
+ return process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 80;
148
+ }
149
+
150
+ function termWidth() {
151
+ return Math.max(24, rawWidth() - WIDTH_MARGIN);
152
+ }
153
+
124
154
  // Active SGR state after consuming `s`, starting from `prev`. A reset clears it;
125
155
  // any other colour code accumulates. Good enough to re-open colour at a break.
126
156
  function sgrAfter(prev, s) {
@@ -131,7 +161,7 @@ function sgrAfter(prev, s) {
131
161
  return active;
132
162
  }
133
163
 
134
- function wrapAnsi(s, width = process.stdout.columns) {
164
+ function wrapAnsi(s, width) {
135
165
  if (!width || width < 24) return s;
136
166
  return s.split('\n').map((line) => {
137
167
  if (visW(line) <= width) return line;
@@ -168,7 +198,7 @@ function installSmartWrap() {
168
198
  const orig = console.log.bind(console);
169
199
  const wrapped = (...args) => {
170
200
  if (args.length && args.every((a) => typeof a === 'string')) {
171
- orig(wrapAnsi(args.join(' ')));
201
+ orig(wrapAnsi(args.join(' '), termWidth()));
172
202
  } else {
173
203
  orig(...args);
174
204
  }
@@ -334,7 +364,7 @@ module.exports = {
334
364
  spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
335
365
  randomTip, TOUR_PAGES,
336
366
  // Smart wrap
337
- wrapAnsi, installSmartWrap,
367
+ wrapAnsi, installSmartWrap, termWidth, rawWidth, setWidth,
338
368
  // ANSI helpers
339
369
  hide, show, up, clearLine, sleep,
340
370
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.57",
3
+ "version": "0.15.59",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"