@pugi/cli 0.1.0-beta.35 → 0.1.0-beta.37

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.
@@ -135,9 +135,9 @@ function effectiveMode(ctx) {
135
135
  const global = getGlobalDefaultMode(ctx.homeDir);
136
136
  if (global)
137
137
  return global;
138
- // Defensive: PERMISSION_MODES[1] is 'ask' (the canonical default). We
139
- // index off the canonical list rather than re-import DEFAULT_PERMISSION_MODE
140
- // here to keep the symbol surface narrow.
141
- return PERMISSION_MODES[1] ?? 'ask';
138
+ // Wave 7 canonical default — `PERMISSION_MODES[0]` is `default` (the
139
+ // CC-parity ask-every-call ground state). Fall back literally if the
140
+ // array is empty (defensive should never happen).
141
+ return PERMISSION_MODES[0] ?? 'default';
142
142
  }
143
143
  //# sourceMappingURL=plan.js.map
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.35');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.37');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.
@@ -107,6 +107,11 @@ export function InputBox(props) {
107
107
  // panes when Ctrl+L wipes the terminal (the parent React tree is
108
108
  // otherwise stable and would not redraw on a stdout.write alone).
109
109
  const [, setRedrawTick] = useState(0);
110
+ // Wave 7 Shift+Tab toast — flashed for 2s after a mode cycle so the
111
+ // operator sees `Mode → acceptEdits` под the input divider. Cleared
112
+ // by a setTimeout so a quick second Shift+Tab refreshes the toast.
113
+ const [modeCycleToast, setModeCycleToast] = useState(null);
114
+ const modeCycleTimerRef = useRef(null);
110
115
  const now = props.now ?? Date.now;
111
116
  const { stdout } = useStdout();
112
117
  const columns = stdout?.columns ?? FALLBACK_COLUMNS;
@@ -197,6 +202,24 @@ export function InputBox(props) {
197
202
  }
198
203
  return;
199
204
  }
205
+ // Wave 7 — Claude Code parity: Shift+Tab cycles permission mode.
206
+ // The host owns the cycle logic + persistence; we just intercept
207
+ // the chord and surface a one-line toast on success. Place this
208
+ // BEFORE the search-mode and palette branches so a Shift+Tab fires
209
+ // even while reverse-search is active (operator habit-driven).
210
+ if (key.shift && key.tab && props.onCyclePermissionMode) {
211
+ const nextMode = props.onCyclePermissionMode();
212
+ if (nextMode) {
213
+ setModeCycleToast(`Mode → ${nextMode}`);
214
+ if (modeCycleTimerRef.current)
215
+ clearTimeout(modeCycleTimerRef.current);
216
+ modeCycleTimerRef.current = setTimeout(() => {
217
+ setModeCycleToast(null);
218
+ modeCycleTimerRef.current = null;
219
+ }, 2_000);
220
+ }
221
+ return;
222
+ }
200
223
  // Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
201
224
  // Esc cancels (restoring the pre-search draft), backspace shortens
202
225
  // the query, typed characters extend it.
@@ -543,7 +566,7 @@ export function InputBox(props) {
543
566
  : Math.min(paletteIndex, paletteView.rows.length - 1);
544
567
  const divider = '─'.repeat(innerWidth);
545
568
  const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
546
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
569
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), modeCycleToast ? (_jsx(Box, { children: _jsx(Text, { color: "#3da9fc", bold: true, children: ` ${modeCycleToast}` }) })) : null, line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Shift+Tab mode · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
547
570
  }
548
571
  /**
549
572
  * Render the line with the cursor glyph inserted at `cursor`. The cursor
@@ -49,15 +49,21 @@ export function PermissionsPicker(props) {
49
49
  props.onCancel();
50
50
  return;
51
51
  }
52
- // Number shortcuts mirror the legacy text table order.
52
+ // Wave 7: number shortcuts mirror the canonical 6-mode order.
53
+ // Operators coming from Claude Code reach the same mode with the
54
+ // same digit (1=default, 2=acceptEdits, …, 6=bypassPermissions).
53
55
  if (input === '1')
54
- props.onSelect('plan');
56
+ props.onSelect('default');
55
57
  if (input === '2')
56
- props.onSelect('ask');
58
+ props.onSelect('acceptEdits');
57
59
  if (input === '3')
58
- props.onSelect('allow');
60
+ props.onSelect('plan');
59
61
  if (input === '4')
60
- props.onSelect('bypass');
62
+ props.onSelect('auto');
63
+ if (input === '5')
64
+ props.onSelect('dontAsk');
65
+ if (input === '6')
66
+ props.onSelect('bypassPermissions');
61
67
  });
62
68
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Permission mode" }), _jsx(Text, { dimColor: true, children: ` (current: ${props.currentMode} — ${props.sourceLabel})` })] }), props.firstRun ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "First time? Mode = Ask \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E. Use /permissions \u043A change later." }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: ITEMS.map((item, itemIndex) => {
63
69
  const isSelected = itemIndex === index;
@@ -71,7 +77,9 @@ function PickerRow({ isSelected, isCurrent, title, hint, }) {
71
77
  // mode (separate from cursor focus) so an operator instantly sees
72
78
  // which row is "what I have now" vs "what I'm hovering".
73
79
  const indicator = isSelected ? '▸ ' : ' ';
74
- const padded = title.padEnd(10, ' ');
80
+ // Wave 7: longest title is `BypassPermissions` (17 chars). Bump the
81
+ // pad column так the gloss column stays aligned across all 6 rows.
82
+ const padded = title.padEnd(18, ' ');
75
83
  const currentMarker = isCurrent ? ' ●' : ' ';
76
84
  return (_jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [indicator, padded] }), _jsx(Text, { color: isCurrent ? 'green' : undefined, children: currentMarker }), _jsx(Text, { dimColor: true, children: ` ${hint}` })] }));
77
85
  }
package/dist/tui/repl.js CHANGED
@@ -196,6 +196,34 @@ export function Repl(props) {
196
196
  const verdict = props.session.walkbackLastTurn();
197
197
  return verdict === 'walked-back' ? 'walked-back' : 'nothing';
198
198
  }, [props.session, modalActive]);
199
+ // Wave 7 — Shift+Tab cycles the 6 canonical permission modes (CC
200
+ // parity). Refuses while a modal is active so the operator does not
201
+ // accidentally flip mode mid-prompt; otherwise resolves the current
202
+ // mode through the workspace > global > default merge, advances via
203
+ // `nextPermissionMode`, и persists к .pugi/session.json. Returns the
204
+ // new mode string so the InputBox can flash a one-line toast.
205
+ const handleCyclePermissionMode = useCallback(() => {
206
+ if (modalActive)
207
+ return null;
208
+ try {
209
+ // Lazy-require так this code path doesn't drag the permissions
210
+ // module into the splash + boot stages where it isn't needed.
211
+ // The require is sync but the inner work is pure JSON IO.
212
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
213
+ const perm = require('../core/permissions/index.js');
214
+ const workspaceRoot = process.cwd();
215
+ const current = perm.resolveMode({ workspaceRoot });
216
+ const next = perm.nextPermissionMode(current);
217
+ perm.setCurrentMode(workspaceRoot, next);
218
+ return next;
219
+ }
220
+ catch {
221
+ // Persistence is best-effort — if .pugi/session.json is read-only
222
+ // или ENOENT-on-parent the toast is suppressed so we don't lie
223
+ // about the flip к the operator.
224
+ return null;
225
+ }
226
+ }, [modalActive]);
199
227
  // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
200
228
  // box pinned to alt-screen BOTTOM, conversation grows above it.
201
229
  // Beta.3's height={rows} fix broke keystroke focus - raw echo at
@@ -204,7 +232,7 @@ export function Repl(props) {
204
232
  // input, and the input stays the sole focusable surface adjacent
205
233
  // to the cursor row, so all keystrokes route through it.
206
234
  const altScreenRows = process.stdout.rows ?? 24;
207
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, now: props.now,
235
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
208
236
  // Slug from process.cwd() (full path) so two workspaces with
209
237
  // the same basename do not share history. state.workspaceLabel
210
238
  // is the basename only. Codex review P2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.35",
3
+ "version": "0.1.0-beta.37",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "undici": "^8.3.0",
56
56
  "zod": "^3.23.0",
57
57
  "@pugi/personas": "0.1.2",
58
- "@pugi/sdk": "0.1.0-beta.35"
58
+ "@pugi/sdk": "0.1.0-beta.37"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^22.0.0",