diff --git a/apps/gateway/src/commands/command-executor.service.ts b/apps/gateway/src/commands/command-executor.service.ts index 29c16f2..3714cf4 100644 --- a/apps/gateway/src/commands/command-executor.service.ts +++ b/apps/gateway/src/commands/command-executor.service.ts @@ -291,7 +291,7 @@ export class CommandExecutorService { return { command: 'provider', success: true, - message: `Open this URL to authenticate with ${providerName}:\n${loginUrl}\n\n(URL copied to clipboard)`, + message: `Open this URL to authenticate with ${providerName}:\n${loginUrl}`, conversationId, data: { loginUrl, pollToken, provider: providerName }, }; diff --git a/docs/scratchpads/BUG-CLI-scratchpad.md b/docs/scratchpads/BUG-CLI-scratchpad.md new file mode 100644 index 0000000..a69911e --- /dev/null +++ b/docs/scratchpads/BUG-CLI-scratchpad.md @@ -0,0 +1,40 @@ +# BUG-CLI Scratchpad + +## Objective +Fix 4 CLI/TUI polish bugs in a single PR (issues #192, #193, #194, #199). + +## Issues +- #192: Ctrl+T leaks 't' into input +- #193: Duplicate React keys in CommandAutocomplete +- #194: /provider login false clipboard claim +- #199: TUI shows hardcoded version "0.0.0" + +## Plan and Fixes + +### Bug #192 — Ctrl+T character leak +- Location: `packages/cli/src/tui/app.tsx` +- Fix: Added `ctrlJustFired` ref. Set synchronously in Ctrl+T/L/N/K handlers, cleared via microtask. + In the `onChange` wrapper passed to `InputBar`, if `ctrlJustFired.current` is true, suppress the + leaked character and return early. + +### Bug #193 — Duplicate React keys +- Location: `packages/cli/src/tui/components/command-autocomplete.tsx` +- Fix: Changed `key={cmd.name}` to `key={`${cmd.execution}-${cmd.name}`}` for uniqueness. +- Also: `packages/cli/src/tui/commands/registry.ts` — `getAll()` now deduplicates gateway commands + that share a name with local commands. Local commands take precedence. + +### Bug #194 — False clipboard claim +- Location: `apps/gateway/src/commands/command-executor.service.ts` +- Fix: Removed the `\n\n(URL copied to clipboard)` suffix from the provider login message. + +### Bug #199 — Hardcoded version "0.0.0" +- Location: `packages/cli/src/cli.ts` + `packages/cli/src/tui/app.tsx` +- Fix: `cli.ts` reads version from `../package.json` via `createRequire`. Passes `version: CLI_VERSION` + to TuiApp in both render calls. TuiApp has new optional `version` prop (defaults to '0.0.0'), + passes it to TopBar instead of hardcoded `"0.0.0"`. + +## Quality Gates +- CLI typecheck: PASSED +- CLI lint: PASSED +- Prettier format:check: PASSED +- Gateway lint: PASSED diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index ab6f219..f54c846 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,14 +1,18 @@ #!/usr/bin/env node +import { createRequire } from 'module'; import { Command } from 'commander'; import { createQualityRailsCli } from '@mosaic/quality-rails'; import { registerAgentCommand } from './commands/agent.js'; import { registerMissionCommand } from './commands/mission.js'; import { registerPrdyCommand } from './commands/prdy.js'; +const _require = createRequire(import.meta.url); +const CLI_VERSION: string = (_require('../package.json') as { version: string }).version; + const program = new Command(); -program.name('mosaic').description('Mosaic Stack CLI').version('0.0.0'); +program.name('mosaic').description('Mosaic Stack CLI').version(CLI_VERSION); // ─── login ────────────────────────────────────────────────────────────── @@ -176,6 +180,7 @@ program agentId, agentName: agentName ?? undefined, projectId, + version: CLI_VERSION, }), { exitOnCtrlC: false }, ); @@ -249,6 +254,7 @@ sessionsCmd gatewayUrl: opts.gateway, conversationId: id, sessionCookie: session.cookie, + version: CLI_VERSION, }), ); }); diff --git a/packages/cli/src/tui/app.tsx b/packages/cli/src/tui/app.tsx index c99b033..0004660 100644 --- a/packages/cli/src/tui/app.tsx +++ b/packages/cli/src/tui/app.tsx @@ -24,6 +24,8 @@ export interface TuiAppProps { agentId?: string; agentName?: string; projectId?: string; + /** CLI package version passed from the entry point (cli.ts). */ + version?: string; } export function TuiApp({ @@ -35,6 +37,7 @@ export function TuiApp({ agentId, agentName, projectId: _projectId, + version = '0.0.0', }: TuiAppProps) { const { exit } = useApp(); const gitInfo = useGitInfo(); @@ -77,6 +80,9 @@ export function TuiApp({ const [tuiInput, setTuiInput] = useState(''); // Ctrl+C double-press: first press with empty input shows hint; second exits const ctrlCPendingExit = useRef(false); + // Flag to suppress the character that ink-text-input leaks when a Ctrl+key + // combo is handled by the top-level useInput handler (e.g. Ctrl+T → 't'). + const ctrlJustFired = useRef(false); const handleLocalCommand = useCallback( (parsed: ParsedCommand) => { @@ -189,14 +195,23 @@ export function TuiApp({ ctrlCPendingExit.current = false; // Ctrl+L: toggle sidebar (refresh on open) if (key.ctrl && ch === 'l') { + ctrlJustFired.current = true; + queueMicrotask(() => { + ctrlJustFired.current = false; + }); const willOpen = !appMode.sidebarOpen; appMode.toggleSidebar(); if (willOpen) { void conversations.refresh(); } + return; } // Ctrl+N: create new conversation and switch to it if (key.ctrl && ch === 'n') { + ctrlJustFired.current = true; + queueMicrotask(() => { + ctrlJustFired.current = false; + }); void conversations .createConversation() .then((conv) => { @@ -206,15 +221,21 @@ export function TuiApp({ } }) .catch(() => {}); + return; } // Ctrl+K: toggle search mode if (key.ctrl && ch === 'k') { + ctrlJustFired.current = true; + queueMicrotask(() => { + ctrlJustFired.current = false; + }); if (appMode.mode === 'search') { search.clear(); appMode.setMode('chat'); } else { appMode.setMode('search'); } + return; } // Page Up / Page Down: scroll message history (only in chat mode) if (appMode.mode === 'chat') { @@ -227,6 +248,10 @@ export function TuiApp({ } // Ctrl+T: cycle thinking level if (key.ctrl && ch === 't') { + ctrlJustFired.current = true; + queueMicrotask(() => { + ctrlJustFired.current = false; + }); const levels = socket.availableThinkingLevels; if (levels.length > 0) { const currentIdx = levels.indexOf(socket.thinkingLevel); @@ -236,6 +261,7 @@ export function TuiApp({ socket.setThinkingLevel(next); } } + return; } // Escape: return to chat from sidebar/search; in chat, scroll to bottom if (key.escape) { @@ -292,7 +318,18 @@ export function TuiApp({ { + // Suppress the character that ink-text-input leaks when a Ctrl+key + // combo fires (e.g. Ctrl+T inserts 't'). The ctrlJustFired ref is + // set synchronously in the useInput handler and cleared via a + // microtask, so this callback sees it as still true on the same + // event-loop tick. + if (ctrlJustFired.current) { + ctrlJustFired.current = false; + return; + } + setTuiInput(val); + }} onSubmit={socket.sendMessage} onSystemMessage={socket.addSystemMessage} onLocalCommand={handleLocalCommand} @@ -311,7 +348,7 @@ export function TuiApp({ c.name)); + const dedupedGateway = gateway.filter((c) => !localNames.has(c.name)); + return [...LOCAL_COMMANDS, ...dedupedGateway]; } getLocalCommands(): CommandDef[] { diff --git a/packages/cli/src/tui/components/command-autocomplete.tsx b/packages/cli/src/tui/components/command-autocomplete.tsx index 290d18d..6fdcf65 100644 --- a/packages/cli/src/tui/components/command-autocomplete.tsx +++ b/packages/cli/src/tui/components/command-autocomplete.tsx @@ -27,7 +27,7 @@ export function CommandAutocomplete({ return ( {filtered.slice(0, 8).map((cmd, i) => ( - + {i === clampedIndex ? '▶ ' : ' '}/{cmd.name}